mirror of
https://github.com/bettercap/bettercap
synced 2025-08-19 13:09:49 -07:00
Compare commits
421 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec2753fad | ||
|
|
42da612113 | ||
|
|
fc65cde728 | ||
|
|
cc475ddfba | ||
|
|
cfc6d55462 | ||
|
|
ccf4fa09e2 | ||
|
|
1e235181aa | ||
|
|
453c417e92 |
||
|
|
d1925cd926 |
||
|
|
d60d4612f2 |
||
|
|
8bd6052851 |
||
|
|
a23ba5fcba |
||
|
|
be76c0a7da |
||
|
|
faee64a2c0 | ||
|
|
0f68fcca8b | ||
|
|
5a6a5fbbdf | ||
|
|
fa7e95c420 | ||
|
|
ad102afa2f | ||
|
|
c154546fba |
||
|
|
db1b386326 |
||
|
|
183837e216 | ||
|
|
0216ea69f9 | ||
|
|
fecd81118d | ||
|
|
61891e86a3 | ||
|
|
0b64530cea | ||
|
|
39d9254462 | ||
|
|
ceb5ecd12f | ||
|
|
47077d877c | ||
|
|
414d18a6da | ||
|
|
da2292fbb7 |
||
|
|
b331be47d6 |
||
|
|
0865d5af52 |
||
|
|
1c78ffa7be | ||
|
|
58da4b6fce |
||
|
|
159f065058 |
||
|
|
3440e9999a |
||
|
|
d28692eef6 |
||
|
|
2317c28062 |
||
|
|
5069224e64 | ||
|
|
0356082947 | ||
|
|
84acb9556e | ||
|
|
aa819862eb | ||
|
|
fed98adffa | ||
|
|
948756208a |
||
|
|
4f51c57dd4 |
||
|
|
04ed02f420 | ||
|
|
a53d561ddd | ||
|
|
84846b11dc |
||
|
|
9ebd958218 | ||
|
|
3a360e4622 | ||
|
|
7a2ecb15f6 | ||
|
|
69b3daa5b9 | ||
|
|
2662831fab | ||
|
|
6ff2839e15 |
||
|
|
1303b8e0d1 |
||
|
|
2c157d2c5c |
||
|
|
3608e76fb6 |
||
|
|
862d2c0825 |
||
|
|
cdf870dd4f | ||
|
|
93554a8448 |
||
|
|
6d75d9e8e2 |
||
|
|
f9ab25aa8b |
||
|
|
dd05670e1f |
||
|
|
4320b98e80 |
||
|
|
fc02767e72 |
||
|
|
0ea15563b1 |
||
|
|
e9fee2f2fa |
||
|
|
99e7f78a22 |
||
|
|
84db5ed9bf |
||
|
|
fd1f3bc1d2 |
||
|
|
053ca5be55 |
||
|
|
2c6f048cec |
||
|
|
890b83501c |
||
|
|
f8884da78c |
||
|
|
0b6fade8fd |
||
|
|
df91176308 |
||
|
|
5da2cd8d29 |
||
|
|
4eb923f972 |
||
|
|
876449e105 |
||
|
|
f3001aa565 |
||
|
|
1c657fdf53 |
||
|
|
25c6339275 |
||
|
|
5e97fbb6eb |
||
|
|
12556bc6be |
||
|
|
c8c1072cc0 |
||
|
|
086eed49d5 |
||
|
|
d03d778e46 |
||
|
|
0ea1dec113 |
||
|
|
63ff51efdf | ||
|
|
8eedf6d90c | ||
|
|
61a9069e50 | ||
|
|
1d7a49a952 |
||
|
|
243d3e7016 |
||
|
|
30257fd547 |
||
|
|
9ed0fadd24 | ||
|
|
3e8063c2c7 |
||
|
|
3cea30a277 |
||
|
|
fdca49678e |
||
|
|
91f5213526 |
||
|
|
159aed5080 |
||
|
|
c4e45b368d |
||
|
|
169b0cb8c9 | ||
|
|
a7e4572416 |
||
|
|
01a144d69b |
||
|
|
cb5f7679d8 |
||
|
|
00854261a4 |
||
|
|
906162e5fa |
||
|
|
08e248e38c |
||
|
|
6d242022fb |
||
|
|
6de6de7418 | ||
|
|
ccb2774814 | ||
|
|
40f3906115 | ||
|
|
27d245625c | ||
|
|
32995aada3 | ||
|
|
fe9481cb42 | ||
|
|
d0d1029a5a |
||
|
|
c5017ed020 | ||
|
|
88d813543a | ||
|
|
55edafb33c | ||
|
|
c5d93825bd | ||
|
|
43f1013f0d | ||
|
|
a49d561dce | ||
|
|
e190737c91 | ||
|
|
30c4c320a6 | ||
|
|
7e1cb69071 | ||
|
|
02871b0ae6 | ||
|
|
ef69151a7f | ||
|
|
ae466b702a | ||
|
|
ea8e96c285 | ||
|
|
520592d1a5 | ||
|
|
3b4cdc60cb | ||
|
|
ba29bea0cd | ||
|
|
209725d623 | ||
|
|
d2f13a3293 | ||
|
|
fabf3bb8e9 | ||
|
|
5969acd55d | ||
|
|
bd959586c5 | ||
|
|
a234c20650 | ||
|
|
8446d66d12 | ||
|
|
5652d15426 | ||
|
|
7b4fc3d31d | ||
|
|
26c532316a | ||
|
|
2966153adf | ||
|
|
76e094f687 | ||
|
|
6af2de6de9 | ||
|
|
17ba1be16c | ||
|
|
b0a197b377 | ||
|
|
e656a6cbfa | ||
|
|
51a5b4ad6e | ||
|
|
91d360327a | ||
|
|
67cc9680ed | ||
|
|
756dc3d71a |
||
|
|
531da20048 | ||
|
|
acda32e304 | ||
|
|
75478a21f6 | ||
|
|
5bc9dd9259 | ||
|
|
97b4dcb46e | ||
|
|
c3999d6bb5 | ||
|
|
cf6fba6151 | ||
|
|
3775295a2c | ||
|
|
b2035daf54 | ||
|
|
00c5b2c9c6 | ||
|
|
b1ac9cda7d | ||
|
|
5786ffdaa9 | ||
|
|
72afa07d28 | ||
|
|
1c56622cde | ||
|
|
4c7599566c | ||
|
|
c4c7b8c43d | ||
|
|
bb847fcf8a | ||
|
|
7702207ee9 | ||
|
|
840f819484 | ||
|
|
31d93e7c39 | ||
|
|
f0126c28fb | ||
|
|
26b2c300b8 | ||
|
|
575022fac4 | ||
|
|
a4c99df51e | ||
|
|
81e18d20b7 | ||
|
|
81adcc96e6 | ||
|
|
7d85483214 | ||
|
|
ac2d333609 | ||
|
|
9f61ec7f13 | ||
|
|
f3132cee34 | ||
|
|
d8aeecb99f | ||
|
|
3ec7b01bed | ||
|
|
0202028524 | ||
|
|
ef9a3ef85b | ||
|
|
5cc7260ca9 | ||
|
|
77ae56cc62 | ||
|
|
1b91eb348b | ||
|
|
d8e11287c6 | ||
|
|
08da91ed5c | ||
|
|
b0d56e4f5e | ||
|
|
23e074b686 | ||
|
|
2d03782fe1 | ||
|
|
8d8af63577 | ||
|
|
6a6e942ea4 | ||
|
|
0ceb938f10 | ||
|
|
6282fe3451 | ||
|
|
d9a91d393e | ||
|
|
e45c9cc053 | ||
|
|
cc66b6459f | ||
|
|
c5d20220a1 | ||
|
|
d733381322 | ||
|
|
e1e8a0b78d | ||
|
|
9266ee942f | ||
|
|
13cab7b637 | ||
|
|
f6192653ef | ||
|
|
72b14502c3 | ||
|
|
235017c294 | ||
|
|
86e87ab656 | ||
|
|
cdefa3c9d3 | ||
|
|
ee944d9640 | ||
|
|
fcf285aabb | ||
|
|
7f22425dcc | ||
|
|
1df51fd13a | ||
|
|
afdc68f512 | ||
|
|
9ab2e13f31 | ||
|
|
6f1920f478 | ||
|
|
69744e6b63 | ||
|
|
7636ca2808 | ||
|
|
9d5c38c693 | ||
|
|
2659a559c9 | ||
|
|
76e136a18e | ||
|
|
93de427f9a | ||
|
|
9e7fda751a | ||
|
|
fd05df613e | ||
|
|
5fe3ef3d52 | ||
|
|
9937e797ae | ||
|
|
780032b116 | ||
|
|
41fa4cd850 | ||
|
|
107c8fdf99 | ||
|
|
856c0d5a7d | ||
|
|
0343e002a9 | ||
|
|
d68da2108d | ||
|
|
7605f4afa3 | ||
|
|
dc621f5934 | ||
|
|
3e16c6dad0 | ||
|
|
dd71378ce7 | ||
|
|
5d2c173d5e | ||
|
|
632d703087 | ||
|
|
8b24723c18 | ||
|
|
9abf7c809a | ||
|
|
7beb27cfca | ||
|
|
b12ba7947b | ||
|
|
06623ddfb9 |
||
|
|
2499d5147f | ||
|
|
5858743b6e | ||
|
|
0dc5f66e27 | ||
|
|
02fa241d06 | ||
|
|
c2ab5f4756 | ||
|
|
7371a85828 |
||
|
|
9b6694f565 | ||
|
|
6951fbb8dd | ||
|
|
9bf1474615 |
||
|
|
9cd1609306 |
||
|
|
3df89fb7e5 |
||
|
|
74647db825 |
||
|
|
474215ebd9 |
||
|
|
826f13e47a | ||
|
|
ca2e257fbb |
||
|
|
71822229a0 | ||
|
|
043bd4593b | ||
|
|
a950d3b767 | ||
|
|
5af1be3356 | ||
|
|
4dc7bae48c | ||
|
|
924ff5753d |
||
|
|
ee35550f70 |
||
|
|
76a7820da5 |
||
|
|
bdc389eaee |
||
|
|
32d997ea5a |
||
|
|
786dacf8ca |
||
|
|
cdd483e698 |
||
|
|
27bae1cd3b |
||
|
|
e5f8c168c3 | ||
|
|
58ca59bc6f | ||
|
|
8e4a00091e | ||
|
|
44e24204e5 | ||
|
|
24a5dfe48f |
||
|
|
3f154cccd9 | ||
|
|
e224eea8c6 | ||
|
|
4f3f55f648 | ||
|
|
a4fb94ce68 | ||
|
|
11d96069ae | ||
|
|
c81db63a10 |
||
|
|
eaf2b96407 |
||
|
|
28371084d3 | ||
|
|
eff8135d99 | ||
|
|
22de9d3d4f | ||
|
|
fd160bf7ca |
||
|
|
628c0b79fb | ||
|
|
2bc3d871ef | ||
|
|
68924c34c4 | ||
|
|
ed4239fad5 |
||
|
|
b9a546ec9d |
||
|
|
0193d13ca0 |
||
|
|
a20fb139f5 |
||
|
|
3bd813f545 |
||
|
|
ac96bc8d2f |
||
|
|
5389228034 |
||
|
|
c6740a5750 |
||
|
|
aba29e03f6 |
||
|
|
e255eba69f |
||
|
|
44a17602ed |
||
|
|
eee94e993c |
||
|
|
7fd9d18625 |
||
|
|
e81f36c582 | ||
|
|
74e3303963 | ||
|
|
f10ccfb4c6 | ||
|
|
8b867c29ed |
||
|
|
81ae731b9f | ||
|
|
59dce4ced6 | ||
|
|
d0ecfd499f | ||
|
|
0598272384 |
||
|
|
c78a67d439 |
||
|
|
d7f95dc97d |
||
|
|
754b6b3841 |
||
|
|
cb8a87460b |
||
|
|
ef2cd0063d | ||
|
|
c8ecaf99e0 | ||
|
|
e9dad78ec2 | ||
|
|
9020c53820 | ||
|
|
0637451390 |
||
|
|
c1770b3aa6 |
||
|
|
f10159ec19 | ||
|
|
58f4214756 | ||
|
|
118a348e3e |
||
|
|
6c2c0da22c | ||
|
|
4690a23ace | ||
|
|
0e2fd008e4 | ||
|
|
9404620468 | ||
|
|
daf2f943e2 | ||
|
|
badd13181d | ||
|
|
b9cc36b6b6 | ||
|
|
dd71ccf416 | ||
|
|
dfe64ee4db | ||
|
|
82dd30c777 | ||
|
|
f42dcb72d5 | ||
|
|
4fc84f2907 | ||
|
|
8c00207e7e | ||
|
|
831020983c |
||
|
|
821ce9aea2 |
||
|
|
c38de3a511 | ||
|
|
568c166fe1 | ||
|
|
ee14e96963 | ||
|
|
05a185434b | ||
|
|
e3078c7136 | ||
|
|
131aa846b6 | ||
|
|
22c95c0c4d | ||
|
|
6206fc1b61 |
||
|
|
af85f9292a |
||
|
|
80f7428afe | ||
|
|
eb384d67c1 | ||
|
|
421df5035f | ||
|
|
fee99c4116 |
||
|
|
f2b6d9b708 | ||
|
|
4dac3b9373 | ||
|
|
c9ae0f360e | ||
|
|
c47e3f6195 | ||
|
|
43a93fd866 | ||
|
|
88a83192ef | ||
|
|
bc7d1d9663 | ||
|
|
71ac5bb264 | ||
|
|
93b7e7f2ed | ||
|
|
0042b77c36 | ||
|
|
1d306e6cd2 | ||
|
|
2b4188bb52 | ||
|
|
3c506b7809 | ||
|
|
d5fb7b6754 | ||
|
|
6393dc1ea5 | ||
|
|
1be487843b | ||
|
|
e465f9b145 | ||
|
|
71634058a7 | ||
|
|
5b8cb9a82c | ||
|
|
db275429c2 | ||
|
|
6aa8f45d20 | ||
|
|
bfe307ffe6 | ||
|
|
31b06638d8 | ||
|
|
2dcfea02ce | ||
|
|
662f5fb973 | ||
|
|
906969f1b3 | ||
|
|
8827a2af84 | ||
|
|
2b1ff7d59f | ||
|
|
3c20f2c9aa |
||
|
|
fb7bed9b6b |
||
|
|
40727063ec | ||
|
|
d5e5abcb9b | ||
|
|
c2be8a440b | ||
|
|
fad6172b40 |
||
|
|
b0f7e764dc | ||
|
|
4d5e930e6c |
||
|
|
a6d5d5d048 | ||
|
|
c1520206a5 |
||
|
|
57436a811c | ||
|
|
cbc1432358 | ||
|
|
bef4c6abaa | ||
|
|
d0b5c34763 | ||
|
|
16891c4048 |
||
|
|
6b821d2577 |
||
|
|
240c4c3219 | ||
|
|
cea53b969e | ||
|
|
c68c88030d |
||
|
|
0d17ba3573 |
||
|
|
6dd86c44fa | ||
|
|
c4bbc129b6 | ||
|
|
67a0063ee4 | ||
|
|
4f5f89b6e1 | ||
|
|
d63122bab3 | ||
|
|
161124a3f4 | ||
|
|
b64dffb53f | ||
|
|
3e12bb0290 | ||
|
|
1fea9f02fb |
||
|
|
9a10f0bf2a |
||
|
|
6392e21c40 | ||
|
|
b3da4e30f7 | ||
|
|
ff91392866 | ||
|
|
21aa14fd2f |
||
|
|
80a3e71193 | ||
|
|
7c4da8a550 |
||
|
|
30f2fb0df1 |
||
|
|
e0ff16f9f1 | ||
|
|
e4414b7a45 |
1952 changed files with 398731 additions and 73406 deletions
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
*.js linguist-vendored
|
||||
/Dockerfile linguist-vendored
|
||||
/release.py linguist-vendored
|
||||
/**/*.js linguist-vendored
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: evilsocket
|
||||
patreon: evilsocket
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
|
|
|||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Bettercap Documentation
|
||||
url: https://www.bettercap.org/
|
||||
about: Please read the instructions before asking for help.
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
---
|
||||
name: General Issue
|
||||
about: Write a general issue or bug report.
|
||||
---
|
||||
|
||||
# Prerequisites
|
||||
|
||||
Please, before creating this issue make sure that you read the [README](https://github.com/bettercap/bettercap/blob/master/README.md), that you are running the [latest stable version](https://github.com/bettercap/bettercap/releases) and that you already searched [other issues](https://github.com/bettercap/bettercap/issues?q=is%3Aopen+is%3Aissue+label%3Abug) to see if your problem or request was already reported.
|
||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
version: 2
|
||||
updates:
|
||||
# GitHub Actions
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: daily
|
||||
117
.github/workflows/build-and-deploy.yml
vendored
Normal file
117
.github/workflows/build-and-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*' # Match version tags
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.os.pretty }} ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.os.runs-on }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- name: darwin
|
||||
runs-on: [macos-latest]
|
||||
pretty: 🍎 macOS
|
||||
- name: linux
|
||||
runs-on: [ubuntu-latest]
|
||||
pretty: 🐧 Linux
|
||||
- name: windows
|
||||
runs-on: [windows-latest]
|
||||
pretty: 🪟 Windows
|
||||
output: bettercap.exe
|
||||
arch: [amd64, arm64]
|
||||
go: [1.24.x]
|
||||
exclude:
|
||||
- os:
|
||||
name: darwin
|
||||
arch: amd64
|
||||
# Linux ARM64 images are not yet publicly available (https://github.com/actions/runner-images)
|
||||
- os:
|
||||
name: linux
|
||||
arch: arm64
|
||||
- os:
|
||||
name: windows
|
||||
arch: arm64
|
||||
|
||||
env:
|
||||
OUTPUT: ${{ matrix.os.output || 'bettercap' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: Install Dependencies
|
||||
if: ${{ matrix.os.name == 'linux' }}
|
||||
run: sudo apt-get update && sudo apt-get install -y p7zip-full libpcap-dev libnetfilter-queue-dev libusb-1.0-0-dev
|
||||
|
||||
- name: Install Dependencies (macOS)
|
||||
if: ${{ matrix.os.name == 'macos' }}
|
||||
run: brew install libpcap libusb p7zip
|
||||
|
||||
- name: Install libusb via mingw (Windows)
|
||||
if: ${{ matrix.os.name == 'windows' }}
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
install: |-
|
||||
mingw64/mingw-w64-x86_64-libusb
|
||||
mingw64/mingw-w64-x86_64-pkg-config
|
||||
|
||||
- name: Install other Dependencies (Windows)
|
||||
if: ${{ matrix.os.name == 'windows' }}
|
||||
run: |
|
||||
choco install openssl.light -y
|
||||
choco install make -y
|
||||
choco install 7zip -y
|
||||
choco install zadig -y
|
||||
curl -L "https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip" -o "C:\wpcap-sdk.zip"
|
||||
7z x -y "C:\wpcap-sdk.zip" -o"C:\winpcap"
|
||||
echo "D:\a\_temp\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Build
|
||||
run: make -e TARGET="${{ env.OUTPUT }}"
|
||||
|
||||
- name: Verify Build
|
||||
run: |
|
||||
file "${{ env.OUTPUT }}"
|
||||
openssl dgst -sha256 "${{ env.OUTPUT }}" | tee bettercap_${{ matrix.os.name }}_${{ matrix.arch }}.sha256
|
||||
7z a "bettercap_${{ matrix.os.name }}_${{ matrix.arch }}.zip" "${{ env.OUTPUT }}" "bettercap_${{ matrix.os.name }}_${{ matrix.arch }}.sha256"
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-artifacts-${{ matrix.os.name }}-${{ matrix.arch }}
|
||||
path: |
|
||||
bettercap_*.zip
|
||||
bettercap_*.sha256
|
||||
|
||||
deploy:
|
||||
needs: [build]
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: release-artifacts-*
|
||||
merge-multiple: true
|
||||
path: dist/
|
||||
|
||||
- name: Release Assets
|
||||
run: ls -l dist
|
||||
|
||||
- name: Upload Release Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: dist/bettercap_*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
30
.github/workflows/build-and-push-docker.yml
vendored
Normal file
30
.github/workflows/build-and-push-docker.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: bettercap/bettercap:latest
|
||||
33
.github/workflows/test-on-linux.yml
vendored
Normal file
33
.github/workflows/test-on-linux.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: Linux tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
go-version: ['1.24.x']
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y p7zip-full libpcap-dev libnetfilter-queue-dev libusb-1.0-0-dev
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
env GO111MODULE=on make test
|
||||
|
||||
33
.github/workflows/test-on-macos.yml
vendored
Normal file
33
.github/workflows/test-on-macos.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: macOS tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
go-version: ['1.24.x']
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: brew install libpcap libusb p7zip
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
env GO111MODULE=on make test
|
||||
|
||||
47
.github/workflows/test-on-windows.yml
vendored
Normal file
47
.github/workflows/test-on-windows.yml
vendored
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
name: Windows tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest]
|
||||
go-version: ['1.24.x']
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Install libusb via mingw
|
||||
uses: msys2/setup-msys2@v2
|
||||
with:
|
||||
install: |-
|
||||
mingw64/mingw-w64-x86_64-libusb
|
||||
mingw64/mingw-w64-x86_64-pkg-config
|
||||
|
||||
- name: Install other dependencies
|
||||
run: |
|
||||
choco install openssl.light -y
|
||||
choco install make -y
|
||||
choco install 7zip -y
|
||||
choco install zadig -y
|
||||
curl -L "https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip" -o "C:\wpcap-sdk.zip"
|
||||
7z x -y "C:\wpcap-sdk.zip" -o"C:\winpcap"
|
||||
|
||||
- run: echo "D:\a\_temp\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Run Tests
|
||||
run: env GO111MODULE=on make test
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
|||
*.sw*
|
||||
*.tar.gz
|
||||
*.prof*
|
||||
_example/config.js
|
||||
pcaps
|
||||
build
|
||||
bettercap
|
||||
|
|
@ -13,3 +14,7 @@ stage/
|
|||
|
||||
/snap
|
||||
.idea
|
||||
/cover.out
|
||||
/can-data
|
||||
/test*.yml
|
||||
/zerochaos.js
|
||||
159
.travis.yml
159
.travis.yml
|
|
@ -1,159 +0,0 @@
|
|||
# Globals
|
||||
sudo: false
|
||||
language: go
|
||||
go:
|
||||
- 1.15.x
|
||||
env:
|
||||
global:
|
||||
- VERSION=$(echo ${TRAVIS_BRANCH} | sed "s/\//_/g")
|
||||
- OUTPUT="bettercap"
|
||||
cache:
|
||||
apt: true
|
||||
|
||||
# Includes
|
||||
linux_deps: &linux_deps
|
||||
os: linux
|
||||
dist: bionic
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- p7zip-full
|
||||
- libpcap-dev
|
||||
- libnetfilter-queue-dev
|
||||
- libusb-1.0-0-dev
|
||||
update: true
|
||||
|
||||
finish: &finish
|
||||
after_success:
|
||||
- file "${OUTPUT}"
|
||||
- openssl dgst -sha256 "${OUTPUT}" | tee bettercap_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.sha256
|
||||
- 7z a "bettercap_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.zip" "${OUTPUT}" "bettercap_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.sha256"
|
||||
- ls -la bettercap*
|
||||
|
||||
cross_deps: &cross_deps
|
||||
<<: *linux_deps
|
||||
before_install:
|
||||
- wget --show-progress -qcO "qemu.deb" "https://debian.grena.ge/debian/pool/main/q/qemu/qemu-user-static_4.2-3_amd64.deb"
|
||||
- sudo dpkg -i "qemu.deb"
|
||||
|
||||
normal_install: &normal_install
|
||||
install:
|
||||
- make -e TARGET="${OUTPUT}"
|
||||
<<: *finish
|
||||
|
||||
cross_install: &cross_install
|
||||
install:
|
||||
- sudo builder/arm_builder.sh bettercap make -e TARGET="${OUTPUT}"
|
||||
<<: *finish
|
||||
|
||||
# Tasks
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux - amd64
|
||||
if: tag IS present
|
||||
arch: amd64
|
||||
env:
|
||||
- TARGET_OS=linux
|
||||
- TARGET_ARCH=amd64
|
||||
<<: *linux_deps
|
||||
<<: *normal_install
|
||||
|
||||
- name: Linux - aarch64
|
||||
if: tag IS present
|
||||
arch: arm64
|
||||
env:
|
||||
- TARGET_OS=linux
|
||||
- TARGET_ARCH=aarch64
|
||||
- GEM_HOME=~/.ruby
|
||||
- PATH=$PATH:~/.ruby/bin
|
||||
<<: *linux_deps
|
||||
<<: *normal_install
|
||||
before_install:
|
||||
- mkdir -p ~/.ruby
|
||||
|
||||
- name: Linux - armhf
|
||||
if: tag IS present
|
||||
language: minimal
|
||||
arch: amd64
|
||||
env:
|
||||
- TARGET_OS=linux
|
||||
- TARGET_ARCH=armhf
|
||||
<<: *cross_deps
|
||||
<<: *cross_install
|
||||
|
||||
- name: OSX - amd64
|
||||
if: tag IS present
|
||||
os: osx
|
||||
arch: amd64
|
||||
addons:
|
||||
homebrew:
|
||||
packages:
|
||||
- libpcap
|
||||
- libusb
|
||||
- p7zip
|
||||
update: true
|
||||
env:
|
||||
- TARGET_OS=darwin
|
||||
- TARGET_ARCH=amd64
|
||||
- PATH="/usr/local/opt/libpcap/bin:$PATH"
|
||||
- LDFLAGS="-L/usr/local/opt/libpcap/lib $LDFLAGS"
|
||||
- CPPFLAGS="-I/usr/local/opt/libpcap/include $CPPFLAGS"
|
||||
- PKG_CONFIG_PATH="/usr/local/opt/libpcap/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
<<: *normal_install
|
||||
|
||||
- name: Windows - amd64
|
||||
if: tag IS present
|
||||
os: windows
|
||||
arch: amd64
|
||||
env:
|
||||
- TARGET_OS=windows
|
||||
- TARGET_ARCH=amd64
|
||||
- PKG_CONFIG_PATH="c:/pkg-config"
|
||||
- OUTPUT=bettercap.exe
|
||||
- CGO_CFLAGS="-I/c/npcap/Include -I/c/libusb/include/libusb-1.0"
|
||||
- CGO_LDFLAGS="-L/c/npcap/Lib/x64 -L/c/libusb/MinGW64/static"
|
||||
before_install:
|
||||
- choco install openssl.light -y
|
||||
- choco install make -y
|
||||
- choco install 7zip -y
|
||||
- choco install pkgconfiglite -y
|
||||
- mkdir /c/pkg-config
|
||||
- choco install zadig -y
|
||||
- curl -L "https://github.com/libusb/libusb/releases/download/v1.0.22/libusb-1.0.22.7z"
|
||||
-o "/c/libusb.7z"
|
||||
- 7z x -y "/c/libusb.7z" -o"/c/libusb"
|
||||
- choco install npcap --version 0.86 -y
|
||||
- curl -L "https://nmap.org/npcap/dist/npcap-sdk-1.03.zip" -o "c:/npcap.zip"
|
||||
- 7z x -y "/c/npcap.zip" -o"/c/npcap"
|
||||
- cp builder/libusb.pc /c/pkg-config/libusb.pc
|
||||
- cp builder/libusb.pc /c/pkg-config/libusb-1.0.pc
|
||||
<<: *normal_install
|
||||
|
||||
- name: Linux - tests
|
||||
if: tag IS blank
|
||||
os: linux
|
||||
arch: amd64
|
||||
allow_failures:
|
||||
- go: master
|
||||
fast_finish: true
|
||||
<<: *linux_deps
|
||||
script:
|
||||
- env GO111MODULE=on make test
|
||||
after_success:
|
||||
- base <(curl -s https://codecov.io/bash)
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: gaQDeYOe/8lL3++jok73kSNtJVyj5Dk8RdxerjSa3hsVrL5IljsNsGGXocesCQ4ubFrnOO26RmO1FxMKmqYBpewRwQ6GKqZjc7IbwR9Cy0c0AyRRULnCsXue3NxIQBobqAwKtaaqDPHZcX1eOVgDnrheMpT5nt9YN2Xyv9zdFAmjfhUxv8K3nyv9eOMHYy0TmcKanQSXcYTHnUONt4Af5XA2NZGTtLUB+FAEf93vLqyqmmkX0EJciYu3HSZmCPFLLACi1WDSvt+e4TlozrutMpgm3JNzZ3eg6IsesRzxy/s2HeOnVJLMCadGjqap98xfSY6V00cUdCny+n8xfDgCzMljM0bEMDUhIs97AFdLXJZKPRGrNSmnurIcJ+NaVrFS5BMiLwQ2J6WiRvDaCWROVd+Vml/bWWZIUsMxVapEN5vbtw8R/gSVQyZnZUXLrArIBQxenSFlMcWDi+VMF38GrQgAB/ddlMZqWjVubpWOSN45Eity0SsLAgsAuNjH1YCeCr0zj1sG08NPsnTPSKr+661iuOTpsdgu/4crF6qcFcl/kvJsw6tyFPVLO5yzbX9q4O778vXRduzPuBeD63eFuHD8pwceGxWWxN9vnQtX6OqRKmEsrLP7aL9dkI2zgp7TOj058hNQefQ5FD25yfKNCUfp/tnxa6XrkrPzWq/SX7c=
|
||||
skip_cleanup: true
|
||||
file_glob: true
|
||||
file:
|
||||
- bettercap_*.zip
|
||||
- bettercap_*.sha256
|
||||
on:
|
||||
tags: true
|
||||
repo: bettercap/bettercap
|
||||
branches:
|
||||
only:
|
||||
- "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]+?$/"
|
||||
16
Dockerfile
vendored
16
Dockerfile
vendored
|
|
@ -1,13 +1,11 @@
|
|||
# build stage
|
||||
FROM golang:alpine AS build-env
|
||||
FROM golang:1.24-alpine AS build-env
|
||||
|
||||
ENV SRC_DIR $GOPATH/src/github.com/bettercap/bettercap
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache bash gcc g++ binutils-gold iptables wireless-tools build-base libpcap-dev libusb-dev linux-headers libnetfilter_queue-dev git
|
||||
|
||||
RUN apk add --update ca-certificates
|
||||
RUN apk add --no-cache --update bash iptables wireless-tools build-base libpcap-dev libusb-dev linux-headers libnetfilter_queue-dev git
|
||||
|
||||
WORKDIR $SRC_DIR
|
||||
ADD . $SRC_DIR
|
||||
WORKDIR $GOPATH/src/github.com/bettercap/bettercap
|
||||
ADD . $GOPATH/src/github.com/bettercap/bettercap
|
||||
RUN make
|
||||
|
||||
# get caplets
|
||||
|
|
@ -16,8 +14,8 @@ RUN git clone https://github.com/bettercap/caplets /usr/local/share/bettercap/ca
|
|||
|
||||
# final stage
|
||||
FROM alpine
|
||||
RUN apk add --update ca-certificates
|
||||
RUN apk add --no-cache --update bash iproute2 libpcap libusb-dev libnetfilter_queue wireless-tools
|
||||
RUN apk add --no-cache ca-certificates
|
||||
RUN apk add --no-cache bash iproute2 libpcap libusb-dev libnetfilter_queue wireless-tools iw
|
||||
COPY --from=build-env /go/src/github.com/bettercap/bettercap/bettercap /app/
|
||||
COPY --from=build-env /usr/local/share/bettercap/caplets /app/
|
||||
WORKDIR /app
|
||||
|
|
|
|||
17
Makefile
17
Makefile
|
|
@ -2,15 +2,14 @@ TARGET ?= bettercap
|
|||
PACKAGES ?= core firewall log modules network packets session tls
|
||||
PREFIX ?= /usr/local
|
||||
GO ?= go
|
||||
GOFLAGS ?=
|
||||
|
||||
all: build
|
||||
|
||||
build: resources
|
||||
$(GO) $(GOFLAGS) build -o $(TARGET) .
|
||||
$(GO) build $(GOFLAGS) -o $(TARGET) .
|
||||
|
||||
build_with_race_detector: resources
|
||||
$(GO) $(GOFLAGS) build -race -o $(TARGET) .
|
||||
$(GO) build $(GOFLAGS) -race -o $(TARGET) .
|
||||
|
||||
resources: network/manuf.go
|
||||
|
||||
|
|
@ -18,20 +17,20 @@ network/manuf.go:
|
|||
@python3 ./network/make_manuf.py
|
||||
|
||||
install:
|
||||
@mkdir -p $(PREFIX)/share/bettercap/caplets
|
||||
@cp bettercap $(PREFIX)/bin/
|
||||
@mkdir -p $(DESTDIR)$(PREFIX)/share/bettercap/caplets
|
||||
@cp bettercap $(DESTDIR)$(PREFIX)/bin/
|
||||
|
||||
docker:
|
||||
@docker build -t bettercap:latest .
|
||||
|
||||
test:
|
||||
$(GO) $(GOFLAGS) test -covermode=atomic -coverprofile=cover.out ./...
|
||||
$(GO) test -covermode=atomic -coverprofile=cover.out ./...
|
||||
|
||||
html_coverage: test
|
||||
$(GO) $(GOFLAGS) tool cover -html=cover.out -o cover.out.html
|
||||
$(GO) tool cover -html=cover.out -o cover.out.html
|
||||
|
||||
benchmark: server_deps
|
||||
$(GO) $(GOFLAGS) test -v -run=doNotRunTests -bench=. -benchmem ./...
|
||||
$(GO) test -v -run=doNotRunTests -bench=. -benchmem ./...
|
||||
|
||||
fmt:
|
||||
$(GO) fmt -s -w $(PACKAGES)
|
||||
|
|
@ -40,4 +39,4 @@ clean:
|
|||
$(RM) $(TARGET)
|
||||
$(RM) -r build
|
||||
|
||||
.PHONY: all build build_with_race_detector resources install docker test html_coverage benchmark fmt clean
|
||||
.PHONY: all build build_with_race_detector resources install docker test html_coverage benchmark fmt clean
|
||||
|
|
|
|||
41
README.md
41
README.md
|
|
@ -1,23 +1,36 @@
|
|||
<p align="center">
|
||||
<small>Join the project community on our server!</small>
|
||||
<br/><br/>
|
||||
<a href="https://discord.gg/https://discord.gg/btZpkp45gQ" target="_blank" title="Join our community!">
|
||||
<img src="https://dcbadge.limes.pink/api/server/https://discord.gg/btZpkp45gQ"/>
|
||||
</a>
|
||||
</p>
|
||||
<hr/>
|
||||
|
||||
<p align="center">
|
||||
<img alt="BetterCap" src="https://raw.githubusercontent.com/bettercap/media/master/logo.png" height="140" />
|
||||
<p align="center">
|
||||
<a href="https://github.com/bettercap/bettercap/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/bettercap/bettercap.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/bettercap/bettercap/blob/master/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-GPL3-brightgreen.svg?style=flat-square"></a>
|
||||
<a href="https://travis-ci.org/bettercap/bettercap"><img alt="Travis" src="https://img.shields.io/travis/bettercap/bettercap/master.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/bettercap/bettercap/actions/workflows/test-on-linux.yml"><img alt="Tests on Linux" src="https://github.com/bettercap/bettercap/actions/workflows/test-on-linux.yml/badge.svg"></a>
|
||||
<a href="https://github.com/bettercap/bettercap/actions/workflows/test-on-macos.yml"><img alt="Tests on macOS" src="https://github.com/bettercap/bettercap/actions/workflows/test-on-macos.yml/badge.svg"></a>
|
||||
<a href="https://github.com/bettercap/bettercap/actions/workflows/test-on-windows.yml"><img alt="Tests on Windows" src="https://github.com/bettercap/bettercap/actions/workflows/test-on-windows.yml/badge.svg"></a>
|
||||
<a href="https://hub.docker.com/r/bettercap/bettercap"><img alt="Docker Hub" src="https://img.shields.io/docker/v/bettercap/bettercap?logo=docker"></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
bettercap is a powerful, easily extensible and portable framework written in Go which aims to offer to security researchers, red teamers and reverse engineers an **easy to use**, **all-in-one solution** with all the features they might possibly need for performing reconnaissance and attacking [WiFi](https://www.bettercap.org/modules/wifi/) networks, [Bluetooth Low Energy](https://www.bettercap.org/modules/ble/) devices, wireless [HID](https://www.bettercap.org/modules/hid/) devices and [Ethernet](https://www.bettercap.org/modules/ethernet) networks.
|
||||
bettercap is a powerful, easily extensible and portable framework written in Go which aims to offer to security researchers, red teamers and reverse engineers an **easy to use**, **all-in-one solution** with all the features they might possibly need for performing reconnaissance and attacking [WiFi](https://www.bettercap.org/modules/wifi/) networks, [Bluetooth Low Energy](https://www.bettercap.org/modules/ble/) devices, [CAN-bus](https://www.bettercap.org/modules/canbus/), wireless [HID](https://www.bettercap.org/modules/hid/) devices and [Ethernet](https://www.bettercap.org/modules/ethernet) networks.
|
||||
|
||||

|
||||
|
||||
## Main Features
|
||||
|
||||
* **WiFi** networks scanning, [deauthentication attack](https://www.evilsocket.net/2018/07/28/Project-PITA-Writeup-build-a-mini-mass-deauther-using-bettercap-and-a-Raspberry-Pi-Zero-W/), [clientless PMKID association attack](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) and automatic WPA/WPA2 client handshakes capture.
|
||||
* **WiFi** networks scanning, [deauthentication attack](https://www.evilsocket.net/2018/07/28/Project-PITA-Writeup-build-a-mini-mass-deauther-using-bettercap-and-a-Raspberry-Pi-Zero-W/), [clientless PMKID association attack](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) and automatic WPA/WPA2/WPA3 client handshakes capture.
|
||||
* **Bluetooth Low Energy** devices scanning, characteristics enumeration, reading and writing.
|
||||
* 2.4Ghz wireless devices scanning and **MouseJacking** attacks with over-the-air HID frames injection (with DuckyScript support).
|
||||
* **CAN-bus** and **DBC** support for decoding, injecting and fuzzing frames.
|
||||
* Passive and active IP network hosts probing and recon.
|
||||
* **ARP, DNS and DHCPv6 spoofers** for MITM attacks on IP based networks.
|
||||
* **ARP, DNS, NDP and DHCPv6 spoofers** for MITM attacks on IPv4 and IPv6 based networks.
|
||||
* **Proxies at packet level, TCP level and HTTP/HTTPS** application level fully scriptable with easy to implement **javascript plugins**.
|
||||
* A powerful **network sniffer** for **credentials harvesting** which can also be used as a **network protocol fuzzer**.
|
||||
* A very fast port scanner.
|
||||
|
|
@ -25,25 +38,15 @@ bettercap is a powerful, easily extensible and portable framework written in Go
|
|||
* **A very convenient [web UI](https://www.bettercap.org/usage/#web-ui).**
|
||||
* [More!](https://www.bettercap.org/modules/)
|
||||
|
||||
## About the 1.x Legacy Version
|
||||
## Contributors
|
||||
|
||||
While the first version (up to 1.6.2) of bettercap was implemented in Ruby and only offered basic MITM, sniffing and proxying capabilities, the 2.x is a complete reimplementation using the [Go programming language](https://golang.org/).
|
||||
|
||||
This ground-up rewrite offered several advantages:
|
||||
|
||||
* bettercap can now be distributed as a **single binary** with very few dependencies, for basically **any OS and any architecture**.
|
||||
* 1.x proxies, although highly optimized and event based, **[used to bottleneck the entire network](https://en.wikipedia.org/wiki/Global_interpreter_lock)** when performing a MITM attack, while the new version adds almost no overhead.
|
||||
* Due to such **performance and functional limitations**, most of the features that the 2.x version is offering were simply impossible to implement properly (read as: without killing the entire network ... or your computer).
|
||||
|
||||
For this reason, **any version prior to 2.x is considered deprecated** and any type of support has been dropped in favor of the new implementation. An archived copy of the legacy documentation is [available here](https://www.bettercap.org/legacy/), however **it is strongly suggested to upgrade**.
|
||||
|
||||
## Documentation and Examples
|
||||
|
||||
The project is documented [here](https://www.bettercap.org/).
|
||||
<a href="https://github.com/bettercap/bettercap/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bettercap/bettercap" alt="bettercap project contributors" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
`bettercap` is made with ♥ by [the dev team](https://github.com/orgs/bettercap/people) and it's released under the GPL 3 license.
|
||||
`bettercap` is made with ♥ and released under the GPL 3 license.
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
|
|
|
|||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Feature updates and security fixes are streamlined only to the latest version, make sure to check [the release page](https://github.com/bettercap/bettercap/releases) periodically.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
For non critical bugs and vulnerabilities feel free to open an issue and tag `@evilsocket`, for more severe reports send an email to `evilsocket AT gmail DOT com`.
|
||||
141
build.sh
141
build.sh
|
|
@ -1,141 +0,0 @@
|
|||
#!/bin/bash
|
||||
BUILD_FOLDER=build
|
||||
VERSION=$(cat core/banner.go | grep Version | cut -d '"' -f 2)
|
||||
|
||||
bin_dep() {
|
||||
BIN=$1
|
||||
which $BIN > /dev/null || { echo "@ Dependency $BIN not found !"; exit 1; }
|
||||
}
|
||||
|
||||
host_dep() {
|
||||
HOST=$1
|
||||
ping -c 1 $HOST > /dev/null || { echo "@ Virtual machine host $HOST not visible !"; exit 1; }
|
||||
}
|
||||
|
||||
create_exe_archive() {
|
||||
bin_dep 'zip'
|
||||
|
||||
OUTPUT=$1
|
||||
|
||||
echo "@ Creating archive $OUTPUT ..."
|
||||
zip -j "$OUTPUT" bettercap.exe ../README.md ../LICENSE.md > /dev/null
|
||||
rm -rf bettercap bettercap.exe
|
||||
}
|
||||
|
||||
create_archive() {
|
||||
bin_dep 'zip'
|
||||
|
||||
OUTPUT=$1
|
||||
|
||||
echo "@ Creating archive $OUTPUT ..."
|
||||
zip -j "$OUTPUT" bettercap ../README.md ../LICENSE.md > /dev/null
|
||||
rm -rf bettercap bettercap.exe
|
||||
}
|
||||
|
||||
build_linux_amd64() {
|
||||
echo "@ Building linux/amd64 ..."
|
||||
go build -o bettercap ..
|
||||
}
|
||||
|
||||
|
||||
build_linux_armv6l() {
|
||||
host_dep 'arc.local'
|
||||
|
||||
DIR=/home/pi/gocode/src/github.com/bettercap/bettercap
|
||||
|
||||
echo "@ Updating repo on arm6l host ..."
|
||||
ssh pi@arc.local "cd $DIR && rm -rf '$OUTPUT' && git checkout . && git checkout master && git pull" > /dev/null
|
||||
|
||||
echo "@ Building linux/armv6l ..."
|
||||
ssh pi@arc.local "export GOPATH=/home/pi/gocode && cd '$DIR' && PATH=$PATH:/usr/local/bin && go get ./... && go build -o bettercap ." > /dev/null
|
||||
|
||||
scp -C pi@arc.local:$DIR/bettercap . > /dev/null
|
||||
}
|
||||
|
||||
build_macos_amd64() {
|
||||
host_dep 'osxvm'
|
||||
|
||||
DIR=/Users/evilsocket/gocode/src/github.com/bettercap/bettercap
|
||||
|
||||
echo "@ Updating repo on MacOS VM ..."
|
||||
ssh osxvm "cd $DIR && rm -rf '$OUTPUT' && git checkout . && git checkout master && git pull" > /dev/null
|
||||
|
||||
echo "@ Building darwin/amd64 ..."
|
||||
ssh osxvm "export GOPATH=/Users/evilsocket/gocode && cd '$DIR' && PATH=$PATH:/usr/local/bin && go get ./... && go build -o bettercap ." > /dev/null
|
||||
|
||||
scp -C osxvm:$DIR/bettercap . > /dev/null
|
||||
}
|
||||
|
||||
build_windows_amd64() {
|
||||
host_dep 'winvm'
|
||||
|
||||
DIR=c:/Users/evilsocket/gopath/src/github.com/bettercap/bettercap
|
||||
|
||||
echo "@ Updating repo on Windows VM ..."
|
||||
ssh winvm "cd $DIR && git checkout . && git checkout master && git pull && go get ./..." > /dev/null
|
||||
|
||||
echo "@ Building windows/amd64 ..."
|
||||
ssh winvm "cd $DIR && go build -o bettercap.exe ." > /dev/null
|
||||
|
||||
scp -C winvm:$DIR/bettercap.exe . > /dev/null
|
||||
}
|
||||
|
||||
build_android_arm() {
|
||||
host_dep 'shield'
|
||||
|
||||
BASE=/data/data/com.termux/files
|
||||
THEPATH="$BASE/usr/bin:$BASE/usr/bin/applets:/system/xbin:/system/bin"
|
||||
LPATH="$BASE/usr/lib"
|
||||
GPATH=$BASE/home/go
|
||||
DIR=$GPATH/src/github.com/bettercap/bettercap
|
||||
|
||||
echo "@ Updating repo on Android host ..."
|
||||
ssh -p 8022 root@shield "su -c 'export PATH=$THEPATH && export LD_LIBRARY_PATH="$LPATH" && cd "$DIR" && rm -rf bettercap* && git pull && export GOPATH=$GPATH && go get ./...'"
|
||||
|
||||
echo "@ Building android/arm ..."
|
||||
ssh -p 8022 root@shield "su -c 'export PATH=$THEPATH && export LD_LIBRARY_PATH="$LPATH" && cd "$DIR" && export GOPATH=$GPATH && go build -o bettercap . && setenforce 0'"
|
||||
|
||||
echo "@ Downloading bettercap ..."
|
||||
scp -C -P 8022 root@shield:$DIR/bettercap .
|
||||
}
|
||||
|
||||
rm -rf $BUILD_FOLDER
|
||||
mkdir $BUILD_FOLDER
|
||||
cd $BUILD_FOLDER
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
WHAT=all
|
||||
else
|
||||
WHAT="$1"
|
||||
fi
|
||||
|
||||
printf "@ Building for $WHAT ...\n\n"
|
||||
|
||||
if [[ "$WHAT" == "all" || "$WHAT" == "linux_amd64" ]]; then
|
||||
build_linux_amd64 && create_archive bettercap_linux_amd64_$VERSION.zip
|
||||
fi
|
||||
|
||||
if [[ "$WHAT" == "all" || "$WHAT" == "linux_armv6l" ]]; then
|
||||
build_linux_armv6l && create_archive bettercap_linux_armv6l_$VERSION.zip
|
||||
fi
|
||||
|
||||
if [[ "$WHAT" == "all" || "$WHAT" == "osx" || "$WHAT" == "mac" || "$WHAT" == "macos" ]]; then
|
||||
build_macos_amd64 && create_archive bettercap_macos_amd64_$VERSION.zip
|
||||
fi
|
||||
|
||||
if [[ "$WHAT" == "all" || "$WHAT" == "win" || "$WHAT" == "windows" ]]; then
|
||||
build_windows_amd64 && create_exe_archive bettercap_windows_amd64_$VERSION.zip
|
||||
fi
|
||||
|
||||
if [[ "$WHAT" == "all" || "$WHAT" == "android" ]]; then
|
||||
build_android_arm && create_archive bettercap_android_armv7l_$VERSION.zip
|
||||
fi
|
||||
|
||||
sha256sum * > checksums.txt
|
||||
|
||||
echo
|
||||
echo
|
||||
du -sh *
|
||||
|
||||
cd --
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
PROGRAM="${1}"
|
||||
shift
|
||||
COMMAND="${*}"
|
||||
|
||||
IMAGE="https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2019-07-12/2019-07-10-raspbian-buster-lite.zip"
|
||||
GOLANG="https://dl.google.com/go/go1.13.1.linux-armv6l.tar.gz"
|
||||
|
||||
REPO_DIR="${PWD}"
|
||||
TMP_DIR="/tmp/builder"
|
||||
MNT_DIR="${TMP_DIR}/mnt"
|
||||
|
||||
if ! systemctl is-active systemd-binfmt.service >/dev/null 2>&1; then
|
||||
mkdir -p "/lib/binfmt.d"
|
||||
echo ':qemu-arm:M::\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff:/usr/bin/qemu-arm-static:F' > /lib/binfmt.d/qemu-arm-static.conf
|
||||
systemctl restart systemd-binfmt.service
|
||||
fi
|
||||
|
||||
mkdir -p "${TMP_DIR}"
|
||||
wget --show-progress -qcO "${TMP_DIR}/raspbian.zip" "${IMAGE}"
|
||||
gunzip -c "${TMP_DIR}/raspbian.zip" > "${TMP_DIR}/raspbian.img"
|
||||
truncate "${TMP_DIR}/raspbian.img" --size=+2G
|
||||
parted --script "${TMP_DIR}/raspbian.img" resizepart 2 100%
|
||||
|
||||
LOOP_PATH="$(losetup --find --partscan --show "${TMP_DIR}/raspbian.img")"
|
||||
e2fsck -y -f "${LOOP_PATH}p2"
|
||||
resize2fs "${LOOP_PATH}p2"
|
||||
partprobe "${LOOP_PATH}"
|
||||
|
||||
mkdir -p "${MNT_DIR}"
|
||||
mountpoint -q "${MNT_DIR}" && umount -R "${MNT_DIR}"
|
||||
mount -o rw "${LOOP_PATH}p2" "${MNT_DIR}"
|
||||
mount -o rw "${LOOP_PATH}p1" "${MNT_DIR}/boot"
|
||||
|
||||
mount --bind /dev "${MNT_DIR}/dev/"
|
||||
mount --bind /sys "${MNT_DIR}/sys/"
|
||||
mount --bind /proc "${MNT_DIR}/proc/"
|
||||
mount --bind /dev/pts "${MNT_DIR}/dev/pts"
|
||||
mount | grep "${MNT_DIR}"
|
||||
df -h
|
||||
|
||||
cp /usr/bin/qemu-arm-static "${MNT_DIR}/usr/bin"
|
||||
cp /etc/resolv.conf "${MNT_DIR}/etc/resolv.conf"
|
||||
|
||||
mkdir -p "${MNT_DIR}/root/src/${PROGRAM}"
|
||||
mount --bind "${REPO_DIR}" "${MNT_DIR}/root/src/${PROGRAM}"
|
||||
|
||||
cp "${MNT_DIR}/etc/ld.so.preload" "${MNT_DIR}/etc/_ld.so.preload"
|
||||
touch "${MNT_DIR}/etc/ld.so.preload"
|
||||
|
||||
chroot "${MNT_DIR}" bin/bash -x <<EOF
|
||||
set -eu
|
||||
|
||||
export LANG="C"
|
||||
export LC_ALL="C"
|
||||
export LC_CTYPE="C"
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/bin"
|
||||
|
||||
wget --show-progress -qcO /tmp/golang.tar.gz "${GOLANG}"
|
||||
tar -C /usr/local -xzf /tmp/golang.tar.gz
|
||||
export GOROOT="/usr/local/go"
|
||||
export GOPATH="/root"
|
||||
|
||||
apt-get -y update
|
||||
apt-get install wget libpcap-dev libusb-1.0-0-dev libnetfilter-queue-dev build-essential git
|
||||
|
||||
cd "/root/src/${PROGRAM}"
|
||||
${COMMAND}
|
||||
EOF
|
||||
echo "Build finished"
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
prefix=c:/libusb
|
||||
exec_prefix=${prefix}
|
||||
libdir=${prefix}/MinGW64/static
|
||||
includedir=${prefix}/include
|
||||
|
||||
Name: libusb-1.0
|
||||
Description: C API for USB device access from Linux, Mac OS X, Windows, OpenBSD/NetBSD and Solaris userspace
|
||||
Version: 1.0.18
|
||||
Libs: -L${libdir} -lusb-1.0
|
||||
Cflags: -I${includedir}/libusb-1.0
|
||||
378
caplets/caplet_test.go
Normal file
378
caplets/caplet_test.go
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
package caplets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewCaplet(t *testing.T) {
|
||||
name := "test-caplet"
|
||||
path := "/path/to/caplet.cap"
|
||||
size := int64(1024)
|
||||
|
||||
cap := NewCaplet(name, path, size)
|
||||
|
||||
if cap.Name != name {
|
||||
t.Errorf("expected name %s, got %s", name, cap.Name)
|
||||
}
|
||||
if cap.Path != path {
|
||||
t.Errorf("expected path %s, got %s", path, cap.Path)
|
||||
}
|
||||
if cap.Size != size {
|
||||
t.Errorf("expected size %d, got %d", size, cap.Size)
|
||||
}
|
||||
if cap.Code == nil {
|
||||
t.Error("Code should not be nil")
|
||||
}
|
||||
if cap.Scripts == nil {
|
||||
t.Error("Scripts should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapletEval(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code []string
|
||||
argv []string
|
||||
wantLines []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty code",
|
||||
code: []string{},
|
||||
argv: nil,
|
||||
wantLines: []string{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "skip comments and empty lines",
|
||||
code: []string{
|
||||
"# this is a comment",
|
||||
"",
|
||||
"set test value",
|
||||
"# another comment",
|
||||
"set another value",
|
||||
},
|
||||
argv: nil,
|
||||
wantLines: []string{
|
||||
"set test value",
|
||||
"set another value",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "variable substitution",
|
||||
code: []string{
|
||||
"set param $0",
|
||||
"set value $1",
|
||||
"run $0 $1 $2",
|
||||
},
|
||||
argv: []string{"arg0", "arg1", "arg2"},
|
||||
wantLines: []string{
|
||||
"set param arg0",
|
||||
"set value arg1",
|
||||
"run arg0 arg1 arg2",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "multiple occurrences of same variable",
|
||||
code: []string{
|
||||
"$0 $0 $1 $0",
|
||||
},
|
||||
argv: []string{"foo", "bar"},
|
||||
wantLines: []string{
|
||||
"foo foo bar foo",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing argv values",
|
||||
code: []string{
|
||||
"set $0 $1 $2",
|
||||
},
|
||||
argv: []string{"only_one"},
|
||||
wantLines: []string{
|
||||
"set only_one $1 $2",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempFile, err := ioutil.TempFile("", "test-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
cap := NewCaplet("test", tempFile.Name(), 100)
|
||||
cap.Code = tt.code
|
||||
|
||||
var gotLines []string
|
||||
err = cap.Eval(tt.argv, func(line string) error {
|
||||
gotLines = append(gotLines, line)
|
||||
return nil
|
||||
})
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Eval() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if len(gotLines) != len(tt.wantLines) {
|
||||
t.Errorf("got %d lines, want %d", len(gotLines), len(tt.wantLines))
|
||||
return
|
||||
}
|
||||
|
||||
for i, line := range gotLines {
|
||||
if line != tt.wantLines[i] {
|
||||
t.Errorf("line %d: got %q, want %q", i, line, tt.wantLines[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapletEvalError(t *testing.T) {
|
||||
tempFile, err := ioutil.TempFile("", "test-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
cap := NewCaplet("test", tempFile.Name(), 100)
|
||||
cap.Code = []string{
|
||||
"first line",
|
||||
"error line",
|
||||
"third line",
|
||||
}
|
||||
|
||||
expectedErr := errors.New("test error")
|
||||
var executedLines []string
|
||||
|
||||
err = cap.Eval(nil, func(line string) error {
|
||||
executedLines = append(executedLines, line)
|
||||
if line == "error line" {
|
||||
return expectedErr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != expectedErr {
|
||||
t.Errorf("expected error %v, got %v", expectedErr, err)
|
||||
}
|
||||
|
||||
// Should have executed first two lines before error
|
||||
if len(executedLines) != 2 {
|
||||
t.Errorf("expected 2 executed lines, got %d", len(executedLines))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapletEvalWithChdirPath(t *testing.T) {
|
||||
// Create a temporary caplet file to test with
|
||||
tempFile, err := ioutil.TempFile("", "test-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
cap := NewCaplet("test", tempFile.Name(), 100)
|
||||
cap.Code = []string{"test command"}
|
||||
|
||||
executed := false
|
||||
err = cap.Eval(nil, func(line string) error {
|
||||
executed = true
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !executed {
|
||||
t.Error("callback was not executed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewScript(t *testing.T) {
|
||||
path := "/path/to/script.js"
|
||||
size := int64(2048)
|
||||
|
||||
script := newScript(path, size)
|
||||
|
||||
if script.Path != path {
|
||||
t.Errorf("expected path %s, got %s", path, script.Path)
|
||||
}
|
||||
if script.Size != size {
|
||||
t.Errorf("expected size %d, got %d", size, script.Size)
|
||||
}
|
||||
if script.Code == nil {
|
||||
t.Error("Code should not be nil")
|
||||
}
|
||||
if len(script.Code) != 0 {
|
||||
t.Errorf("expected empty Code slice, got %d elements", len(script.Code))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapletEvalCommentAtStartOfLine(t *testing.T) {
|
||||
tempFile, err := ioutil.TempFile("", "test-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
cap := NewCaplet("test", tempFile.Name(), 100)
|
||||
cap.Code = []string{
|
||||
"# comment",
|
||||
" # not a comment (has space before #)",
|
||||
" # not a comment (has tab before #)",
|
||||
"command # inline comment",
|
||||
}
|
||||
|
||||
var gotLines []string
|
||||
err = cap.Eval(nil, func(line string) error {
|
||||
gotLines = append(gotLines, line)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedLines := []string{
|
||||
" # not a comment (has space before #)",
|
||||
" # not a comment (has tab before #)",
|
||||
"command # inline comment",
|
||||
}
|
||||
|
||||
if len(gotLines) != len(expectedLines) {
|
||||
t.Errorf("got %d lines, want %d", len(gotLines), len(expectedLines))
|
||||
return
|
||||
}
|
||||
|
||||
for i, line := range gotLines {
|
||||
if line != expectedLines[i] {
|
||||
t.Errorf("line %d: got %q, want %q", i, line, expectedLines[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapletEvalArgvSubstitutionEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
code string
|
||||
argv []string
|
||||
wantLine string
|
||||
}{
|
||||
{
|
||||
name: "double digit substitution $10",
|
||||
code: "$1$0",
|
||||
argv: []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"},
|
||||
wantLine: "10",
|
||||
},
|
||||
{
|
||||
name: "no space between variables",
|
||||
code: "$0$1$2",
|
||||
argv: []string{"a", "b", "c"},
|
||||
wantLine: "abc",
|
||||
},
|
||||
{
|
||||
name: "variables in quotes",
|
||||
code: `"$0" '$1'`,
|
||||
argv: []string{"foo", "bar"},
|
||||
wantLine: `"foo" 'bar'`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempFile, err := ioutil.TempFile("", "test-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
cap := NewCaplet("test", tempFile.Name(), 100)
|
||||
cap.Code = []string{tt.code}
|
||||
|
||||
var gotLine string
|
||||
err = cap.Eval(tt.argv, func(line string) error {
|
||||
gotLine = line
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if gotLine != tt.wantLine {
|
||||
t.Errorf("got line %q, want %q", gotLine, tt.wantLine)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapletStructFields(t *testing.T) {
|
||||
// Test that Caplet properly embeds Script
|
||||
tempFile, err := ioutil.TempFile("", "test-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
tempFile.Close()
|
||||
|
||||
cap := NewCaplet("test", tempFile.Name(), 100)
|
||||
|
||||
// These fields should be accessible due to embedding
|
||||
_ = cap.Path
|
||||
_ = cap.Size
|
||||
_ = cap.Code
|
||||
|
||||
// And these are Caplet's own fields
|
||||
_ = cap.Name
|
||||
_ = cap.Scripts
|
||||
}
|
||||
|
||||
func BenchmarkCapletEval(b *testing.B) {
|
||||
cap := NewCaplet("bench", "/tmp/bench.cap", 100)
|
||||
cap.Code = []string{
|
||||
"set param1 $0",
|
||||
"set param2 $1",
|
||||
"# comment line",
|
||||
"",
|
||||
"run command $0 $1 $2",
|
||||
"another command",
|
||||
}
|
||||
argv := []string{"arg0", "arg1", "arg2"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = cap.Eval(argv, func(line string) error {
|
||||
// Do nothing, just measure evaluation overhead
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVariableSubstitution(b *testing.B) {
|
||||
line := "command $0 $1 $2 $3 $4 $5 $6 $7 $8 $9"
|
||||
argv := []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := line
|
||||
for j, arg := range argv {
|
||||
what := "$" + string(rune('0'+j))
|
||||
result = strings.Replace(result, what, arg, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ const (
|
|||
InstallArchive = "https://github.com/bettercap/caplets/archive/master.zip"
|
||||
)
|
||||
|
||||
func getInstallBase() string {
|
||||
func getDefaultInstallBase() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("ALLUSERSPROFILE"), "bettercap")
|
||||
}
|
||||
|
|
@ -28,10 +28,18 @@ func getUserHomeDir() string {
|
|||
}
|
||||
|
||||
var (
|
||||
InstallBase = getInstallBase()
|
||||
InstallBase = ""
|
||||
InstallPathArchive = ""
|
||||
InstallPath = ""
|
||||
ArchivePath = ""
|
||||
LoadPaths = []string(nil)
|
||||
)
|
||||
|
||||
func Setup(base string) error {
|
||||
InstallBase = base
|
||||
InstallPathArchive = filepath.Join(InstallBase, "caplets-master")
|
||||
InstallPath = filepath.Join(InstallBase, "caplets")
|
||||
ArchivePath = filepath.Join(os.TempDir(), "caplets.zip")
|
||||
InstallPath = filepath.Join(InstallBase, "caplets")
|
||||
ArchivePath = filepath.Join(os.TempDir(), "caplets.zip")
|
||||
|
||||
LoadPaths = []string{
|
||||
"./",
|
||||
|
|
@ -39,9 +47,7 @@ var (
|
|||
InstallPath,
|
||||
filepath.Join(getUserHomeDir(), "caplets"),
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, path := range str.SplitBy(str.Trim(os.Getenv(EnvVarName)), string(os.PathListSeparator)) {
|
||||
if path = str.Trim(path); len(path) > 0 {
|
||||
LoadPaths = append(LoadPaths, path)
|
||||
|
|
@ -51,4 +57,11 @@ func init() {
|
|||
for i, path := range LoadPaths {
|
||||
LoadPaths[i], _ = filepath.Abs(path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// init with defaults
|
||||
Setup(getDefaultInstallBase())
|
||||
}
|
||||
|
|
|
|||
308
caplets/env_test.go
Normal file
308
caplets/env_test.go
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
package caplets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDefaultInstallBase(t *testing.T) {
|
||||
base := getDefaultInstallBase()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
expected := filepath.Join(os.Getenv("ALLUSERSPROFILE"), "bettercap")
|
||||
if base != expected {
|
||||
t.Errorf("on windows, expected %s, got %s", expected, base)
|
||||
}
|
||||
} else {
|
||||
expected := "/usr/local/share/bettercap/"
|
||||
if base != expected {
|
||||
t.Errorf("on non-windows, expected %s, got %s", expected, base)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserHomeDir(t *testing.T) {
|
||||
home := getUserHomeDir()
|
||||
|
||||
// Should return a non-empty string
|
||||
if home == "" {
|
||||
t.Error("getUserHomeDir returned empty string")
|
||||
}
|
||||
|
||||
// Should be an absolute path
|
||||
if !filepath.IsAbs(home) {
|
||||
t.Errorf("expected absolute path, got %s", home)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
// Save original values
|
||||
origInstallBase := InstallBase
|
||||
origInstallPathArchive := InstallPathArchive
|
||||
origInstallPath := InstallPath
|
||||
origArchivePath := ArchivePath
|
||||
origLoadPaths := LoadPaths
|
||||
|
||||
// Test with custom base
|
||||
testBase := "/custom/base"
|
||||
err := Setup(testBase)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Setup returned error: %v", err)
|
||||
}
|
||||
|
||||
// Check that paths are set correctly
|
||||
if InstallBase != testBase {
|
||||
t.Errorf("expected InstallBase %s, got %s", testBase, InstallBase)
|
||||
}
|
||||
|
||||
expectedArchivePath := filepath.Join(testBase, "caplets-master")
|
||||
if InstallPathArchive != expectedArchivePath {
|
||||
t.Errorf("expected InstallPathArchive %s, got %s", expectedArchivePath, InstallPathArchive)
|
||||
}
|
||||
|
||||
expectedInstallPath := filepath.Join(testBase, "caplets")
|
||||
if InstallPath != expectedInstallPath {
|
||||
t.Errorf("expected InstallPath %s, got %s", expectedInstallPath, InstallPath)
|
||||
}
|
||||
|
||||
expectedTempPath := filepath.Join(os.TempDir(), "caplets.zip")
|
||||
if ArchivePath != expectedTempPath {
|
||||
t.Errorf("expected ArchivePath %s, got %s", expectedTempPath, ArchivePath)
|
||||
}
|
||||
|
||||
// Check LoadPaths contains expected paths
|
||||
expectedInLoadPaths := []string{
|
||||
"./",
|
||||
"./caplets/",
|
||||
InstallPath,
|
||||
filepath.Join(getUserHomeDir(), "caplets"),
|
||||
}
|
||||
|
||||
for _, expected := range expectedInLoadPaths {
|
||||
absExpected, _ := filepath.Abs(expected)
|
||||
found := false
|
||||
for _, path := range LoadPaths {
|
||||
if path == absExpected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected path %s not found in LoadPaths", absExpected)
|
||||
}
|
||||
}
|
||||
|
||||
// All paths should be absolute
|
||||
for _, path := range LoadPaths {
|
||||
if !filepath.IsAbs(path) {
|
||||
t.Errorf("LoadPath %s is not absolute", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
InstallBase = origInstallBase
|
||||
InstallPathArchive = origInstallPathArchive
|
||||
InstallPath = origInstallPath
|
||||
ArchivePath = origArchivePath
|
||||
LoadPaths = origLoadPaths
|
||||
}
|
||||
|
||||
func TestSetupWithEnvironmentVariable(t *testing.T) {
|
||||
// Save original values
|
||||
origEnv := os.Getenv(EnvVarName)
|
||||
origLoadPaths := LoadPaths
|
||||
|
||||
// Set environment variable with multiple paths
|
||||
testPaths := []string{"/path1", "/path2", "/path3"}
|
||||
os.Setenv(EnvVarName, strings.Join(testPaths, string(os.PathListSeparator)))
|
||||
|
||||
// Run setup
|
||||
err := Setup("/test/base")
|
||||
if err != nil {
|
||||
t.Errorf("Setup returned error: %v", err)
|
||||
}
|
||||
|
||||
// Check that custom paths from env var are in LoadPaths
|
||||
for _, testPath := range testPaths {
|
||||
absTestPath, _ := filepath.Abs(testPath)
|
||||
found := false
|
||||
for _, path := range LoadPaths {
|
||||
if path == absTestPath {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected env path %s not found in LoadPaths", absTestPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
if origEnv == "" {
|
||||
os.Unsetenv(EnvVarName)
|
||||
} else {
|
||||
os.Setenv(EnvVarName, origEnv)
|
||||
}
|
||||
LoadPaths = origLoadPaths
|
||||
}
|
||||
|
||||
func TestSetupWithEmptyEnvironmentVariable(t *testing.T) {
|
||||
// Save original values
|
||||
origEnv := os.Getenv(EnvVarName)
|
||||
origLoadPaths := LoadPaths
|
||||
|
||||
// Set empty environment variable
|
||||
os.Setenv(EnvVarName, "")
|
||||
|
||||
// Count LoadPaths before setup
|
||||
err := Setup("/test/base")
|
||||
if err != nil {
|
||||
t.Errorf("Setup returned error: %v", err)
|
||||
}
|
||||
|
||||
// Should have only the default paths (4)
|
||||
if len(LoadPaths) != 4 {
|
||||
t.Errorf("expected 4 default LoadPaths, got %d", len(LoadPaths))
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
if origEnv == "" {
|
||||
os.Unsetenv(EnvVarName)
|
||||
} else {
|
||||
os.Setenv(EnvVarName, origEnv)
|
||||
}
|
||||
LoadPaths = origLoadPaths
|
||||
}
|
||||
|
||||
func TestSetupWithWhitespaceInEnvironmentVariable(t *testing.T) {
|
||||
// Save original values
|
||||
origEnv := os.Getenv(EnvVarName)
|
||||
origLoadPaths := LoadPaths
|
||||
|
||||
// Set environment variable with whitespace
|
||||
testPaths := []string{" /path1 ", " ", "/path2 "}
|
||||
os.Setenv(EnvVarName, strings.Join(testPaths, string(os.PathListSeparator)))
|
||||
|
||||
// Run setup
|
||||
err := Setup("/test/base")
|
||||
if err != nil {
|
||||
t.Errorf("Setup returned error: %v", err)
|
||||
}
|
||||
|
||||
// Should have added only non-empty paths after trimming
|
||||
expectedPaths := []string{"/path1", "/path2"}
|
||||
foundCount := 0
|
||||
for _, expectedPath := range expectedPaths {
|
||||
absExpected, _ := filepath.Abs(expectedPath)
|
||||
for _, path := range LoadPaths {
|
||||
if path == absExpected {
|
||||
foundCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if foundCount != len(expectedPaths) {
|
||||
t.Errorf("expected to find %d paths from env, found %d", len(expectedPaths), foundCount)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
if origEnv == "" {
|
||||
os.Unsetenv(EnvVarName)
|
||||
} else {
|
||||
os.Setenv(EnvVarName, origEnv)
|
||||
}
|
||||
LoadPaths = origLoadPaths
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
// Test that constants have expected values
|
||||
if EnvVarName != "CAPSPATH" {
|
||||
t.Errorf("expected EnvVarName to be 'CAPSPATH', got %s", EnvVarName)
|
||||
}
|
||||
|
||||
if Suffix != ".cap" {
|
||||
t.Errorf("expected Suffix to be '.cap', got %s", Suffix)
|
||||
}
|
||||
|
||||
if InstallArchive != "https://github.com/bettercap/caplets/archive/master.zip" {
|
||||
t.Errorf("unexpected InstallArchive value: %s", InstallArchive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
// The init function should have been called already
|
||||
// Check that paths are initialized
|
||||
if InstallBase == "" {
|
||||
t.Error("InstallBase not initialized")
|
||||
}
|
||||
|
||||
if InstallPath == "" {
|
||||
t.Error("InstallPath not initialized")
|
||||
}
|
||||
|
||||
if InstallPathArchive == "" {
|
||||
t.Error("InstallPathArchive not initialized")
|
||||
}
|
||||
|
||||
if ArchivePath == "" {
|
||||
t.Error("ArchivePath not initialized")
|
||||
}
|
||||
|
||||
if LoadPaths == nil || len(LoadPaths) == 0 {
|
||||
t.Error("LoadPaths not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupMultipleTimes(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
|
||||
// Setup multiple times with different bases
|
||||
bases := []string{"/base1", "/base2", "/base3"}
|
||||
|
||||
for _, base := range bases {
|
||||
err := Setup(base)
|
||||
if err != nil {
|
||||
t.Errorf("Setup(%s) returned error: %v", base, err)
|
||||
}
|
||||
|
||||
// Check that InstallBase is updated
|
||||
if InstallBase != base {
|
||||
t.Errorf("expected InstallBase %s, got %s", base, InstallBase)
|
||||
}
|
||||
|
||||
// LoadPaths should be recreated each time
|
||||
if len(LoadPaths) < 4 {
|
||||
t.Errorf("LoadPaths should have at least 4 entries, got %d", len(LoadPaths))
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
}
|
||||
|
||||
func BenchmarkSetup(b *testing.B) {
|
||||
// Save original values
|
||||
origEnv := os.Getenv(EnvVarName)
|
||||
|
||||
// Set a complex environment
|
||||
paths := []string{"/p1", "/p2", "/p3", "/p4", "/p5"}
|
||||
os.Setenv(EnvVarName, strings.Join(paths, string(os.PathListSeparator)))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Setup("/benchmark/base")
|
||||
}
|
||||
|
||||
// Restore
|
||||
if origEnv == "" {
|
||||
os.Unsetenv(EnvVarName)
|
||||
} else {
|
||||
os.Setenv(EnvVarName, origEnv)
|
||||
}
|
||||
}
|
||||
511
caplets/manager_test.go
Normal file
511
caplets/manager_test.go
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
package caplets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createTestCaplet(t testing.TB, dir string, name string, content []string) string {
|
||||
filename := filepath.Join(dir, name)
|
||||
data := strings.Join(content, "\n")
|
||||
err := ioutil.WriteFile(filename, []byte(data), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test caplet: %v", err)
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directories
|
||||
tempDir, err := ioutil.TempDir("", "caplets-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create subdirectories
|
||||
dir1 := filepath.Join(tempDir, "dir1")
|
||||
dir2 := filepath.Join(tempDir, "dir2")
|
||||
subdir := filepath.Join(dir1, "subdir")
|
||||
|
||||
os.Mkdir(dir1, 0755)
|
||||
os.Mkdir(dir2, 0755)
|
||||
os.Mkdir(subdir, 0755)
|
||||
|
||||
// Create test caplets
|
||||
createTestCaplet(t, dir1, "test1.cap", []string{"# Test caplet 1", "set test 1"})
|
||||
createTestCaplet(t, dir1, "test2.cap", []string{"# Test caplet 2", "set test 2"})
|
||||
createTestCaplet(t, dir2, "test3.cap", []string{"# Test caplet 3", "set test 3"})
|
||||
createTestCaplet(t, subdir, "nested.cap", []string{"# Nested caplet", "set nested test"})
|
||||
|
||||
// Also create a non-caplet file
|
||||
ioutil.WriteFile(filepath.Join(dir1, "notacaplet.txt"), []byte("not a caplet"), 0644)
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{dir1, dir2}
|
||||
|
||||
// Call List()
|
||||
caplets := List()
|
||||
|
||||
// Check results
|
||||
if len(caplets) != 4 {
|
||||
t.Errorf("expected 4 caplets, got %d", len(caplets))
|
||||
}
|
||||
|
||||
// Check names (should be sorted)
|
||||
expectedNames := []string{filepath.Join("subdir", "nested"), "test1", "test2", "test3"}
|
||||
sort.Strings(expectedNames)
|
||||
|
||||
gotNames := make([]string, len(caplets))
|
||||
for i, cap := range caplets {
|
||||
gotNames[i] = cap.Name
|
||||
}
|
||||
|
||||
for i, expected := range expectedNames {
|
||||
if i >= len(gotNames) || gotNames[i] != expected {
|
||||
t.Errorf("expected caplet %d to be %s, got %s", i, expected, gotNames[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestListEmptyDirectories(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := ioutil.TempDir("", "caplets-empty-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Set LoadPaths to empty directory
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
// Call List()
|
||||
caplets := List()
|
||||
|
||||
// Should return empty list
|
||||
if len(caplets) != 0 {
|
||||
t.Errorf("expected 0 caplets, got %d", len(caplets))
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := ioutil.TempDir("", "caplets-load-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test caplet
|
||||
capletContent := []string{
|
||||
"# Test caplet",
|
||||
"set param value",
|
||||
"",
|
||||
"# Another comment",
|
||||
"run command",
|
||||
}
|
||||
createTestCaplet(t, tempDir, "test.cap", capletContent)
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
// Test loading without .cap extension
|
||||
cap, err := Load("test")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if cap == nil {
|
||||
t.Error("caplet is nil")
|
||||
} else {
|
||||
if cap.Name != "test" {
|
||||
t.Errorf("expected name 'test', got %s", cap.Name)
|
||||
}
|
||||
if len(cap.Code) != len(capletContent) {
|
||||
t.Errorf("expected %d lines, got %d", len(capletContent), len(cap.Code))
|
||||
}
|
||||
}
|
||||
|
||||
// Test loading from cache
|
||||
// Note: The Load function caches with the suffix, so we need to use the same name with suffix
|
||||
cap2, err := Load("test.cap")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error on cache hit: %v", err)
|
||||
}
|
||||
if cap2 == nil {
|
||||
t.Error("caplet is nil on cache hit")
|
||||
}
|
||||
|
||||
// Test loading with .cap extension
|
||||
// Note: Load caches by the name parameter, so "test.cap" is a different cache key
|
||||
cap3, err := Load("test.cap")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with .cap extension: %v", err)
|
||||
}
|
||||
if cap3 == nil {
|
||||
t.Error("caplet is nil")
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestLoadAbsolutePath(t *testing.T) {
|
||||
// Save original values
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp file
|
||||
tempFile, err := ioutil.TempFile("", "test-absolute-*.cap")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
// Write content
|
||||
content := "# Absolute path test\nset test absolute"
|
||||
tempFile.WriteString(content)
|
||||
tempFile.Close()
|
||||
|
||||
// Load with absolute path
|
||||
cap, err := Load(tempFile.Name())
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if cap == nil {
|
||||
t.Error("caplet is nil")
|
||||
} else {
|
||||
if cap.Path != tempFile.Name() {
|
||||
t.Errorf("expected path %s, got %s", tempFile.Name(), cap.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestLoadNotFound(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Set empty LoadPaths
|
||||
LoadPaths = []string{}
|
||||
|
||||
// Try to load non-existent caplet
|
||||
cap, err := Load("nonexistent")
|
||||
if err == nil {
|
||||
t.Error("expected error for non-existent caplet")
|
||||
}
|
||||
if cap != nil {
|
||||
t.Error("expected nil caplet for non-existent file")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
t.Errorf("expected 'not found' error, got: %v", err)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestLoadWithFolder(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directory structure
|
||||
tempDir, err := ioutil.TempDir("", "caplets-folder-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create a caplet folder
|
||||
capletDir := filepath.Join(tempDir, "mycaplet")
|
||||
os.Mkdir(capletDir, 0755)
|
||||
|
||||
// Create main caplet file
|
||||
mainContent := []string{"# Main caplet", "set main test"}
|
||||
createTestCaplet(t, capletDir, "mycaplet.cap", mainContent)
|
||||
|
||||
// Create additional files
|
||||
jsContent := []string{"// JavaScript file", "console.log('test');"}
|
||||
createTestCaplet(t, capletDir, "script.js", jsContent)
|
||||
|
||||
capContent := []string{"# Sub caplet", "set sub test"}
|
||||
createTestCaplet(t, capletDir, "sub.cap", capContent)
|
||||
|
||||
// Create a file that should be ignored
|
||||
ioutil.WriteFile(filepath.Join(capletDir, "readme.txt"), []byte("readme"), 0644)
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
// Load the caplet
|
||||
cap, err := Load("mycaplet/mycaplet")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if cap == nil {
|
||||
t.Fatal("caplet is nil")
|
||||
}
|
||||
|
||||
// Check main caplet
|
||||
if cap.Name != "mycaplet/mycaplet" {
|
||||
t.Errorf("expected name 'mycaplet/mycaplet', got %s", cap.Name)
|
||||
}
|
||||
if len(cap.Code) != len(mainContent) {
|
||||
t.Errorf("expected %d lines in main, got %d", len(mainContent), len(cap.Code))
|
||||
}
|
||||
|
||||
// Check additional scripts
|
||||
if len(cap.Scripts) != 2 {
|
||||
t.Errorf("expected 2 additional scripts, got %d", len(cap.Scripts))
|
||||
}
|
||||
|
||||
// Find and check the .js file
|
||||
foundJS := false
|
||||
foundCap := false
|
||||
for _, script := range cap.Scripts {
|
||||
if strings.HasSuffix(script.Path, "script.js") {
|
||||
foundJS = true
|
||||
if len(script.Code) != len(jsContent) {
|
||||
t.Errorf("expected %d lines in JS, got %d", len(jsContent), len(script.Code))
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(script.Path, "sub.cap") {
|
||||
foundCap = true
|
||||
if len(script.Code) != len(capContent) {
|
||||
t.Errorf("expected %d lines in sub.cap, got %d", len(capContent), len(script.Code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundJS {
|
||||
t.Error("script.js not found in Scripts")
|
||||
}
|
||||
if !foundCap {
|
||||
t.Error("sub.cap not found in Scripts")
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestCacheConcurrency(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := ioutil.TempDir("", "caplets-concurrent-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test caplets
|
||||
for i := 0; i < 5; i++ {
|
||||
name := fmt.Sprintf("test%d.cap", i)
|
||||
content := []string{fmt.Sprintf("# Test %d", i)}
|
||||
createTestCaplet(t, tempDir, name, content)
|
||||
}
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
// Run concurrent loads
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, 50)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
for j := 0; j < 5; j++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
name := fmt.Sprintf("test%d", idx)
|
||||
_, err := Load(name)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}(j)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
// Check for errors
|
||||
for err := range errors {
|
||||
t.Errorf("concurrent load error: %v", err)
|
||||
}
|
||||
|
||||
// Verify cache has all entries
|
||||
if len(cache) != 5 {
|
||||
t.Errorf("expected 5 cached entries, got %d", len(cache))
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func TestLoadPathPriority(t *testing.T) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directories
|
||||
tempDir1, _ := ioutil.TempDir("", "caplets-priority1-")
|
||||
tempDir2, _ := ioutil.TempDir("", "caplets-priority2-")
|
||||
defer os.RemoveAll(tempDir1)
|
||||
defer os.RemoveAll(tempDir2)
|
||||
|
||||
// Create same-named caplet in both directories
|
||||
createTestCaplet(t, tempDir1, "test.cap", []string{"# From dir1"})
|
||||
createTestCaplet(t, tempDir2, "test.cap", []string{"# From dir2"})
|
||||
|
||||
// Set LoadPaths with tempDir1 first
|
||||
LoadPaths = []string{tempDir1, tempDir2}
|
||||
|
||||
// Load caplet
|
||||
cap, err := Load("test")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Should load from first directory
|
||||
if cap != nil && len(cap.Code) > 0 {
|
||||
if cap.Code[0] != "# From dir1" {
|
||||
t.Error("caplet not loaded from first directory in LoadPaths")
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func BenchmarkLoad(b *testing.B) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
|
||||
// Create temp directory
|
||||
tempDir, _ := ioutil.TempDir("", "caplets-bench-")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test caplet
|
||||
content := make([]string, 100)
|
||||
for i := range content {
|
||||
content[i] = fmt.Sprintf("command %d", i)
|
||||
}
|
||||
createTestCaplet(b, tempDir, "bench.cap", content)
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Clear cache to measure loading time
|
||||
cache = make(map[string]*Caplet)
|
||||
Load("bench")
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func BenchmarkLoadFromCache(b *testing.B) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
cache = make(map[string]*Caplet)
|
||||
|
||||
// Create temp directory
|
||||
tempDir, _ := ioutil.TempDir("", "caplets-bench-cache-")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create test caplet
|
||||
createTestCaplet(b, tempDir, "bench.cap", []string{"# Benchmark"})
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
// Pre-load into cache
|
||||
Load("bench")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Load("bench")
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
||||
func BenchmarkList(b *testing.B) {
|
||||
// Save original values
|
||||
origLoadPaths := LoadPaths
|
||||
origCache := cache
|
||||
|
||||
// Create temp directory
|
||||
tempDir, _ := ioutil.TempDir("", "caplets-bench-list-")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create multiple caplets
|
||||
for i := 0; i < 20; i++ {
|
||||
name := fmt.Sprintf("test%d.cap", i)
|
||||
createTestCaplet(b, tempDir, name, []string{fmt.Sprintf("# Test %d", i)})
|
||||
}
|
||||
|
||||
// Set LoadPaths
|
||||
LoadPaths = []string{tempDir}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache = make(map[string]*Caplet)
|
||||
List()
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
LoadPaths = origLoadPaths
|
||||
cache = origCache
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package core
|
|||
|
||||
const (
|
||||
Name = "bettercap"
|
||||
Version = "2.30"
|
||||
Version = "2.41.4"
|
||||
Author = "Simone 'evilsocket' Margaritelli"
|
||||
Website = "https://bettercap.org/"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"os/exec"
|
||||
"sort"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
||||
|
|
@ -36,12 +37,15 @@ func HasBinary(executable string) bool {
|
|||
func Exec(executable string, args []string) (string, error) {
|
||||
path, err := exec.LookPath(executable)
|
||||
if err != nil {
|
||||
log.Warning("executable %s not found in $PATH", executable)
|
||||
return "", err
|
||||
}
|
||||
|
||||
raw, err := exec.Command(path, args...).CombinedOutput()
|
||||
|
||||
log.Debug("exec=%s args=%v ret_err=%v ret_out=%s", path, args, err, string(raw))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return str.Trim(string(raw)), err
|
||||
} else {
|
||||
return str.Trim(string(raw)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
||||
func hasInt(a []int, v int) bool {
|
||||
|
|
@ -81,85 +78,6 @@ func TestCoreUniqueIntsSorted(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func sameStrings(a []string, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestCoreExec(t *testing.T) {
|
||||
var units = []struct {
|
||||
exec string
|
||||
args []string
|
||||
out string
|
||||
err string
|
||||
stdout string
|
||||
}{
|
||||
{"foo", []string{}, "", `exec: "foo": executable file not found in $PATH`, ""},
|
||||
{"ps", []string{"-someinvalidflag"}, "", "exit status 1", ""},
|
||||
{"true", []string{}, "", "", ""},
|
||||
{"head", []string{"/path/to/file/that/does/not/exist"}, "", "exit status 1", ""},
|
||||
}
|
||||
|
||||
for _, u := range units {
|
||||
var buf bytes.Buffer
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
gotOut, gotErr := Exec(u.exec, u.args)
|
||||
w.Close()
|
||||
io.Copy(&buf, r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
gotStdout := str.Trim(buf.String())
|
||||
if gotOut != u.out {
|
||||
t.Fatalf("expected output '%s', got '%s'", u.out, gotOut)
|
||||
} else if u.err == "" && gotErr != nil {
|
||||
t.Fatalf("expected no error, got '%s'", gotErr)
|
||||
} else if u.err != "" && gotErr == nil {
|
||||
t.Fatalf("expected error '%s', got none", u.err)
|
||||
} else if u.err != "" && gotErr != nil && gotErr.Error() != u.err {
|
||||
t.Fatalf("expected error '%s', got '%s'", u.err, gotErr)
|
||||
} else if gotStdout != "" {
|
||||
t.Fatalf("expected empty stdout, got '%s'", gotStdout)
|
||||
}
|
||||
}
|
||||
|
||||
for _, u := range units {
|
||||
var buf bytes.Buffer
|
||||
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
gotOut, gotErr := Exec(u.exec, u.args)
|
||||
w.Close()
|
||||
io.Copy(&buf, r)
|
||||
os.Stdout = oldStdout
|
||||
|
||||
gotStdout := str.Trim(buf.String())
|
||||
if gotOut != u.out {
|
||||
t.Fatalf("expected output '%s', got '%s'", u.out, gotOut)
|
||||
} else if u.err == "" && gotErr != nil {
|
||||
t.Fatalf("expected no error, got '%s'", gotErr)
|
||||
} else if u.err != "" && gotErr == nil {
|
||||
t.Fatalf("expected error '%s', got none", u.err)
|
||||
} else if u.err != "" && gotErr != nil && gotErr.Error() != u.err {
|
||||
t.Fatalf("expected error '%s', got '%s'", u.err, gotErr)
|
||||
} else if gotStdout != u.stdout {
|
||||
t.Fatalf("expected stdout '%s', got '%s'", u.stdout, gotStdout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreExists(t *testing.T) {
|
||||
var units = []struct {
|
||||
what string
|
||||
|
|
@ -179,3 +97,144 @@ func TestCoreExists(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasBinary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
executable string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "common shell",
|
||||
executable: "sh",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "echo command",
|
||||
executable: "echo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-existent binary",
|
||||
executable: "this-binary-definitely-does-not-exist-12345",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
executable: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := HasBinary(tt.executable)
|
||||
if got != tt.expected {
|
||||
t.Errorf("HasBinary(%q) = %v, want %v", tt.executable, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExec(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
executable string
|
||||
args []string
|
||||
wantError bool
|
||||
contains string
|
||||
}{
|
||||
{
|
||||
name: "echo with args",
|
||||
executable: "echo",
|
||||
args: []string{"hello", "world"},
|
||||
wantError: false,
|
||||
contains: "hello world",
|
||||
},
|
||||
{
|
||||
name: "echo empty",
|
||||
executable: "echo",
|
||||
args: []string{},
|
||||
wantError: false,
|
||||
contains: "",
|
||||
},
|
||||
{
|
||||
name: "non-existent command",
|
||||
executable: "this-command-does-not-exist-12345",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
contains: "",
|
||||
},
|
||||
{
|
||||
name: "true command",
|
||||
executable: "true",
|
||||
args: []string{},
|
||||
wantError: false,
|
||||
contains: "",
|
||||
},
|
||||
{
|
||||
name: "false command",
|
||||
executable: "false",
|
||||
args: []string{},
|
||||
wantError: true,
|
||||
contains: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Skip platform-specific commands if not available
|
||||
if !HasBinary(tt.executable) && !tt.wantError {
|
||||
t.Skipf("%s not found in PATH", tt.executable)
|
||||
}
|
||||
|
||||
output, err := Exec(tt.executable, tt.args)
|
||||
|
||||
if tt.wantError {
|
||||
if err == nil {
|
||||
t.Errorf("Exec(%q, %v) expected error but got none", tt.executable, tt.args)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Exec(%q, %v) unexpected error: %v", tt.executable, tt.args, err)
|
||||
}
|
||||
if tt.contains != "" && output != tt.contains {
|
||||
t.Errorf("Exec(%q, %v) = %q, want %q", tt.executable, tt.args, output, tt.contains)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecWithOutput(t *testing.T) {
|
||||
// Test that Exec properly captures and trims output
|
||||
if HasBinary("printf") {
|
||||
output, err := Exec("printf", []string{" hello world \n"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if output != "hello world" {
|
||||
t.Errorf("expected trimmed output 'hello world', got %q", output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUniqueInts(b *testing.B) {
|
||||
// Create a slice with duplicates
|
||||
input := make([]int, 1000)
|
||||
for i := 0; i < 1000; i++ {
|
||||
input[i] = i % 100 // This creates 10 duplicates of each number 0-99
|
||||
}
|
||||
|
||||
b.Run("unsorted", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = UniqueInts(input, false)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("sorted", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = UniqueInts(input, true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package core
|
||||
|
||||
import "flag"
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
InterfaceName *string
|
||||
|
|
@ -16,6 +18,9 @@ type Options struct {
|
|||
Commands *string
|
||||
CpuProfile *string
|
||||
MemProfile *string
|
||||
CapletsPath *string
|
||||
Script *string
|
||||
PcapBufSize *int
|
||||
}
|
||||
|
||||
func ParseOptions() (Options, error) {
|
||||
|
|
@ -33,6 +38,9 @@ func ParseOptions() (Options, error) {
|
|||
Commands: flag.String("eval", "", "Run one or more commands separated by ; in the interactive session, used to set variables via command line."),
|
||||
CpuProfile: flag.String("cpu-profile", "", "Write cpu profile `file`."),
|
||||
MemProfile: flag.String("mem-profile", "", "Write memory profile to `file`."),
|
||||
CapletsPath: flag.String("caplets-path", "", "Specify an alternative base path for caplets."),
|
||||
Script: flag.String("script", "", "Load a session script."),
|
||||
PcapBufSize: flag.Int("pcap-buf-size", -1, "PCAP buffer size, leave to 0 for the default value."),
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/core"
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/v2/core"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,27 +4,32 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/core"
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/v2/core"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
||||
type LinuxFirewall struct {
|
||||
iface *network.Endpoint
|
||||
forwarding bool
|
||||
restore bool
|
||||
redirections map[string]*Redirection
|
||||
}
|
||||
|
||||
const (
|
||||
IPV4ForwardingFile = "/proc/sys/net/ipv4/ip_forward"
|
||||
IPV6ForwardingFile = "/proc/sys/net/ipv6/conf/all/forwarding"
|
||||
)
|
||||
|
||||
func Make(iface *network.Endpoint) FirewallManager {
|
||||
firewall := &LinuxFirewall{
|
||||
iface: iface,
|
||||
forwarding: false,
|
||||
restore: false,
|
||||
redirections: make(map[string]*Redirection),
|
||||
}
|
||||
|
||||
|
|
@ -61,15 +66,32 @@ func (f LinuxFirewall) IsForwardingEnabled() bool {
|
|||
}
|
||||
|
||||
func (f LinuxFirewall) EnableForwarding(enabled bool) error {
|
||||
return f.enableFeature(IPV4ForwardingFile, enabled)
|
||||
if err := f.enableFeature(IPV4ForwardingFile, enabled); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fs.Exists(IPV6ForwardingFile) {
|
||||
return f.enableFeature(IPV6ForwardingFile, enabled)
|
||||
}
|
||||
|
||||
f.restore = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []string) {
|
||||
action := "-A"
|
||||
destination := ""
|
||||
|
||||
if !enabled {
|
||||
action = "-D"
|
||||
}
|
||||
|
||||
if strings.Count(r.DstAddress, ":") < 2 {
|
||||
destination = r.DstAddress
|
||||
} else {
|
||||
destination = fmt.Sprintf("[%s]", r.DstAddress)
|
||||
}
|
||||
|
||||
if r.SrcAddress == "" {
|
||||
cmdLine = []string{
|
||||
"-t", "nat",
|
||||
|
|
@ -78,7 +100,7 @@ func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []
|
|||
"-p", r.Protocol,
|
||||
"--dport", fmt.Sprintf("%d", r.SrcPort),
|
||||
"-j", "DNAT",
|
||||
"--to", fmt.Sprintf("%s:%d", r.DstAddress, r.DstPort),
|
||||
"--to", fmt.Sprintf("%s:%d", destination, r.DstPort),
|
||||
}
|
||||
} else {
|
||||
cmdLine = []string{
|
||||
|
|
@ -89,7 +111,7 @@ func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []
|
|||
"-d", r.SrcAddress,
|
||||
"--dport", fmt.Sprintf("%d", r.SrcPort),
|
||||
"-j", "DNAT",
|
||||
"--to", fmt.Sprintf("%s:%d", r.DstAddress, r.DstPort),
|
||||
"--to", fmt.Sprintf("%s:%d", destination, r.DstPort),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +122,13 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
|
|||
cmdLine := f.getCommandLine(r, enabled)
|
||||
rkey := r.String()
|
||||
_, found := f.redirections[rkey]
|
||||
cmd := ""
|
||||
|
||||
if strings.Count(r.DstAddress, ":") < 2 {
|
||||
cmd = "iptables"
|
||||
} else {
|
||||
cmd = "ip6tables"
|
||||
}
|
||||
|
||||
if enabled {
|
||||
if found {
|
||||
|
|
@ -109,9 +138,9 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
|
|||
f.redirections[rkey] = r
|
||||
|
||||
// accept all
|
||||
if _, err := core.Exec("iptables", []string{"-P", "FORWARD", "ACCEPT"}); err != nil {
|
||||
if _, err := core.Exec(cmd, []string{"-P", "FORWARD", "ACCEPT"}); err != nil {
|
||||
return err
|
||||
} else if _, err := core.Exec("iptables", cmdLine); err != nil {
|
||||
} else if _, err := core.Exec(cmd, cmdLine); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
|
@ -121,7 +150,7 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
|
|||
|
||||
delete(f.redirections, r.String())
|
||||
|
||||
if _, err := core.Exec("iptables", cmdLine); err != nil {
|
||||
if _, err := core.Exec(cmd, cmdLine); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -130,6 +159,9 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
|
|||
}
|
||||
|
||||
func (f LinuxFirewall) Restore() {
|
||||
if f.restore == false {
|
||||
return
|
||||
}
|
||||
for _, r := range f.redirections {
|
||||
if err := f.EnableRedirection(r, false); err != nil {
|
||||
fmt.Printf("%s", err)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/core"
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/v2/core"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
)
|
||||
|
||||
type WindowsFirewall struct {
|
||||
|
|
|
|||
268
firewall/redirection_test.go
Normal file
268
firewall/redirection_test.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package firewall
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewRedirection(t *testing.T) {
|
||||
iface := "eth0"
|
||||
proto := "tcp"
|
||||
portFrom := 8080
|
||||
addrTo := "192.168.1.100"
|
||||
portTo := 9090
|
||||
|
||||
r := NewRedirection(iface, proto, portFrom, addrTo, portTo)
|
||||
|
||||
if r == nil {
|
||||
t.Fatal("NewRedirection returned nil")
|
||||
}
|
||||
|
||||
if r.Interface != iface {
|
||||
t.Errorf("expected Interface %s, got %s", iface, r.Interface)
|
||||
}
|
||||
|
||||
if r.Protocol != proto {
|
||||
t.Errorf("expected Protocol %s, got %s", proto, r.Protocol)
|
||||
}
|
||||
|
||||
if r.SrcAddress != "" {
|
||||
t.Errorf("expected empty SrcAddress, got %s", r.SrcAddress)
|
||||
}
|
||||
|
||||
if r.SrcPort != portFrom {
|
||||
t.Errorf("expected SrcPort %d, got %d", portFrom, r.SrcPort)
|
||||
}
|
||||
|
||||
if r.DstAddress != addrTo {
|
||||
t.Errorf("expected DstAddress %s, got %s", addrTo, r.DstAddress)
|
||||
}
|
||||
|
||||
if r.DstPort != portTo {
|
||||
t.Errorf("expected DstPort %d, got %d", portTo, r.DstPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectionString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
r Redirection
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic redirection",
|
||||
r: Redirection{
|
||||
Interface: "eth0",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "",
|
||||
SrcPort: 8080,
|
||||
DstAddress: "192.168.1.100",
|
||||
DstPort: 9090,
|
||||
},
|
||||
want: "[eth0] (tcp) :8080 -> 192.168.1.100:9090",
|
||||
},
|
||||
{
|
||||
name: "with source address",
|
||||
r: Redirection{
|
||||
Interface: "wlan0",
|
||||
Protocol: "udp",
|
||||
SrcAddress: "192.168.1.50",
|
||||
SrcPort: 53,
|
||||
DstAddress: "8.8.8.8",
|
||||
DstPort: 53,
|
||||
},
|
||||
want: "[wlan0] (udp) 192.168.1.50:53 -> 8.8.8.8:53",
|
||||
},
|
||||
{
|
||||
name: "localhost redirection",
|
||||
r: Redirection{
|
||||
Interface: "lo",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "127.0.0.1",
|
||||
SrcPort: 80,
|
||||
DstAddress: "127.0.0.1",
|
||||
DstPort: 8080,
|
||||
},
|
||||
want: "[lo] (tcp) 127.0.0.1:80 -> 127.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
name: "high port numbers",
|
||||
r: Redirection{
|
||||
Interface: "eth1",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "",
|
||||
SrcPort: 65535,
|
||||
DstAddress: "10.0.0.1",
|
||||
DstPort: 65534,
|
||||
},
|
||||
want: "[eth1] (tcp) :65535 -> 10.0.0.1:65534",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.r.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("String() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRedirectionVariousProtocols(t *testing.T) {
|
||||
protocols := []string{"tcp", "udp", "icmp", "any"}
|
||||
|
||||
for _, proto := range protocols {
|
||||
t.Run(proto, func(t *testing.T) {
|
||||
r := NewRedirection("eth0", proto, 1234, "10.0.0.1", 5678)
|
||||
if r.Protocol != proto {
|
||||
t.Errorf("expected protocol %s, got %s", proto, r.Protocol)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRedirectionVariousInterfaces(t *testing.T) {
|
||||
interfaces := []string{"eth0", "wlan0", "lo", "docker0", "br0", "tun0"}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
t.Run(iface, func(t *testing.T) {
|
||||
r := NewRedirection(iface, "tcp", 80, "192.168.1.1", 8080)
|
||||
if r.Interface != iface {
|
||||
t.Errorf("expected interface %s, got %s", iface, r.Interface)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectionStringEmptyFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
r Redirection
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty interface",
|
||||
r: Redirection{
|
||||
Interface: "",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "",
|
||||
SrcPort: 80,
|
||||
DstAddress: "192.168.1.1",
|
||||
DstPort: 8080,
|
||||
},
|
||||
want: "[] (tcp) :80 -> 192.168.1.1:8080",
|
||||
},
|
||||
{
|
||||
name: "empty protocol",
|
||||
r: Redirection{
|
||||
Interface: "eth0",
|
||||
Protocol: "",
|
||||
SrcAddress: "",
|
||||
SrcPort: 80,
|
||||
DstAddress: "192.168.1.1",
|
||||
DstPort: 8080,
|
||||
},
|
||||
want: "[eth0] () :80 -> 192.168.1.1:8080",
|
||||
},
|
||||
{
|
||||
name: "empty destination",
|
||||
r: Redirection{
|
||||
Interface: "eth0",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "",
|
||||
SrcPort: 80,
|
||||
DstAddress: "",
|
||||
DstPort: 8080,
|
||||
},
|
||||
want: "[eth0] (tcp) :80 -> :8080",
|
||||
},
|
||||
{
|
||||
name: "all empty strings",
|
||||
r: Redirection{
|
||||
Interface: "",
|
||||
Protocol: "",
|
||||
SrcAddress: "",
|
||||
SrcPort: 0,
|
||||
DstAddress: "",
|
||||
DstPort: 0,
|
||||
},
|
||||
want: "[] () :0 -> :0",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.r.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("String() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirectionStructCopy(t *testing.T) {
|
||||
// Test that Redirection can be safely copied
|
||||
original := NewRedirection("eth0", "tcp", 80, "192.168.1.1", 8080)
|
||||
original.SrcAddress = "10.0.0.1"
|
||||
|
||||
// Create a copy
|
||||
copy := *original
|
||||
|
||||
// Modify the copy
|
||||
copy.Interface = "wlan0"
|
||||
copy.SrcPort = 443
|
||||
|
||||
// Verify original is unchanged
|
||||
if original.Interface != "eth0" {
|
||||
t.Error("original Interface was modified")
|
||||
}
|
||||
if original.SrcPort != 80 {
|
||||
t.Error("original SrcPort was modified")
|
||||
}
|
||||
|
||||
// Verify copy has new values
|
||||
if copy.Interface != "wlan0" {
|
||||
t.Error("copy Interface was not set correctly")
|
||||
}
|
||||
if copy.SrcPort != 443 {
|
||||
t.Error("copy SrcPort was not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewRedirection(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = NewRedirection("eth0", "tcp", 80, "192.168.1.1", 8080)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRedirectionString(b *testing.B) {
|
||||
r := Redirection{
|
||||
Interface: "eth0",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "192.168.1.50",
|
||||
SrcPort: 8080,
|
||||
DstAddress: "192.168.1.100",
|
||||
DstPort: 9090,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRedirectionStringEmpty(b *testing.B) {
|
||||
r := Redirection{
|
||||
Interface: "eth0",
|
||||
Protocol: "tcp",
|
||||
SrcAddress: "",
|
||||
SrcPort: 8080,
|
||||
DstAddress: "192.168.1.100",
|
||||
DstPort: 9090,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String()
|
||||
}
|
||||
}
|
||||
84
go.mod
84
go.mod
|
|
@ -1,49 +1,67 @@
|
|||
module github.com/bettercap/bettercap
|
||||
module github.com/bettercap/bettercap/v2
|
||||
|
||||
go 1.12
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||
github.com/adrianmo/go-nmea v1.3.0
|
||||
github.com/antchfx/jsonquery v1.1.4
|
||||
github.com/antchfx/xpath v1.1.10 // indirect
|
||||
github.com/bettercap/gatt v0.0.0-20191018133023-569d3d9372bb
|
||||
github.com/adrianmo/go-nmea v1.10.0
|
||||
github.com/antchfx/jsonquery v1.3.6
|
||||
github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0
|
||||
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb
|
||||
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf
|
||||
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3
|
||||
github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/elazarl/goproxy v0.0.0-20200809112317-0581fc3aee2d
|
||||
github.com/evilsocket/islazy v1.10.6
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/elazarl/goproxy v1.7.2
|
||||
github.com/evilsocket/islazy v1.11.0
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.0
|
||||
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe
|
||||
github.com/google/go-github v17.0.0+incompatible
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/gopacket v1.1.18
|
||||
github.com/google/gousb v2.1.0+incompatible
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/mdns v1.0.3
|
||||
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b
|
||||
github.com/jpillora/go-tld v1.0.0
|
||||
github.com/koppacetic/go-gpsd v0.4.0
|
||||
github.com/kr/binarydist v0.1.0 // indirect
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/gousb v1.1.3
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-bexpr v0.1.14
|
||||
github.com/inconshreveable/go-vhost v1.0.0
|
||||
github.com/jpillora/go-tld v1.2.1
|
||||
github.com/malfunkt/iprange v0.9.0
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b
|
||||
github.com/miekg/dns v1.1.67
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/phin1x/go-ipp v1.6.1
|
||||
github.com/robertkrimen/otto v0.5.1
|
||||
github.com/stratoberry/go-gpsd v1.3.0
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
|
||||
go.einride.tech/can v0.14.0
|
||||
golang.org/x/net v0.42.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/antchfx/xpath v1.3.4 // indirect
|
||||
github.com/chzyer/logex v1.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/kr/binarydist v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab // indirect
|
||||
github.com/miekg/dns v1.1.31
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
|
||||
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
|
||||
golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321
|
||||
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
honnef.co/go/tools v0.0.0-2019.2.1 // indirect
|
||||
)
|
||||
|
|
|
|||
302
go.sum
302
go.sum
|
|
@ -1,226 +1,192 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
|
||||
github.com/adrianmo/go-nmea v1.1.0 h1:0NILSj14nj6LvVQHo/afHbyPgGz5qvp5PM6jmMyAQzY=
|
||||
github.com/adrianmo/go-nmea v1.1.0/go.mod h1:HHPxPAm2kmev+61qmkZh7xgZF/7qHtSpsWppip2Ipv8=
|
||||
github.com/adrianmo/go-nmea v1.3.0 h1:BFrLRj/oIh+DYujIKpuQievq7X3NDHYq57kNgsfr2GY=
|
||||
github.com/adrianmo/go-nmea v1.3.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg=
|
||||
github.com/antchfx/jsonquery v0.0.0-20180821084212-a2896be8c82b h1:oREWiN8d6BYorETYz2PH2Kk0CtUdp0RETyab7ep4jNY=
|
||||
github.com/antchfx/jsonquery v0.0.0-20180821084212-a2896be8c82b/go.mod h1:h7950pvPrUZzJIflNqsELgDQovTpPNa0rAHf8NwjegY=
|
||||
github.com/antchfx/jsonquery v1.0.0 h1:1Yhk496SrCoY6fJkFZqpXEqbwOw5sFtLns9la4NoK3I=
|
||||
github.com/antchfx/jsonquery v1.0.0/go.mod h1:h7950pvPrUZzJIflNqsELgDQovTpPNa0rAHf8NwjegY=
|
||||
github.com/antchfx/jsonquery v1.1.4 h1:+OlFO3QS9wjU0MKx9MgHm5f6o6hdd4e9mUTp0wTjxlM=
|
||||
github.com/antchfx/jsonquery v1.1.4/go.mod h1:cHs8r6Bymd8j6HI6Ej1IJbjahKvLBcIEh54dfmo+E9A=
|
||||
github.com/antchfx/xpath v1.0.0 h1:Q5gFgh2O40VTSwMOVbFE7nFNRBu3tS21Tn0KAWeEjtk=
|
||||
github.com/antchfx/xpath v1.0.0/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xpath v1.1.0 h1:mJTvYpiHvxNQRD4Lbfin/FodHVCHh2a5KrOFr4ZxMOI=
|
||||
github.com/antchfx/xpath v1.1.0/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xpath v1.1.7/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xpath v1.1.10 h1:cJ0pOvEdN/WvYXxvRrzQH9x5QWKpzHacYO8qzCcDYAg=
|
||||
github.com/antchfx/xpath v1.1.10/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/bettercap/gatt v0.0.0-20190418085356-fac16c0ad797 h1:P9DK7Ij21WQAPFTUix8rblTVKDdwJEimCVO5foIr2ik=
|
||||
github.com/bettercap/gatt v0.0.0-20190418085356-fac16c0ad797/go.mod h1:xbRFD+l8RcbQ3DscCiYX0dgEnXbwozZgm6oP2GXic+0=
|
||||
github.com/bettercap/gatt v0.0.0-20191018133023-569d3d9372bb h1:GOaknHS3DCZcBbQRScDl6uecgkrkK7YFToIA5Uc6lHU=
|
||||
github.com/bettercap/gatt v0.0.0-20191018133023-569d3d9372bb/go.mod h1:xbRFD+l8RcbQ3DscCiYX0dgEnXbwozZgm6oP2GXic+0=
|
||||
github.com/adrianmo/go-nmea v1.10.0 h1:L1aYaebZ4cXFCoXNSeDeQa0tApvSKvIbqMsK+iaRiCo=
|
||||
github.com/adrianmo/go-nmea v1.10.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg=
|
||||
github.com/antchfx/jsonquery v1.3.6 h1:TaSfeAh7n6T11I74bsZ1FswreIfrbJ0X+OyLflx6mx4=
|
||||
github.com/antchfx/jsonquery v1.3.6/go.mod h1:fGzSGJn9Y826Qd3pC8Wx45avuUwpkePsACQJYy+58BU=
|
||||
github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
|
||||
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0 h1:HiFUGV/7eGWG/YJAf9HcKOUmxIj+7LVzC8zD57VX1qo=
|
||||
github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0/go.mod h1:oafnPgaBI4gqJiYkueCyR4dqygiWGXTGOE0gmmAVeeQ=
|
||||
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb h1:JWAAJk4ny+bT3VrtcX+e7mcmWtWUeUM0xVcocSAUuWc=
|
||||
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb/go.mod h1:g6WiaSRgMTiChuk7jYyFSEtpgaw1F0wAsBfspG3bu0M=
|
||||
github.com/bettercap/readline v0.0.0-20180208083827-9cec905dd291 h1:6GtREdpf6N/trykGvhwfr0nyo3V/yncz0JvNbu+z7S8=
|
||||
github.com/bettercap/readline v0.0.0-20180208083827-9cec905dd291/go.mod h1:03rWiUf60r1miMVzMEtgtkq7RdZniecZFw3/Zgvyxcs=
|
||||
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf h1:pwGPRc5PIp4KCF9QbKn0iLVMhfigUMw4IzGZEZ81m1I=
|
||||
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf/go.mod h1:03rWiUf60r1miMVzMEtgtkq7RdZniecZFw3/Zgvyxcs=
|
||||
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3 h1:pC4ZAk7UtDIbrRKzMMiIL1TVkiKlgtgcJodqKB53Rl4=
|
||||
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3/go.mod h1:kqVwnx6DKuOHMZcBnzsgp2Lq2JZHDtFtm92b5hxdRaM=
|
||||
github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef h1:uhLIhHeIRlFbAI1mOHkz3vN23T+QdhA9MgnvnJaQyL0=
|
||||
github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef/go.mod h1:xn8SYXvxzI99iSN8+Kh3wCvt2fhr27vPPf8ju9FwRS0=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f h1:8GDPb0tCY8LQ+OJ3dbHb5sA6YZWXFORQYZx5sdsTlMs=
|
||||
github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8=
|
||||
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy v0.0.0-20200809112317-0581fc3aee2d h1:rtM8HsT3NG37YPjz8sYSbUSdElP9lUsQENYzJDZDUBE=
|
||||
github.com/elazarl/goproxy v0.0.0-20200809112317-0581fc3aee2d/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f h1:AUj1VoZUfhPhOPHULCQQDnGhRelpFWHMLhQVWDsS0v4=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||
github.com/evilsocket/islazy v1.10.4 h1:Z5373Kn5Gh2EWch1Tb/Qxb6vyQ7lw704bmKi7sY4Ecs=
|
||||
github.com/evilsocket/islazy v1.10.4/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
|
||||
github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
|
||||
github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
|
||||
github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.0 h1:NTCxS9b0GSbHkWv1a7oOvZn679fsyDkaSkRvOYpQ9Oo=
|
||||
github.com/florianl/go-nfqueue/v2 v2.0.0/go.mod h1:M2tBLIj62QpwqjwV0qfcjqGOqP3qiTuXr2uSRBXH9Qk=
|
||||
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe h1:8P+/htb3mwwpeGdJg69yBF/RofK7c6Fjz5Ypa/bTqbY=
|
||||
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
|
||||
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
|
||||
github.com/google/gopacket v1.1.18 h1:lum7VRA9kdlvBi7/v2p7/zcbkduHaCH/SVVyurs7OpY=
|
||||
github.com/google/gopacket v1.1.18/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
|
||||
github.com/google/gousb v0.0.0-20190525092738-2dc560e6bea3 h1:3RtjTHQgWbD7dsPreVSkA5/LGfNnAZyjOtDXhargyEE=
|
||||
github.com/google/gousb v0.0.0-20190525092738-2dc560e6bea3/go.mod h1:Tl4HdAs1ThE3gECkNwz+1MWicX6FXddhJEw7L8jRDiI=
|
||||
github.com/google/gousb v0.0.0-20190812193832-18f4c1d8a750 h1:DVKHLo3yE4psTjD9aM2pY7EHoicaQbgmaxxvvHC6ZSM=
|
||||
github.com/google/gousb v0.0.0-20190812193832-18f4c1d8a750/go.mod h1:Tl4HdAs1ThE3gECkNwz+1MWicX6FXddhJEw7L8jRDiI=
|
||||
github.com/google/gousb v2.1.0+incompatible h1:ApzMDjF3FeO219QwWybJxYfFhXQzPLOEy0o+w9k5DNI=
|
||||
github.com/google/gousb v2.1.0+incompatible/go.mod h1:Tl4HdAs1ThE3gECkNwz+1MWicX6FXddhJEw7L8jRDiI=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/mdns v1.0.1 h1:XFSOubp8KWB+Jd2PDyaX5xUd5bhSP/+pTDZVDMzZJM8=
|
||||
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
github.com/hashicorp/mdns v1.0.3 h1:hPneYJlzSjxFBmUlnDGXRykxBZ++dQAJhU57gCO7TzI=
|
||||
github.com/hashicorp/mdns v1.0.3/go.mod h1:P9sIDVQGUBr2GtS4qS2QCBdtgqP7TBt6d8looU5l5r4=
|
||||
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE=
|
||||
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
|
||||
github.com/jpillora/go-tld v0.0.0-20190202073305-f16ca3b7b383 h1:/ODEnccTd4m/1YbCDCZBIoo+W1BC+PjOzvfX8y++b5s=
|
||||
github.com/jpillora/go-tld v0.0.0-20190202073305-f16ca3b7b383/go.mod h1:7H/4k+TVAFSXCq+KcvelJ5hClsXnabjF52BLYqgaqcQ=
|
||||
github.com/jpillora/go-tld v1.0.0 h1:W0Wz3fYT9WCDNJXcXc58uV7sriLnVeELeOU5MP5X42M=
|
||||
github.com/jpillora/go-tld v1.0.0/go.mod h1:kitBxOF//DR5FxYeIGw+etdiiTIq5S7bx0dwy1GUNAk=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/koppacetic/go-gpsd v0.4.0 h1:/T3cRvi1ZsWbxCZPB9pPor0HjIw3HuD+MSvaxV5QqQ8=
|
||||
github.com/koppacetic/go-gpsd v0.4.0/go.mod h1:mhcLuh9X++WHepbL3jEmEwnx1OkQDepZMihv12RO4qk=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o=
|
||||
github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/go-bexpr v0.1.14 h1:uKDeyuOhWhT1r5CiMTjdVY4Aoxdxs6EtwgTGnlosyp4=
|
||||
github.com/hashicorp/go-bexpr v0.1.14/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8=
|
||||
github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8=
|
||||
github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jpillora/go-tld v1.2.1 h1:kDKOkmXLlskqjcvNs7w5XHLep7c8WM7Xd4HQjxllVMk=
|
||||
github.com/jpillora/go-tld v1.2.1/go.mod h1:plzIl7xr5UWKGy7R+giuv+L/nOjrPjsoWxy/ST9OBUk=
|
||||
github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
|
||||
github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/malfunkt/iprange v0.9.0 h1:VCs0PKLUPotNVQTpVNszsut4lP7OCGNBwX+lOYBrnVQ=
|
||||
github.com/malfunkt/iprange v0.9.0/go.mod h1:TRGqO/f95gh3LOndUGTL46+W0GXA91WTqyZ0Quwvt4U=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b h1:r12blE3QRYlW1WBiBEe007O6NrTb/P54OjR5d4WLEGk=
|
||||
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b/go.mod h1:p4K2+UAoap8Jzsadsxc0KG0OZjmmCthTPUyZqAVkjBY=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY=
|
||||
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.14 h1:wkQWn9wIp4mZbwW8XV6Km6owkvRPbOiV004ZM2CkGvA=
|
||||
github.com/miekg/dns v1.1.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
|
||||
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.31 h1:sJFOl9BgwbYAWOGEwr61FU28pqsBNdpRBnhGXtO06Oo=
|
||||
github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
|
||||
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
|
||||
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||
github.com/phin1x/go-ipp v1.6.1 h1:oxJXi92BO2FZhNcG3twjnxKFH1liTQ46vbbZx+IN/80=
|
||||
github.com/phin1x/go-ipp v1.6.1/go.mod h1:GZwyNds6grdLi2xRBX22Cvt7Dh7ITWsML0bjrqBF5uo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
|
||||
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac h1:kYPjbEN6YPYWWHI6ky1J813KzIq/8+Wg4TO4xU7A/KU=
|
||||
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
|
||||
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
|
||||
github.com/stratoberry/go-gpsd v1.3.0 h1:JxJOEC4SgD0QY65AE7B1CtJtweP73nqJghZeLNU9J+c=
|
||||
github.com/stratoberry/go-gpsd v1.3.0/go.mod h1:nVf/vTgfYxOMxiQdy9BtJjojbFRtG8H3wNula++VgkU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb h1:EavwSqheIJl3nb91HhkL73DwnT2Fk8W3yM7T7TuLZvA=
|
||||
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb/go.mod h1:I0ZT9x8wStY6VOxtNOrLpnDURFs7HS0z1e1vhuKUEVc=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
|
||||
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
go.einride.tech/can v0.14.0 h1:OkQ0jsjCk4ijgTMjD43V1NKQyDztpX7Vo/NrvmnsAXE=
|
||||
go.einride.tech/can v0.14.0/go.mod h1:615YuRGnWfndMGD+f3Ud1sp1xJLP1oj14dKRtb2CXDQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
|
||||
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.0.0-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA=
|
||||
golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/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-20200925080053-05aa5d4ee321 h1:lleNcKRbcaC8MqgLwghIkzZ2JBQAb7QQ9MiwRt1BisA=
|
||||
golang.org/x/net v0.0.0-20200925080053-05aa5d4ee321/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191018095205-727590c5006e h1:ZtoklVMHQy6BFRHkbG6JzK+S6rX82//Yeok1vMlizfQ=
|
||||
golang.org/x/sys v0.0.0-20191018095205-727590c5006e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
|
||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46 h1:V066+OYJ66oTjnhm4Yrn7SXIwSCiDQJxpBxmvqb1N1c=
|
||||
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.0-2019.2.1 h1:fW1wbZIKRbRK56ETe5SYloH5SdLzhXOFet2KlpRKDqg=
|
||||
honnef.co/go/tools v0.0.0-2019.2.1/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
|
|
|
|||
29
js/crypto.go
Normal file
29
js/crypto.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
func cryptoSha1(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("Crypto.sha1: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
arg := argv[0]
|
||||
if (!arg.IsString()) {
|
||||
return ReportError("Crypto.sha1: single argument must be a string.")
|
||||
}
|
||||
|
||||
hasher := sha1.New()
|
||||
hasher.Write([]byte(arg.String()))
|
||||
v, err := otto.ToValue(string(hasher.Sum(nil)))
|
||||
if err != nil {
|
||||
return ReportError("Crypto.sha1: could not convert to string: %s", err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
164
js/data.go
Normal file
164
js/data.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
func textEncode(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("textEncode: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
arg := argv[0]
|
||||
if (!arg.IsString()) {
|
||||
return ReportError("textEncode: single argument must be a string.")
|
||||
}
|
||||
|
||||
encoded := []byte(arg.String())
|
||||
vm := otto.New()
|
||||
v, err := vm.ToValue(encoded)
|
||||
if err != nil {
|
||||
return ReportError("textEncode: could not convert to []uint8: %s", err.Error())
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func textDecode(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("textDecode: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
arg, err := argv[0].Export()
|
||||
if err != nil {
|
||||
return ReportError("textDecode: could not export argument value: %s", err.Error())
|
||||
}
|
||||
byteArr, ok := arg.([]uint8)
|
||||
if !ok {
|
||||
return ReportError("textDecode: single argument must be of type []uint8.")
|
||||
}
|
||||
|
||||
decoded := string(byteArr)
|
||||
v, err := otto.ToValue(decoded)
|
||||
if err != nil {
|
||||
return ReportError("textDecode: could not convert to string: %s", err.Error())
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func btoa(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("btoa: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
arg := argv[0]
|
||||
if (!arg.IsString()) {
|
||||
return ReportError("btoa: single argument must be a string.")
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte(arg.String()))
|
||||
v, err := otto.ToValue(encoded)
|
||||
if err != nil {
|
||||
return ReportError("btoa: could not convert to string: %s", err.Error())
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func atob(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("atob: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
arg := argv[0]
|
||||
if (!arg.IsString()) {
|
||||
return ReportError("atob: single argument must be a string.")
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(arg.String())
|
||||
if err != nil {
|
||||
return ReportError("atob: could not decode string: %s", err.Error())
|
||||
}
|
||||
|
||||
v, err := otto.ToValue(string(decoded))
|
||||
if err != nil {
|
||||
return ReportError("atob: could not convert to string: %s", err.Error())
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func gzipCompress(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("gzipCompress: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
arg := argv[0]
|
||||
if (!arg.IsString()) {
|
||||
return ReportError("gzipCompress: single argument must be a string.")
|
||||
}
|
||||
|
||||
uncompressedBytes := []byte(arg.String())
|
||||
|
||||
var writerBuffer bytes.Buffer
|
||||
gzipWriter := gzip.NewWriter(&writerBuffer)
|
||||
_, err := gzipWriter.Write(uncompressedBytes)
|
||||
if err != nil {
|
||||
return ReportError("gzipCompress: could not compress data: %s", err.Error())
|
||||
}
|
||||
gzipWriter.Close()
|
||||
|
||||
compressedBytes := writerBuffer.Bytes()
|
||||
|
||||
v, err := otto.ToValue(string(compressedBytes))
|
||||
if err != nil {
|
||||
return ReportError("gzipCompress: could not convert to string: %s", err.Error())
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func gzipDecompress(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("gzipDecompress: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
compressedBytes := []byte(argv[0].String())
|
||||
readerBuffer := bytes.NewBuffer(compressedBytes)
|
||||
|
||||
gzipReader, err := gzip.NewReader(readerBuffer)
|
||||
if err != nil {
|
||||
return ReportError("gzipDecompress: could not create gzip reader: %s", err.Error())
|
||||
}
|
||||
|
||||
var decompressedBuffer bytes.Buffer
|
||||
_, err = decompressedBuffer.ReadFrom(gzipReader)
|
||||
if err != nil {
|
||||
return ReportError("gzipDecompress: could not decompress data: %s", err.Error())
|
||||
}
|
||||
|
||||
decompressedBytes := decompressedBuffer.Bytes()
|
||||
v, err := otto.ToValue(string(decompressedBytes))
|
||||
if err != nil {
|
||||
return ReportError("gzipDecompress: could not convert to string: %s", err.Error())
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
514
js/data_test.go
Normal file
514
js/data_test.go
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
func TestBtoa(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple string",
|
||||
input: "hello world",
|
||||
expected: base64.StdEncoding.EncodeToString([]byte("hello world")),
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: base64.StdEncoding.EncodeToString([]byte("")),
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
input: "!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||
expected: base64.StdEncoding.EncodeToString([]byte("!@#$%^&*()_+-=[]{}|;:,.<>?")),
|
||||
},
|
||||
{
|
||||
name: "unicode string",
|
||||
input: "Hello 世界 🌍",
|
||||
expected: base64.StdEncoding.EncodeToString([]byte("Hello 世界 🌍")),
|
||||
},
|
||||
{
|
||||
name: "newlines and tabs",
|
||||
input: "line1\nline2\ttab",
|
||||
expected: base64.StdEncoding.EncodeToString([]byte("line1\nline2\ttab")),
|
||||
},
|
||||
{
|
||||
name: "long string",
|
||||
input: strings.Repeat("a", 1000),
|
||||
expected: base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", 1000))),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create call with argument
|
||||
arg, _ := vm.ToValue(tt.input)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := btoa(call)
|
||||
|
||||
// Check if result is an error
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("btoa returned undefined")
|
||||
}
|
||||
|
||||
// Get string value
|
||||
resultStr, err := result.ToString()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert result to string: %v", err)
|
||||
}
|
||||
|
||||
if resultStr != tt.expected {
|
||||
t.Errorf("btoa(%q) = %q, want %q", tt.input, resultStr, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtob(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "simple base64",
|
||||
input: base64.StdEncoding.EncodeToString([]byte("hello world")),
|
||||
expected: "hello world",
|
||||
},
|
||||
{
|
||||
name: "empty base64",
|
||||
input: base64.StdEncoding.EncodeToString([]byte("")),
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "special characters base64",
|
||||
input: base64.StdEncoding.EncodeToString([]byte("!@#$%^&*()_+-=[]{}|;:,.<>?")),
|
||||
expected: "!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||
},
|
||||
{
|
||||
name: "unicode base64",
|
||||
input: base64.StdEncoding.EncodeToString([]byte("Hello 世界 🌍")),
|
||||
expected: "Hello 世界 🌍",
|
||||
},
|
||||
{
|
||||
name: "invalid base64",
|
||||
input: "not valid base64!",
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid padding",
|
||||
input: "SGVsbG8gV29ybGQ", // Missing padding
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "long base64",
|
||||
input: base64.StdEncoding.EncodeToString([]byte(strings.Repeat("a", 1000))),
|
||||
expected: strings.Repeat("a", 1000),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create call with argument
|
||||
arg, _ := vm.ToValue(tt.input)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := atob(call)
|
||||
|
||||
// Get string value
|
||||
resultStr, err := result.ToString()
|
||||
if err != nil && !tt.wantError {
|
||||
t.Fatalf("failed to convert result to string: %v", err)
|
||||
}
|
||||
|
||||
if tt.wantError {
|
||||
// Should return undefined (NullValue) on error
|
||||
if !result.IsUndefined() {
|
||||
t.Errorf("expected undefined for error case, got %q", resultStr)
|
||||
}
|
||||
} else {
|
||||
if resultStr != tt.expected {
|
||||
t.Errorf("atob(%q) = %q, want %q", tt.input, resultStr, tt.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGzipCompress(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "simple string",
|
||||
input: "hello world",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
name: "repeated pattern",
|
||||
input: strings.Repeat("abcd", 100),
|
||||
},
|
||||
{
|
||||
name: "random text",
|
||||
input: "The quick brown fox jumps over the lazy dog. " + strings.Repeat("Lorem ipsum dolor sit amet. ", 10),
|
||||
},
|
||||
{
|
||||
name: "unicode text",
|
||||
input: "Hello 世界 🌍 " + strings.Repeat("测试数据 ", 50),
|
||||
},
|
||||
{
|
||||
name: "binary-like data",
|
||||
input: string([]byte{0, 1, 2, 3, 255, 254, 253, 252}),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create call with argument
|
||||
arg, _ := vm.ToValue(tt.input)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := gzipCompress(call)
|
||||
|
||||
// Get compressed data
|
||||
compressed, err := result.ToString()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert result to string: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's actually compressed (for non-empty strings, compressed should be different)
|
||||
if tt.input != "" && compressed == tt.input {
|
||||
t.Error("compressed data is same as input")
|
||||
}
|
||||
|
||||
// Verify gzip header (should start with 0x1f, 0x8b)
|
||||
if len(compressed) >= 2 {
|
||||
if compressed[0] != 0x1f || compressed[1] != 0x8b {
|
||||
t.Error("compressed data doesn't have valid gzip header")
|
||||
}
|
||||
}
|
||||
|
||||
// Now decompress to verify
|
||||
argCompressed, _ := vm.ToValue(compressed)
|
||||
callDecompress := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argCompressed},
|
||||
}
|
||||
|
||||
resultDecompressed := gzipDecompress(callDecompress)
|
||||
decompressed, err := resultDecompressed.ToString()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decompress: %v", err)
|
||||
}
|
||||
|
||||
if decompressed != tt.input {
|
||||
t.Errorf("round-trip failed: got %q, want %q", decompressed, tt.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGzipCompressInvalidArgs(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []otto.Value
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []otto.Value{},
|
||||
},
|
||||
{
|
||||
name: "too many arguments",
|
||||
args: func() []otto.Value {
|
||||
arg1, _ := vm.ToValue("test")
|
||||
arg2, _ := vm.ToValue("extra")
|
||||
return []otto.Value{arg1, arg2}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: tt.args,
|
||||
}
|
||||
|
||||
result := gzipCompress(call)
|
||||
|
||||
// Should return undefined (NullValue) on error
|
||||
if !result.IsUndefined() {
|
||||
resultStr, _ := result.ToString()
|
||||
t.Errorf("expected undefined for error case, got %q", resultStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGzipDecompress(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
// First compress some data
|
||||
originalData := "This is test data for decompression"
|
||||
arg, _ := vm.ToValue(originalData)
|
||||
compressCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
compressedResult := gzipCompress(compressCall)
|
||||
compressedData, _ := compressedResult.ToString()
|
||||
|
||||
t.Run("valid decompression", func(t *testing.T) {
|
||||
argCompressed, _ := vm.ToValue(compressedData)
|
||||
decompressCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argCompressed},
|
||||
}
|
||||
|
||||
result := gzipDecompress(decompressCall)
|
||||
decompressed, err := result.ToString()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert result to string: %v", err)
|
||||
}
|
||||
|
||||
if decompressed != originalData {
|
||||
t.Errorf("decompressed data doesn't match original: got %q, want %q", decompressed, originalData)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid gzip data", func(t *testing.T) {
|
||||
argInvalid, _ := vm.ToValue("not gzip data")
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argInvalid},
|
||||
}
|
||||
|
||||
result := gzipDecompress(call)
|
||||
|
||||
// Should return undefined (NullValue) on error
|
||||
if !result.IsUndefined() {
|
||||
resultStr, _ := result.ToString()
|
||||
t.Errorf("expected undefined for error case, got %q", resultStr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("corrupted gzip data", func(t *testing.T) {
|
||||
// Create corrupted gzip by taking valid gzip and modifying it
|
||||
corruptedData := compressedData[:len(compressedData)/2] + "corrupted"
|
||||
|
||||
argCorrupted, _ := vm.ToValue(corruptedData)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argCorrupted},
|
||||
}
|
||||
|
||||
result := gzipDecompress(call)
|
||||
|
||||
// Should return undefined (NullValue) on error
|
||||
if !result.IsUndefined() {
|
||||
resultStr, _ := result.ToString()
|
||||
t.Errorf("expected undefined for error case, got %q", resultStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGzipDecompressInvalidArgs(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []otto.Value
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []otto.Value{},
|
||||
},
|
||||
{
|
||||
name: "too many arguments",
|
||||
args: func() []otto.Value {
|
||||
arg1, _ := vm.ToValue("test")
|
||||
arg2, _ := vm.ToValue("extra")
|
||||
return []otto.Value{arg1, arg2}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: tt.args,
|
||||
}
|
||||
|
||||
result := gzipDecompress(call)
|
||||
|
||||
// Should return undefined (NullValue) on error
|
||||
if !result.IsUndefined() {
|
||||
resultStr, _ := result.ToString()
|
||||
t.Errorf("expected undefined for error case, got %q", resultStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBtoaAtobRoundTrip(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
testStrings := []string{
|
||||
"simple",
|
||||
"",
|
||||
"with spaces and\nnewlines\ttabs",
|
||||
"special!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||
"unicode 世界 🌍",
|
||||
strings.Repeat("long string ", 100),
|
||||
}
|
||||
|
||||
for _, original := range testStrings {
|
||||
t.Run(original, func(t *testing.T) {
|
||||
// Encode with btoa
|
||||
argOriginal, _ := vm.ToValue(original)
|
||||
encodeCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argOriginal},
|
||||
}
|
||||
|
||||
encoded := btoa(encodeCall)
|
||||
encodedStr, _ := encoded.ToString()
|
||||
|
||||
// Decode with atob
|
||||
argEncoded, _ := vm.ToValue(encodedStr)
|
||||
decodeCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argEncoded},
|
||||
}
|
||||
|
||||
decoded := atob(decodeCall)
|
||||
decodedStr, _ := decoded.ToString()
|
||||
|
||||
if decodedStr != original {
|
||||
t.Errorf("round-trip failed: got %q, want %q", decodedStr, original)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGzipCompressDecompressRoundTrip(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
testData := []string{
|
||||
"simple",
|
||||
"",
|
||||
strings.Repeat("repetitive data ", 100),
|
||||
"unicode 世界 🌍 " + strings.Repeat("测试 ", 50),
|
||||
string([]byte{0, 1, 2, 3, 255, 254, 253, 252}),
|
||||
}
|
||||
|
||||
for _, original := range testData {
|
||||
t.Run(original, func(t *testing.T) {
|
||||
// Compress
|
||||
argOriginal, _ := vm.ToValue(original)
|
||||
compressCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argOriginal},
|
||||
}
|
||||
|
||||
compressed := gzipCompress(compressCall)
|
||||
compressedStr, _ := compressed.ToString()
|
||||
|
||||
// Decompress
|
||||
argCompressed, _ := vm.ToValue(compressedStr)
|
||||
decompressCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argCompressed},
|
||||
}
|
||||
|
||||
decompressed := gzipDecompress(decompressCall)
|
||||
decompressedStr, _ := decompressed.ToString()
|
||||
|
||||
if decompressedStr != original {
|
||||
t.Errorf("round-trip failed: got %q, want %q", decompressedStr, original)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBtoa(b *testing.B) {
|
||||
vm := otto.New()
|
||||
arg, _ := vm.ToValue("The quick brown fox jumps over the lazy dog")
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = btoa(call)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAtob(b *testing.B) {
|
||||
vm := otto.New()
|
||||
encoded := base64.StdEncoding.EncodeToString([]byte("The quick brown fox jumps over the lazy dog"))
|
||||
arg, _ := vm.ToValue(encoded)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = atob(call)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGzipCompress(b *testing.B) {
|
||||
vm := otto.New()
|
||||
data := strings.Repeat("The quick brown fox jumps over the lazy dog. ", 10)
|
||||
arg, _ := vm.ToValue(data)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = gzipCompress(call)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGzipDecompress(b *testing.B) {
|
||||
vm := otto.New()
|
||||
|
||||
// First compress some data
|
||||
data := strings.Repeat("The quick brown fox jumps over the lazy dog. ", 10)
|
||||
argData, _ := vm.ToValue(data)
|
||||
compressCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argData},
|
||||
}
|
||||
compressed := gzipCompress(compressCall)
|
||||
compressedStr, _ := compressed.ToString()
|
||||
|
||||
// Benchmark decompression
|
||||
argCompressed, _ := vm.ToValue(compressedStr)
|
||||
decompressCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argCompressed},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = gzipDecompress(decompressCall)
|
||||
}
|
||||
}
|
||||
70
js/fs.go
Normal file
70
js/fs.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"github.com/robertkrimen/otto"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func readDir(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("readDir: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
path := argv[0].String()
|
||||
dir, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return ReportError("Could not read directory %s: %s", path, err)
|
||||
}
|
||||
|
||||
entry_list := []string{}
|
||||
for _, file := range dir {
|
||||
entry_list = append(entry_list, file.Name())
|
||||
}
|
||||
|
||||
v, err := otto.Otto.ToValue(*call.Otto, entry_list)
|
||||
if err != nil {
|
||||
return ReportError("Could not convert to array: %s", err)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func readFile(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 1 {
|
||||
return ReportError("readFile: expected 1 argument, %d given instead.", argc)
|
||||
}
|
||||
|
||||
filename := argv[0].String()
|
||||
raw, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return ReportError("Could not read file %s: %s", filename, err)
|
||||
}
|
||||
|
||||
v, err := otto.ToValue(string(raw))
|
||||
if err != nil {
|
||||
return ReportError("Could not convert to string: %s", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func writeFile(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc != 2 {
|
||||
return ReportError("writeFile: expected 2 arguments, %d given instead.", argc)
|
||||
}
|
||||
|
||||
filename := argv[0].String()
|
||||
data := argv[1].String()
|
||||
|
||||
err := ioutil.WriteFile(filename, []byte(data), 0644)
|
||||
if err != nil {
|
||||
return ReportError("Could not write %d bytes to %s: %s", len(data), filename, err)
|
||||
}
|
||||
|
||||
return otto.NullValue()
|
||||
}
|
||||
684
js/fs_test.go
Normal file
684
js/fs_test.go
Normal file
|
|
@ -0,0 +1,684 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
func TestReadDir(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tmpDir, err := ioutil.TempDir("", "js_test_readdir_*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create some test files and subdirectories
|
||||
testFiles := []string{"file1.txt", "file2.log", ".hidden"}
|
||||
testDirs := []string{"subdir1", "subdir2"}
|
||||
|
||||
for _, name := range testFiles {
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpDir, name), []byte("test"), 0644); err != nil {
|
||||
t.Fatalf("failed to create test file %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range testDirs {
|
||||
if err := os.Mkdir(filepath.Join(tmpDir, name), 0755); err != nil {
|
||||
t.Fatalf("failed to create test dir %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("valid directory", func(t *testing.T) {
|
||||
arg, _ := vm.ToValue(tmpDir)
|
||||
call := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readDir(call)
|
||||
|
||||
// Check if result is not undefined
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("readDir returned undefined")
|
||||
}
|
||||
|
||||
// Convert to Go slice
|
||||
export, err := result.Export()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to export result: %v", err)
|
||||
}
|
||||
|
||||
entries, ok := export.([]string)
|
||||
if !ok {
|
||||
t.Fatalf("expected []string, got %T", export)
|
||||
}
|
||||
|
||||
// Check all expected entries are present
|
||||
expectedEntries := append(testFiles, testDirs...)
|
||||
if len(entries) != len(expectedEntries) {
|
||||
t.Errorf("expected %d entries, got %d", len(expectedEntries), len(entries))
|
||||
}
|
||||
|
||||
// Check each entry exists
|
||||
for _, expected := range expectedEntries {
|
||||
found := false
|
||||
for _, entry := range entries {
|
||||
if entry == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected entry %s not found", expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent directory", func(t *testing.T) {
|
||||
arg, _ := vm.ToValue("/path/that/does/not/exist")
|
||||
call := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readDir(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined for non-existent directory")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file instead of directory", func(t *testing.T) {
|
||||
// Create a file
|
||||
testFile := filepath.Join(tmpDir, "notadir.txt")
|
||||
ioutil.WriteFile(testFile, []byte("test"), 0644)
|
||||
|
||||
arg, _ := vm.ToValue(testFile)
|
||||
call := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readDir(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined when passing file instead of directory")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid arguments", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []otto.Value
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []otto.Value{},
|
||||
},
|
||||
{
|
||||
name: "too many arguments",
|
||||
args: func() []otto.Value {
|
||||
arg1, _ := vm.ToValue(tmpDir)
|
||||
arg2, _ := vm.ToValue("extra")
|
||||
return []otto.Value{arg1, arg2}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
call := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: tt.args,
|
||||
}
|
||||
|
||||
result := readDir(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined for invalid arguments")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty directory", func(t *testing.T) {
|
||||
emptyDir := filepath.Join(tmpDir, "empty")
|
||||
os.Mkdir(emptyDir, 0755)
|
||||
|
||||
arg, _ := vm.ToValue(emptyDir)
|
||||
call := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readDir(call)
|
||||
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("readDir returned undefined for empty directory")
|
||||
}
|
||||
|
||||
export, _ := result.Export()
|
||||
entries, _ := export.([]string)
|
||||
|
||||
if len(entries) != 0 {
|
||||
t.Errorf("expected 0 entries for empty directory, got %d", len(entries))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tmpDir, err := ioutil.TempDir("", "js_test_readfile_*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
t.Run("valid file", func(t *testing.T) {
|
||||
testContent := "Hello, World!\nThis is a test file.\n特殊字符测试 🌍"
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
ioutil.WriteFile(testFile, []byte(testContent), 0644)
|
||||
|
||||
arg, _ := vm.ToValue(testFile)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("readFile returned undefined")
|
||||
}
|
||||
|
||||
content, err := result.ToString()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to convert result to string: %v", err)
|
||||
}
|
||||
|
||||
if content != testContent {
|
||||
t.Errorf("expected content %q, got %q", testContent, content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-existent file", func(t *testing.T) {
|
||||
arg, _ := vm.ToValue("/path/that/does/not/exist.txt")
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined for non-existent file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directory instead of file", func(t *testing.T) {
|
||||
arg, _ := vm.ToValue(tmpDir)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined when passing directory instead of file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty file", func(t *testing.T) {
|
||||
emptyFile := filepath.Join(tmpDir, "empty.txt")
|
||||
ioutil.WriteFile(emptyFile, []byte(""), 0644)
|
||||
|
||||
arg, _ := vm.ToValue(emptyFile)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("readFile returned undefined for empty file")
|
||||
}
|
||||
|
||||
content, _ := result.ToString()
|
||||
if content != "" {
|
||||
t.Errorf("expected empty string, got %q", content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("binary file", func(t *testing.T) {
|
||||
binaryContent := []byte{0, 1, 2, 3, 255, 254, 253, 252}
|
||||
binaryFile := filepath.Join(tmpDir, "binary.bin")
|
||||
ioutil.WriteFile(binaryFile, binaryContent, 0644)
|
||||
|
||||
arg, _ := vm.ToValue(binaryFile)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("readFile returned undefined for binary file")
|
||||
}
|
||||
|
||||
content, _ := result.ToString()
|
||||
if content != string(binaryContent) {
|
||||
t.Error("binary content mismatch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid arguments", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []otto.Value
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []otto.Value{},
|
||||
},
|
||||
{
|
||||
name: "too many arguments",
|
||||
args: func() []otto.Value {
|
||||
arg1, _ := vm.ToValue("file.txt")
|
||||
arg2, _ := vm.ToValue("extra")
|
||||
return []otto.Value{arg1, arg2}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: tt.args,
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined for invalid arguments")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("large file", func(t *testing.T) {
|
||||
// Create a 1MB file
|
||||
largeContent := strings.Repeat("A", 1024*1024)
|
||||
largeFile := filepath.Join(tmpDir, "large.txt")
|
||||
ioutil.WriteFile(largeFile, []byte(largeContent), 0644)
|
||||
|
||||
arg, _ := vm.ToValue(largeFile)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
result := readFile(call)
|
||||
|
||||
if result.IsUndefined() {
|
||||
t.Fatal("readFile returned undefined for large file")
|
||||
}
|
||||
|
||||
content, _ := result.ToString()
|
||||
if len(content) != len(largeContent) {
|
||||
t.Errorf("expected content length %d, got %d", len(largeContent), len(content))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tmpDir, err := ioutil.TempDir("", "js_test_writefile_*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
t.Run("write new file", func(t *testing.T) {
|
||||
testFile := filepath.Join(tmpDir, "new_file.txt")
|
||||
testContent := "Hello, World!\nThis is a new file.\n特殊字符测试 🌍"
|
||||
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue(testContent)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
|
||||
result := writeFile(call)
|
||||
|
||||
// writeFile returns null on success
|
||||
if !result.IsNull() {
|
||||
t.Error("expected null return value for successful write")
|
||||
}
|
||||
|
||||
// Verify file was created with correct content
|
||||
content, err := ioutil.ReadFile(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read written file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != testContent {
|
||||
t.Errorf("expected content %q, got %q", testContent, string(content))
|
||||
}
|
||||
|
||||
// Check file permissions
|
||||
info, _ := os.Stat(testFile)
|
||||
if runtime.GOOS == "windows" {
|
||||
// On Windows, permissions are different - just check that file exists and is readable
|
||||
if info.Mode()&0400 == 0 {
|
||||
t.Error("expected file to be readable on Windows")
|
||||
}
|
||||
} else {
|
||||
// On Unix-like systems, check exact permissions
|
||||
if info.Mode().Perm() != 0644 {
|
||||
t.Errorf("expected permissions 0644, got %v", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("overwrite existing file", func(t *testing.T) {
|
||||
testFile := filepath.Join(tmpDir, "existing.txt")
|
||||
oldContent := "Old content"
|
||||
newContent := "New content that is longer than the old content"
|
||||
|
||||
// Create initial file
|
||||
ioutil.WriteFile(testFile, []byte(oldContent), 0644)
|
||||
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue(newContent)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
|
||||
result := writeFile(call)
|
||||
|
||||
if !result.IsNull() {
|
||||
t.Error("expected null return value for successful write")
|
||||
}
|
||||
|
||||
// Verify file was overwritten
|
||||
content, _ := ioutil.ReadFile(testFile)
|
||||
if string(content) != newContent {
|
||||
t.Errorf("expected content %q, got %q", newContent, string(content))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write to non-existent directory", func(t *testing.T) {
|
||||
testFile := filepath.Join(tmpDir, "nonexistent", "subdir", "file.txt")
|
||||
testContent := "test"
|
||||
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue(testContent)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
|
||||
result := writeFile(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined when writing to non-existent directory")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write empty content", func(t *testing.T) {
|
||||
testFile := filepath.Join(tmpDir, "empty.txt")
|
||||
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue("")
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
|
||||
result := writeFile(call)
|
||||
|
||||
if !result.IsNull() {
|
||||
t.Error("expected null return value for successful write")
|
||||
}
|
||||
|
||||
// Verify empty file was created
|
||||
content, _ := ioutil.ReadFile(testFile)
|
||||
if len(content) != 0 {
|
||||
t.Errorf("expected empty file, got %d bytes", len(content))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid arguments", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []otto.Value
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []otto.Value{},
|
||||
},
|
||||
{
|
||||
name: "one argument",
|
||||
args: func() []otto.Value {
|
||||
arg, _ := vm.ToValue("file.txt")
|
||||
return []otto.Value{arg}
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "too many arguments",
|
||||
args: func() []otto.Value {
|
||||
arg1, _ := vm.ToValue("file.txt")
|
||||
arg2, _ := vm.ToValue("content")
|
||||
arg3, _ := vm.ToValue("extra")
|
||||
return []otto.Value{arg1, arg2, arg3}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: tt.args,
|
||||
}
|
||||
|
||||
result := writeFile(call)
|
||||
|
||||
// Should return undefined (error)
|
||||
if !result.IsUndefined() {
|
||||
t.Error("expected undefined for invalid arguments")
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("write binary content", func(t *testing.T) {
|
||||
testFile := filepath.Join(tmpDir, "binary.bin")
|
||||
binaryContent := string([]byte{0, 1, 2, 3, 255, 254, 253, 252})
|
||||
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue(binaryContent)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
|
||||
result := writeFile(call)
|
||||
|
||||
if !result.IsNull() {
|
||||
t.Error("expected null return value for successful write")
|
||||
}
|
||||
|
||||
// Verify binary content
|
||||
content, _ := ioutil.ReadFile(testFile)
|
||||
if string(content) != binaryContent {
|
||||
t.Error("binary content mismatch")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileSystemIntegration(t *testing.T) {
|
||||
vm := otto.New()
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tmpDir, err := ioutil.TempDir("", "js_test_integration_*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
t.Run("write then read file", func(t *testing.T) {
|
||||
testFile := filepath.Join(tmpDir, "roundtrip.txt")
|
||||
testContent := "Round-trip test content\nLine 2\nLine 3"
|
||||
|
||||
// Write file
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue(testContent)
|
||||
writeCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
|
||||
writeResult := writeFile(writeCall)
|
||||
if !writeResult.IsNull() {
|
||||
t.Fatal("write failed")
|
||||
}
|
||||
|
||||
// Read file back
|
||||
readCall := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile},
|
||||
}
|
||||
|
||||
readResult := readFile(readCall)
|
||||
if readResult.IsUndefined() {
|
||||
t.Fatal("read failed")
|
||||
}
|
||||
|
||||
readContent, _ := readResult.ToString()
|
||||
if readContent != testContent {
|
||||
t.Errorf("round-trip failed: expected %q, got %q", testContent, readContent)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create files then list directory", func(t *testing.T) {
|
||||
// Create multiple files
|
||||
files := []string{"file1.txt", "file2.txt", "file3.txt"}
|
||||
for _, name := range files {
|
||||
path := filepath.Join(tmpDir, name)
|
||||
argFile, _ := vm.ToValue(path)
|
||||
argContent, _ := vm.ToValue("content of " + name)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
writeFile(call)
|
||||
}
|
||||
|
||||
// List directory
|
||||
argDir, _ := vm.ToValue(tmpDir)
|
||||
listCall := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: []otto.Value{argDir},
|
||||
}
|
||||
|
||||
listResult := readDir(listCall)
|
||||
if listResult.IsUndefined() {
|
||||
t.Fatal("readDir failed")
|
||||
}
|
||||
|
||||
export, _ := listResult.Export()
|
||||
entries, _ := export.([]string)
|
||||
|
||||
// Check all files are listed
|
||||
for _, expected := range files {
|
||||
found := false
|
||||
for _, entry := range entries {
|
||||
if entry == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected file %s not found in directory listing", expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkReadFile(b *testing.B) {
|
||||
vm := otto.New()
|
||||
|
||||
// Create test file
|
||||
tmpFile, _ := ioutil.TempFile("", "bench_readfile_*")
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
content := strings.Repeat("Benchmark test content line\n", 100)
|
||||
ioutil.WriteFile(tmpFile.Name(), []byte(content), 0644)
|
||||
|
||||
arg, _ := vm.ToValue(tmpFile.Name())
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = readFile(call)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteFile(b *testing.B) {
|
||||
vm := otto.New()
|
||||
|
||||
tmpDir, _ := ioutil.TempDir("", "bench_writefile_*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
content := strings.Repeat("Benchmark test content line\n", 100)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testFile := filepath.Join(tmpDir, fmt.Sprintf("bench_%d.txt", i))
|
||||
argFile, _ := vm.ToValue(testFile)
|
||||
argContent, _ := vm.ToValue(content)
|
||||
call := otto.FunctionCall{
|
||||
ArgumentList: []otto.Value{argFile, argContent},
|
||||
}
|
||||
_ = writeFile(call)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkReadDir(b *testing.B) {
|
||||
vm := otto.New()
|
||||
|
||||
// Create test directory with files
|
||||
tmpDir, _ := ioutil.TempDir("", "bench_readdir_*")
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create 100 files
|
||||
for i := 0; i < 100; i++ {
|
||||
name := filepath.Join(tmpDir, fmt.Sprintf("file_%d.txt", i))
|
||||
ioutil.WriteFile(name, []byte("test"), 0644)
|
||||
}
|
||||
|
||||
arg, _ := vm.ToValue(tmpDir)
|
||||
call := otto.FunctionCall{
|
||||
Otto: vm,
|
||||
ArgumentList: []otto.Value{arg},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = readDir(call)
|
||||
}
|
||||
}
|
||||
155
js/http.go
Normal file
155
js/http.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
type httpPackage struct {
|
||||
}
|
||||
|
||||
type httpResponse struct {
|
||||
Error error
|
||||
Response *http.Response
|
||||
Raw []byte
|
||||
Body string
|
||||
JSON interface{}
|
||||
}
|
||||
|
||||
func (c httpPackage) Encode(s string) string {
|
||||
return url.QueryEscape(s)
|
||||
}
|
||||
|
||||
func (c httpPackage) Request(method string, uri string,
|
||||
headers map[string]string,
|
||||
form map[string]string,
|
||||
json string) httpResponse {
|
||||
var reader io.Reader
|
||||
|
||||
if form != nil {
|
||||
data := url.Values{}
|
||||
for k, v := range form {
|
||||
data.Set(k, v)
|
||||
}
|
||||
reader = bytes.NewBufferString(data.Encode())
|
||||
} else if json != "" {
|
||||
reader = strings.NewReader(json)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, uri, reader)
|
||||
if err != nil {
|
||||
return httpResponse{Error: err}
|
||||
}
|
||||
|
||||
if form != nil {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else if json != "" {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
for name, value := range headers {
|
||||
req.Header.Add(name, value)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return httpResponse{Error: err}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return httpResponse{Error: err}
|
||||
}
|
||||
|
||||
res := httpResponse{
|
||||
Response: resp,
|
||||
Raw: raw,
|
||||
Body: string(raw),
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
res.Error = fmt.Errorf("%s", resp.Status)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (c httpPackage) Get(url string, headers map[string]string) httpResponse {
|
||||
return c.Request("GET", url, headers, nil, "")
|
||||
}
|
||||
|
||||
func (c httpPackage) PostForm(url string, headers map[string]string, form map[string]string) httpResponse {
|
||||
return c.Request("POST", url, headers, form, "")
|
||||
}
|
||||
|
||||
func (c httpPackage) PostJSON(url string, headers map[string]string, json string) httpResponse {
|
||||
return c.Request("POST", url, headers, nil, json)
|
||||
}
|
||||
|
||||
func httpRequest(call otto.FunctionCall) otto.Value {
|
||||
argv := call.ArgumentList
|
||||
argc := len(argv)
|
||||
if argc < 2 {
|
||||
return ReportError("httpRequest: expected 2 or more, %d given instead.", argc)
|
||||
}
|
||||
|
||||
method := argv[0].String()
|
||||
url := argv[1].String()
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest(method, url, nil)
|
||||
if argc >= 3 {
|
||||
data := argv[2].String()
|
||||
req, err = http.NewRequest(method, url, bytes.NewBuffer([]byte(data)))
|
||||
if err != nil {
|
||||
return ReportError("Could create request to url %s: %s", url, err)
|
||||
}
|
||||
|
||||
if argc > 3 {
|
||||
headers := argv[3].Object()
|
||||
for _, key := range headers.Keys() {
|
||||
v, err := headers.Get(key)
|
||||
if err != nil {
|
||||
return ReportError("Could add header %s to request: %s", key, err)
|
||||
}
|
||||
req.Header.Add(key, v.String())
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
return ReportError("Could create request to url %s: %s", url, err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ReportError("Could not request url %s: %s", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ReportError("Could not read response: %s", err)
|
||||
}
|
||||
|
||||
object, err := otto.New().Object("({})")
|
||||
if err != nil {
|
||||
return ReportError("Could not create response object: %s", err)
|
||||
}
|
||||
|
||||
err = object.Set("body", string(body))
|
||||
if err != nil {
|
||||
return ReportError("Could not populate response object: %s", err)
|
||||
}
|
||||
|
||||
v, err := otto.ToValue(object)
|
||||
if err != nil {
|
||||
return ReportError("Could not convert to object: %s", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
45
js/init.go
Normal file
45
js/init.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/evilsocket/islazy/plugin"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
var NullValue = otto.Value{}
|
||||
|
||||
func ReportError(format string, args ...interface{}) otto.Value {
|
||||
log.Error(format, args...)
|
||||
return NullValue
|
||||
}
|
||||
|
||||
func init() {
|
||||
// TODO: refactor this in packages
|
||||
|
||||
plugin.Defines["readDir"] = readDir
|
||||
plugin.Defines["readFile"] = readFile
|
||||
plugin.Defines["writeFile"] = writeFile
|
||||
|
||||
plugin.Defines["log"] = flog
|
||||
plugin.Defines["log_debug"] = log_debug
|
||||
plugin.Defines["log_info"] = log_info
|
||||
plugin.Defines["log_warn"] = log_warn
|
||||
plugin.Defines["log_error"] = log_error
|
||||
plugin.Defines["log_fatal"] = log_fatal
|
||||
|
||||
plugin.Defines["Crypto"] = map[string]interface{}{
|
||||
"sha1": cryptoSha1,
|
||||
}
|
||||
|
||||
plugin.Defines["btoa"] = btoa
|
||||
plugin.Defines["atob"] = atob
|
||||
plugin.Defines["gzipCompress"] = gzipCompress
|
||||
plugin.Defines["gzipDecompress"] = gzipDecompress
|
||||
plugin.Defines["textEncode"] = textEncode
|
||||
plugin.Defines["textDecode"] = textDecode
|
||||
|
||||
plugin.Defines["httpRequest"] = httpRequest
|
||||
plugin.Defines["http"] = httpPackage{}
|
||||
|
||||
plugin.Defines["random"] = randomPackage{}
|
||||
}
|
||||
48
js/log.go
Normal file
48
js/log.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
func flog(call otto.FunctionCall) otto.Value {
|
||||
for _, v := range call.ArgumentList {
|
||||
log.Info("%s", v.String())
|
||||
}
|
||||
return otto.Value{}
|
||||
}
|
||||
|
||||
func log_debug(call otto.FunctionCall) otto.Value {
|
||||
for _, v := range call.ArgumentList {
|
||||
log.Debug("%s", v.String())
|
||||
}
|
||||
return otto.Value{}
|
||||
}
|
||||
|
||||
func log_info(call otto.FunctionCall) otto.Value {
|
||||
for _, v := range call.ArgumentList {
|
||||
log.Info("%s", v.String())
|
||||
}
|
||||
return otto.Value{}
|
||||
}
|
||||
|
||||
func log_warn(call otto.FunctionCall) otto.Value {
|
||||
for _, v := range call.ArgumentList {
|
||||
log.Warning("%s", v.String())
|
||||
}
|
||||
return otto.Value{}
|
||||
}
|
||||
|
||||
func log_error(call otto.FunctionCall) otto.Value {
|
||||
for _, v := range call.ArgumentList {
|
||||
log.Error("%s", v.String())
|
||||
}
|
||||
return otto.Value{}
|
||||
}
|
||||
|
||||
func log_fatal(call otto.FunctionCall) otto.Value {
|
||||
for _, v := range call.ArgumentList {
|
||||
log.Fatal("%s", v.String())
|
||||
}
|
||||
return otto.Value{}
|
||||
}
|
||||
27
js/random.go
Normal file
27
js/random.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
)
|
||||
|
||||
type randomPackage struct {
|
||||
}
|
||||
|
||||
func (c randomPackage) String(size int, charset string) string {
|
||||
runes := []rune(charset)
|
||||
nrunes := len(runes)
|
||||
buf := make([]rune, size)
|
||||
for i := range buf {
|
||||
buf[i] = runes[rand.Intn(nrunes)]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func (c randomPackage) Mac() string {
|
||||
hw := make([]byte, 6)
|
||||
rand.Read(hw)
|
||||
return network.NormalizeMac(net.HardwareAddr(hw).String())
|
||||
}
|
||||
307
js/random_test.go
Normal file
307
js/random_test.go
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
package js
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
r := randomPackage{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
charset string
|
||||
}{
|
||||
{
|
||||
name: "alphanumeric",
|
||||
size: 10,
|
||||
charset: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
},
|
||||
{
|
||||
name: "numbers only",
|
||||
size: 20,
|
||||
charset: "0123456789",
|
||||
},
|
||||
{
|
||||
name: "lowercase letters",
|
||||
size: 15,
|
||||
charset: "abcdefghijklmnopqrstuvwxyz",
|
||||
},
|
||||
{
|
||||
name: "uppercase letters",
|
||||
size: 8,
|
||||
charset: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
size: 12,
|
||||
charset: "!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||
},
|
||||
{
|
||||
name: "unicode characters",
|
||||
size: 5,
|
||||
charset: "αβγδεζηθικλμνξοπρστυφχψω",
|
||||
},
|
||||
{
|
||||
name: "mixed unicode and ascii",
|
||||
size: 10,
|
||||
charset: "abc123αβγ",
|
||||
},
|
||||
{
|
||||
name: "single character",
|
||||
size: 100,
|
||||
charset: "a",
|
||||
},
|
||||
{
|
||||
name: "empty size",
|
||||
size: 0,
|
||||
charset: "abcdef",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := r.String(tt.size, tt.charset)
|
||||
|
||||
// Check length
|
||||
if len([]rune(result)) != tt.size {
|
||||
t.Errorf("expected length %d, got %d", tt.size, len([]rune(result)))
|
||||
}
|
||||
|
||||
// Check that all characters are from the charset
|
||||
for _, char := range result {
|
||||
if !strings.ContainsRune(tt.charset, char) {
|
||||
t.Errorf("character %c not in charset %s", char, tt.charset)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomStringDistribution(t *testing.T) {
|
||||
r := randomPackage{}
|
||||
charset := "ab"
|
||||
size := 1000
|
||||
|
||||
// Generate many single-character strings
|
||||
counts := make(map[rune]int)
|
||||
for i := 0; i < size; i++ {
|
||||
result := r.String(1, charset)
|
||||
if len(result) == 1 {
|
||||
counts[rune(result[0])]++
|
||||
}
|
||||
}
|
||||
|
||||
// Check that both characters appear (very high probability)
|
||||
if len(counts) != 2 {
|
||||
t.Errorf("expected both characters to appear, got %d unique characters", len(counts))
|
||||
}
|
||||
|
||||
// Check distribution is reasonable (not perfect due to randomness)
|
||||
for char, count := range counts {
|
||||
ratio := float64(count) / float64(size)
|
||||
if ratio < 0.3 || ratio > 0.7 {
|
||||
t.Errorf("character %c appeared %d times (%.2f%%), expected around 50%%",
|
||||
char, count, ratio*100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomMac(t *testing.T) {
|
||||
r := randomPackage{}
|
||||
macRegex := regexp.MustCompile(`^([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
|
||||
|
||||
// Generate multiple MAC addresses
|
||||
macs := make(map[string]bool)
|
||||
for i := 0; i < 100; i++ {
|
||||
mac := r.Mac()
|
||||
|
||||
// Check format
|
||||
if !macRegex.MatchString(mac) {
|
||||
t.Errorf("invalid MAC format: %s", mac)
|
||||
}
|
||||
|
||||
// Check it's a valid MAC
|
||||
_, err := net.ParseMAC(mac)
|
||||
if err != nil {
|
||||
t.Errorf("invalid MAC address: %s, error: %v", mac, err)
|
||||
}
|
||||
|
||||
// Store for uniqueness check
|
||||
macs[mac] = true
|
||||
}
|
||||
|
||||
// Check that we get different MACs (very high probability)
|
||||
if len(macs) < 95 {
|
||||
t.Errorf("expected at least 95 unique MACs out of 100, got %d", len(macs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomMacNormalization(t *testing.T) {
|
||||
r := randomPackage{}
|
||||
|
||||
// Generate several MACs and check they're normalized
|
||||
for i := 0; i < 10; i++ {
|
||||
mac := r.Mac()
|
||||
|
||||
// Check lowercase
|
||||
if mac != strings.ToLower(mac) {
|
||||
t.Errorf("MAC not normalized to lowercase: %s", mac)
|
||||
}
|
||||
|
||||
// Check separator is colon
|
||||
if strings.Contains(mac, "-") {
|
||||
t.Errorf("MAC contains hyphen instead of colon: %s", mac)
|
||||
}
|
||||
|
||||
// Check length
|
||||
if len(mac) != 17 { // 6 bytes * 2 chars + 5 colons
|
||||
t.Errorf("MAC has wrong length: %s (len=%d)", mac, len(mac))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomStringEdgeCases(t *testing.T) {
|
||||
r := randomPackage{}
|
||||
|
||||
// Test with various edge cases
|
||||
tests := []struct {
|
||||
name string
|
||||
size int
|
||||
charset string
|
||||
}{
|
||||
{
|
||||
name: "zero size",
|
||||
size: 0,
|
||||
charset: "abc",
|
||||
},
|
||||
{
|
||||
name: "very large size",
|
||||
size: 10000,
|
||||
charset: "abc",
|
||||
},
|
||||
{
|
||||
name: "size larger than charset",
|
||||
size: 10,
|
||||
charset: "ab",
|
||||
},
|
||||
{
|
||||
name: "single char charset with large size",
|
||||
size: 1000,
|
||||
charset: "x",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := r.String(tt.size, tt.charset)
|
||||
|
||||
if len([]rune(result)) != tt.size {
|
||||
t.Errorf("expected length %d, got %d", tt.size, len([]rune(result)))
|
||||
}
|
||||
|
||||
// Check all characters are from charset
|
||||
for _, c := range result {
|
||||
if !strings.ContainsRune(tt.charset, c) {
|
||||
t.Errorf("character %c not in charset %s", c, tt.charset)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomStringNegativeSize(t *testing.T) {
|
||||
r := randomPackage{}
|
||||
|
||||
// Test that negative size causes panic
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for negative size but didn't get one")
|
||||
}
|
||||
}()
|
||||
|
||||
// This should panic
|
||||
_ = r.String(-1, "abc")
|
||||
}
|
||||
|
||||
func TestRandomPackageInstance(t *testing.T) {
|
||||
// Test that we can create multiple instances
|
||||
r1 := randomPackage{}
|
||||
r2 := randomPackage{}
|
||||
|
||||
// Both should work independently
|
||||
s1 := r1.String(5, "abc")
|
||||
s2 := r2.String(5, "xyz")
|
||||
|
||||
if len(s1) != 5 {
|
||||
t.Errorf("r1.String returned wrong length: %d", len(s1))
|
||||
}
|
||||
if len(s2) != 5 {
|
||||
t.Errorf("r2.String returned wrong length: %d", len(s2))
|
||||
}
|
||||
|
||||
// Check correct charset usage
|
||||
for _, c := range s1 {
|
||||
if !strings.ContainsRune("abc", c) {
|
||||
t.Errorf("r1 produced character outside charset: %c", c)
|
||||
}
|
||||
}
|
||||
for _, c := range s2 {
|
||||
if !strings.ContainsRune("xyz", c) {
|
||||
t.Errorf("r2 produced character outside charset: %c", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomString(b *testing.B) {
|
||||
r := randomPackage{}
|
||||
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
b.Run("size-10", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String(10, charset)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("size-100", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String(100, charset)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("size-1000", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String(1000, charset)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkRandomMac(b *testing.B) {
|
||||
r := randomPackage{}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.Mac()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRandomStringCharsets(b *testing.B) {
|
||||
r := randomPackage{}
|
||||
|
||||
charsets := map[string]string{
|
||||
"small": "abc",
|
||||
"medium": "abcdefghijklmnopqrstuvwxyz",
|
||||
"large": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?",
|
||||
"unicode": "αβγδεζηθικλμνξοπρστυφχψωABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
}
|
||||
|
||||
for name, charset := range charsets {
|
||||
b.Run(name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = r.String(20, charset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
28
log/log.go
28
log/log.go
|
|
@ -1,27 +1,39 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/session"
|
||||
|
||||
ll "github.com/evilsocket/islazy/log"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
)
|
||||
|
||||
type logFunction func(level log.Verbosity, format string, args ...interface{})
|
||||
|
||||
var Logger = (logFunction)(nil)
|
||||
|
||||
func Debug(format string, args ...interface{}) {
|
||||
session.I.Events.Log(ll.DEBUG, format, args...)
|
||||
if Logger != nil {
|
||||
Logger(log.DEBUG, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Info(format string, args ...interface{}) {
|
||||
session.I.Events.Log(ll.INFO, format, args...)
|
||||
if Logger != nil {
|
||||
Logger(log.INFO, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Warning(format string, args ...interface{}) {
|
||||
session.I.Events.Log(ll.WARNING, format, args...)
|
||||
if Logger != nil {
|
||||
Logger(log.WARNING, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Error(format string, args ...interface{}) {
|
||||
session.I.Events.Log(ll.ERROR, format, args...)
|
||||
if Logger != nil {
|
||||
Logger(log.ERROR, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func Fatal(format string, args ...interface{}) {
|
||||
session.I.Events.Log(ll.FATAL, format, args...)
|
||||
if Logger != nil {
|
||||
Logger(log.FATAL, format, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
log/log_test.go
Normal file
106
log/log_test.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package log
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/evilsocket/islazy/log"
|
||||
)
|
||||
|
||||
var called bool
|
||||
var calledLevel log.Verbosity
|
||||
var calledFormat string
|
||||
var calledArgs []interface{}
|
||||
|
||||
func mockLogger(level log.Verbosity, format string, args ...interface{}) {
|
||||
called = true
|
||||
calledLevel = level
|
||||
calledFormat = format
|
||||
calledArgs = args
|
||||
}
|
||||
|
||||
func reset() {
|
||||
called = false
|
||||
calledLevel = log.DEBUG
|
||||
calledFormat = ""
|
||||
calledArgs = nil
|
||||
}
|
||||
|
||||
func TestLoggerNil(t *testing.T) {
|
||||
reset()
|
||||
Logger = nil
|
||||
|
||||
Debug("test")
|
||||
if called {
|
||||
t.Error("Debug should not call if Logger is nil")
|
||||
}
|
||||
|
||||
Info("test")
|
||||
if called {
|
||||
t.Error("Info should not call if Logger is nil")
|
||||
}
|
||||
|
||||
Warning("test")
|
||||
if called {
|
||||
t.Error("Warning should not call if Logger is nil")
|
||||
}
|
||||
|
||||
Error("test")
|
||||
if called {
|
||||
t.Error("Error should not call if Logger is nil")
|
||||
}
|
||||
|
||||
Fatal("test")
|
||||
if called {
|
||||
t.Error("Fatal should not call if Logger is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDebug(t *testing.T) {
|
||||
reset()
|
||||
Logger = mockLogger
|
||||
|
||||
Debug("test %d", 42)
|
||||
if !called || calledLevel != log.DEBUG || calledFormat != "test %d" || len(calledArgs) != 1 || calledArgs[0] != 42 {
|
||||
t.Errorf("Debug not called correctly: level=%v format=%s args=%v", calledLevel, calledFormat, calledArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
reset()
|
||||
Logger = mockLogger
|
||||
|
||||
Info("test %s", "info")
|
||||
if !called || calledLevel != log.INFO || calledFormat != "test %s" || len(calledArgs) != 1 || calledArgs[0] != "info" {
|
||||
t.Errorf("Info not called correctly: level=%v format=%s args=%v", calledLevel, calledFormat, calledArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarning(t *testing.T) {
|
||||
reset()
|
||||
Logger = mockLogger
|
||||
|
||||
Warning("test %f", 3.14)
|
||||
if !called || calledLevel != log.WARNING || calledFormat != "test %f" || len(calledArgs) != 1 || calledArgs[0] != 3.14 {
|
||||
t.Errorf("Warning not called correctly: level=%v format=%s args=%v", calledLevel, calledFormat, calledArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
reset()
|
||||
Logger = mockLogger
|
||||
|
||||
Error("test error")
|
||||
if !called || calledLevel != log.ERROR || calledFormat != "test error" || len(calledArgs) != 0 {
|
||||
t.Errorf("Error not called correctly: level=%v format=%s args=%v", calledLevel, calledFormat, calledArgs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFatal(t *testing.T) {
|
||||
reset()
|
||||
Logger = mockLogger
|
||||
|
||||
Fatal("test fatal")
|
||||
if !called || calledLevel != log.FATAL || calledFormat != "test fatal" || len(calledArgs) != 0 {
|
||||
t.Errorf("Fatal not called correctly: level=%v format=%s args=%v", calledLevel, calledFormat, calledArgs)
|
||||
}
|
||||
}
|
||||
8
main.go
8
main.go
|
|
@ -8,10 +8,10 @@ import (
|
|||
|
||||
"runtime"
|
||||
|
||||
"github.com/bettercap/bettercap/core"
|
||||
"github.com/bettercap/bettercap/log"
|
||||
"github.com/bettercap/bettercap/modules"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/core"
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/bettercap/bettercap/v2/modules"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
|
|
|
|||
88
main_test.go
Normal file
88
main_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExitPrompt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "yes lowercase",
|
||||
input: "y\n",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "yes uppercase",
|
||||
input: "Y\n",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "no lowercase",
|
||||
input: "n\n",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "no uppercase",
|
||||
input: "N\n",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "invalid input",
|
||||
input: "maybe\n",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "\n",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := strings.NewReader(tt.input)
|
||||
r := bytes.NewReader([]byte(tt.input))
|
||||
|
||||
// Mock stdin by reading from our buffer
|
||||
// This is a simplified test - in production you'd want to properly mock stdin
|
||||
_ = oldStdin
|
||||
_ = r
|
||||
|
||||
// For now, we'll test the string comparison logic directly
|
||||
input := strings.TrimSpace(strings.TrimSuffix(tt.input, "\n"))
|
||||
result := strings.ToLower(input) == "y"
|
||||
|
||||
if result != tt.expected {
|
||||
t.Errorf("exitPrompt() with input %q = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test some utility functions that would be refactored from main
|
||||
func TestVersionString(t *testing.T) {
|
||||
// This tests the version string formatting logic
|
||||
version := "2.32.0"
|
||||
os := "darwin"
|
||||
arch := "amd64"
|
||||
goVersion := "go1.19"
|
||||
|
||||
expected := "bettercap v2.32.0 (built for darwin amd64 with go1.19)"
|
||||
result := formatVersion("bettercap", version, os, arch, goVersion)
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("formatVersion() = %v, want %v", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function that would be refactored from main
|
||||
func formatVersion(name, version, os, arch, goVersion string) string {
|
||||
return name + " v" + version + " (built for " + os + " " + arch + " with " + goVersion + ")"
|
||||
}
|
||||
|
|
@ -1,13 +1,20 @@
|
|||
package any_proxy
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/firewall"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/firewall"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
||||
type AnyProxy struct {
|
||||
session.SessionModule
|
||||
Redirection *firewall.Redirection
|
||||
// not using map[int]*firewall.Redirection to preserve order
|
||||
ports []int
|
||||
redirections []*firewall.Redirection
|
||||
}
|
||||
|
||||
func NewAnyProxy(s *session.Session) *AnyProxy {
|
||||
|
|
@ -25,9 +32,11 @@ func NewAnyProxy(s *session.Session) *AnyProxy {
|
|||
"(TCP|UDP)",
|
||||
"Proxy protocol."))
|
||||
|
||||
mod.AddParam(session.NewIntParameter("any.proxy.src_port",
|
||||
mod.AddParam(session.NewStringParameter("any.proxy.src_port",
|
||||
"80",
|
||||
"Remote port to redirect when the module is activated."))
|
||||
"",
|
||||
"Remote port to redirect when the module is activated, "+
|
||||
"also supported a comma separated list of ports and/or port-ranges."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("any.proxy.src_address",
|
||||
"",
|
||||
|
|
@ -36,7 +45,7 @@ func NewAnyProxy(s *session.Session) *AnyProxy {
|
|||
|
||||
mod.AddParam(session.NewStringParameter("any.proxy.dst_address",
|
||||
session.ParamIfaceAddress,
|
||||
session.IPv4Validator,
|
||||
"",
|
||||
"Address where the proxy is listening."))
|
||||
|
||||
mod.AddParam(session.NewIntParameter("any.proxy.dst_port",
|
||||
|
|
@ -72,7 +81,7 @@ func (mod *AnyProxy) Author() string {
|
|||
|
||||
func (mod *AnyProxy) Configure() error {
|
||||
var err error
|
||||
var srcPort int
|
||||
var srcPorts string
|
||||
var dstPort int
|
||||
var iface string
|
||||
var protocol string
|
||||
|
|
@ -85,8 +94,6 @@ func (mod *AnyProxy) Configure() error {
|
|||
return err
|
||||
} else if err, protocol = mod.StringParam("any.proxy.protocol"); err != nil {
|
||||
return err
|
||||
} else if err, srcPort = mod.IntParam("any.proxy.src_port"); err != nil {
|
||||
return err
|
||||
} else if err, dstPort = mod.IntParam("any.proxy.dst_port"); err != nil {
|
||||
return err
|
||||
} else if err, srcAddress = mod.StringParam("any.proxy.src_address"); err != nil {
|
||||
|
|
@ -95,27 +102,71 @@ func (mod *AnyProxy) Configure() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err, srcPorts = mod.StringParam("any.proxy.src_port"); err != nil {
|
||||
return err
|
||||
} else {
|
||||
var ports []int
|
||||
// srcPorts can be a single port, a list of ports or a list of ranges, or a mix.
|
||||
for _, token := range str.Comma(str.Trim(srcPorts)) {
|
||||
if p, err := strconv.Atoi(token); err == nil {
|
||||
// simple case, integer port
|
||||
ports = append(ports, p)
|
||||
} else if strings.Contains(token, "-") {
|
||||
// port range
|
||||
if parts := strings.Split(token, "-"); len(parts) == 2 {
|
||||
if from, err := strconv.Atoi(str.Trim(parts[0])); err != nil {
|
||||
return fmt.Errorf("invalid start port %s: %s", parts[0], err)
|
||||
} else if from < 1 || from > 65535 {
|
||||
return fmt.Errorf("port %s out of valid range", parts[0])
|
||||
} else if to, err := strconv.Atoi(str.Trim(parts[1])); err != nil {
|
||||
return fmt.Errorf("invalid end port %s: %s", parts[1], err)
|
||||
} else if to < 1 || to > 65535 {
|
||||
return fmt.Errorf("port %s out of valid range", parts[1])
|
||||
} else if from > to {
|
||||
return fmt.Errorf("start port should be lower than end port")
|
||||
} else {
|
||||
for p := from; p <= to; p++ {
|
||||
ports = append(ports, p)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("can't parse '%s' as range", token)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("can't parse '%s' as port or range", token)
|
||||
}
|
||||
}
|
||||
|
||||
// after parsing and validation, create a redirection per source port
|
||||
mod.ports = ports
|
||||
mod.redirections = nil
|
||||
for _, port := range mod.ports {
|
||||
redir := firewall.NewRedirection(iface,
|
||||
protocol,
|
||||
port,
|
||||
dstAddress,
|
||||
dstPort)
|
||||
|
||||
if srcAddress != "" {
|
||||
redir.SrcAddress = srcAddress
|
||||
}
|
||||
|
||||
mod.redirections = append(mod.redirections, redir)
|
||||
}
|
||||
}
|
||||
|
||||
if !mod.Session.Firewall.IsForwardingEnabled() {
|
||||
mod.Info("Enabling forwarding.")
|
||||
mod.Session.Firewall.EnableForwarding(true)
|
||||
}
|
||||
|
||||
mod.Redirection = firewall.NewRedirection(iface,
|
||||
protocol,
|
||||
srcPort,
|
||||
dstAddress,
|
||||
dstPort)
|
||||
|
||||
if srcAddress != "" {
|
||||
mod.Redirection.SrcAddress = srcAddress
|
||||
for _, redir := range mod.redirections {
|
||||
if err := mod.Session.Firewall.EnableRedirection(redir, true); err != nil {
|
||||
return err
|
||||
}
|
||||
mod.Info("applied redirection %s", redir.String())
|
||||
}
|
||||
|
||||
if err := mod.Session.Firewall.EnableRedirection(mod.Redirection, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.Info("Applied redirection %s", mod.Redirection.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -128,13 +179,11 @@ func (mod *AnyProxy) Start() error {
|
|||
}
|
||||
|
||||
func (mod *AnyProxy) Stop() error {
|
||||
if mod.Redirection != nil {
|
||||
mod.Info("Disabling redirection %s", mod.Redirection.String())
|
||||
if err := mod.Session.Firewall.EnableRedirection(mod.Redirection, false); err != nil {
|
||||
for _, redir := range mod.redirections {
|
||||
mod.Info("disabling redirection %s", redir.String())
|
||||
if err := mod.Session.Firewall.EnableRedirection(redir, false); err != nil {
|
||||
return err
|
||||
}
|
||||
mod.Redirection = nil
|
||||
}
|
||||
|
||||
return mod.SetRunning(false, func() {})
|
||||
}
|
||||
|
|
|
|||
218
modules/any_proxy/any_proxy_test.go
Normal file
218
modules/any_proxy/any_proxy_test.go
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
package any_proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
)
|
||||
|
||||
var (
|
||||
testSession *session.Session
|
||||
sessionOnce sync.Once
|
||||
)
|
||||
|
||||
func createMockSession(t *testing.T) *session.Session {
|
||||
sessionOnce.Do(func() {
|
||||
var err error
|
||||
testSession, err = session.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
})
|
||||
return testSession
|
||||
}
|
||||
|
||||
func TestNewAnyProxy(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewAnyProxy(s)
|
||||
|
||||
if mod == nil {
|
||||
t.Fatal("NewAnyProxy returned nil")
|
||||
}
|
||||
|
||||
if mod.Name() != "any.proxy" {
|
||||
t.Errorf("Expected name 'any.proxy', got '%s'", mod.Name())
|
||||
}
|
||||
|
||||
if mod.Author() != "Simone Margaritelli <evilsocket@gmail.com>" {
|
||||
t.Errorf("Unexpected author: %s", mod.Author())
|
||||
}
|
||||
|
||||
if mod.Description() == "" {
|
||||
t.Error("Empty description")
|
||||
}
|
||||
|
||||
// Check handlers
|
||||
handlers := mod.Handlers()
|
||||
if len(handlers) != 2 {
|
||||
t.Errorf("Expected 2 handlers, got %d", len(handlers))
|
||||
}
|
||||
|
||||
handlerNames := make(map[string]bool)
|
||||
for _, h := range handlers {
|
||||
handlerNames[h.Name] = true
|
||||
}
|
||||
|
||||
if !handlerNames["any.proxy on"] {
|
||||
t.Error("Handler 'any.proxy on' not found")
|
||||
}
|
||||
if !handlerNames["any.proxy off"] {
|
||||
t.Error("Handler 'any.proxy off' not found")
|
||||
}
|
||||
|
||||
// Check that parameters were added (but don't try to get values as that requires session interface)
|
||||
expectedParams := 6 // iface, protocol, src_port, src_address, dst_address, dst_port
|
||||
// This is a simplified check - in a real test we'd mock the interface
|
||||
_ = expectedParams
|
||||
}
|
||||
|
||||
// Test port parsing logic directly
|
||||
func TestPortParsingLogic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
portString string
|
||||
expectPorts []int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "single port",
|
||||
portString: "80",
|
||||
expectPorts: []int{80},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "multiple ports",
|
||||
portString: "80,443,8080",
|
||||
expectPorts: []int{80, 443, 8080},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "port range",
|
||||
portString: "8000-8003",
|
||||
expectPorts: []int{8000, 8001, 8002, 8003},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
portString: "not-a-port",
|
||||
expectPorts: nil,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ports, err := parsePortsString(tt.portString)
|
||||
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Error("Expected error but got none")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
} else {
|
||||
if len(ports) != len(tt.expectPorts) {
|
||||
t.Errorf("Expected %d ports, got %d", len(tt.expectPorts), len(ports))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to test port parsing logic
|
||||
func parsePortsString(portsStr string) ([]int, error) {
|
||||
var ports []int
|
||||
tokens := strings.Split(strings.ReplaceAll(portsStr, " ", ""), ",")
|
||||
|
||||
for _, token := range tokens {
|
||||
if token == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if p, err := strconv.Atoi(token); err == nil {
|
||||
if p < 1 || p > 65535 {
|
||||
return nil, fmt.Errorf("port %d out of range", p)
|
||||
}
|
||||
ports = append(ports, p)
|
||||
} else if strings.Contains(token, "-") {
|
||||
parts := strings.Split(token, "-")
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("invalid range format")
|
||||
}
|
||||
|
||||
from, err1 := strconv.Atoi(parts[0])
|
||||
to, err2 := strconv.Atoi(parts[1])
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
return nil, fmt.Errorf("invalid range values")
|
||||
}
|
||||
|
||||
if from < 1 || from > 65535 || to < 1 || to > 65535 {
|
||||
return nil, fmt.Errorf("port range out of bounds")
|
||||
}
|
||||
|
||||
if from > to {
|
||||
return nil, fmt.Errorf("invalid range order")
|
||||
}
|
||||
|
||||
for p := from; p <= to; p++ {
|
||||
ports = append(ports, p)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid port format: %s", token)
|
||||
}
|
||||
}
|
||||
|
||||
return ports, nil
|
||||
}
|
||||
|
||||
func TestStartStop(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewAnyProxy(s)
|
||||
|
||||
// Initially should not be running
|
||||
if mod.Running() {
|
||||
t.Error("Module should not be running initially")
|
||||
}
|
||||
|
||||
// Note: Start() will fail because it requires firewall operations
|
||||
// which need proper network setup and possibly root permissions
|
||||
// We're just testing that the methods exist and basic flow
|
||||
}
|
||||
|
||||
// Test error cases in port parsing
|
||||
func TestPortParsingErrors(t *testing.T) {
|
||||
errorCases := []string{
|
||||
"0", // out of range
|
||||
"65536", // out of range
|
||||
"abc", // not a number
|
||||
"80-", // incomplete range
|
||||
"-80", // incomplete range
|
||||
"100-50", // inverted range
|
||||
"80-abc", // invalid end
|
||||
"xyz-100", // invalid start
|
||||
"80--100", // malformed
|
||||
// Remove these as our parser handles empty tokens correctly
|
||||
}
|
||||
|
||||
for _, portStr := range errorCases {
|
||||
_, err := parsePortsString(portStr)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for port string '%s', but got none", portStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkPortParsing(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
parsePortsString("80,443,8000-8010,9000")
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/tls"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/bettercap/bettercap/v2/tls"
|
||||
|
||||
"github.com/bettercap/recording"
|
||||
|
||||
|
|
@ -90,12 +90,12 @@ func NewRestAPI(s *session.Session) *RestAPI {
|
|||
"Value of the Access-Control-Allow-Origin header of the API server."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("api.rest.username",
|
||||
"",
|
||||
"user",
|
||||
"",
|
||||
"API authentication username."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("api.rest.password",
|
||||
"",
|
||||
"pass",
|
||||
"",
|
||||
"API authentication password."))
|
||||
|
||||
|
|
@ -230,6 +230,7 @@ func (mod *RestAPI) Configure() error {
|
|||
}
|
||||
}
|
||||
|
||||
mod.server = &http.Server{}
|
||||
mod.server.Addr = fmt.Sprintf("%s:%d", ip, port)
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
|
|
|||
|
|
@ -5,17 +5,22 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var (
|
||||
ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
|
||||
)
|
||||
|
||||
type CommandRequest struct {
|
||||
Command string `json:"cmd"`
|
||||
}
|
||||
|
|
@ -220,6 +225,10 @@ func (mod *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) {
|
|||
http.Error(w, "Bad Request", 400)
|
||||
}
|
||||
|
||||
rescueStdout := os.Stdout
|
||||
stdoutReader, stdoutWriter, _ := os.Pipe()
|
||||
os.Stdout = stdoutWriter
|
||||
|
||||
for _, aCommand := range session.ParseCommands(cmd.Command) {
|
||||
if err = mod.Session.Run(aCommand); err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
|
|
@ -227,7 +236,12 @@ func (mod *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
mod.toJSON(w, APIResponse{Success: true})
|
||||
stdoutWriter.Close()
|
||||
out, _ := io.ReadAll(stdoutReader)
|
||||
os.Stdout = rescueStdout
|
||||
|
||||
// remove ANSI escape sequences (bash color codes) from output
|
||||
mod.toJSON(w, APIResponse{Success: true, Message: ansiEscapeRegex.ReplaceAllString(string(out), "")})
|
||||
}
|
||||
|
||||
func (mod *RestAPI) getEvents(limit int) []session.Event {
|
||||
|
|
@ -379,7 +393,7 @@ func (mod *RestAPI) readFile(fileName string, w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
func (mod *RestAPI) writeFile(fileName string, w http.ResponseWriter, r *http.Request) {
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
data, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("invalid file upload: %s", err)
|
||||
mod.Warning(msg)
|
||||
|
|
@ -387,7 +401,7 @@ func (mod *RestAPI) writeFile(fileName string, w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fileName, data, 0666)
|
||||
err = os.WriteFile(fileName, data, 0666)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("can't write to %s: %s", fileName, err)
|
||||
mod.Warning(msg)
|
||||
|
|
@ -426,7 +440,14 @@ func (mod *RestAPI) fileRoute(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
fileName := r.URL.Query().Get("name")
|
||||
if fileName, err = fs.Expand(fileName); err != nil {
|
||||
mod.Warning("can't expand %s: %v", fileName, err)
|
||||
http.Error(w, "Bad Request", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if fileName != "" && r.Method == "GET" {
|
||||
mod.readFile(fileName, w, r)
|
||||
|
|
|
|||
671
modules/api_rest/api_rest_test.go
Normal file
671
modules/api_rest/api_rest_test.go
Normal file
|
|
@ -0,0 +1,671 @@
|
|||
package api_rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
)
|
||||
|
||||
var (
|
||||
testSession *session.Session
|
||||
sessionOnce sync.Once
|
||||
)
|
||||
|
||||
func createMockSession(t *testing.T) *session.Session {
|
||||
sessionOnce.Do(func() {
|
||||
var err error
|
||||
testSession, err = session.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
})
|
||||
return testSession
|
||||
}
|
||||
|
||||
func TestNewRestAPI(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
if mod == nil {
|
||||
t.Fatal("NewRestAPI returned nil")
|
||||
}
|
||||
|
||||
if mod.Name() != "api.rest" {
|
||||
t.Errorf("Expected name 'api.rest', got '%s'", mod.Name())
|
||||
}
|
||||
|
||||
if mod.Author() != "Simone Margaritelli <evilsocket@gmail.com>" {
|
||||
t.Errorf("Unexpected author: %s", mod.Author())
|
||||
}
|
||||
|
||||
if mod.Description() == "" {
|
||||
t.Error("Empty description")
|
||||
}
|
||||
|
||||
// Check handlers
|
||||
handlers := mod.Handlers()
|
||||
expectedHandlers := []string{
|
||||
"api.rest on",
|
||||
"api.rest off",
|
||||
"api.rest.record off",
|
||||
"api.rest.record FILENAME",
|
||||
"api.rest.replay off",
|
||||
"api.rest.replay FILENAME",
|
||||
}
|
||||
|
||||
if len(handlers) != len(expectedHandlers) {
|
||||
t.Errorf("Expected %d handlers, got %d", len(expectedHandlers), len(handlers))
|
||||
}
|
||||
|
||||
handlerNames := make(map[string]bool)
|
||||
for _, h := range handlers {
|
||||
handlerNames[h.Name] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedHandlers {
|
||||
if !handlerNames[expected] {
|
||||
t.Errorf("Handler '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// Check initial state
|
||||
if mod.recording {
|
||||
t.Error("Should not be recording initially")
|
||||
}
|
||||
if mod.replaying {
|
||||
t.Error("Should not be replaying initially")
|
||||
}
|
||||
if mod.useWebsocket {
|
||||
t.Error("Should not use websocket by default")
|
||||
}
|
||||
if mod.allowOrigin != "*" {
|
||||
t.Errorf("Expected default allowOrigin '*', got '%s'", mod.allowOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTLS(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Initially should not be TLS
|
||||
if mod.isTLS() {
|
||||
t.Error("Should not be TLS without cert and key")
|
||||
}
|
||||
|
||||
// Set cert and key
|
||||
mod.certFile = "cert.pem"
|
||||
mod.keyFile = "key.pem"
|
||||
|
||||
if !mod.isTLS() {
|
||||
t.Error("Should be TLS with cert and key")
|
||||
}
|
||||
|
||||
// Only cert
|
||||
mod.certFile = "cert.pem"
|
||||
mod.keyFile = ""
|
||||
|
||||
if mod.isTLS() {
|
||||
t.Error("Should not be TLS with only cert")
|
||||
}
|
||||
|
||||
// Only key
|
||||
mod.certFile = ""
|
||||
mod.keyFile = "key.pem"
|
||||
|
||||
if mod.isTLS() {
|
||||
t.Error("Should not be TLS with only key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateStore(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Check that state variables are properly stored
|
||||
stateKeys := []string{
|
||||
"recording",
|
||||
"rec_clock",
|
||||
"replaying",
|
||||
"loading",
|
||||
"load_progress",
|
||||
"rec_time",
|
||||
"rec_filename",
|
||||
"rec_frames",
|
||||
"rec_cur_frame",
|
||||
"rec_started",
|
||||
"rec_stopped",
|
||||
}
|
||||
|
||||
for _, key := range stateKeys {
|
||||
val, exists := mod.State.Load(key)
|
||||
if !exists || val == nil {
|
||||
t.Errorf("State key '%s' not found", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameters(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Check that all parameters are registered
|
||||
paramNames := []string{
|
||||
"api.rest.address",
|
||||
"api.rest.port",
|
||||
"api.rest.alloworigin",
|
||||
"api.rest.username",
|
||||
"api.rest.password",
|
||||
"api.rest.certificate",
|
||||
"api.rest.key",
|
||||
"api.rest.websocket",
|
||||
"api.rest.record.clock",
|
||||
}
|
||||
|
||||
// Parameters are stored in the session environment
|
||||
// We'll just check they can be accessed without error
|
||||
for _, param := range paramNames {
|
||||
// This is a simplified check
|
||||
_ = param
|
||||
}
|
||||
|
||||
// Ensure mod is used
|
||||
if mod == nil {
|
||||
t.Error("Module should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSSessionStructs(t *testing.T) {
|
||||
// Test struct creation
|
||||
req := JSSessionRequest{
|
||||
Command: "test command",
|
||||
}
|
||||
|
||||
if req.Command != "test command" {
|
||||
t.Errorf("Expected command 'test command', got '%s'", req.Command)
|
||||
}
|
||||
|
||||
resp := JSSessionResponse{
|
||||
Error: "test error",
|
||||
}
|
||||
|
||||
if resp.Error != "test error" {
|
||||
t.Errorf("Expected error 'test error', got '%s'", resp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultValues(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Check default values
|
||||
if mod.recClock != 1 {
|
||||
t.Errorf("Expected default recClock 1, got %d", mod.recClock)
|
||||
}
|
||||
|
||||
if mod.recTime != 0 {
|
||||
t.Errorf("Expected default recTime 0, got %d", mod.recTime)
|
||||
}
|
||||
|
||||
if mod.recordFileName != "" {
|
||||
t.Errorf("Expected empty recordFileName, got '%s'", mod.recordFileName)
|
||||
}
|
||||
|
||||
if mod.upgrader.ReadBufferSize != 1024 {
|
||||
t.Errorf("Expected ReadBufferSize 1024, got %d", mod.upgrader.ReadBufferSize)
|
||||
}
|
||||
|
||||
if mod.upgrader.WriteBufferSize != 1024 {
|
||||
t.Errorf("Expected WriteBufferSize 1024, got %d", mod.upgrader.WriteBufferSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunningState(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Initially should not be running
|
||||
if mod.Running() {
|
||||
t.Error("Module should not be running initially")
|
||||
}
|
||||
|
||||
// Note: Cannot test actual Start/Stop without proper server setup
|
||||
}
|
||||
|
||||
func TestRecordingState(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test recording state changes
|
||||
mod.recording = true
|
||||
if !mod.recording {
|
||||
t.Error("Recording flag should be true")
|
||||
}
|
||||
|
||||
mod.recording = false
|
||||
if mod.recording {
|
||||
t.Error("Recording flag should be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplayingState(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test replaying state changes
|
||||
mod.replaying = true
|
||||
if !mod.replaying {
|
||||
t.Error("Replaying flag should be true")
|
||||
}
|
||||
|
||||
mod.replaying = false
|
||||
if mod.replaying {
|
||||
t.Error("Replaying flag should be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureErrors(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test configuration validation
|
||||
testCases := []struct {
|
||||
name string
|
||||
setup func()
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "invalid address",
|
||||
setup: func() {
|
||||
s.Env.Set("api.rest.address", "999.999.999.999")
|
||||
},
|
||||
expected: "address",
|
||||
},
|
||||
{
|
||||
name: "invalid port",
|
||||
setup: func() {
|
||||
s.Env.Set("api.rest.address", "127.0.0.1")
|
||||
s.Env.Set("api.rest.port", "not-a-port")
|
||||
},
|
||||
expected: "port",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.setup()
|
||||
// Configure may fail due to parameter validation
|
||||
_ = mod.Configure()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerConfiguration(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Set valid parameters
|
||||
s.Env.Set("api.rest.address", "127.0.0.1")
|
||||
s.Env.Set("api.rest.port", "8081")
|
||||
s.Env.Set("api.rest.username", "testuser")
|
||||
s.Env.Set("api.rest.password", "testpass")
|
||||
s.Env.Set("api.rest.websocket", "true")
|
||||
s.Env.Set("api.rest.alloworigin", "http://localhost:3000")
|
||||
|
||||
// This might fail due to TLS cert generation, but we're testing the flow
|
||||
_ = mod.Configure()
|
||||
|
||||
// Check that values were set
|
||||
if mod.username != "" && mod.username != "testuser" {
|
||||
t.Logf("Username set to: %s", mod.username)
|
||||
}
|
||||
if mod.password != "" && mod.password != "testpass" {
|
||||
t.Logf("Password set to: %s", mod.password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuitChannel(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test quit channel is created
|
||||
if mod.quit == nil {
|
||||
t.Error("Quit channel should not be nil")
|
||||
}
|
||||
|
||||
// Test sending to quit channel doesn't block
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
select {
|
||||
case mod.quit <- true:
|
||||
done <- true
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
done <- false
|
||||
}
|
||||
}()
|
||||
|
||||
// Start reading from quit channel
|
||||
go func() {
|
||||
<-mod.quit
|
||||
}()
|
||||
|
||||
if !<-done {
|
||||
t.Error("Sending to quit channel timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordWaitGroup(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test wait group is initialized
|
||||
if mod.recordWait == nil {
|
||||
t.Error("Record wait group should not be nil")
|
||||
}
|
||||
|
||||
// Test wait group operations
|
||||
mod.recordWait.Add(1)
|
||||
done := make(chan bool)
|
||||
|
||||
go func() {
|
||||
mod.recordWait.Done()
|
||||
done <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
mod.recordWait.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Success
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Error("Wait group operation timed out")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartErrors(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test start when replaying
|
||||
mod.replaying = true
|
||||
err := mod.Start()
|
||||
if err == nil {
|
||||
t.Error("Expected error when starting while replaying")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureAlreadyRunning(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Simulate running state
|
||||
mod.SetRunning(true, func() {})
|
||||
|
||||
err := mod.Configure()
|
||||
if err == nil {
|
||||
t.Error("Expected error when configuring while running")
|
||||
}
|
||||
|
||||
// Reset
|
||||
mod.SetRunning(false, func() {})
|
||||
}
|
||||
|
||||
func TestServerAddr(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Set parameters
|
||||
s.Env.Set("api.rest.address", "192.168.1.100")
|
||||
s.Env.Set("api.rest.port", "9090")
|
||||
|
||||
// Configure may fail but we can check server addr format
|
||||
_ = mod.Configure()
|
||||
|
||||
expectedAddr := "192.168.1.100:9090"
|
||||
if mod.server != nil && mod.server.Addr != "" && mod.server.Addr != expectedAddr {
|
||||
t.Logf("Server addr: %s", mod.server.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTLSConfiguration(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Test with TLS params
|
||||
s.Env.Set("api.rest.certificate", "/tmp/test.crt")
|
||||
s.Env.Set("api.rest.key", "/tmp/test.key")
|
||||
|
||||
// Configure will attempt to expand paths and check files
|
||||
_ = mod.Configure()
|
||||
|
||||
// Just verify the attempt was made
|
||||
t.Logf("Attempted TLS configuration")
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkNewRestAPI(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = NewRestAPI(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsTLS(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
mod := NewRestAPI(s)
|
||||
mod.certFile = "cert.pem"
|
||||
mod.keyFile = "key.pem"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = mod.isTLS()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConfigure(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mod := NewRestAPI(s)
|
||||
s.Env.Set("api.rest.address", "127.0.0.1")
|
||||
s.Env.Set("api.rest.port", "8081")
|
||||
_ = mod.Configure()
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for controller functionality
|
||||
func TestCommandRequest(t *testing.T) {
|
||||
cmd := CommandRequest{
|
||||
Command: "help",
|
||||
}
|
||||
|
||||
if cmd.Command != "help" {
|
||||
t.Errorf("Expected command 'help', got '%s'", cmd.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIResponse(t *testing.T) {
|
||||
resp := APIResponse{
|
||||
Success: true,
|
||||
Message: "Operation completed",
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Error("Expected success to be true")
|
||||
}
|
||||
|
||||
if resp.Message != "Operation completed" {
|
||||
t.Errorf("Expected message 'Operation completed', got '%s'", resp.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthNoCredentials(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// No username/password set - should allow access
|
||||
req, _ := http.NewRequest("GET", "/test", nil)
|
||||
|
||||
if !mod.checkAuth(req) {
|
||||
t.Error("Expected auth to pass with no credentials set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuthWithCredentials(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Set credentials
|
||||
mod.username = "testuser"
|
||||
mod.password = "testpass"
|
||||
|
||||
// Test without auth header
|
||||
req1, _ := http.NewRequest("GET", "/test", nil)
|
||||
if mod.checkAuth(req1) {
|
||||
t.Error("Expected auth to fail without credentials")
|
||||
}
|
||||
|
||||
// Test with wrong credentials
|
||||
req2, _ := http.NewRequest("GET", "/test", nil)
|
||||
req2.SetBasicAuth("wronguser", "wrongpass")
|
||||
if mod.checkAuth(req2) {
|
||||
t.Error("Expected auth to fail with wrong credentials")
|
||||
}
|
||||
|
||||
// Test with correct credentials
|
||||
req3, _ := http.NewRequest("GET", "/test", nil)
|
||||
req3.SetBasicAuth("testuser", "testpass")
|
||||
if !mod.checkAuth(req3) {
|
||||
t.Error("Expected auth to pass with correct credentials")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEventsEmpty(t *testing.T) {
|
||||
// Skip this test if running with others due to shared session state
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping in short mode due to shared session state")
|
||||
}
|
||||
|
||||
// Create a fresh session using the singleton
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Record initial event count
|
||||
initialCount := len(mod.getEvents(0))
|
||||
|
||||
// Get events - we can't guarantee zero events due to session initialization
|
||||
events := mod.getEvents(0)
|
||||
if len(events) < initialCount {
|
||||
t.Errorf("Event count should not decrease, got %d", len(events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEventsWithLimit(t *testing.T) {
|
||||
// Create session using the singleton
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
// Record initial state
|
||||
initialEvents := mod.getEvents(0)
|
||||
initialCount := len(initialEvents)
|
||||
|
||||
// Add some test events
|
||||
testEventCount := 10
|
||||
for i := 0; i < testEventCount; i++ {
|
||||
s.Events.Add(fmt.Sprintf("test.event.limit.%d", i), nil)
|
||||
}
|
||||
|
||||
// Get all events
|
||||
allEvents := mod.getEvents(0)
|
||||
expectedTotal := initialCount + testEventCount
|
||||
if len(allEvents) != expectedTotal {
|
||||
t.Errorf("Expected %d total events, got %d", expectedTotal, len(allEvents))
|
||||
}
|
||||
|
||||
// Test limit functionality - get last 5 events
|
||||
limitedEvents := mod.getEvents(5)
|
||||
if len(limitedEvents) != 5 {
|
||||
t.Errorf("Expected 5 events when limiting, got %d", len(limitedEvents))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetSecurityHeaders(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
mod.allowOrigin = "http://localhost:3000"
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
mod.setSecurityHeaders(w)
|
||||
|
||||
headers := w.Header()
|
||||
|
||||
// Check security headers
|
||||
if headers.Get("X-Frame-Options") != "DENY" {
|
||||
t.Error("X-Frame-Options header not set correctly")
|
||||
}
|
||||
|
||||
if headers.Get("X-Content-Type-Options") != "nosniff" {
|
||||
t.Error("X-Content-Type-Options header not set correctly")
|
||||
}
|
||||
|
||||
if headers.Get("X-XSS-Protection") != "1; mode=block" {
|
||||
t.Error("X-XSS-Protection header not set correctly")
|
||||
}
|
||||
|
||||
if headers.Get("Access-Control-Allow-Origin") != "http://localhost:3000" {
|
||||
t.Error("Access-Control-Allow-Origin header not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorsRoute(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
req, _ := http.NewRequest("OPTIONS", "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
mod.corsRoute(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Errorf("Expected status %d, got %d", http.StatusNoContent, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewRestAPI(s)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
testData := map[string]string{
|
||||
"key": "value",
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
mod.toJSON(w, testData)
|
||||
|
||||
// Check content type
|
||||
if w.Header().Get("Content-Type") != "application/json" {
|
||||
t.Error("Content-Type header not set to application/json")
|
||||
}
|
||||
|
||||
// Check JSON response
|
||||
var result map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&result); err != nil {
|
||||
t.Errorf("Failed to decode JSON response: %v", err)
|
||||
}
|
||||
|
||||
if result["key"] != "value" || result["foo"] != "bar" {
|
||||
t.Error("JSON response doesn't match expected data")
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,26 +3,28 @@ package arp_spoof
|
|||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/packets"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/packets"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/malfunkt/iprange"
|
||||
)
|
||||
|
||||
type ArpSpoofer struct {
|
||||
session.SessionModule
|
||||
addresses []net.IP
|
||||
macs []net.HardwareAddr
|
||||
wAddresses []net.IP
|
||||
wMacs []net.HardwareAddr
|
||||
fullDuplex bool
|
||||
internal bool
|
||||
ban bool
|
||||
waitGroup *sync.WaitGroup
|
||||
addresses []net.IP
|
||||
macs []net.HardwareAddr
|
||||
wAddresses []net.IP
|
||||
wMacs []net.HardwareAddr
|
||||
fullDuplex bool
|
||||
internal bool
|
||||
ban bool
|
||||
skipRestore bool
|
||||
waitGroup *sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewArpSpoofer(s *session.Session) *ArpSpoofer {
|
||||
|
|
@ -35,6 +37,7 @@ func NewArpSpoofer(s *session.Session) *ArpSpoofer {
|
|||
ban: false,
|
||||
internal: false,
|
||||
fullDuplex: false,
|
||||
skipRestore: false,
|
||||
waitGroup: &sync.WaitGroup{},
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +55,20 @@ func NewArpSpoofer(s *session.Session) *ArpSpoofer {
|
|||
"false",
|
||||
"If true, both the targets and the gateway will be attacked, otherwise only the target (if the router has ARP spoofing protections in place this will make the attack fail)."))
|
||||
|
||||
noRestore := session.NewBoolParameter("arp.spoof.skip_restore",
|
||||
"false",
|
||||
"If set to true, targets arp cache won't be restored when spoofing is stopped.")
|
||||
|
||||
mod.AddObservableParam(noRestore, func(v string) {
|
||||
if strings.ToLower(v) == "true" || v == "1" {
|
||||
mod.skipRestore = true
|
||||
mod.Warning("arp cache restoration after spoofing disabled")
|
||||
} else {
|
||||
mod.skipRestore = false
|
||||
mod.Debug("arp cache restoration after spoofing enabled")
|
||||
}
|
||||
})
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("arp.spoof on", "",
|
||||
"Start ARP spoofer.",
|
||||
func(args []string) error {
|
||||
|
|
@ -171,20 +188,24 @@ func (mod *ArpSpoofer) Start() error {
|
|||
}
|
||||
|
||||
func (mod *ArpSpoofer) unSpoof() error {
|
||||
nTargets := len(mod.addresses) + len(mod.macs)
|
||||
mod.Info("restoring ARP cache of %d targets.", nTargets)
|
||||
mod.arpSpoofTargets(mod.Session.Gateway.IP, mod.Session.Gateway.HW, false, false)
|
||||
if !mod.skipRestore {
|
||||
nTargets := len(mod.addresses) + len(mod.macs)
|
||||
mod.Info("restoring ARP cache of %d targets.", nTargets)
|
||||
mod.arpSpoofTargets(mod.Session.Gateway.IP, mod.Session.Gateway.HW, false, false)
|
||||
|
||||
if mod.internal {
|
||||
list, _ := iprange.ParseList(mod.Session.Interface.CIDR())
|
||||
neighbours := list.Expand()
|
||||
for _, address := range neighbours {
|
||||
if !mod.Session.Skip(address) {
|
||||
if realMAC, err := mod.Session.FindMAC(address, false); err == nil {
|
||||
mod.arpSpoofTargets(address, realMAC, false, false)
|
||||
if mod.internal {
|
||||
list, _ := iprange.ParseList(mod.Session.Interface.CIDR())
|
||||
neighbours := list.Expand()
|
||||
for _, address := range neighbours {
|
||||
if !mod.Session.Skip(address) {
|
||||
if realMAC, err := mod.Session.FindMAC(address, false); err == nil {
|
||||
mod.arpSpoofTargets(address, realMAC, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mod.Warning("arp cache restoration is disabled")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -260,47 +281,51 @@ func (mod *ArpSpoofer) arpSpoofTargets(saddr net.IP, smac net.HardwareAddr, chec
|
|||
}
|
||||
}
|
||||
|
||||
for ip, mac := range mod.getTargets(probe) {
|
||||
if check_running && !mod.Running() {
|
||||
return
|
||||
} else if mod.isWhitelisted(ip, mac) {
|
||||
mod.Debug("%s (%s) is whitelisted, skipping from spoofing loop.", ip, mac)
|
||||
continue
|
||||
} else if saddr.String() == ip {
|
||||
continue
|
||||
}
|
||||
|
||||
rawIP := net.ParseIP(ip)
|
||||
if err, pkt := packets.NewARPReply(saddr, smac, rawIP, mac); err != nil {
|
||||
mod.Error("error while creating ARP spoof packet for %s: %s", ip, err)
|
||||
} else {
|
||||
mod.Debug("sending %d bytes of ARP packet to %s:%s.", len(pkt), ip, mac.String())
|
||||
mod.Session.Queue.Send(pkt)
|
||||
}
|
||||
|
||||
if mod.fullDuplex && isGW {
|
||||
err := error(nil)
|
||||
gwPacket := []byte(nil)
|
||||
|
||||
if isSpoofing {
|
||||
mod.Debug("telling the gw we are %s", ip)
|
||||
// we told the target we're te gateway, not let's tell the
|
||||
// gateway that we are the target
|
||||
if err, gwPacket = packets.NewARPReply(rawIP, ourHW, gwIP, gwHW); err != nil {
|
||||
mod.Error("error while creating ARP spoof packet: %s", err)
|
||||
}
|
||||
} else {
|
||||
mod.Debug("telling the gw %s is %s", ip, mac)
|
||||
// send the gateway the original MAC of the target
|
||||
if err, gwPacket = packets.NewARPReply(rawIP, mac, gwIP, gwHW); err != nil {
|
||||
mod.Error("error while creating ARP spoof packet: %s", err)
|
||||
}
|
||||
if targets := mod.getTargets(probe); len(targets) == 0 {
|
||||
mod.Warning("could not find spoof targets")
|
||||
} else {
|
||||
for ip, mac := range targets {
|
||||
if check_running && !mod.Running() {
|
||||
return
|
||||
} else if mod.isWhitelisted(ip, mac) {
|
||||
mod.Debug("%s (%s) is whitelisted, skipping from spoofing loop.", ip, mac)
|
||||
continue
|
||||
} else if saddr.String() == ip {
|
||||
continue
|
||||
}
|
||||
|
||||
if gwPacket != nil {
|
||||
mod.Debug("sending %d bytes of ARP packet to the gateway", len(gwPacket))
|
||||
if err = mod.Session.Queue.Send(gwPacket); err != nil {
|
||||
mod.Error("error while sending packet: %v", err)
|
||||
rawIP := net.ParseIP(ip)
|
||||
if err, pkt := packets.NewARPReply(saddr, smac, rawIP, mac); err != nil {
|
||||
mod.Error("error while creating ARP spoof packet for %s: %s", ip, err)
|
||||
} else {
|
||||
mod.Debug("sending %d bytes of ARP packet to %s:%s.", len(pkt), ip, mac.String())
|
||||
mod.Session.Queue.Send(pkt)
|
||||
}
|
||||
|
||||
if mod.fullDuplex && isGW {
|
||||
err := error(nil)
|
||||
gwPacket := []byte(nil)
|
||||
|
||||
if isSpoofing {
|
||||
mod.Debug("telling the gw we are %s", ip)
|
||||
// we told the target we're te gateway, not let's tell the
|
||||
// gateway that we are the target
|
||||
if err, gwPacket = packets.NewARPReply(rawIP, ourHW, gwIP, gwHW); err != nil {
|
||||
mod.Error("error while creating ARP spoof packet: %s", err)
|
||||
}
|
||||
} else {
|
||||
mod.Debug("telling the gw %s is %s", ip, mac)
|
||||
// send the gateway the original MAC of the target
|
||||
if err, gwPacket = packets.NewARPReply(rawIP, mac, gwIP, gwHW); err != nil {
|
||||
mod.Error("error while creating ARP spoof packet: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if gwPacket != nil {
|
||||
mod.Debug("sending %d bytes of ARP packet to the gateway", len(gwPacket))
|
||||
if err = mod.Session.Queue.Send(gwPacket); err != nil {
|
||||
mod.Error("error while sending packet: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
785
modules/arp_spoof/arp_spoof_test.go
Normal file
785
modules/arp_spoof/arp_spoof_test.go
Normal file
|
|
@ -0,0 +1,785 @@
|
|||
package arp_spoof
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/firewall"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/packets"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/data"
|
||||
)
|
||||
|
||||
// MockFirewall implements a mock firewall for testing
|
||||
type MockFirewall struct {
|
||||
forwardingEnabled bool
|
||||
redirections []firewall.Redirection
|
||||
}
|
||||
|
||||
func NewMockFirewall() *MockFirewall {
|
||||
return &MockFirewall{
|
||||
forwardingEnabled: false,
|
||||
redirections: make([]firewall.Redirection, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockFirewall) IsForwardingEnabled() bool {
|
||||
return m.forwardingEnabled
|
||||
}
|
||||
|
||||
func (m *MockFirewall) EnableForwarding(enabled bool) error {
|
||||
m.forwardingEnabled = enabled
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockFirewall) EnableRedirection(r *firewall.Redirection, enabled bool) error {
|
||||
if enabled {
|
||||
m.redirections = append(m.redirections, *r)
|
||||
} else {
|
||||
for i, red := range m.redirections {
|
||||
if red.String() == r.String() {
|
||||
m.redirections = append(m.redirections[:i], m.redirections[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockFirewall) DisableRedirection(r *firewall.Redirection, enabled bool) error {
|
||||
return m.EnableRedirection(r, false)
|
||||
}
|
||||
|
||||
func (m *MockFirewall) Restore() {
|
||||
m.redirections = make([]firewall.Redirection, 0)
|
||||
m.forwardingEnabled = false
|
||||
}
|
||||
|
||||
// MockPacketQueue extends packets.Queue to capture sent packets
|
||||
type MockPacketQueue struct {
|
||||
*packets.Queue
|
||||
sync.Mutex
|
||||
sentPackets [][]byte
|
||||
}
|
||||
|
||||
func NewMockPacketQueue() *MockPacketQueue {
|
||||
q := &packets.Queue{
|
||||
Traffic: sync.Map{},
|
||||
Stats: packets.Stats{},
|
||||
}
|
||||
return &MockPacketQueue{
|
||||
Queue: q,
|
||||
sentPackets: make([][]byte, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockPacketQueue) Send(data []byte) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// Store a copy of the packet
|
||||
packet := make([]byte, len(data))
|
||||
copy(packet, data)
|
||||
m.sentPackets = append(m.sentPackets, packet)
|
||||
|
||||
// Also update stats like the real queue would
|
||||
m.TrackSent(uint64(len(data)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockPacketQueue) GetSentPackets() [][]byte {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.sentPackets
|
||||
}
|
||||
|
||||
func (m *MockPacketQueue) ClearSentPackets() {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
m.sentPackets = make([][]byte, 0)
|
||||
}
|
||||
|
||||
// MockSession for testing
|
||||
type MockSession struct {
|
||||
*session.Session
|
||||
findMACResults map[string]net.HardwareAddr
|
||||
skipIPs map[string]bool
|
||||
mockQueue *MockPacketQueue
|
||||
}
|
||||
|
||||
// Override session methods to use our mocks
|
||||
func setupMockSession(mockSess *MockSession) {
|
||||
// Replace the Session's FindMAC method behavior by manipulating the LAN
|
||||
// Since we can't override methods directly, we'll ensure the LAN has the data
|
||||
for ip, mac := range mockSess.findMACResults {
|
||||
mockSess.Lan.AddIfNew(ip, mac.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockSession) FindMAC(ip net.IP, probe bool) (net.HardwareAddr, error) {
|
||||
// First check our mock results
|
||||
if mac, ok := m.findMACResults[ip.String()]; ok {
|
||||
return mac, nil
|
||||
}
|
||||
// Then check the LAN
|
||||
if e, found := m.Lan.Get(ip.String()); found && e != nil {
|
||||
return e.HW, nil
|
||||
}
|
||||
return nil, fmt.Errorf("MAC not found for %s", ip.String())
|
||||
}
|
||||
|
||||
func (m *MockSession) Skip(ip net.IP) bool {
|
||||
if m.skipIPs == nil {
|
||||
return false
|
||||
}
|
||||
return m.skipIPs[ip.String()]
|
||||
}
|
||||
|
||||
// MockNetRecon implements a minimal net.recon module for testing
|
||||
type MockNetRecon struct {
|
||||
session.SessionModule
|
||||
}
|
||||
|
||||
func NewMockNetRecon(s *session.Session) *MockNetRecon {
|
||||
mod := &MockNetRecon{
|
||||
SessionModule: session.NewSessionModule("net.recon", s),
|
||||
}
|
||||
|
||||
// Add handlers
|
||||
mod.AddHandler(session.NewModuleHandler("net.recon on", "",
|
||||
"Start net.recon",
|
||||
func(args []string) error {
|
||||
return mod.Start()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("net.recon off", "",
|
||||
"Stop net.recon",
|
||||
func(args []string) error {
|
||||
return mod.Stop()
|
||||
}))
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
func (m *MockNetRecon) Name() string {
|
||||
return "net.recon"
|
||||
}
|
||||
|
||||
func (m *MockNetRecon) Description() string {
|
||||
return "Mock net.recon module"
|
||||
}
|
||||
|
||||
func (m *MockNetRecon) Author() string {
|
||||
return "test"
|
||||
}
|
||||
|
||||
func (m *MockNetRecon) Configure() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockNetRecon) Start() error {
|
||||
return m.SetRunning(true, nil)
|
||||
}
|
||||
|
||||
func (m *MockNetRecon) Stop() error {
|
||||
return m.SetRunning(false, nil)
|
||||
}
|
||||
|
||||
// Create a mock session for testing
|
||||
func createMockSession() (*MockSession, *MockPacketQueue, *MockFirewall) {
|
||||
// Create interface
|
||||
iface := &network.Endpoint{
|
||||
IpAddress: "192.168.1.100",
|
||||
HwAddress: "aa:bb:cc:dd:ee:ff",
|
||||
Hostname: "eth0",
|
||||
}
|
||||
iface.SetIP("192.168.1.100")
|
||||
iface.SetBits(24)
|
||||
|
||||
// Parse interface addresses
|
||||
ifaceIP := net.ParseIP("192.168.1.100")
|
||||
ifaceHW, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
|
||||
iface.IP = ifaceIP
|
||||
iface.HW = ifaceHW
|
||||
|
||||
// Create gateway
|
||||
gateway := &network.Endpoint{
|
||||
IpAddress: "192.168.1.1",
|
||||
HwAddress: "11:22:33:44:55:66",
|
||||
}
|
||||
gatewayIP := net.ParseIP("192.168.1.1")
|
||||
gatewayHW, _ := net.ParseMAC("11:22:33:44:55:66")
|
||||
gateway.IP = gatewayIP
|
||||
gateway.HW = gatewayHW
|
||||
|
||||
// Create mock queue and firewall
|
||||
mockQueue := NewMockPacketQueue()
|
||||
mockFirewall := NewMockFirewall()
|
||||
|
||||
// Create environment
|
||||
env, _ := session.NewEnvironment("")
|
||||
|
||||
// Create LAN
|
||||
aliases, _ := data.NewUnsortedKV("", 0)
|
||||
lan := network.NewLAN(iface, gateway, aliases, func(e *network.Endpoint) {}, func(e *network.Endpoint) {})
|
||||
|
||||
// Create session
|
||||
sess := &session.Session{
|
||||
Interface: iface,
|
||||
Gateway: gateway,
|
||||
Lan: lan,
|
||||
StartedAt: time.Now(),
|
||||
Active: true,
|
||||
Env: env,
|
||||
Queue: mockQueue.Queue,
|
||||
Firewall: mockFirewall,
|
||||
Modules: make(session.ModuleList, 0),
|
||||
}
|
||||
|
||||
// Initialize events
|
||||
sess.Events = session.NewEventPool(false, false)
|
||||
|
||||
// Add mock net.recon module
|
||||
mockNetRecon := NewMockNetRecon(sess)
|
||||
sess.Modules = append(sess.Modules, mockNetRecon)
|
||||
|
||||
// Create mock session wrapper
|
||||
mockSess := &MockSession{
|
||||
Session: sess,
|
||||
findMACResults: make(map[string]net.HardwareAddr),
|
||||
skipIPs: make(map[string]bool),
|
||||
mockQueue: mockQueue,
|
||||
}
|
||||
|
||||
return mockSess, mockQueue, mockFirewall
|
||||
}
|
||||
|
||||
func TestNewArpSpoofer(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
if mod == nil {
|
||||
t.Fatal("NewArpSpoofer returned nil")
|
||||
}
|
||||
|
||||
if mod.Name() != "arp.spoof" {
|
||||
t.Errorf("expected module name 'arp.spoof', got '%s'", mod.Name())
|
||||
}
|
||||
|
||||
if mod.Author() != "Simone Margaritelli <evilsocket@gmail.com>" {
|
||||
t.Errorf("unexpected author: %s", mod.Author())
|
||||
}
|
||||
|
||||
// Check parameters
|
||||
params := []string{"arp.spoof.targets", "arp.spoof.whitelist", "arp.spoof.internal", "arp.spoof.fullduplex", "arp.spoof.skip_restore"}
|
||||
for _, param := range params {
|
||||
if !mod.Session.Env.Has(param) {
|
||||
t.Errorf("parameter %s not registered", param)
|
||||
}
|
||||
}
|
||||
|
||||
// Check handlers
|
||||
handlers := mod.Handlers()
|
||||
expectedHandlers := []string{"arp.spoof on", "arp.ban on", "arp.spoof off", "arp.ban off"}
|
||||
handlerMap := make(map[string]bool)
|
||||
|
||||
for _, h := range handlers {
|
||||
handlerMap[h.Name] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedHandlers {
|
||||
if !handlerMap[expected] {
|
||||
t.Errorf("Expected handler '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArpSpooferConfigure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params map[string]string
|
||||
setupMock func(*MockSession)
|
||||
expectErr bool
|
||||
validate func(*ArpSpoofer) error
|
||||
}{
|
||||
{
|
||||
name: "default configuration",
|
||||
params: map[string]string{
|
||||
"arp.spoof.targets": "192.168.1.10",
|
||||
"arp.spoof.whitelist": "",
|
||||
"arp.spoof.internal": "false",
|
||||
"arp.spoof.fullduplex": "false",
|
||||
"arp.spoof.skip_restore": "false",
|
||||
},
|
||||
setupMock: func(ms *MockSession) {
|
||||
ms.Lan.AddIfNew("192.168.1.10", "aa:aa:aa:aa:aa:aa")
|
||||
},
|
||||
expectErr: false,
|
||||
validate: func(mod *ArpSpoofer) error {
|
||||
if mod.internal {
|
||||
return fmt.Errorf("expected internal to be false")
|
||||
}
|
||||
if mod.fullDuplex {
|
||||
return fmt.Errorf("expected fullDuplex to be false")
|
||||
}
|
||||
if mod.skipRestore {
|
||||
return fmt.Errorf("expected skipRestore to be false")
|
||||
}
|
||||
if len(mod.addresses) != 1 {
|
||||
return fmt.Errorf("expected 1 address, got %d", len(mod.addresses))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple targets and whitelist",
|
||||
params: map[string]string{
|
||||
"arp.spoof.targets": "192.168.1.10,192.168.1.20",
|
||||
"arp.spoof.whitelist": "192.168.1.30",
|
||||
"arp.spoof.internal": "true",
|
||||
"arp.spoof.fullduplex": "true",
|
||||
"arp.spoof.skip_restore": "true",
|
||||
},
|
||||
setupMock: func(ms *MockSession) {
|
||||
ms.Lan.AddIfNew("192.168.1.10", "aa:aa:aa:aa:aa:aa")
|
||||
ms.Lan.AddIfNew("192.168.1.20", "bb:bb:bb:bb:bb:bb")
|
||||
ms.Lan.AddIfNew("192.168.1.30", "cc:cc:cc:cc:cc:cc")
|
||||
},
|
||||
expectErr: false,
|
||||
validate: func(mod *ArpSpoofer) error {
|
||||
if !mod.internal {
|
||||
return fmt.Errorf("expected internal to be true")
|
||||
}
|
||||
if !mod.fullDuplex {
|
||||
return fmt.Errorf("expected fullDuplex to be true")
|
||||
}
|
||||
if !mod.skipRestore {
|
||||
return fmt.Errorf("expected skipRestore to be true")
|
||||
}
|
||||
if len(mod.addresses) != 2 {
|
||||
return fmt.Errorf("expected 2 addresses, got %d", len(mod.addresses))
|
||||
}
|
||||
if len(mod.wAddresses) != 1 {
|
||||
return fmt.Errorf("expected 1 whitelisted address, got %d", len(mod.wAddresses))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MAC address targets",
|
||||
params: map[string]string{
|
||||
"arp.spoof.targets": "aa:aa:aa:aa:aa:aa",
|
||||
"arp.spoof.whitelist": "",
|
||||
"arp.spoof.internal": "false",
|
||||
"arp.spoof.fullduplex": "false",
|
||||
"arp.spoof.skip_restore": "false",
|
||||
},
|
||||
setupMock: func(ms *MockSession) {
|
||||
ms.Lan.AddIfNew("192.168.1.10", "aa:aa:aa:aa:aa:aa")
|
||||
},
|
||||
expectErr: false,
|
||||
validate: func(mod *ArpSpoofer) error {
|
||||
if len(mod.macs) != 1 {
|
||||
return fmt.Errorf("expected 1 MAC address, got %d", len(mod.macs))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid target",
|
||||
params: map[string]string{
|
||||
"arp.spoof.targets": "invalid-target",
|
||||
"arp.spoof.whitelist": "",
|
||||
"arp.spoof.internal": "false",
|
||||
"arp.spoof.fullduplex": "false",
|
||||
"arp.spoof.skip_restore": "false",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Set parameters
|
||||
for k, v := range tt.params {
|
||||
mockSess.Env.Set(k, v)
|
||||
}
|
||||
|
||||
// Setup mock
|
||||
if tt.setupMock != nil {
|
||||
tt.setupMock(mockSess)
|
||||
}
|
||||
|
||||
err := mod.Configure()
|
||||
|
||||
if tt.expectErr && err == nil {
|
||||
t.Error("expected error but got none")
|
||||
} else if !tt.expectErr && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !tt.expectErr && tt.validate != nil {
|
||||
if err := tt.validate(mod); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArpSpooferStartStop(t *testing.T) {
|
||||
mockSess, _, mockFirewall := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Setup targets
|
||||
targetIP := "192.168.1.10"
|
||||
targetMAC, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
mockSess.Lan.AddIfNew(targetIP, targetMAC.String())
|
||||
mockSess.findMACResults[targetIP] = targetMAC
|
||||
|
||||
// Configure
|
||||
mockSess.Env.Set("arp.spoof.targets", targetIP)
|
||||
mockSess.Env.Set("arp.spoof.fullduplex", "false")
|
||||
mockSess.Env.Set("arp.spoof.internal", "false")
|
||||
|
||||
// Start the spoofer
|
||||
err := mod.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start spoofer: %v", err)
|
||||
}
|
||||
|
||||
if !mod.Running() {
|
||||
t.Error("Spoofer should be running after Start()")
|
||||
}
|
||||
|
||||
// Check that forwarding was enabled
|
||||
if !mockFirewall.IsForwardingEnabled() {
|
||||
t.Error("Forwarding should be enabled after starting spoofer")
|
||||
}
|
||||
|
||||
// Let it run for a bit
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Stop the spoofer
|
||||
err = mod.Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stop spoofer: %v", err)
|
||||
}
|
||||
|
||||
if mod.Running() {
|
||||
t.Error("Spoofer should not be running after Stop()")
|
||||
}
|
||||
|
||||
// Note: We can't easily verify packet sending without modifying the actual module
|
||||
// to use an interface for the queue. The module behavior is verified through
|
||||
// state changes (running state, forwarding enabled, etc.)
|
||||
}
|
||||
|
||||
func TestArpSpooferBanMode(t *testing.T) {
|
||||
mockSess, _, mockFirewall := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Setup targets
|
||||
targetIP := "192.168.1.10"
|
||||
targetMAC, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
mockSess.Lan.AddIfNew(targetIP, targetMAC.String())
|
||||
mockSess.findMACResults[targetIP] = targetMAC
|
||||
|
||||
// Configure
|
||||
mockSess.Env.Set("arp.spoof.targets", targetIP)
|
||||
|
||||
// Find and execute the ban handler
|
||||
handlers := mod.Handlers()
|
||||
for _, h := range handlers {
|
||||
if h.Name == "arp.ban on" {
|
||||
err := h.Exec([]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start ban mode: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !mod.ban {
|
||||
t.Error("Ban mode should be enabled")
|
||||
}
|
||||
|
||||
// Check that forwarding was NOT enabled
|
||||
if mockFirewall.IsForwardingEnabled() {
|
||||
t.Error("Forwarding should NOT be enabled in ban mode")
|
||||
}
|
||||
|
||||
// Let it run for a bit
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Stop using ban off handler
|
||||
for _, h := range handlers {
|
||||
if h.Name == "arp.ban off" {
|
||||
err := h.Exec([]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to stop ban mode: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if mod.ban {
|
||||
t.Error("Ban mode should be disabled after stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArpSpooferWhitelisting(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Add some IPs and MACs to whitelist
|
||||
whitelistIP := net.ParseIP("192.168.1.50")
|
||||
whitelistMAC, _ := net.ParseMAC("ff:ff:ff:ff:ff:ff")
|
||||
|
||||
mod.wAddresses = []net.IP{whitelistIP}
|
||||
mod.wMacs = []net.HardwareAddr{whitelistMAC}
|
||||
|
||||
// Test IP whitelisting
|
||||
if !mod.isWhitelisted("192.168.1.50", nil) {
|
||||
t.Error("IP should be whitelisted")
|
||||
}
|
||||
|
||||
if mod.isWhitelisted("192.168.1.60", nil) {
|
||||
t.Error("IP should not be whitelisted")
|
||||
}
|
||||
|
||||
// Test MAC whitelisting
|
||||
if !mod.isWhitelisted("", whitelistMAC) {
|
||||
t.Error("MAC should be whitelisted")
|
||||
}
|
||||
|
||||
otherMAC, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
if mod.isWhitelisted("", otherMAC) {
|
||||
t.Error("MAC should not be whitelisted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArpSpooferFullDuplex(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Setup targets
|
||||
targetIP := "192.168.1.10"
|
||||
targetMAC, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
mockSess.Lan.AddIfNew(targetIP, targetMAC.String())
|
||||
mockSess.findMACResults[targetIP] = targetMAC
|
||||
|
||||
// Configure with full duplex
|
||||
mockSess.Env.Set("arp.spoof.targets", targetIP)
|
||||
mockSess.Env.Set("arp.spoof.fullduplex", "true")
|
||||
|
||||
// Verify configuration
|
||||
err := mod.Configure()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to configure: %v", err)
|
||||
}
|
||||
|
||||
if !mod.fullDuplex {
|
||||
t.Error("Full duplex mode should be enabled")
|
||||
}
|
||||
|
||||
// Start the spoofer
|
||||
err = mod.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start spoofer: %v", err)
|
||||
}
|
||||
|
||||
if !mod.Running() {
|
||||
t.Error("Module should be running")
|
||||
}
|
||||
|
||||
// Let it run for a bit
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Stop
|
||||
mod.Stop()
|
||||
}
|
||||
|
||||
func TestArpSpooferInternalMode(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Setup multiple targets
|
||||
targets := map[string]string{
|
||||
"192.168.1.10": "aa:aa:aa:aa:aa:aa",
|
||||
"192.168.1.20": "bb:bb:bb:bb:bb:bb",
|
||||
"192.168.1.30": "cc:cc:cc:cc:cc:cc",
|
||||
}
|
||||
|
||||
for ip, mac := range targets {
|
||||
mockSess.Lan.AddIfNew(ip, mac)
|
||||
hwAddr, _ := net.ParseMAC(mac)
|
||||
mockSess.findMACResults[ip] = hwAddr
|
||||
}
|
||||
|
||||
// Configure with internal mode
|
||||
mockSess.Env.Set("arp.spoof.targets", "192.168.1.10,192.168.1.20")
|
||||
mockSess.Env.Set("arp.spoof.internal", "true")
|
||||
|
||||
// Verify configuration
|
||||
err := mod.Configure()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to configure: %v", err)
|
||||
}
|
||||
|
||||
if !mod.internal {
|
||||
t.Error("Internal mode should be enabled")
|
||||
}
|
||||
|
||||
// Start the spoofer
|
||||
err = mod.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to start spoofer: %v", err)
|
||||
}
|
||||
|
||||
if !mod.Running() {
|
||||
t.Error("Module should be running")
|
||||
}
|
||||
|
||||
// Let it run briefly
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Stop
|
||||
mod.Stop()
|
||||
}
|
||||
|
||||
func TestArpSpooferGetTargets(t *testing.T) {
|
||||
// This test verifies the getTargets logic without actually calling it
|
||||
// since the method uses Session.FindMAC which can't be easily mocked
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Test address and MAC parsing
|
||||
targetIP := net.ParseIP("192.168.1.10")
|
||||
targetMAC, _ := net.ParseMAC("aa:aa:aa:aa:aa:aa")
|
||||
|
||||
// Add targets by IP
|
||||
mod.addresses = []net.IP{targetIP}
|
||||
|
||||
// Verify addresses were set correctly
|
||||
if len(mod.addresses) != 1 {
|
||||
t.Errorf("expected 1 address, got %d", len(mod.addresses))
|
||||
}
|
||||
|
||||
if !mod.addresses[0].Equal(targetIP) {
|
||||
t.Errorf("expected address %s, got %s", targetIP, mod.addresses[0])
|
||||
}
|
||||
|
||||
// Add targets by MAC
|
||||
mod.macs = []net.HardwareAddr{targetMAC}
|
||||
|
||||
// Verify MACs were set correctly
|
||||
if len(mod.macs) != 1 {
|
||||
t.Errorf("expected 1 MAC, got %d", len(mod.macs))
|
||||
}
|
||||
|
||||
if !bytes.Equal(mod.macs[0], targetMAC) {
|
||||
t.Errorf("expected MAC %s, got %s", targetMAC, mod.macs[0])
|
||||
}
|
||||
|
||||
// Note: The actual getTargets method would look up these addresses/MACs
|
||||
// in the network, but we can't easily test that without refactoring
|
||||
// the module to use dependency injection for network operations
|
||||
}
|
||||
|
||||
func TestArpSpooferSkipRestore(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// The skip_restore parameter is set up with an observer in NewArpSpoofer
|
||||
// We'll test it by changing the parameter value, which triggers the observer
|
||||
mockSess.Env.Set("arp.spoof.skip_restore", "true")
|
||||
|
||||
// Configure to trigger parameter reading
|
||||
mod.Configure()
|
||||
|
||||
// Check the observer worked by checking if skipRestore was set
|
||||
// Note: The actual observer is triggered during module creation
|
||||
// so we test the functionality indirectly through the module's behavior
|
||||
|
||||
// Start and stop to see if restoration is skipped
|
||||
mockSess.Env.Set("arp.spoof.targets", "192.168.1.10")
|
||||
mockSess.Lan.AddIfNew("192.168.1.10", "aa:aa:aa:aa:aa:aa")
|
||||
|
||||
mod.Start()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
mod.Stop()
|
||||
|
||||
// With skip_restore true, the module should have skipRestore set
|
||||
// We can't directly test the observer, but we verify the behavior
|
||||
}
|
||||
|
||||
func TestArpSpooferEmptyTargets(t *testing.T) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Configure with empty targets
|
||||
mockSess.Env.Set("arp.spoof.targets", "")
|
||||
|
||||
// Start should not error but should not actually start
|
||||
err := mod.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("Start with empty targets should not error: %v", err)
|
||||
}
|
||||
|
||||
// Module should not be running
|
||||
if mod.Running() {
|
||||
t.Error("Module should not be running with empty targets")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmarks
|
||||
func BenchmarkArpSpooferGetTargets(b *testing.B) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Setup targets
|
||||
for i := 0; i < 10; i++ {
|
||||
ip := fmt.Sprintf("192.168.1.%d", i+10)
|
||||
mac := fmt.Sprintf("aa:bb:cc:dd:ee:%02x", i)
|
||||
mockSess.Lan.AddIfNew(ip, mac)
|
||||
hwAddr, _ := net.ParseMAC(mac)
|
||||
mockSess.findMACResults[ip] = hwAddr
|
||||
mod.addresses = append(mod.addresses, net.ParseIP(ip))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = mod.getTargets(false)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArpSpooferWhitelisting(b *testing.B) {
|
||||
mockSess, _, _ := createMockSession()
|
||||
mod := NewArpSpoofer(mockSess.Session)
|
||||
|
||||
// Add many whitelist entries
|
||||
for i := 0; i < 100; i++ {
|
||||
ip := net.ParseIP(fmt.Sprintf("192.168.1.%d", i))
|
||||
mod.wAddresses = append(mod.wAddresses, ip)
|
||||
}
|
||||
|
||||
testMAC, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = mod.isWhitelisted("192.168.1.50", testMAC)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,10 @@ import (
|
|||
"github.com/bettercap/gatt"
|
||||
)
|
||||
|
||||
var defaultBLEClientOptions = []gatt.Option{
|
||||
gatt.MacDeviceRole(gatt.CentralManager),
|
||||
func getClientOptions(deviceID int) []gatt.Option {
|
||||
return []gatt.Option{
|
||||
gatt.MacDeviceRole(gatt.CentralManager),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import (
|
|||
// "github.com/bettercap/gatt/linux/cmd"
|
||||
)
|
||||
|
||||
var defaultBLEClientOptions = []gatt.Option{
|
||||
gatt.LnxMaxConnections(255),
|
||||
gatt.LnxDeviceID(-1, true),
|
||||
func getClientOptions(deviceID int) []gatt.Option {
|
||||
return []gatt.Option{
|
||||
gatt.LnxMaxConnections(255),
|
||||
gatt.LnxDeviceID(deviceID, true),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// +build !windows
|
||||
//go:build !windows && !freebsd && !openbsd && !netbsd
|
||||
// +build !windows,!freebsd,!openbsd,!netbsd
|
||||
|
||||
package ble
|
||||
|
||||
|
|
@ -8,9 +9,9 @@ import (
|
|||
golog "log"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/modules/utils"
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/modules/utils"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/bettercap/gatt"
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ func NewBLERecon(s *session.Session) *BLERecon {
|
|||
"Enumerate services and characteristics for the given BLE device.",
|
||||
func(args []string) error {
|
||||
if mod.isEnumerating() {
|
||||
return fmt.Errorf("An enumeration for %s is already running, please wait.", mod.currDevice.Device.ID())
|
||||
return fmt.Errorf("an enumeration for %s is already running, please wait.", mod.currDevice.Device.ID())
|
||||
}
|
||||
|
||||
mod.writeData = nil
|
||||
|
|
@ -99,11 +100,11 @@ func NewBLERecon(s *session.Session) *BLERecon {
|
|||
mac := network.NormalizeMac(args[0])
|
||||
uuid, err := gatt.ParseUUID(args[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing %s: %s", args[1], err)
|
||||
return fmt.Errorf("error parsing %s: %s", args[1], err)
|
||||
}
|
||||
data, err := hex.DecodeString(args[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error parsing %s: %s", args[2], err)
|
||||
return fmt.Errorf("error parsing %s: %s", args[2], err)
|
||||
}
|
||||
|
||||
return mod.writeBuffer(mac, uuid, data)
|
||||
|
|
@ -166,7 +167,7 @@ func (mod *BLERecon) Configure() (err error) {
|
|||
golog.SetFlags(0)
|
||||
golog.SetOutput(dummyWriter{mod})
|
||||
|
||||
if mod.gattDevice, err = gatt.NewDevice(defaultBLEClientOptions...); err != nil {
|
||||
if mod.gattDevice, err = gatt.NewDevice(getClientOptions(mod.deviceId)...); err != nil {
|
||||
mod.Debug("error while creating new gatt device: %v", err)
|
||||
return err
|
||||
}
|
||||
|
|
@ -189,11 +190,15 @@ func (mod *BLERecon) Configure() (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
const blePrompt = "{blb}{fw}BLE {fb}{reset} {bold}» {reset}"
|
||||
|
||||
func (mod *BLERecon) Start() error {
|
||||
if err := mod.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.SetPrompt(blePrompt)
|
||||
|
||||
return mod.SetRunning(true, func() {
|
||||
go mod.pruner()
|
||||
|
||||
|
|
@ -220,6 +225,8 @@ func (mod *BLERecon) Start() error {
|
|||
}
|
||||
|
||||
func (mod *BLERecon) Stop() error {
|
||||
mod.SetPrompt(session.DefaultPrompt)
|
||||
|
||||
return mod.SetRunning(false, func() {
|
||||
mod.quit <- true
|
||||
<-mod.done
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// +build !windows
|
||||
//go:build !windows && !freebsd && !openbsd && !netbsd
|
||||
// +build !windows,!freebsd,!openbsd,!netbsd
|
||||
|
||||
package ble
|
||||
|
||||
|
|
|
|||
321
modules/ble/ble_recon_test.go
Normal file
321
modules/ble/ble_recon_test.go
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
//go:build !windows && !freebsd && !openbsd && !netbsd
|
||||
// +build !windows,!freebsd,!openbsd,!netbsd
|
||||
|
||||
package ble
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
)
|
||||
|
||||
var (
|
||||
testSession *session.Session
|
||||
sessionOnce sync.Once
|
||||
)
|
||||
|
||||
func createMockSession(t *testing.T) *session.Session {
|
||||
sessionOnce.Do(func() {
|
||||
var err error
|
||||
testSession, err = session.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
})
|
||||
return testSession
|
||||
}
|
||||
|
||||
func TestNewBLERecon(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
if mod == nil {
|
||||
t.Fatal("NewBLERecon returned nil")
|
||||
}
|
||||
|
||||
if mod.Name() != "ble.recon" {
|
||||
t.Errorf("Expected name 'ble.recon', got '%s'", mod.Name())
|
||||
}
|
||||
|
||||
if mod.Author() != "Simone Margaritelli <evilsocket@gmail.com>" {
|
||||
t.Errorf("Unexpected author: %s", mod.Author())
|
||||
}
|
||||
|
||||
if mod.Description() == "" {
|
||||
t.Error("Empty description")
|
||||
}
|
||||
|
||||
// Check initial values
|
||||
if mod.deviceId != -1 {
|
||||
t.Errorf("Expected deviceId -1, got %d", mod.deviceId)
|
||||
}
|
||||
|
||||
if mod.connected {
|
||||
t.Error("Should not be connected initially")
|
||||
}
|
||||
|
||||
if mod.connTimeout != 5 {
|
||||
t.Errorf("Expected connection timeout 5, got %d", mod.connTimeout)
|
||||
}
|
||||
|
||||
if mod.devTTL != 30 {
|
||||
t.Errorf("Expected device TTL 30, got %d", mod.devTTL)
|
||||
}
|
||||
|
||||
// Check channels
|
||||
if mod.quit == nil {
|
||||
t.Error("Quit channel should not be nil")
|
||||
}
|
||||
|
||||
if mod.done == nil {
|
||||
t.Error("Done channel should not be nil")
|
||||
}
|
||||
|
||||
// Check handlers
|
||||
handlers := mod.Handlers()
|
||||
expectedHandlers := []string{
|
||||
"ble.recon on",
|
||||
"ble.recon off",
|
||||
"ble.clear",
|
||||
"ble.show",
|
||||
"ble.enum MAC",
|
||||
"ble.write MAC UUID HEX_DATA",
|
||||
}
|
||||
|
||||
if len(handlers) != len(expectedHandlers) {
|
||||
t.Errorf("Expected %d handlers, got %d", len(expectedHandlers), len(handlers))
|
||||
}
|
||||
|
||||
handlerNames := make(map[string]bool)
|
||||
for _, h := range handlers {
|
||||
handlerNames[h.Name] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedHandlers {
|
||||
if !handlerNames[expected] {
|
||||
t.Errorf("Handler '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsEnumerating(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Initially should not be enumerating
|
||||
if mod.isEnumerating() {
|
||||
t.Error("Should not be enumerating initially")
|
||||
}
|
||||
|
||||
// When currDevice is set, should be enumerating
|
||||
// We can't create a real BLE device here, but we can test the logic
|
||||
}
|
||||
|
||||
func TestDummyWriter(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
writer := dummyWriter{mod}
|
||||
testData := []byte("test log message")
|
||||
|
||||
n, err := writer.Write(testData)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if n != len(testData) {
|
||||
t.Errorf("Expected to write %d bytes, wrote %d", len(testData), n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameters(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Check that parameters are registered
|
||||
paramNames := []string{
|
||||
"ble.device",
|
||||
"ble.timeout",
|
||||
"ble.ttl",
|
||||
}
|
||||
|
||||
// Parameters are stored in the session environment
|
||||
// We'll just ensure the module was created properly
|
||||
for _, param := range paramNames {
|
||||
// This is a simplified check
|
||||
_ = param
|
||||
}
|
||||
|
||||
if mod == nil {
|
||||
t.Error("Module should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunningState(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Initially should not be running
|
||||
if mod.Running() {
|
||||
t.Error("Module should not be running initially")
|
||||
}
|
||||
|
||||
// Note: Cannot test actual Start/Stop without BLE hardware
|
||||
}
|
||||
|
||||
func TestChannels(t *testing.T) {
|
||||
// Skip this test as channel operations might hang in certain environments
|
||||
t.Skip("Skipping channel test to prevent potential hangs")
|
||||
}
|
||||
|
||||
func TestClearHandler(t *testing.T) {
|
||||
// Skip this test as it requires BLE to be initialized in the session
|
||||
t.Skip("Skipping clear handler test - requires initialized BLE in session")
|
||||
}
|
||||
|
||||
func TestBLEPrompt(t *testing.T) {
|
||||
expected := "{blb}{fw}BLE {fb}{reset} {bold}» {reset}"
|
||||
if blePrompt != expected {
|
||||
t.Errorf("Expected prompt '%s', got '%s'", expected, blePrompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCurrentDevice(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Test setting nil device
|
||||
mod.setCurrentDevice(nil)
|
||||
if mod.currDevice != nil {
|
||||
t.Error("Current device should be nil")
|
||||
}
|
||||
if mod.connected {
|
||||
t.Error("Should not be connected after setting nil device")
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewSelector(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Check that view selector is initialized
|
||||
if mod.selector == nil {
|
||||
t.Error("View selector should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBLEAliveInterval(t *testing.T) {
|
||||
expected := time.Duration(5) * time.Second
|
||||
if bleAliveInterval != expected {
|
||||
t.Errorf("Expected alive interval %v, got %v", expected, bleAliveInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestColNames(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Test without name
|
||||
cols := mod.colNames(false)
|
||||
expectedCols := []string{"RSSI", "MAC", "Vendor", "Flags", "Connect", "Seen"}
|
||||
if len(cols) != len(expectedCols) {
|
||||
t.Errorf("Expected %d columns, got %d", len(expectedCols), len(cols))
|
||||
}
|
||||
|
||||
// Test with name
|
||||
colsWithName := mod.colNames(true)
|
||||
expectedColsWithName := []string{"RSSI", "MAC", "Name", "Vendor", "Flags", "Connect", "Seen"}
|
||||
if len(colsWithName) != len(expectedColsWithName) {
|
||||
t.Errorf("Expected %d columns with name, got %d", len(expectedColsWithName), len(colsWithName))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoFilter(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// Without expression, should always return true
|
||||
result := mod.doFilter(nil)
|
||||
if !result {
|
||||
t.Error("doFilter should return true when no expression is set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShow(t *testing.T) {
|
||||
// Skip this test as it requires BLE to be initialized in the session
|
||||
t.Skip("Skipping show test - requires initialized BLE in session")
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
// Skip this test as it may hang trying to access BLE hardware
|
||||
t.Skip("Skipping configure test - may hang accessing BLE hardware")
|
||||
}
|
||||
|
||||
func TestGetRow(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
// We can't create a real BLE device without hardware, but we can test the logic
|
||||
// by ensuring the method exists and would handle nil gracefully
|
||||
_ = mod
|
||||
}
|
||||
|
||||
func TestDoSelection(t *testing.T) {
|
||||
// Skip this test as it requires BLE to be initialized in the session
|
||||
t.Skip("Skipping doSelection test - requires initialized BLE in session")
|
||||
}
|
||||
|
||||
func TestWriteBuffer(t *testing.T) {
|
||||
// Skip this test as it may hang trying to access BLE hardware
|
||||
t.Skip("Skipping writeBuffer test - may hang accessing BLE hardware")
|
||||
}
|
||||
|
||||
func TestEnumAllTheThings(t *testing.T) {
|
||||
// Skip this test as it may hang trying to access BLE hardware
|
||||
t.Skip("Skipping enumAllTheThings test - may hang accessing BLE hardware")
|
||||
}
|
||||
|
||||
// Benchmark tests - using singleton session to avoid flag redefinition
|
||||
func BenchmarkNewBLERecon(b *testing.B) {
|
||||
// Use a test instance to get singleton session
|
||||
s := createMockSession(&testing.T{})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = NewBLERecon(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsEnumerating(b *testing.B) {
|
||||
s := createMockSession(&testing.T{})
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = mod.isEnumerating()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDummyWriter(b *testing.B) {
|
||||
s := createMockSession(&testing.T{})
|
||||
mod := NewBLERecon(s)
|
||||
writer := dummyWriter{mod}
|
||||
testData := []byte("benchmark log message")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writer.Write(testData)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoFilter(b *testing.B) {
|
||||
s := createMockSession(&testing.T{})
|
||||
mod := NewBLERecon(s)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mod.doFilter(nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
// +build !windows
|
||||
//go:build !windows && !freebsd && !openbsd && !netbsd
|
||||
// +build !windows,!freebsd,!openbsd,!netbsd
|
||||
|
||||
package ble
|
||||
|
||||
|
|
@ -6,7 +7,7 @@ import (
|
|||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
|
||||
"github.com/evilsocket/islazy/ops"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// +build !windows
|
||||
//go:build !windows && !freebsd && !openbsd && !netbsd
|
||||
// +build !windows,!freebsd,!openbsd,!netbsd
|
||||
|
||||
package ble
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/gatt"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// +build !windows
|
||||
//go:build !windows && !freebsd && !openbsd && !netbsd
|
||||
// +build !windows,!freebsd,!openbsd,!netbsd
|
||||
|
||||
package ble
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
)
|
||||
|
||||
type ByBLERSSISorter []*network.BLEDevice
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// +build windows
|
||||
//go:build windows || freebsd || netbsd || openbsd
|
||||
// +build windows freebsd netbsd openbsd
|
||||
|
||||
package ble
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
)
|
||||
|
||||
type BLERecon struct {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ import (
|
|||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/acarl005/stripansi"
|
||||
"github.com/bettercap/bettercap/modules/events_stream"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/modules/events_stream"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
irc "github.com/thoj/go-ircevent"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type settings struct {
|
||||
|
|
|
|||
356
modules/c2/c2_test.go
Normal file
356
modules/c2/c2_test.go
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
package c2
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
)
|
||||
|
||||
var (
|
||||
testSession *session.Session
|
||||
sessionOnce sync.Once
|
||||
)
|
||||
|
||||
func createMockSession(t *testing.T) *session.Session {
|
||||
sessionOnce.Do(func() {
|
||||
var err error
|
||||
testSession, err = session.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
})
|
||||
return testSession
|
||||
}
|
||||
|
||||
func TestNewC2(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
if mod == nil {
|
||||
t.Fatal("NewC2 returned nil")
|
||||
}
|
||||
|
||||
if mod.Name() != "c2" {
|
||||
t.Errorf("Expected name 'c2', got '%s'", mod.Name())
|
||||
}
|
||||
|
||||
if mod.Author() != "Simone Margaritelli <evilsocket@gmail.com>" {
|
||||
t.Errorf("Unexpected author: %s", mod.Author())
|
||||
}
|
||||
|
||||
if mod.Description() == "" {
|
||||
t.Error("Empty description")
|
||||
}
|
||||
|
||||
// Check default settings
|
||||
if mod.settings.server != "localhost:6697" {
|
||||
t.Errorf("Expected default server 'localhost:6697', got '%s'", mod.settings.server)
|
||||
}
|
||||
|
||||
if !mod.settings.tls {
|
||||
t.Error("Expected TLS to be enabled by default")
|
||||
}
|
||||
|
||||
if mod.settings.tlsVerify {
|
||||
t.Error("Expected TLS verify to be disabled by default")
|
||||
}
|
||||
|
||||
if mod.settings.nick != "bettercap" {
|
||||
t.Errorf("Expected default nick 'bettercap', got '%s'", mod.settings.nick)
|
||||
}
|
||||
|
||||
if mod.settings.user != "bettercap" {
|
||||
t.Errorf("Expected default user 'bettercap', got '%s'", mod.settings.user)
|
||||
}
|
||||
|
||||
if mod.settings.operator != "admin" {
|
||||
t.Errorf("Expected default operator 'admin', got '%s'", mod.settings.operator)
|
||||
}
|
||||
|
||||
// Check channels
|
||||
if mod.quit == nil {
|
||||
t.Error("Quit channel should not be nil")
|
||||
}
|
||||
|
||||
// Check maps
|
||||
if mod.templates == nil {
|
||||
t.Error("Templates map should not be nil")
|
||||
}
|
||||
|
||||
if mod.channels == nil {
|
||||
t.Error("Channels map should not be nil")
|
||||
}
|
||||
|
||||
// Check handlers
|
||||
handlers := mod.Handlers()
|
||||
expectedHandlers := []string{
|
||||
"c2 on",
|
||||
"c2 off",
|
||||
"c2.channel.set EVENT_TYPE CHANNEL",
|
||||
"c2.channel.clear EVENT_TYPE",
|
||||
"c2.template.set EVENT_TYPE TEMPLATE",
|
||||
"c2.template.clear EVENT_TYPE",
|
||||
}
|
||||
|
||||
if len(handlers) != len(expectedHandlers) {
|
||||
t.Errorf("Expected %d handlers, got %d", len(expectedHandlers), len(handlers))
|
||||
}
|
||||
|
||||
handlerNames := make(map[string]bool)
|
||||
for _, h := range handlers {
|
||||
handlerNames[h.Name] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedHandlers {
|
||||
if !handlerNames[expected] {
|
||||
t.Errorf("Handler '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultSettings(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
// Check default channel settings
|
||||
if mod.settings.eventsChannel != "#events" {
|
||||
t.Errorf("Expected default events channel '#events', got '%s'", mod.settings.eventsChannel)
|
||||
}
|
||||
|
||||
if mod.settings.outputChannel != "#events" {
|
||||
t.Errorf("Expected default output channel '#events', got '%s'", mod.settings.outputChannel)
|
||||
}
|
||||
|
||||
if mod.settings.controlChannel != "#events" {
|
||||
t.Errorf("Expected default control channel '#events', got '%s'", mod.settings.controlChannel)
|
||||
}
|
||||
|
||||
if mod.settings.password != "password" {
|
||||
t.Errorf("Expected default password 'password', got '%s'", mod.settings.password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunningState(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
// Initially should not be running
|
||||
if mod.Running() {
|
||||
t.Error("Module should not be running initially")
|
||||
}
|
||||
|
||||
// Note: Cannot test actual Start/Stop without IRC server
|
||||
}
|
||||
|
||||
func TestEventContext(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
|
||||
ctx := eventContext{
|
||||
Session: s,
|
||||
Event: session.Event{Tag: "test.event"},
|
||||
}
|
||||
|
||||
if ctx.Session == nil {
|
||||
t.Error("Session should not be nil")
|
||||
}
|
||||
|
||||
if ctx.Event.Tag != "test.event" {
|
||||
t.Errorf("Expected event tag 'test.event', got '%s'", ctx.Event.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelHandlers(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
// Test channel.set handler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.channel.set EVENT_TYPE CHANNEL" {
|
||||
err := h.Exec([]string{"test.event", "#test"})
|
||||
if err != nil {
|
||||
t.Errorf("channel.set handler failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify channel was set
|
||||
if channel, found := mod.channels["test.event"]; !found {
|
||||
t.Error("Channel was not set")
|
||||
} else if channel != "#test" {
|
||||
t.Errorf("Expected channel '#test', got '%s'", channel)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Test channel.clear handler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.channel.clear EVENT_TYPE" {
|
||||
err := h.Exec([]string{"test.event"})
|
||||
if err != nil {
|
||||
t.Errorf("channel.clear handler failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify channel was cleared
|
||||
if _, found := mod.channels["test.event"]; found {
|
||||
t.Error("Channel was not cleared")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateHandlers(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
// Test template.set handler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.template.set EVENT_TYPE TEMPLATE" {
|
||||
err := h.Exec([]string{"test.event", "Event: {{.Event.Tag}}"})
|
||||
if err != nil {
|
||||
t.Errorf("template.set handler failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify template was set
|
||||
if tpl, found := mod.templates["test.event"]; !found {
|
||||
t.Error("Template was not set")
|
||||
} else if tpl == nil {
|
||||
t.Error("Template is nil")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Test template.clear handler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.template.clear EVENT_TYPE" {
|
||||
err := h.Exec([]string{"test.event"})
|
||||
if err != nil {
|
||||
t.Errorf("template.clear handler failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify template was cleared
|
||||
if _, found := mod.templates["test.event"]; found {
|
||||
t.Error("Template was not cleared")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearNonExistent(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
// Test clearing non-existent channel
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.channel.clear EVENT_TYPE" {
|
||||
err := h.Exec([]string{"non.existent"})
|
||||
if err == nil {
|
||||
t.Error("Expected error when clearing non-existent channel")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Test clearing non-existent template
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.template.clear EVENT_TYPE" {
|
||||
err := h.Exec([]string{"non.existent"})
|
||||
if err == nil {
|
||||
t.Error("Expected error when clearing non-existent template")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameters(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewC2(s)
|
||||
|
||||
// Check that all parameters are registered
|
||||
paramNames := []string{
|
||||
"c2.server",
|
||||
"c2.server.tls",
|
||||
"c2.server.tls.verify",
|
||||
"c2.operator",
|
||||
"c2.nick",
|
||||
"c2.username",
|
||||
"c2.password",
|
||||
"c2.sasl.username",
|
||||
"c2.sasl.password",
|
||||
"c2.channel.output",
|
||||
"c2.channel.events",
|
||||
"c2.channel.control",
|
||||
}
|
||||
|
||||
// Parameters are stored in the session environment
|
||||
for _, param := range paramNames {
|
||||
// This is a simplified check
|
||||
_ = param
|
||||
}
|
||||
|
||||
if mod == nil {
|
||||
t.Error("Module should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateExecution(t *testing.T) {
|
||||
// Test template parsing and execution
|
||||
tmpl, err := template.New("test").Parse("Event: {{.Event.Tag}}")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse template: %v", err)
|
||||
}
|
||||
|
||||
if tmpl == nil {
|
||||
t.Error("Template should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkNewC2(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = NewC2(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkChannelSet(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
mod := NewC2(s)
|
||||
|
||||
var handler *session.ModuleHandler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.channel.set EVENT_TYPE CHANNEL" {
|
||||
handler = &h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
handler.Exec([]string{"test.event", "#test"})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTemplateSet(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
mod := NewC2(s)
|
||||
|
||||
var handler *session.ModuleHandler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "c2.template.set EVENT_TYPE TEMPLATE" {
|
||||
handler = &h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
handler.Exec([]string{"test.event", "Event: {{.Event.Tag}}"})
|
||||
}
|
||||
}
|
||||
133
modules/can/can.go
Normal file
133
modules/can/can.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"go.einride.tech/can/pkg/socketcan"
|
||||
)
|
||||
|
||||
type CANModule struct {
|
||||
session.SessionModule
|
||||
|
||||
transport string
|
||||
deviceName string
|
||||
dumpName string
|
||||
dumpInject bool
|
||||
filter string
|
||||
filterExpr *bexpr.Evaluator
|
||||
dbc *DBC
|
||||
obd2 *OBD2
|
||||
conn net.Conn
|
||||
recv *socketcan.Receiver
|
||||
send *socketcan.Transmitter
|
||||
}
|
||||
|
||||
func NewCanModule(s *session.Session) *CANModule {
|
||||
mod := &CANModule{
|
||||
SessionModule: session.NewSessionModule("can", s),
|
||||
filter: "",
|
||||
dbc: &DBC{},
|
||||
obd2: &OBD2{},
|
||||
filterExpr: nil,
|
||||
transport: "can",
|
||||
deviceName: "can0",
|
||||
dumpName: "",
|
||||
dumpInject: false,
|
||||
}
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.device",
|
||||
mod.deviceName,
|
||||
"",
|
||||
"CAN-bus device."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.dump",
|
||||
mod.dumpName,
|
||||
"",
|
||||
"Load CAN traffic from this candump log file."))
|
||||
|
||||
mod.AddParam(session.NewBoolParameter("can.dump.inject",
|
||||
fmt.Sprintf("%v", mod.dumpInject),
|
||||
"Write CAN traffic read form the candump log file to the selected can.device."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.transport",
|
||||
mod.transport,
|
||||
"",
|
||||
"Network type, can be 'can' for SocketCAN or 'udp'."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("can.filter",
|
||||
"",
|
||||
"",
|
||||
"Optional boolean expression to select frames to report."))
|
||||
|
||||
mod.AddParam(session.NewBoolParameter("can.parse.obd2",
|
||||
"false",
|
||||
"Enable built in OBD2 PID parsing."))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.recon on", "",
|
||||
"Start CAN-bus discovery.",
|
||||
func(args []string) error {
|
||||
return mod.Start()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.recon off", "",
|
||||
"Stop CAN-bus discovery.",
|
||||
func(args []string) error {
|
||||
return mod.Stop()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.clear", "",
|
||||
"Clear everything collected by the discovery module.",
|
||||
func(args []string) error {
|
||||
mod.Session.CAN.Clear()
|
||||
return nil
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.show", "",
|
||||
"Show a list of detected CAN devices.",
|
||||
func(args []string) error {
|
||||
return mod.Show()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.dbc.load NAME", "can.dbc.load (.+)",
|
||||
"Load a DBC file from the list of available ones or from disk.",
|
||||
func(args []string) error {
|
||||
return mod.dbcLoad(args[0])
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.inject FRAME_EXPRESSION", `(?i)^can\.inject\s+([a-fA-F0-9#R]+)$`,
|
||||
"Parse FRAME_EXPRESSION as 'id#data' and inject it as a CAN frame.",
|
||||
func(args []string) error {
|
||||
if !mod.Running() {
|
||||
return errors.New("can module not running")
|
||||
}
|
||||
return mod.Inject(args[0])
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("can.fuzz ID_OR_NODE_NAME OPTIONAL_SIZE", `(?i)^can\.fuzz\s+([^\s]+)\s*(\d*)$`,
|
||||
"If an hexadecimal frame ID is specified, create a randomized version of it and inject it. If a node name is specified, a random message for the given node will be instead used.",
|
||||
func(args []string) error {
|
||||
if !mod.Running() {
|
||||
return errors.New("can module not running")
|
||||
}
|
||||
|
||||
return mod.Fuzz(args[0], args[1])
|
||||
}))
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
func (mod *CANModule) Name() string {
|
||||
return "can"
|
||||
}
|
||||
|
||||
func (mod *CANModule) Description() string {
|
||||
return "A scanner and frames injection module for CAN-bus."
|
||||
}
|
||||
|
||||
func (mod *CANModule) Author() string {
|
||||
return "Simone Margaritelli <evilsocket@gmail.com>"
|
||||
}
|
||||
173
modules/can/can_dbc.go
Normal file
173
modules/can/can_dbc.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"go.einride.tech/can/pkg/descriptor"
|
||||
)
|
||||
|
||||
type DBC struct {
|
||||
sync.RWMutex
|
||||
|
||||
path string
|
||||
db *descriptor.Database
|
||||
}
|
||||
|
||||
func (dbc *DBC) Loaded() bool {
|
||||
dbc.RLock()
|
||||
defer dbc.RUnlock()
|
||||
|
||||
return dbc.db != nil
|
||||
}
|
||||
|
||||
func (dbc *DBC) LoadFile(mod *CANModule, path string) error {
|
||||
input, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't read %s: %v", path, err)
|
||||
}
|
||||
return dbc.LoadData(mod, path, input)
|
||||
}
|
||||
|
||||
func (dbc *DBC) LoadData(mod *CANModule, name string, input []byte) error {
|
||||
dbc.Lock()
|
||||
defer dbc.Unlock()
|
||||
|
||||
mod.Debug("compiling %s ...", name)
|
||||
|
||||
result, err := dbcCompile(name, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't compile %s: %v", name, err)
|
||||
}
|
||||
|
||||
for _, warning := range result.Warnings {
|
||||
mod.Warning("%v", warning)
|
||||
}
|
||||
|
||||
dbc.path = name
|
||||
dbc.db = result.Database
|
||||
|
||||
mod.Info("%s loaded", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbc *DBC) Parse(mod *CANModule, msg *Message) bool {
|
||||
dbc.RLock()
|
||||
defer dbc.RUnlock()
|
||||
|
||||
// did we load any DBC database?
|
||||
if dbc.db == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// if the database contains this message id
|
||||
if message, found := dbc.db.Message(msg.Frame.ID); found {
|
||||
msg.Name = message.Name
|
||||
|
||||
// find source full info in DBC nodes
|
||||
sourceName := message.SenderNode
|
||||
sourceDesc := ""
|
||||
if sender, found := dbc.db.Node(message.SenderNode); found {
|
||||
sourceName = sender.Name
|
||||
sourceDesc = sender.Description
|
||||
}
|
||||
|
||||
// add CAN source if new
|
||||
_, msg.Source = mod.Session.CAN.AddIfNew(sourceName, sourceDesc, msg.Frame.Data[:])
|
||||
|
||||
// parse signals
|
||||
for _, signal := range message.Signals {
|
||||
var value string
|
||||
|
||||
if signal.Length <= 32 && signal.IsFloat {
|
||||
value = fmt.Sprintf("%f", signal.UnmarshalFloat(msg.Frame.Data))
|
||||
} else if signal.Length == 1 {
|
||||
value = fmt.Sprintf("%v", signal.UnmarshalBool(msg.Frame.Data))
|
||||
} else if signal.IsSigned {
|
||||
value = fmt.Sprintf("%d", signal.UnmarshalSigned(msg.Frame.Data))
|
||||
} else {
|
||||
value = fmt.Sprintf("%d", signal.UnmarshalUnsigned(msg.Frame.Data))
|
||||
}
|
||||
msg.Signals[signal.Name] = str.Trim(fmt.Sprintf("%s %s", value, signal.Unit))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (dbc *DBC) MessagesBySender(senderId string) []*descriptor.Message {
|
||||
dbc.RLock()
|
||||
defer dbc.RUnlock()
|
||||
|
||||
fromSender := make([]*descriptor.Message, 0)
|
||||
|
||||
if dbc.db == nil {
|
||||
return fromSender
|
||||
}
|
||||
|
||||
for _, msg := range dbc.db.Messages {
|
||||
if msg.SenderNode == senderId {
|
||||
fromSender = append(fromSender, msg)
|
||||
}
|
||||
}
|
||||
|
||||
return fromSender
|
||||
}
|
||||
|
||||
func (dbc *DBC) MessageById(frameID uint32) *descriptor.Message {
|
||||
dbc.RLock()
|
||||
defer dbc.RUnlock()
|
||||
|
||||
if dbc.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if message, found := dbc.db.Message(frameID); found {
|
||||
return message
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dbc *DBC) Messages() []*descriptor.Message {
|
||||
dbc.RLock()
|
||||
defer dbc.RUnlock()
|
||||
|
||||
if dbc.db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return dbc.db.Messages
|
||||
}
|
||||
|
||||
func (dbc *DBC) AvailableMessages() []string {
|
||||
avail := []string{}
|
||||
for _, msg := range dbc.Messages() {
|
||||
avail = append(avail, fmt.Sprintf("%d (%s)", msg.ID, msg.Name))
|
||||
}
|
||||
return avail
|
||||
}
|
||||
|
||||
func (dbc *DBC) Senders() []string {
|
||||
dbc.RLock()
|
||||
defer dbc.RUnlock()
|
||||
|
||||
senders := make([]string, 0)
|
||||
if dbc.db == nil {
|
||||
return senders
|
||||
}
|
||||
|
||||
uniq := make(map[string]bool)
|
||||
for _, msg := range dbc.db.Messages {
|
||||
uniq[msg.SenderNode] = true
|
||||
}
|
||||
|
||||
for sender, _ := range uniq {
|
||||
senders = append(senders, sender)
|
||||
}
|
||||
|
||||
return senders
|
||||
}
|
||||
227
modules/can/can_dbc_compile.go
Normal file
227
modules/can/can_dbc_compile.go
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"go.einride.tech/can/pkg/dbc"
|
||||
"go.einride.tech/can/pkg/descriptor"
|
||||
)
|
||||
|
||||
type CompileResult struct {
|
||||
Database *descriptor.Database
|
||||
Warnings []error
|
||||
}
|
||||
|
||||
func dbcCompile(sourceFile string, data []byte) (result *CompileResult, err error) {
|
||||
p := dbc.NewParser(sourceFile, data)
|
||||
if err := p.Parse(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse DBC source file: %w", err)
|
||||
}
|
||||
defs := p.Defs()
|
||||
c := &compiler{
|
||||
db: &descriptor.Database{SourceFile: sourceFile},
|
||||
defs: defs,
|
||||
}
|
||||
c.collectDescriptors()
|
||||
c.addMetadata()
|
||||
c.sortDescriptors()
|
||||
return &CompileResult{Database: c.db, Warnings: c.warnings}, nil
|
||||
}
|
||||
|
||||
type compileError struct {
|
||||
def dbc.Def
|
||||
reason string
|
||||
}
|
||||
|
||||
func (e *compileError) Error() string {
|
||||
return fmt.Sprintf("failed to compile: %v (%v)", e.reason, e.def)
|
||||
}
|
||||
|
||||
type compiler struct {
|
||||
db *descriptor.Database
|
||||
defs []dbc.Def
|
||||
warnings []error
|
||||
}
|
||||
|
||||
func (c *compiler) addWarning(warning error) {
|
||||
c.warnings = append(c.warnings, warning)
|
||||
}
|
||||
|
||||
func (c *compiler) collectDescriptors() {
|
||||
for _, def := range c.defs {
|
||||
switch def := def.(type) {
|
||||
case *dbc.VersionDef:
|
||||
c.db.Version = def.Version
|
||||
case *dbc.MessageDef:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
message := &descriptor.Message{
|
||||
Name: string(def.Name),
|
||||
ID: def.MessageID.ToCAN(),
|
||||
IsExtended: def.MessageID.IsExtended(),
|
||||
Length: uint8(def.Size),
|
||||
SenderNode: string(def.Transmitter),
|
||||
}
|
||||
for _, signalDef := range def.Signals {
|
||||
signal := &descriptor.Signal{
|
||||
Name: string(signalDef.Name),
|
||||
IsBigEndian: signalDef.IsBigEndian,
|
||||
IsSigned: signalDef.IsSigned,
|
||||
IsMultiplexer: signalDef.IsMultiplexerSwitch,
|
||||
IsMultiplexed: signalDef.IsMultiplexed,
|
||||
MultiplexerValue: uint(signalDef.MultiplexerSwitch),
|
||||
Start: uint8(signalDef.StartBit),
|
||||
Length: uint8(signalDef.Size),
|
||||
Scale: signalDef.Factor,
|
||||
Offset: signalDef.Offset,
|
||||
Min: signalDef.Minimum,
|
||||
Max: signalDef.Maximum,
|
||||
Unit: signalDef.Unit,
|
||||
}
|
||||
for _, receiver := range signalDef.Receivers {
|
||||
signal.ReceiverNodes = append(signal.ReceiverNodes, string(receiver))
|
||||
}
|
||||
message.Signals = append(message.Signals, signal)
|
||||
}
|
||||
c.db.Messages = append(c.db.Messages, message)
|
||||
case *dbc.NodesDef:
|
||||
for _, node := range def.NodeNames {
|
||||
c.db.Nodes = append(c.db.Nodes, &descriptor.Node{Name: string(node)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) addMetadata() {
|
||||
for _, def := range c.defs {
|
||||
switch def := def.(type) {
|
||||
case *dbc.SignalValueTypeDef:
|
||||
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
continue
|
||||
}
|
||||
switch def.SignalValueType {
|
||||
case dbc.SignalValueTypeInt:
|
||||
signal.IsFloat = false
|
||||
case dbc.SignalValueTypeFloat32:
|
||||
if signal.Length == 32 {
|
||||
signal.IsFloat = true
|
||||
} else {
|
||||
reason := fmt.Sprintf("incorrect float signal length: %d", signal.Length)
|
||||
c.addWarning(&compileError{def: def, reason: reason})
|
||||
}
|
||||
default:
|
||||
reason := fmt.Sprintf("unsupported signal value type: %v", def.SignalValueType)
|
||||
c.addWarning(&compileError{def: def, reason: reason})
|
||||
}
|
||||
case *dbc.CommentDef:
|
||||
switch def.ObjectType {
|
||||
case dbc.ObjectTypeMessage:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
message, ok := c.db.Message(def.MessageID.ToCAN())
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared message"})
|
||||
continue
|
||||
}
|
||||
message.Description = def.Comment
|
||||
case dbc.ObjectTypeSignal:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
continue
|
||||
}
|
||||
signal.Description = def.Comment
|
||||
case dbc.ObjectTypeNetworkNode:
|
||||
node, ok := c.db.Node(string(def.NodeName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared node"})
|
||||
continue
|
||||
}
|
||||
node.Description = def.Comment
|
||||
}
|
||||
case *dbc.ValueDescriptionsDef:
|
||||
if def.MessageID == dbc.IndependentSignalsMessageID {
|
||||
continue // don't compile
|
||||
}
|
||||
if def.ObjectType != dbc.ObjectTypeSignal {
|
||||
continue // don't compile
|
||||
}
|
||||
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
continue
|
||||
}
|
||||
for _, valueDescription := range def.ValueDescriptions {
|
||||
signal.ValueDescriptions = append(signal.ValueDescriptions, &descriptor.ValueDescription{
|
||||
Description: valueDescription.Description,
|
||||
Value: int64(valueDescription.Value),
|
||||
})
|
||||
}
|
||||
case *dbc.AttributeValueForObjectDef:
|
||||
switch def.ObjectType {
|
||||
case dbc.ObjectTypeMessage:
|
||||
msg, ok := c.db.Message(def.MessageID.ToCAN())
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared message"})
|
||||
continue
|
||||
}
|
||||
switch def.AttributeName {
|
||||
case "GenMsgSendType":
|
||||
if err := msg.SendType.UnmarshalString(def.StringValue); err != nil {
|
||||
c.addWarning(&compileError{def: def, reason: err.Error()})
|
||||
continue
|
||||
}
|
||||
case "GenMsgCycleTime":
|
||||
msg.CycleTime = time.Duration(def.IntValue) * time.Millisecond
|
||||
case "GenMsgDelayTime":
|
||||
msg.DelayTime = time.Duration(def.IntValue) * time.Millisecond
|
||||
}
|
||||
case dbc.ObjectTypeSignal:
|
||||
sig, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
|
||||
if !ok {
|
||||
c.addWarning(&compileError{def: def, reason: "no declared signal"})
|
||||
}
|
||||
if def.AttributeName == "GenSigStartValue" {
|
||||
sig.DefaultValue = int(def.IntValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *compiler) sortDescriptors() {
|
||||
// Sort nodes by name
|
||||
sort.Slice(c.db.Nodes, func(i, j int) bool {
|
||||
return c.db.Nodes[i].Name < c.db.Nodes[j].Name
|
||||
})
|
||||
// Sort messages by ID
|
||||
sort.Slice(c.db.Messages, func(i, j int) bool {
|
||||
return c.db.Messages[i].ID < c.db.Messages[j].ID
|
||||
})
|
||||
for _, m := range c.db.Messages {
|
||||
m := m
|
||||
// Sort signals by start (and multiplexer value)
|
||||
sort.Slice(m.Signals, func(j, k int) bool {
|
||||
if m.Signals[j].MultiplexerValue < m.Signals[k].MultiplexerValue {
|
||||
return true
|
||||
}
|
||||
return m.Signals[j].Start < m.Signals[k].Start
|
||||
})
|
||||
// Sort value descriptions by value
|
||||
for _, s := range m.Signals {
|
||||
s := s
|
||||
sort.Slice(s.ValueDescriptions, func(k, l int) bool {
|
||||
return s.ValueDescriptions[k].Value < s.ValueDescriptions[l].Value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
6
modules/can/can_dbc_load.go
Normal file
6
modules/can/can_dbc_load.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package can
|
||||
|
||||
func (mod *CANModule) dbcLoad(name string) error {
|
||||
// load as file
|
||||
return mod.dbc.LoadFile(mod, name)
|
||||
}
|
||||
114
modules/can/can_dump_reader.go
Normal file
114
modules/can/can_dump_reader.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/evilsocket/islazy/str"
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
// (1700623093.260875) can0 7E0#0322128C00000000
|
||||
var dumpLineParser = regexp.MustCompile(`(?m)^\(([\d\.]+)\)\s+([^\s]+)\s+(.+)`)
|
||||
|
||||
type dumpEntry struct {
|
||||
Time time.Time
|
||||
Device string
|
||||
Frame string
|
||||
}
|
||||
|
||||
func parseTimeval(timeval string) (time.Time, error) {
|
||||
parts := strings.Split(timeval, ".")
|
||||
if len(parts) != 2 {
|
||||
return time.Time{}, fmt.Errorf("invalid timeval format")
|
||||
}
|
||||
|
||||
seconds, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("invalid seconds value: %v", err)
|
||||
}
|
||||
|
||||
microseconds, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("invalid microseconds value: %v", err)
|
||||
}
|
||||
|
||||
return time.Unix(seconds, microseconds*1000), nil
|
||||
}
|
||||
|
||||
func (mod *CANModule) startDumpReader() error {
|
||||
mod.Info("loading CAN dump from %s ...", mod.dumpName)
|
||||
|
||||
file, err := os.Open(mod.dumpName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
entries := make([]dumpEntry, 0)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
if line := str.Trim(scanner.Text()); line != "" {
|
||||
if m := dumpLineParser.FindStringSubmatch(line); len(m) != 4 {
|
||||
mod.Warning("unexpected line: '%s' -> %d matches", line, len(m))
|
||||
} else if timeval, err := parseTimeval(m[1]); err != nil {
|
||||
mod.Warning("can't parse (seconds.microseconds) from line: '%s': %v", line, err)
|
||||
} else {
|
||||
entries = append(entries, dumpEntry{
|
||||
Time: timeval,
|
||||
Device: m[2],
|
||||
Frame: m[3],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numEntries := len(entries)
|
||||
lastEntry := numEntries - 1
|
||||
|
||||
mod.Info("loaded %d entries from candump log", numEntries)
|
||||
|
||||
go func() {
|
||||
mod.Info("candump reader started ...")
|
||||
|
||||
for i, entry := range entries {
|
||||
frame := can.Frame{}
|
||||
if err := frame.UnmarshalString(entry.Frame); err != nil {
|
||||
mod.Error("could not unmarshal CAN frame: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if mod.dumpInject {
|
||||
if err := mod.send.TransmitFrame(context.Background(), frame); err != nil {
|
||||
mod.Error("could not send CAN frame: %v", err)
|
||||
}
|
||||
} else {
|
||||
mod.onFrame(frame)
|
||||
}
|
||||
|
||||
// compute delay before the next frame
|
||||
if i < lastEntry {
|
||||
next := entries[i+1]
|
||||
diff := next.Time.Sub(entry.Time)
|
||||
time.Sleep(diff)
|
||||
}
|
||||
|
||||
if !mod.Running() {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
108
modules/can/can_fuzz.go
Normal file
108
modules/can/can_fuzz.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
func (mod *CANModule) fuzzSelectFrame(id string, rng *rand.Rand) (uint64, error) {
|
||||
// let's try as an hex number first
|
||||
frameID, err := strconv.ParseUint(id, 16, 32)
|
||||
if err != nil {
|
||||
// not a number, use as node name if we have a dbc
|
||||
if mod.dbc.Loaded() {
|
||||
fromSender := mod.dbc.MessagesBySender(id)
|
||||
if len(fromSender) == 0 {
|
||||
return 0, fmt.Errorf("no messages defined in DBC file for node %s, available nodes: %s", id, mod.dbc.Senders())
|
||||
}
|
||||
|
||||
idx := rng.Intn(len(fromSender))
|
||||
selected := fromSender[idx]
|
||||
mod.Info("selected %s > (%d) %s", id, selected.ID, selected.Name)
|
||||
frameID = uint64(selected.ID)
|
||||
} else {
|
||||
// no dbc, just return the error
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return frameID, nil
|
||||
}
|
||||
|
||||
func (mod *CANModule) fuzzGenerateFrame(frameID uint64, size int, rng *rand.Rand) (*can.Frame, error) {
|
||||
dataLen := 0
|
||||
frameData := ([]byte)(nil)
|
||||
|
||||
// if we have a DBC
|
||||
if mod.dbc.Loaded() {
|
||||
if message := mod.dbc.MessageById(uint32(frameID)); message != nil {
|
||||
mod.Info("using message %s", message.Name)
|
||||
dataLen = int(message.Length)
|
||||
frameData = make([]byte, dataLen)
|
||||
if _, err := rand.Read(frameData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("message with id %d not found in DBC file, available ids: %v", frameID, strings.Join(mod.dbc.AvailableMessages(), ", "))
|
||||
}
|
||||
} else {
|
||||
if size <= 0 {
|
||||
// pick randomly
|
||||
dataLen = rng.Intn(int(can.MaxDataLength))
|
||||
} else {
|
||||
// user selected
|
||||
dataLen = size
|
||||
}
|
||||
frameData = make([]byte, dataLen)
|
||||
if _, err := rand.Read(frameData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mod.Warning("no dbc loaded, creating frame with %d bytes of random data", dataLen)
|
||||
}
|
||||
|
||||
frame := can.Frame{
|
||||
ID: uint32(frameID),
|
||||
Length: uint8(dataLen),
|
||||
IsRemote: false,
|
||||
IsExtended: false,
|
||||
}
|
||||
|
||||
copy(frame.Data[:], frameData)
|
||||
|
||||
return &frame, nil
|
||||
}
|
||||
|
||||
func (mod *CANModule) Fuzz(id string, optSize string) error {
|
||||
rncSource := rand.NewSource(time.Now().Unix())
|
||||
rng := rand.New(rncSource)
|
||||
|
||||
fuzzSize := 0
|
||||
if optSize != "" {
|
||||
if num, err := strconv.Atoi(optSize); err != nil {
|
||||
return fmt.Errorf("could not parse numeric size from '%s': %v", optSize, err)
|
||||
} else if num > 8 {
|
||||
return fmt.Errorf("max can frame size is 8, %d given", num)
|
||||
} else {
|
||||
fuzzSize = num
|
||||
}
|
||||
}
|
||||
|
||||
if frameID, err := mod.fuzzSelectFrame(id, rng); err != nil {
|
||||
return err
|
||||
} else if frame, err := mod.fuzzGenerateFrame(frameID, fuzzSize, rng); err != nil {
|
||||
return err
|
||||
} else {
|
||||
mod.Info("injecting %s of CAN frame %d ...",
|
||||
humanize.Bytes(uint64(frame.Length)), frame.ID)
|
||||
if err := mod.send.TransmitFrame(context.Background(), *frame); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
24
modules/can/can_inject.go
Normal file
24
modules/can/can_inject.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
func (mod *CANModule) Inject(expr string) (err error) {
|
||||
frame := can.Frame{}
|
||||
if err := frame.UnmarshalString(expr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.Info("injecting %s of CAN frame %d ...",
|
||||
humanize.Bytes(uint64(frame.Length)), frame.ID)
|
||||
|
||||
if err := mod.send.TransmitFrame(context.Background(), frame); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
24
modules/can/can_message.go
Normal file
24
modules/can/can_message.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
// the raw frame
|
||||
Frame can.Frame
|
||||
// parsed as OBD2
|
||||
OBD2 *OBD2Message
|
||||
// parsed from DBC
|
||||
Name string
|
||||
Source *network.CANDevice
|
||||
Signals map[string]string
|
||||
}
|
||||
|
||||
func NewCanMessage(frame can.Frame) Message {
|
||||
return Message{
|
||||
Frame: frame,
|
||||
Signals: make(map[string]string),
|
||||
}
|
||||
}
|
||||
55
modules/can/can_obd2.go
Normal file
55
modules/can/can_obd2.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type OBD2 struct {
|
||||
sync.RWMutex
|
||||
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (obd *OBD2) Enabled() bool {
|
||||
obd.RLock()
|
||||
defer obd.RUnlock()
|
||||
return obd.enabled
|
||||
}
|
||||
|
||||
func (obd *OBD2) Enable(enable bool) {
|
||||
obd.RLock()
|
||||
defer obd.RUnlock()
|
||||
obd.enabled = enable
|
||||
}
|
||||
|
||||
func (obd *OBD2) Parse(mod *CANModule, msg *Message) bool {
|
||||
obd.RLock()
|
||||
defer obd.RUnlock()
|
||||
|
||||
// did we load any DBC database?
|
||||
if !obd.enabled {
|
||||
return false
|
||||
}
|
||||
|
||||
odbMessage := &OBD2Message{}
|
||||
|
||||
if msg.Frame.ID == OBD2BroadcastRequestID || msg.Frame.ID == OBD2BroadcastRequestID29bit {
|
||||
// parse as request
|
||||
if odbMessage.ParseRequest(msg.Frame) {
|
||||
msg.OBD2 = odbMessage
|
||||
return true
|
||||
}
|
||||
} else if (msg.Frame.ID >= OBD2ECUResponseMinID && msg.Frame.ID <= OBD2ECUResponseMaxID) ||
|
||||
(msg.Frame.ID >= OBD2ECUResponseMinID29bit && msg.Frame.ID <= OBD2ECUResponseMaxID29bit) {
|
||||
// parse as response
|
||||
if odbMessage.ParseResponse(msg.Frame) {
|
||||
msg.OBD2 = odbMessage
|
||||
// add CAN source if new
|
||||
_, msg.Source = mod.Session.CAN.AddIfNew(fmt.Sprintf("ECU_%d", odbMessage.ECU), "", msg.Frame.Data[:])
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
74
modules/can/can_obd2_message.go
Normal file
74
modules/can/can_obd2_message.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// https://en.wikipedia.org/wiki/OBD-II_PIDs
|
||||
|
||||
// https://www.csselectronics.com/pages/obd2-explained-simple-intro
|
||||
// https://www.csselectronics.com/pages/obd2-pid-table-on-board-diagnostics-j1979
|
||||
|
||||
// https://stackoverflow.com/questions/40826932/how-can-i-get-mode-pids-from-raw-obd2-identifier-11-or-29-bit
|
||||
|
||||
// https://github.com/ejvaughan/obdii/blob/master/src/OBDII.c
|
||||
|
||||
const OBD2BroadcastRequestID = 0x7DF
|
||||
const OBD2ECUResponseMinID = 0x7E0
|
||||
const OBD2ECUResponseMaxID = 0x7EF
|
||||
const OBD2BroadcastRequestID29bit = 0x18DB33F1
|
||||
const OBD2ECUResponseMinID29bit = 0x18DAF100
|
||||
const OBD2ECUResponseMaxID29bit = 0x18DAF1FF
|
||||
|
||||
type OBD2Service uint8
|
||||
|
||||
func (s OBD2Service) String() string {
|
||||
switch s {
|
||||
case 0x01:
|
||||
return "Show current data"
|
||||
case 0x02:
|
||||
return "Show freeze frame data"
|
||||
case 0x03:
|
||||
return "Show stored Diagnostic Trouble Codes"
|
||||
case 0x04:
|
||||
return "Clear Diagnostic Trouble Codes and stored values"
|
||||
case 0x05:
|
||||
return "Test results, oxygen sensor monitoring (non CAN only)"
|
||||
case 0x06:
|
||||
return "Test results, other component/system monitoring (Test results, oxygen sensor monitoring for CAN only)"
|
||||
case 0x07:
|
||||
return "Show pending Diagnostic Trouble Codes (detected during current or last driving cycle)"
|
||||
case 0x08:
|
||||
return "Control operation of on-board component/system"
|
||||
case 0x09:
|
||||
return "Request vehicle information"
|
||||
case 0x0A:
|
||||
return "Permanent Diagnostic Trouble Codes (DTCs) (Cleared DTCs)"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("service 0x%x", uint8(s))
|
||||
}
|
||||
|
||||
type OBD2MessageType uint8
|
||||
|
||||
const (
|
||||
OBD2MessageTypeRequest OBD2MessageType = iota
|
||||
OBD2MessageTypeResponse
|
||||
)
|
||||
|
||||
func (t OBD2MessageType) String() string {
|
||||
if t == OBD2MessageTypeRequest {
|
||||
return "request"
|
||||
} else {
|
||||
return "response"
|
||||
}
|
||||
}
|
||||
|
||||
type OBD2Message struct {
|
||||
Type OBD2MessageType
|
||||
ECU uint8
|
||||
Service OBD2Service
|
||||
PID OBD2PID
|
||||
Size uint8
|
||||
Data []uint8
|
||||
}
|
||||
247
modules/can/can_obd2_pid_request.go
Normal file
247
modules/can/can_obd2_pid_request.go
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
var servicePIDS = map[uint8]map[uint16]string{
|
||||
0x01: {
|
||||
0x0: "PIDs supported [$01 - $20]",
|
||||
0x1: "Monitor status since DTCs cleared.",
|
||||
0x2: "DTC that caused freeze frame to be stored.",
|
||||
0x3: "Fuel system status",
|
||||
0x4: "Calculated engine load",
|
||||
0x5: "Engine coolant temperature",
|
||||
0x6: "Short term fuel trim (STFT)—Bank 1",
|
||||
0x7: "Long term fuel trim (LTFT)—Bank 1",
|
||||
0x8: "Short term fuel trim (STFT)—Bank 2",
|
||||
0x9: "Long term fuel trim (LTFT)—Bank 2",
|
||||
0x0A: "Fuel pressure (gauge pressure)",
|
||||
0x0B: "Intake manifold absolute pressure",
|
||||
0x0C: "Engine speed",
|
||||
0x0D: "Vehicle speed",
|
||||
0x0E: "Timing advance",
|
||||
0x0F: "Intake air temperature",
|
||||
0x10: "Mass air flow sensor (MAF) air flow rate",
|
||||
0x11: "Throttle position",
|
||||
0x12: "Commanded secondary air status",
|
||||
0x13: "Oxygen sensors present",
|
||||
0x14: "Oxygen Sensor 1",
|
||||
0x15: "Oxygen Sensor 2",
|
||||
0x16: "Oxygen Sensor 3",
|
||||
0x17: "Oxygen Sensor 4",
|
||||
0x18: "Oxygen Sensor 5",
|
||||
0x19: "Oxygen Sensor 6",
|
||||
0x1A: "Oxygen Sensor 7",
|
||||
0x1B: "Oxygen Sensor 8",
|
||||
0x1C: "OBD standards this vehicle conforms to",
|
||||
0x1D: "Oxygen sensors present",
|
||||
0x1E: "Auxiliary input status",
|
||||
0x1F: "Run time since engine start",
|
||||
0x20: "PIDs supported [$21 - $40]",
|
||||
0x21: "Distance traveled with malfunction indicator lamp (MIL) on",
|
||||
0x22: "Fuel Rail Pressure (relative to manifold vacuum)",
|
||||
0x23: "Fuel Rail Gauge Pressure (diesel, or gasoline direct injection)",
|
||||
0x24: "Oxygen Sensor 1",
|
||||
0x25: "Oxygen Sensor 2",
|
||||
0x26: "Oxygen Sensor 3",
|
||||
0x27: "Oxygen Sensor 4",
|
||||
0x28: "Oxygen Sensor 5",
|
||||
0x29: "Oxygen Sensor 6",
|
||||
0x2A: "Oxygen Sensor 7",
|
||||
0x2B: "Oxygen Sensor 8",
|
||||
0x2C: "Commanded EGR",
|
||||
0x2D: "EGR Error",
|
||||
0x2E: "Commanded evaporative purge",
|
||||
0x2F: "Fuel Tank Level Input",
|
||||
0x30: "Warm-ups since codes cleared",
|
||||
0x31: "Distance traveled since codes cleared",
|
||||
0x32: "Evap. System Vapor Pressure",
|
||||
0x33: "Absolute Barometric Pressure",
|
||||
0x34: "Oxygen Sensor 1",
|
||||
0x35: "Oxygen Sensor 2",
|
||||
0x36: "Oxygen Sensor 3",
|
||||
0x37: "Oxygen Sensor 4",
|
||||
0x38: "Oxygen Sensor 5",
|
||||
0x39: "Oxygen Sensor 6",
|
||||
0x3A: "Oxygen Sensor 7",
|
||||
0x3B: "Oxygen Sensor 8",
|
||||
0x3C: "Catalyst Temperature: Bank 1, Sensor 1",
|
||||
0x3D: "Catalyst Temperature: Bank 2, Sensor 1",
|
||||
0x3E: "Catalyst Temperature: Bank 1, Sensor 2",
|
||||
0x3F: "Catalyst Temperature: Bank 2, Sensor 2",
|
||||
0x40: "PIDs supported [$41 - $60]",
|
||||
0x41: "Monitor status this drive cycle",
|
||||
0x42: "Control module voltage",
|
||||
0x43: "Absolute load value",
|
||||
0x44: "Commanded Air-Fuel Equivalence Ratio (lambda,λ)",
|
||||
0x45: "Relative throttle position",
|
||||
0x46: "Ambient air temperature",
|
||||
0x47: "Absolute throttle position B",
|
||||
0x48: "Absolute throttle position C",
|
||||
0x49: "Accelerator pedal position D",
|
||||
0x4A: "Accelerator pedal position E",
|
||||
0x4B: "Accelerator pedal position F",
|
||||
0x4C: "Commanded throttle actuator",
|
||||
0x4D: "Time run with MIL on",
|
||||
0x4E: "Time since trouble codes cleared",
|
||||
0x4F: "Maximum value for Fuel–Air equivalence ratio, oxygen sensor voltage, oxygen sensor current, and intake manifold absolute pressure",
|
||||
0x50: "Maximum value for air flow rate from mass air flow sensor",
|
||||
0x51: "Fuel Type",
|
||||
0x52: "Ethanol fuel %",
|
||||
0x53: "Absolute Evap system Vapor Pressure",
|
||||
0x54: "Evap system vapor pressure",
|
||||
0x55: "Short term secondary oxygen sensor trim, A: bank 1, B: bank 3",
|
||||
0x56: "Long term secondary oxygen sensor trim, A: bank 1, B: bank 3",
|
||||
0x57: "Short term secondary oxygen sensor trim, A: bank 2, B: bank 4",
|
||||
0x58: "Long term secondary oxygen sensor trim, A: bank 2, B: bank 4",
|
||||
0x59: "Fuel rail absolute pressure",
|
||||
0x5A: "Relative accelerator pedal position",
|
||||
0x5B: "Hybrid battery pack remaining life",
|
||||
0x5C: "Engine oil temperature",
|
||||
0x5D: "Fuel injection timing",
|
||||
0x5E: "Engine fuel rate",
|
||||
0x5F: "Emission requirements to which vehicle is designed",
|
||||
0x60: "PIDs supported [$61 - $80]",
|
||||
0x61: "Driver's demand engine - percent torque",
|
||||
0x62: "Actual engine - percent torque",
|
||||
0x63: "Engine reference torque",
|
||||
0x64: "Engine percent torque data",
|
||||
0x65: "Auxiliary input / output supported",
|
||||
0x66: "Mass air flow sensor",
|
||||
0x67: "Engine coolant temperature",
|
||||
0x68: "Intake air temperature sensor",
|
||||
0x69: "Actual EGR, Commanded EGR, and EGR Error",
|
||||
0x6A: "Commanded Diesel intake air flow control and relative intake air flow position",
|
||||
0x6B: "Exhaust gas recirculation temperature",
|
||||
0x6C: "Commanded throttle actuator control and relative throttle position",
|
||||
0x6D: "Fuel pressure control system",
|
||||
0x6E: "Injection pressure control system",
|
||||
0x6F: "Turbocharger compressor inlet pressure",
|
||||
0x70: "Boost pressure control",
|
||||
0x71: "Variable Geometry turbo (VGT) control",
|
||||
0x72: "Wastegate control",
|
||||
0x73: "Exhaust pressure",
|
||||
0x74: "Turbocharger RPM",
|
||||
0x75: "Turbocharger temperature",
|
||||
0x76: "Turbocharger temperature",
|
||||
0x77: "Charge air cooler temperature (CACT)",
|
||||
0x78: "Exhaust Gas temperature (EGT) Bank 1",
|
||||
0x79: "Exhaust Gas temperature (EGT) Bank 2",
|
||||
0x7A: "Diesel particulate filter (DPF)differential pressure",
|
||||
0x7B: "Diesel particulate filter (DPF)",
|
||||
0x7C: "Diesel Particulate filter (DPF) temperature",
|
||||
0x7D: "NOx NTE (Not-To-Exceed) control area status",
|
||||
0x7E: "PM NTE (Not-To-Exceed) control area status",
|
||||
0x7F: "Engine run time [b]",
|
||||
0x80: "PIDs supported [$81 - $A0]",
|
||||
0x81: "Engine run time for Auxiliary Emissions Control Device(AECD)",
|
||||
0x82: "Engine run time for Auxiliary Emissions Control Device(AECD)",
|
||||
0x83: "NOx sensor",
|
||||
0x84: "Manifold surface temperature",
|
||||
0x85: "NOx reagent system",
|
||||
0x86: "Particulate matter (PM) sensor",
|
||||
0x87: "Intake manifold absolute pressure",
|
||||
0x88: "SCR Induce System",
|
||||
0x89: "Run Time for AECD #11-#15",
|
||||
0x8A: "Run Time for AECD #16-#20",
|
||||
0x8B: "Diesel Aftertreatment",
|
||||
0x8C: "O2 Sensor (Wide Range)",
|
||||
0x8D: "Throttle Position G",
|
||||
0x8E: "Engine Friction - Percent Torque",
|
||||
0x8F: "PM Sensor Bank 1 & 2",
|
||||
0x90: "WWH-OBD Vehicle OBD System Information",
|
||||
0x91: "WWH-OBD Vehicle OBD System Information",
|
||||
0x92: "Fuel System Control",
|
||||
0x93: "WWH-OBD Vehicle OBD Counters support",
|
||||
0x94: "NOx Warning And Inducement System",
|
||||
0x98: "Exhaust Gas Temperature Sensor",
|
||||
0x99: "Exhaust Gas Temperature Sensor",
|
||||
0x9A: "Hybrid/EV Vehicle System Data, Battery, Voltage",
|
||||
0x9B: "Diesel Exhaust Fluid Sensor Data",
|
||||
0x9C: "O2 Sensor Data",
|
||||
0x9D: "Engine Fuel Rate",
|
||||
0x9E: "Engine Exhaust Flow Rate",
|
||||
0x9F: "Fuel System Percentage Use",
|
||||
0xA0: "PIDs supported [$A1 - $C0]",
|
||||
0xA1: "NOx Sensor Corrected Data",
|
||||
0xA2: "Cylinder Fuel Rate",
|
||||
0xA3: "Evap System Vapor Pressure",
|
||||
0xA4: "Transmission Actual Gear",
|
||||
0xA5: "Commanded Diesel Exhaust Fluid Dosing",
|
||||
0xA6: "Odometer [c]",
|
||||
0xA7: "NOx Sensor Concentration Sensors 3 and 4",
|
||||
0xA8: "NOx Sensor Corrected Concentration Sensors 3 and 4",
|
||||
0xA9: "ABS Disable Switch State",
|
||||
0xC0: "PIDs supported [$C1 - $E0]",
|
||||
0xC3: "Fuel Level Input A/B",
|
||||
0xC4: "Exhaust Particulate Control System Diagnostic Time/Count",
|
||||
0xC5: "Fuel Pressure A and B",
|
||||
0xC6: "Multiple system counters",
|
||||
0xC7: "Distance Since Reflash or Module Replacement",
|
||||
0xC8: "NOx Control Diagnostic (NCD) and Particulate Control Diagnostic (PCD) Warning Lamp status",
|
||||
},
|
||||
}
|
||||
|
||||
type OBD2PID struct {
|
||||
ID uint16
|
||||
Name string
|
||||
}
|
||||
|
||||
func (p OBD2PID) String() string {
|
||||
if p.Name != "" {
|
||||
return p.Name
|
||||
}
|
||||
return fmt.Sprintf("pid 0x%d", p.ID)
|
||||
}
|
||||
|
||||
func lookupPID(svcID uint8, data []uint8) OBD2PID {
|
||||
if len(data) == 1 {
|
||||
data = []byte{
|
||||
0x00,
|
||||
data[0],
|
||||
}
|
||||
}
|
||||
|
||||
pid := OBD2PID{
|
||||
ID: binary.BigEndian.Uint16(data),
|
||||
}
|
||||
|
||||
// resolve service
|
||||
if svc, found := servicePIDS[svcID]; found {
|
||||
// resolve PID name
|
||||
if name, found := svc[pid.ID]; found {
|
||||
pid.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
return pid
|
||||
}
|
||||
|
||||
func (msg *OBD2Message) ParseRequest(frame can.Frame) bool {
|
||||
svcID := frame.Data[1]
|
||||
// validate service / mode
|
||||
if svcID > 0x0a {
|
||||
return false
|
||||
}
|
||||
|
||||
msgSize := frame.Data[0]
|
||||
// validate data size
|
||||
if msgSize > 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
data := frame.Data[2 : 1+msgSize]
|
||||
|
||||
msg.PID = lookupPID(svcID, data)
|
||||
msg.Type = OBD2MessageTypeRequest
|
||||
msg.ECU = 0xff // broadcast
|
||||
msg.Size = msgSize - 1
|
||||
msg.Service = OBD2Service(svcID)
|
||||
msg.Data = data
|
||||
|
||||
return true
|
||||
}
|
||||
25
modules/can/can_obd2_pid_response.go
Normal file
25
modules/can/can_obd2_pid_response.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
func (msg *OBD2Message) ParseResponse(frame can.Frame) bool {
|
||||
msgSize := frame.Data[0]
|
||||
// validate data size
|
||||
if msgSize > 7 {
|
||||
// fmt.Printf("invalid response size %d\n", msgSize)
|
||||
return false
|
||||
}
|
||||
|
||||
svcID := frame.Data[1] - 0x40
|
||||
|
||||
msg.Type = OBD2MessageTypeResponse
|
||||
msg.ECU = uint8(uint16(frame.ID) - uint16(OBD2ECUResponseMinID))
|
||||
msg.Size = msgSize - 3
|
||||
msg.Service = OBD2Service(svcID)
|
||||
msg.PID = lookupPID(svcID, []uint8{frame.Data[2]})
|
||||
msg.Data = frame.Data[3 : 3+msg.Size]
|
||||
|
||||
return true
|
||||
}
|
||||
122
modules/can/can_recon.go
Normal file
122
modules/can/can_recon.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"go.einride.tech/can"
|
||||
"go.einride.tech/can/pkg/socketcan"
|
||||
)
|
||||
|
||||
func (mod *CANModule) Configure() error {
|
||||
var err error
|
||||
var parseOBD bool
|
||||
|
||||
if mod.Running() {
|
||||
return session.ErrAlreadyStarted(mod.Name())
|
||||
} else if err, mod.deviceName = mod.StringParam("can.device"); err != nil {
|
||||
return err
|
||||
} else if err, mod.dumpName = mod.StringParam("can.dump"); err != nil {
|
||||
return err
|
||||
} else if err, mod.dumpInject = mod.BoolParam("can.dump.inject"); err != nil {
|
||||
return err
|
||||
} else if err, parseOBD = mod.BoolParam("can.parse.obd2"); err != nil {
|
||||
return err
|
||||
} else if err, mod.transport = mod.StringParam("can.transport"); err != nil {
|
||||
return err
|
||||
} else if mod.transport != "can" && mod.transport != "udp" {
|
||||
return errors.New("invalid transport")
|
||||
} else if err, mod.filter = mod.StringParam("can.filter"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.obd2.Enable(parseOBD)
|
||||
|
||||
if mod.filter != "" {
|
||||
if mod.filterExpr, err = bexpr.CreateEvaluator(mod.filter); err != nil {
|
||||
return err
|
||||
}
|
||||
mod.Warning("filtering frames with expression %s", tui.Bold(mod.filter))
|
||||
}
|
||||
|
||||
if mod.conn, err = socketcan.Dial(mod.transport, mod.deviceName); err != nil {
|
||||
return err
|
||||
}
|
||||
mod.recv = socketcan.NewReceiver(mod.conn)
|
||||
mod.send = socketcan.NewTransmitter(mod.conn)
|
||||
|
||||
if mod.dumpName != "" {
|
||||
if err = mod.startDumpReader(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mod *CANModule) isFilteredOut(frame can.Frame, msg Message) bool {
|
||||
// if we have an active filter
|
||||
if mod.filter != "" {
|
||||
if res, err := mod.filterExpr.Evaluate(map[string]interface{}{
|
||||
"message": msg,
|
||||
"frame": frame,
|
||||
}); err != nil {
|
||||
mod.Error("error evaluating '%s': %v", mod.filter, err)
|
||||
} else if !res {
|
||||
mod.Debug("skipping can message %+v", msg)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (mod *CANModule) onFrame(frame can.Frame) {
|
||||
msg := NewCanMessage(frame)
|
||||
|
||||
// try to parse with DBC if we have any
|
||||
if !mod.dbc.Parse(mod, &msg) {
|
||||
// not parsed, if enabled try ODB2
|
||||
mod.obd2.Parse(mod, &msg)
|
||||
}
|
||||
|
||||
if !mod.isFilteredOut(frame, msg) {
|
||||
mod.Session.Events.Add("can.message", msg)
|
||||
}
|
||||
}
|
||||
|
||||
const canPrompt = "{br}{fw}{env.can.device} {fb}{reset} {bold}» {reset}"
|
||||
|
||||
func (mod *CANModule) Start() error {
|
||||
if err := mod.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.SetPrompt(canPrompt)
|
||||
|
||||
return mod.SetRunning(true, func() {
|
||||
mod.Info("started on %s ...", mod.deviceName)
|
||||
|
||||
for mod.recv.Receive() {
|
||||
frame := mod.recv.Frame()
|
||||
mod.onFrame(frame)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (mod *CANModule) Stop() error {
|
||||
mod.SetPrompt(session.DefaultPrompt)
|
||||
|
||||
return mod.SetRunning(false, func() {
|
||||
if mod.conn != nil {
|
||||
mod.recv.Close()
|
||||
mod.conn.Close()
|
||||
mod.conn = nil
|
||||
mod.recv = nil
|
||||
mod.send = nil
|
||||
mod.filter = ""
|
||||
}
|
||||
})
|
||||
}
|
||||
52
modules/can/can_show.go
Normal file
52
modules/can/can_show.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
var (
|
||||
AliveTimeInterval = time.Duration(5) * time.Minute
|
||||
PresentTimeInterval = time.Duration(1) * time.Minute
|
||||
JustJoinedTimeInterval = time.Duration(10) * time.Second
|
||||
)
|
||||
|
||||
func (mod *CANModule) getRow(dev *network.CANDevice) []string {
|
||||
sinceLastSeen := time.Since(dev.LastSeen)
|
||||
seen := dev.LastSeen.Format("15:04:05")
|
||||
|
||||
if sinceLastSeen <= JustJoinedTimeInterval {
|
||||
seen = tui.Bold(seen)
|
||||
} else if sinceLastSeen > PresentTimeInterval {
|
||||
seen = tui.Dim(seen)
|
||||
}
|
||||
|
||||
return []string{
|
||||
dev.Name,
|
||||
dev.Description,
|
||||
fmt.Sprintf("%d", dev.Frames),
|
||||
humanize.Bytes(dev.Read),
|
||||
seen,
|
||||
}
|
||||
}
|
||||
|
||||
func (mod *CANModule) Show() (err error) {
|
||||
devices := mod.Session.CAN.Devices()
|
||||
|
||||
rows := make([][]string, 0)
|
||||
for _, dev := range devices {
|
||||
rows = append(rows, mod.getRow(dev))
|
||||
}
|
||||
|
||||
tui.Table(mod.Session.Events.Stdout, []string{"Name", "Description", "Frames", "Data", "Seen"}, rows)
|
||||
|
||||
if len(rows) > 0 {
|
||||
mod.Session.Refresh()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
407
modules/can/can_test.go
Normal file
407
modules/can/can_test.go
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
package can
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"go.einride.tech/can"
|
||||
)
|
||||
|
||||
var (
|
||||
testSession *session.Session
|
||||
sessionOnce sync.Once
|
||||
)
|
||||
|
||||
func createMockSession(t *testing.T) *session.Session {
|
||||
sessionOnce.Do(func() {
|
||||
var err error
|
||||
testSession, err = session.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create session: %v", err)
|
||||
}
|
||||
})
|
||||
return testSession
|
||||
}
|
||||
|
||||
func TestNewCanModule(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
if mod == nil {
|
||||
t.Fatal("NewCanModule returned nil")
|
||||
}
|
||||
|
||||
if mod.Name() != "can" {
|
||||
t.Errorf("Expected name 'can', got '%s'", mod.Name())
|
||||
}
|
||||
|
||||
if mod.Author() != "Simone Margaritelli <evilsocket@gmail.com>" {
|
||||
t.Errorf("Unexpected author: %s", mod.Author())
|
||||
}
|
||||
|
||||
if mod.Description() == "" {
|
||||
t.Error("Empty description")
|
||||
}
|
||||
|
||||
// Check default values
|
||||
if mod.transport != "can" {
|
||||
t.Errorf("Expected default transport 'can', got '%s'", mod.transport)
|
||||
}
|
||||
|
||||
if mod.deviceName != "can0" {
|
||||
t.Errorf("Expected default device 'can0', got '%s'", mod.deviceName)
|
||||
}
|
||||
|
||||
if mod.dumpName != "" {
|
||||
t.Errorf("Expected empty dumpName, got '%s'", mod.dumpName)
|
||||
}
|
||||
|
||||
if mod.dumpInject {
|
||||
t.Error("Expected dumpInject to be false by default")
|
||||
}
|
||||
|
||||
if mod.filter != "" {
|
||||
t.Errorf("Expected empty filter, got '%s'", mod.filter)
|
||||
}
|
||||
|
||||
// Check DBC and OBD2
|
||||
if mod.dbc == nil {
|
||||
t.Error("DBC should not be nil")
|
||||
}
|
||||
|
||||
if mod.obd2 == nil {
|
||||
t.Error("OBD2 should not be nil")
|
||||
}
|
||||
|
||||
// Check handlers
|
||||
handlers := mod.Handlers()
|
||||
expectedHandlers := []string{
|
||||
"can.recon on",
|
||||
"can.recon off",
|
||||
"can.clear",
|
||||
"can.show",
|
||||
"can.dbc.load NAME",
|
||||
"can.inject FRAME_EXPRESSION",
|
||||
"can.fuzz ID_OR_NODE_NAME OPTIONAL_SIZE",
|
||||
}
|
||||
|
||||
if len(handlers) != len(expectedHandlers) {
|
||||
t.Errorf("Expected %d handlers, got %d", len(expectedHandlers), len(handlers))
|
||||
}
|
||||
|
||||
handlerNames := make(map[string]bool)
|
||||
for _, h := range handlers {
|
||||
handlerNames[h.Name] = true
|
||||
}
|
||||
|
||||
for _, expected := range expectedHandlers {
|
||||
if !handlerNames[expected] {
|
||||
t.Errorf("Handler '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunningState(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Initially should not be running
|
||||
if mod.Running() {
|
||||
t.Error("Module should not be running initially")
|
||||
}
|
||||
|
||||
// Note: Cannot test actual Start/Stop without CAN hardware
|
||||
}
|
||||
|
||||
func TestClearHandler(t *testing.T) {
|
||||
// Skip this test as it requires CAN to be initialized in the session
|
||||
t.Skip("Skipping clear handler test - requires initialized CAN in session")
|
||||
}
|
||||
|
||||
func TestInjectNotRunning(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Test inject when not running
|
||||
handlers := mod.Handlers()
|
||||
for _, h := range handlers {
|
||||
if h.Name == "can.inject FRAME_EXPRESSION" {
|
||||
err := h.Exec([]string{"123#deadbeef"})
|
||||
if err == nil {
|
||||
t.Error("Expected error when injecting while not running")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzNotRunning(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Test fuzz when not running
|
||||
handlers := mod.Handlers()
|
||||
for _, h := range handlers {
|
||||
if h.Name == "can.fuzz ID_OR_NODE_NAME OPTIONAL_SIZE" {
|
||||
err := h.Exec([]string{"123", ""})
|
||||
if err == nil {
|
||||
t.Error("Expected error when fuzzing while not running")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParameters(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Check that all parameters are registered
|
||||
paramNames := []string{
|
||||
"can.device",
|
||||
"can.dump",
|
||||
"can.dump.inject",
|
||||
"can.transport",
|
||||
"can.filter",
|
||||
"can.parse.obd2",
|
||||
}
|
||||
|
||||
// Parameters are stored in the session environment
|
||||
for _, param := range paramNames {
|
||||
// This is a simplified check
|
||||
_ = param
|
||||
}
|
||||
|
||||
if mod == nil {
|
||||
t.Error("Module should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBC(t *testing.T) {
|
||||
dbc := &DBC{}
|
||||
if dbc == nil {
|
||||
t.Error("DBC should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOBD2(t *testing.T) {
|
||||
obd2 := &OBD2{}
|
||||
if obd2 == nil {
|
||||
t.Error("OBD2 should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowHandler(t *testing.T) {
|
||||
// Skip this test as it requires CAN to be initialized in the session
|
||||
t.Skip("Skipping show handler test - requires initialized CAN in session")
|
||||
}
|
||||
|
||||
func TestDefaultTransport(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
if mod.transport != "can" {
|
||||
t.Errorf("Expected transport 'can', got '%s'", mod.transport)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultDevice(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
if mod.deviceName != "can0" {
|
||||
t.Errorf("Expected device 'can0', got '%s'", mod.deviceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterExpression(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Initially filter should be empty
|
||||
if mod.filter != "" {
|
||||
t.Errorf("Expected empty filter, got '%s'", mod.filter)
|
||||
}
|
||||
|
||||
// filterExpr should be nil initially
|
||||
if mod.filterExpr != nil {
|
||||
t.Error("Expected filterExpr to be nil initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBCStruct(t *testing.T) {
|
||||
// Test DBC struct initialization
|
||||
dbc := &DBC{}
|
||||
if dbc == nil {
|
||||
t.Error("DBC should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOBD2Struct(t *testing.T) {
|
||||
// Test OBD2 struct initialization
|
||||
obd2 := &OBD2{}
|
||||
if obd2 == nil {
|
||||
t.Error("OBD2 should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCANMessage(t *testing.T) {
|
||||
// Test CAN message creation using NewCanMessage
|
||||
frame := can.Frame{}
|
||||
frame.ID = 0x123
|
||||
frame.Data = [8]byte{0x01, 0x02, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00}
|
||||
frame.Length = 4
|
||||
|
||||
msg := NewCanMessage(frame)
|
||||
|
||||
if msg.Frame.ID != 0x123 {
|
||||
t.Errorf("Expected ID 0x123, got 0x%x", msg.Frame.ID)
|
||||
}
|
||||
|
||||
if msg.Frame.Length != 4 {
|
||||
t.Errorf("Expected frame length 4, got %d", msg.Frame.Length)
|
||||
}
|
||||
|
||||
if msg.Signals == nil {
|
||||
t.Error("Signals map should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultParameters(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Test default parameter values exist
|
||||
expectedParams := []string{
|
||||
"can.device",
|
||||
"can.transport",
|
||||
"can.dump",
|
||||
"can.filter",
|
||||
"can.dump.inject",
|
||||
"can.parse.obd2",
|
||||
}
|
||||
|
||||
// Check that parameters are defined
|
||||
params := mod.Parameters()
|
||||
if params == nil {
|
||||
t.Error("Parameters should not be nil")
|
||||
}
|
||||
|
||||
// Just verify we have the expected number of parameters
|
||||
if len(expectedParams) != 6 {
|
||||
t.Error("Expected 6 parameters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerExecution(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Test that we can find all expected handlers
|
||||
handlerTests := []struct {
|
||||
name string
|
||||
args []string
|
||||
shouldFail bool
|
||||
}{
|
||||
{"can.inject FRAME_EXPRESSION", []string{"123#deadbeef"}, true}, // Should fail when not running
|
||||
{"can.fuzz ID_OR_NODE_NAME OPTIONAL_SIZE", []string{"123", "8"}, true}, // Should fail when not running
|
||||
{"can.dbc.load NAME", []string{"test.dbc"}, true}, // Will fail without actual file
|
||||
}
|
||||
|
||||
handlers := mod.Handlers()
|
||||
for _, test := range handlerTests {
|
||||
found := false
|
||||
for _, h := range handlers {
|
||||
if h.Name == test.name {
|
||||
found = true
|
||||
err := h.Exec(test.args)
|
||||
if test.shouldFail && err == nil {
|
||||
t.Errorf("Handler %s should have failed but didn't", test.name)
|
||||
} else if !test.shouldFail && err != nil {
|
||||
t.Errorf("Handler %s failed unexpectedly: %v", test.name, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Handler %s not found", test.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestModuleFields(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Test various fields are initialized correctly
|
||||
if mod.conn != nil {
|
||||
t.Error("conn should be nil initially")
|
||||
}
|
||||
|
||||
if mod.recv != nil {
|
||||
t.Error("recv should be nil initially")
|
||||
}
|
||||
|
||||
if mod.send != nil {
|
||||
t.Error("send should be nil initially")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBCLoadHandler(t *testing.T) {
|
||||
s := createMockSession(t)
|
||||
mod := NewCanModule(s)
|
||||
|
||||
// Find dbc.load handler
|
||||
var dbcHandler *session.ModuleHandler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "can.dbc.load NAME" {
|
||||
dbcHandler = &h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dbcHandler == nil {
|
||||
t.Fatal("DBC load handler not found")
|
||||
}
|
||||
|
||||
// Test with non-existent file
|
||||
err := dbcHandler.Exec([]string{"non_existent.dbc"})
|
||||
if err == nil {
|
||||
t.Error("Expected error when loading non-existent DBC file")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkNewCanModule(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = NewCanModule(s)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkClearHandler(b *testing.B) {
|
||||
// Skip this benchmark as it requires CAN to be initialized
|
||||
b.Skip("Skipping clear handler benchmark - requires initialized CAN in session")
|
||||
}
|
||||
|
||||
func BenchmarkInjectHandler(b *testing.B) {
|
||||
s, _ := session.New()
|
||||
mod := NewCanModule(s)
|
||||
|
||||
var handler *session.ModuleHandler
|
||||
for _, h := range mod.Handlers() {
|
||||
if h.Name == "can.inject FRAME_EXPRESSION" {
|
||||
handler = &h
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// This will fail since module is not running, but we're benchmarking the handler
|
||||
_ = handler.Exec([]string{"123#deadbeef"})
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/bettercap/bettercap/caplets"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/caplets"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/packets"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/packets"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
|
|
@ -83,7 +84,7 @@ func (mod *DHCP6Spoofer) Configure() error {
|
|||
return session.ErrAlreadyStarted(mod.Name())
|
||||
}
|
||||
|
||||
if mod.Handle, err = pcap.OpenLive(mod.Session.Interface.Name(), 65536, true, pcap.BlockForever); err != nil {
|
||||
if mod.Handle, err = network.Capture(mod.Session.Interface.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
185
modules/dns_proxy/dns_proxy.go
Normal file
185
modules/dns_proxy/dns_proxy.go
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/bettercap/bettercap/v2/tls"
|
||||
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
)
|
||||
|
||||
type DnsProxy struct {
|
||||
session.SessionModule
|
||||
proxy *DNSProxy
|
||||
}
|
||||
|
||||
func (mod *DnsProxy) Author() string {
|
||||
return "Yarwin Kolff <@buffermet>"
|
||||
}
|
||||
|
||||
func (mod *DnsProxy) Configure() error {
|
||||
var err error
|
||||
var address string
|
||||
var dnsPort int
|
||||
var doRedirect bool
|
||||
var nameserver string
|
||||
var netProtocol string
|
||||
var proxyPort int
|
||||
var scriptPath string
|
||||
var certFile string
|
||||
var keyFile string
|
||||
var whitelist string
|
||||
var blacklist string
|
||||
|
||||
if mod.Running() {
|
||||
return session.ErrAlreadyStarted(mod.Name())
|
||||
} else if err, dnsPort = mod.IntParam("dns.port"); err != nil {
|
||||
return err
|
||||
} else if err, address = mod.StringParam("dns.proxy.address"); err != nil {
|
||||
return err
|
||||
} else if err, certFile = mod.StringParam("dns.proxy.certificate"); err != nil {
|
||||
return err
|
||||
} else if certFile, err = fs.Expand(certFile); err != nil {
|
||||
return err
|
||||
} else if err, keyFile = mod.StringParam("dns.proxy.key"); err != nil {
|
||||
return err
|
||||
} else if keyFile, err = fs.Expand(keyFile); err != nil {
|
||||
return err
|
||||
} else if err, nameserver = mod.StringParam("dns.proxy.nameserver"); err != nil {
|
||||
return err
|
||||
} else if err, proxyPort = mod.IntParam("dns.proxy.port"); err != nil {
|
||||
return err
|
||||
} else if err, netProtocol = mod.StringParam("dns.proxy.protocol"); err != nil {
|
||||
return err
|
||||
} else if err, doRedirect = mod.BoolParam("dns.proxy.redirect"); err != nil {
|
||||
return err
|
||||
} else if err, scriptPath = mod.StringParam("dns.proxy.script"); err != nil {
|
||||
return err
|
||||
} else if err, blacklist = mod.StringParam("dns.proxy.blacklist"); err != nil {
|
||||
return err
|
||||
} else if err, whitelist = mod.StringParam("dns.proxy.whitelist"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.proxy.Blacklist = str.Comma(blacklist)
|
||||
mod.proxy.Whitelist = str.Comma(whitelist)
|
||||
|
||||
if netProtocol == "tcp-tls" {
|
||||
if !fs.Exists(certFile) || !fs.Exists(keyFile) {
|
||||
cfg, err := tls.CertConfigFromModule("dns.proxy", mod.SessionModule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.Debug("%+v", cfg)
|
||||
mod.Info("generating proxy certification authority TLS key to %s", keyFile)
|
||||
mod.Info("generating proxy certification authority TLS certificate to %s", certFile)
|
||||
if err := tls.Generate(cfg, certFile, keyFile, true); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
mod.Info("loading proxy certification authority TLS key from %s", keyFile)
|
||||
mod.Info("loading proxy certification authority TLS certificate from %s", certFile)
|
||||
}
|
||||
}
|
||||
|
||||
err = mod.proxy.Configure(address, dnsPort, doRedirect, nameserver, netProtocol,
|
||||
proxyPort, scriptPath, certFile, keyFile)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mod *DnsProxy) Description() string {
|
||||
return "A full featured DNS proxy that can be used to manipulate DNS traffic."
|
||||
}
|
||||
|
||||
func (mod *DnsProxy) Name() string {
|
||||
return "dns.proxy"
|
||||
}
|
||||
|
||||
func NewDnsProxy(s *session.Session) *DnsProxy {
|
||||
mod := &DnsProxy{
|
||||
SessionModule: session.NewSessionModule("dns.proxy", s),
|
||||
proxy: NewDNSProxy(s, "dns.proxy"),
|
||||
}
|
||||
|
||||
mod.AddParam(session.NewIntParameter("dns.port",
|
||||
"53",
|
||||
"DNS port to redirect when the proxy is activated."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.address",
|
||||
session.ParamIfaceAddress,
|
||||
session.IPv4Validator,
|
||||
"Address to bind the DNS proxy to."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.blacklist", "", "",
|
||||
"Comma separated list of client IPs to skip while proxying (wildcard allowed)."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.whitelist", "", "",
|
||||
"Comma separated list of client IPs to proxy if the blacklist is used."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.nameserver",
|
||||
"1.1.1.1",
|
||||
session.IPv4Validator,
|
||||
"DNS resolver address."))
|
||||
|
||||
mod.AddParam(session.NewIntParameter("dns.proxy.port",
|
||||
"8053",
|
||||
"Port to bind the DNS proxy to."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.protocol",
|
||||
"udp",
|
||||
"^(udp|tcp|tcp-tls)$",
|
||||
"Network protocol for the DNS proxy server to use. Accepted values: udp, tcp, tcp-tls"))
|
||||
|
||||
mod.AddParam(session.NewBoolParameter("dns.proxy.redirect",
|
||||
"true",
|
||||
"Enable or disable port redirection with iptables."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.certificate",
|
||||
"~/.bettercap-ca.cert.pem",
|
||||
"",
|
||||
"DNS proxy certification authority TLS certificate file."))
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.key",
|
||||
"~/.bettercap-ca.key.pem",
|
||||
"",
|
||||
"DNS proxy certification authority TLS key file."))
|
||||
|
||||
tls.CertConfigToModule("dns.proxy", &mod.SessionModule, tls.DefaultCloudflareDNSConfig)
|
||||
|
||||
mod.AddParam(session.NewStringParameter("dns.proxy.script",
|
||||
"",
|
||||
"",
|
||||
"Path of a JS proxy script."))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("dns.proxy on", "",
|
||||
"Start the DNS proxy.",
|
||||
func(args []string) error {
|
||||
return mod.Start()
|
||||
}))
|
||||
|
||||
mod.AddHandler(session.NewModuleHandler("dns.proxy off", "",
|
||||
"Stop the DNS proxy.",
|
||||
func(args []string) error {
|
||||
return mod.Stop()
|
||||
}))
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
func (mod *DnsProxy) Start() error {
|
||||
if err := mod.Configure(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mod.SetRunning(true, func() {
|
||||
mod.proxy.Start()
|
||||
})
|
||||
}
|
||||
|
||||
func (mod *DnsProxy) Stop() error {
|
||||
return mod.SetRunning(false, func() {
|
||||
mod.proxy.Stop()
|
||||
})
|
||||
}
|
||||
251
modules/dns_proxy/dns_proxy_base.go
Normal file
251
modules/dns_proxy/dns_proxy_base.go
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/firewall"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/log"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
const (
|
||||
dialTimeout = 2 * time.Second
|
||||
readTimeout = 2 * time.Second
|
||||
writeTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
type DNSProxy struct {
|
||||
Name string
|
||||
Address string
|
||||
Server *dns.Server
|
||||
Redirection *firewall.Redirection
|
||||
Nameserver string
|
||||
NetProtocol string
|
||||
Script *DnsProxyScript
|
||||
CertFile string
|
||||
KeyFile string
|
||||
Blacklist []string
|
||||
Whitelist []string
|
||||
Sess *session.Session
|
||||
|
||||
doRedirect bool
|
||||
isRunning bool
|
||||
tag string
|
||||
}
|
||||
|
||||
func (p *DNSProxy) shouldProxy(clientIP string) bool {
|
||||
// check if this client is in the whitelist
|
||||
for _, ip := range p.Whitelist {
|
||||
if clientIP == ip {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// check if this client is in the blacklist
|
||||
for _, ip := range p.Blacklist {
|
||||
if ip == "*" || clientIP == ip {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Configure(address string, dnsPort int, doRedirect bool, nameserver string, netProtocol string, proxyPort int, scriptPath string, certFile string, keyFile string) error {
|
||||
var err error
|
||||
|
||||
p.Address = address
|
||||
p.doRedirect = doRedirect
|
||||
p.CertFile = certFile
|
||||
p.KeyFile = keyFile
|
||||
|
||||
if scriptPath != "" {
|
||||
if err, p.Script = LoadDnsProxyScript(scriptPath, p.Sess); err != nil {
|
||||
return err
|
||||
} else {
|
||||
p.Debug("proxy script %s loaded.", scriptPath)
|
||||
}
|
||||
}
|
||||
|
||||
dnsClient := dns.Client{
|
||||
DialTimeout: dialTimeout,
|
||||
Net: netProtocol,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
}
|
||||
|
||||
resolverAddr := fmt.Sprintf("%s:%d", nameserver, dnsPort)
|
||||
|
||||
handler := dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
|
||||
clientIP := strings.Split(w.RemoteAddr().String(), ":")[0]
|
||||
|
||||
req, res := p.onRequestFilter(req, clientIP)
|
||||
if res == nil {
|
||||
// unused var is time til res
|
||||
res, _, err := dnsClient.Exchange(req, resolverAddr)
|
||||
if err != nil {
|
||||
p.Debug("error while resolving DNS query: %s", err.Error())
|
||||
m.SetRcode(req, dns.RcodeServerFailure)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
res = p.onResponseFilter(req, res, clientIP)
|
||||
if res == nil {
|
||||
p.Debug("response is nil")
|
||||
m.SetRcode(req, dns.RcodeServerFailure)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
} else {
|
||||
if err := w.WriteMsg(res); err != nil {
|
||||
p.Error("Error writing response: %s", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := w.WriteMsg(res); err != nil {
|
||||
p.Error("Error writing response: %s", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
p.Server = &dns.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", address, proxyPort),
|
||||
Net: netProtocol,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
if netProtocol == "tcp-tls" && p.CertFile != "" && p.KeyFile != "" {
|
||||
rawCert, _ := ioutil.ReadFile(p.CertFile)
|
||||
rawKey, _ := ioutil.ReadFile(p.KeyFile)
|
||||
ourCa, err := tls.X509KeyPair(rawCert, rawKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ourCa.Leaf, err = x509.ParseCertificate(ourCa.Certificate[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Server.TLSConfig = &tls.Config{
|
||||
Certificates: []tls.Certificate{ourCa},
|
||||
}
|
||||
}
|
||||
|
||||
if p.doRedirect {
|
||||
if !p.Sess.Firewall.IsForwardingEnabled() {
|
||||
p.Info("enabling forwarding.")
|
||||
p.Sess.Firewall.EnableForwarding(true)
|
||||
}
|
||||
|
||||
redirectProtocol := netProtocol
|
||||
if redirectProtocol == "tcp-tls" {
|
||||
redirectProtocol = "tcp"
|
||||
}
|
||||
p.Redirection = firewall.NewRedirection(p.Sess.Interface.Name(),
|
||||
redirectProtocol,
|
||||
dnsPort,
|
||||
p.Address,
|
||||
proxyPort)
|
||||
|
||||
if err := p.Sess.Firewall.EnableRedirection(p.Redirection, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Debug("applied redirection %s", p.Redirection.String())
|
||||
} else {
|
||||
p.Warning("port redirection disabled, the proxy must be set manually to work")
|
||||
}
|
||||
|
||||
p.Sess.UnkCmdCallback = func(cmd string) bool {
|
||||
if p.Script != nil {
|
||||
return p.Script.OnCommand(cmd)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DNSProxy) dnsWorker() error {
|
||||
p.isRunning = true
|
||||
return p.Server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Debug(format string, args ...interface{}) {
|
||||
p.Sess.Events.Log(log.DEBUG, p.tag+format, args...)
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Info(format string, args ...interface{}) {
|
||||
p.Sess.Events.Log(log.INFO, p.tag+format, args...)
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Warning(format string, args ...interface{}) {
|
||||
p.Sess.Events.Log(log.WARNING, p.tag+format, args...)
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Error(format string, args ...interface{}) {
|
||||
p.Sess.Events.Log(log.ERROR, p.tag+format, args...)
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Fatal(format string, args ...interface{}) {
|
||||
p.Sess.Events.Log(log.FATAL, p.tag+format, args...)
|
||||
}
|
||||
|
||||
func NewDNSProxy(s *session.Session, tag string) *DNSProxy {
|
||||
p := &DNSProxy{
|
||||
Name: "dns.proxy",
|
||||
Sess: s,
|
||||
Server: nil,
|
||||
doRedirect: true,
|
||||
tag: session.AsTag(tag),
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Start() {
|
||||
go func() {
|
||||
p.Info("started on %s", p.Server.Addr)
|
||||
|
||||
err := p.dnsWorker()
|
||||
// TODO: check the dns server closed error
|
||||
if err != nil && err.Error() != "dns: Server closed" {
|
||||
p.Fatal("%s", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *DNSProxy) Stop() error {
|
||||
if p.Script != nil {
|
||||
if p.Script.Plugin.HasFunc("onExit") {
|
||||
if _, err := p.Script.Call("onExit"); err != nil {
|
||||
log.Error("Error while executing onExit callback: %s", "\nTraceback:\n "+err.(*otto.Error).String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.doRedirect && p.Redirection != nil {
|
||||
p.Debug("disabling redirection %s", p.Redirection.String())
|
||||
if err := p.Sess.Firewall.EnableRedirection(p.Redirection, false); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Redirection = nil
|
||||
}
|
||||
|
||||
p.Sess.UnkCmdCallback = nil
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return p.Server.ShutdownContext(ctx)
|
||||
}
|
||||
113
modules/dns_proxy/dns_proxy_base_filters.go
Normal file
113
modules/dns_proxy/dns_proxy_base_filters.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func questionsToStrings(qs []dns.Question) []string {
|
||||
questions := []string{}
|
||||
for _, q := range qs {
|
||||
questions = append(questions, tabsToSpaces(q.String()))
|
||||
}
|
||||
return questions
|
||||
}
|
||||
|
||||
func recordsToStrings(rrs []dns.RR) []string {
|
||||
records := []string{}
|
||||
for _, rr := range rrs {
|
||||
if rr != nil {
|
||||
records = append(records, tabsToSpaces(rr.String()))
|
||||
}
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func tabsToSpaces(s string) string {
|
||||
return strings.ReplaceAll(s, "\t", " ")
|
||||
}
|
||||
|
||||
func (p *DNSProxy) logRequestAction(m *dns.Msg, clientIP string) {
|
||||
p.Sess.Events.Add(p.Name+".spoofed-request", struct {
|
||||
Client string
|
||||
Questions []string
|
||||
}{
|
||||
clientIP,
|
||||
questionsToStrings(m.Question),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *DNSProxy) logResponseAction(m *dns.Msg, clientIP string) {
|
||||
p.Sess.Events.Add(p.Name+".spoofed-response", struct {
|
||||
client string
|
||||
Answers []string
|
||||
Extras []string
|
||||
Nameservers []string
|
||||
Questions []string
|
||||
}{
|
||||
clientIP,
|
||||
recordsToStrings(m.Answer),
|
||||
recordsToStrings(m.Extra),
|
||||
recordsToStrings(m.Ns),
|
||||
questionsToStrings(m.Question),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *DNSProxy) onRequestFilter(query *dns.Msg, clientIP string) (req, res *dns.Msg) {
|
||||
if p.shouldProxy(clientIP) {
|
||||
p.Debug("< %s q[%s]",
|
||||
clientIP,
|
||||
strings.Join(questionsToStrings(query.Question), ","))
|
||||
|
||||
// do we have a proxy script?
|
||||
if p.Script == nil {
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// run the module OnRequest callback if defined
|
||||
jsreq, jsres := p.Script.OnRequest(query, clientIP)
|
||||
if jsreq != nil {
|
||||
// the request has been changed by the script
|
||||
req := jsreq.ToQuery()
|
||||
p.logRequestAction(req, clientIP)
|
||||
return req, nil
|
||||
} else if jsres != nil {
|
||||
// a fake response has been returned by the script
|
||||
res := jsres.ToQuery()
|
||||
p.logResponseAction(res, clientIP)
|
||||
return query, res
|
||||
}
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (p *DNSProxy) onResponseFilter(req, res *dns.Msg, clientIP string) *dns.Msg {
|
||||
if p.shouldProxy(clientIP) {
|
||||
// sometimes it happens ¯\_(ツ)_/¯
|
||||
if res == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.Debug("> %s q[%s] a[%s] e[%s] n[%s]",
|
||||
clientIP,
|
||||
strings.Join(questionsToStrings(res.Question), ","),
|
||||
strings.Join(recordsToStrings(res.Answer), ","),
|
||||
strings.Join(recordsToStrings(res.Extra), ","),
|
||||
strings.Join(recordsToStrings(res.Ns), ","))
|
||||
|
||||
// do we have a proxy script?
|
||||
if p.Script != nil {
|
||||
_, jsres := p.Script.OnResponse(req, res, clientIP)
|
||||
if jsres != nil {
|
||||
// the response has been changed by the script
|
||||
res := jsres.ToQuery()
|
||||
p.logResponseAction(res, clientIP)
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
365
modules/dns_proxy/dns_proxy_js_query.go
Normal file
365
modules/dns_proxy/dns_proxy_js_query.go
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type JSQuery struct {
|
||||
Answers []map[string]interface{}
|
||||
Client map[string]string
|
||||
Compress bool
|
||||
Extras []map[string]interface{}
|
||||
Header JSQueryHeader
|
||||
Nameservers []map[string]interface{}
|
||||
Questions []map[string]interface{}
|
||||
|
||||
refHash string
|
||||
}
|
||||
|
||||
type JSQueryHeader struct {
|
||||
AuthenticatedData bool
|
||||
Authoritative bool
|
||||
CheckingDisabled bool
|
||||
Id uint16
|
||||
Opcode int
|
||||
Rcode int
|
||||
RecursionAvailable bool
|
||||
RecursionDesired bool
|
||||
Response bool
|
||||
Truncated bool
|
||||
Zero bool
|
||||
}
|
||||
|
||||
func jsPropToMap(obj map[string]interface{}, key string) map[string]interface{} {
|
||||
if v, ok := obj[key].(map[string]interface{}); ok {
|
||||
return v
|
||||
}
|
||||
log.Error("error converting JS property to map[string]interface{} where key is: %s", key)
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
|
||||
func jsPropToMapArray(obj map[string]interface{}, key string) []map[string]interface{} {
|
||||
if v, ok := obj[key].([]map[string]interface{}); ok {
|
||||
return v
|
||||
}
|
||||
log.Error("error converting JS property to []map[string]interface{} where key is: %s", key)
|
||||
return []map[string]interface{}{}
|
||||
}
|
||||
|
||||
func jsPropToString(obj map[string]interface{}, key string) string {
|
||||
if v, ok := obj[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
log.Error("error converting JS property to string where key is: %s", key)
|
||||
return ""
|
||||
}
|
||||
|
||||
func jsPropToStringArray(obj map[string]interface{}, key string) []string {
|
||||
if v, ok := obj[key].([]string); ok {
|
||||
return v
|
||||
}
|
||||
log.Error("error converting JS property to []string where key is: %s", key)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func jsPropToUint8(obj map[string]interface{}, key string) uint8 {
|
||||
if v, ok := obj[key].(int64); ok {
|
||||
if v >= 0 && v <= math.MaxUint8 {
|
||||
return uint8(v)
|
||||
}
|
||||
}
|
||||
log.Error("error converting JS property to uint8 where key is: %s", key)
|
||||
return uint8(0)
|
||||
}
|
||||
|
||||
func jsPropToUint8Array(obj map[string]interface{}, key string) []uint8 {
|
||||
if arr, ok := obj[key].([]interface{}); ok {
|
||||
vArr := make([]uint8, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if v, ok := item.(int64); ok {
|
||||
if v >= 0 && v <= math.MaxUint8 {
|
||||
vArr = append(vArr, uint8(v))
|
||||
} else {
|
||||
log.Error("error converting JS property to []uint8 where key is: %s", key)
|
||||
return []uint8{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vArr
|
||||
}
|
||||
log.Error("error converting JS property to []uint8 where key is: %s", key)
|
||||
return []uint8{}
|
||||
}
|
||||
|
||||
func jsPropToUint16(obj map[string]interface{}, key string) uint16 {
|
||||
if v, ok := obj[key].(int64); ok {
|
||||
if v >= 0 && v <= math.MaxUint16 {
|
||||
return uint16(v)
|
||||
}
|
||||
}
|
||||
log.Error("error converting JS property to uint16 where key is: %s", key)
|
||||
return uint16(0)
|
||||
}
|
||||
|
||||
func jsPropToUint16Array(obj map[string]interface{}, key string) []uint16 {
|
||||
if arr, ok := obj[key].([]interface{}); ok {
|
||||
vArr := make([]uint16, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
if v, ok := item.(int64); ok {
|
||||
if v >= 0 && v <= math.MaxUint16 {
|
||||
vArr = append(vArr, uint16(v))
|
||||
} else {
|
||||
log.Error("error converting JS property to []uint16 where key is: %s", key)
|
||||
return []uint16{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return vArr
|
||||
}
|
||||
log.Error("error converting JS property to []uint16 where key is: %s", key)
|
||||
return []uint16{}
|
||||
}
|
||||
|
||||
func jsPropToUint32(obj map[string]interface{}, key string) uint32 {
|
||||
if v, ok := obj[key].(int64); ok {
|
||||
if v >= 0 && v <= math.MaxUint32 {
|
||||
return uint32(v)
|
||||
}
|
||||
}
|
||||
log.Error("error converting JS property to uint32 where key is: %s", key)
|
||||
return uint32(0)
|
||||
}
|
||||
|
||||
func jsPropToUint64(obj map[string]interface{}, key string) uint64 {
|
||||
prop, found := obj[key]
|
||||
if found {
|
||||
switch reflect.TypeOf(prop).String() {
|
||||
case "float64":
|
||||
if f, ok := prop.(float64); ok {
|
||||
bigInt := new(big.Float).SetFloat64(f)
|
||||
v, _ := bigInt.Uint64()
|
||||
if v >= 0 {
|
||||
return v
|
||||
}
|
||||
}
|
||||
break
|
||||
case "int64":
|
||||
if v, ok := prop.(int64); ok {
|
||||
if v >= 0 {
|
||||
return uint64(v)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "uint64":
|
||||
if v, ok := prop.(uint64); ok {
|
||||
return v
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Error("error converting JS property to uint64 where key is: %s", key)
|
||||
return uint64(0)
|
||||
}
|
||||
|
||||
func uint16ArrayToInt64Array(arr []uint16) []int64 {
|
||||
vArr := make([]int64, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
vArr = append(vArr, int64(item))
|
||||
}
|
||||
return vArr
|
||||
}
|
||||
|
||||
func (j *JSQuery) NewHash() string {
|
||||
answers, _ := json.Marshal(j.Answers)
|
||||
extras, _ := json.Marshal(j.Extras)
|
||||
nameservers, _ := json.Marshal(j.Nameservers)
|
||||
questions, _ := json.Marshal(j.Questions)
|
||||
|
||||
headerHash := fmt.Sprintf("%t.%t.%t.%d.%d.%d.%t.%t.%t.%t.%t",
|
||||
j.Header.AuthenticatedData,
|
||||
j.Header.Authoritative,
|
||||
j.Header.CheckingDisabled,
|
||||
j.Header.Id,
|
||||
j.Header.Opcode,
|
||||
j.Header.Rcode,
|
||||
j.Header.RecursionAvailable,
|
||||
j.Header.RecursionDesired,
|
||||
j.Header.Response,
|
||||
j.Header.Truncated,
|
||||
j.Header.Zero)
|
||||
|
||||
hash := fmt.Sprintf("%s.%s.%t.%s.%s.%s.%s",
|
||||
answers,
|
||||
j.Client["IP"],
|
||||
j.Compress,
|
||||
extras,
|
||||
headerHash,
|
||||
nameservers,
|
||||
questions)
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
func NewJSQuery(query *dns.Msg, clientIP string) (jsQuery *JSQuery) {
|
||||
answers := make([]map[string]interface{}, len(query.Answer))
|
||||
extras := make([]map[string]interface{}, len(query.Extra))
|
||||
nameservers := make([]map[string]interface{}, len(query.Ns))
|
||||
questions := make([]map[string]interface{}, len(query.Question))
|
||||
|
||||
for i, rr := range query.Answer {
|
||||
jsRecord, err := NewJSResourceRecord(rr)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
answers[i] = jsRecord
|
||||
}
|
||||
|
||||
for i, rr := range query.Extra {
|
||||
jsRecord, err := NewJSResourceRecord(rr)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
extras[i] = jsRecord
|
||||
}
|
||||
|
||||
for i, rr := range query.Ns {
|
||||
jsRecord, err := NewJSResourceRecord(rr)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
nameservers[i] = jsRecord
|
||||
}
|
||||
|
||||
for i, question := range query.Question {
|
||||
questions[i] = map[string]interface{}{
|
||||
"Name": question.Name,
|
||||
"Qtype": int64(question.Qtype),
|
||||
"Qclass": int64(question.Qclass),
|
||||
}
|
||||
}
|
||||
|
||||
clientMAC := ""
|
||||
clientAlias := ""
|
||||
if endpoint := session.I.Lan.GetByIp(clientIP); endpoint != nil {
|
||||
clientMAC = endpoint.HwAddress
|
||||
clientAlias = endpoint.Alias
|
||||
}
|
||||
client := map[string]string{"IP": clientIP, "MAC": clientMAC, "Alias": clientAlias}
|
||||
|
||||
jsquery := &JSQuery{
|
||||
Answers: answers,
|
||||
Client: client,
|
||||
Compress: query.Compress,
|
||||
Extras: extras,
|
||||
Header: JSQueryHeader{
|
||||
AuthenticatedData: query.MsgHdr.AuthenticatedData,
|
||||
Authoritative: query.MsgHdr.Authoritative,
|
||||
CheckingDisabled: query.MsgHdr.CheckingDisabled,
|
||||
Id: query.MsgHdr.Id,
|
||||
Opcode: query.MsgHdr.Opcode,
|
||||
Rcode: query.MsgHdr.Rcode,
|
||||
RecursionAvailable: query.MsgHdr.RecursionAvailable,
|
||||
RecursionDesired: query.MsgHdr.RecursionDesired,
|
||||
Response: query.MsgHdr.Response,
|
||||
Truncated: query.MsgHdr.Truncated,
|
||||
Zero: query.MsgHdr.Zero,
|
||||
},
|
||||
Nameservers: nameservers,
|
||||
Questions: questions,
|
||||
}
|
||||
jsquery.UpdateHash()
|
||||
|
||||
return jsquery
|
||||
}
|
||||
|
||||
func (j *JSQuery) ToQuery() *dns.Msg {
|
||||
var answers []dns.RR
|
||||
var extras []dns.RR
|
||||
var nameservers []dns.RR
|
||||
var questions []dns.Question
|
||||
|
||||
for _, jsRR := range j.Answers {
|
||||
rr, err := ToRR(jsRR)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
answers = append(answers, rr)
|
||||
}
|
||||
for _, jsRR := range j.Extras {
|
||||
rr, err := ToRR(jsRR)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
extras = append(extras, rr)
|
||||
}
|
||||
for _, jsRR := range j.Nameservers {
|
||||
rr, err := ToRR(jsRR)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
nameservers = append(nameservers, rr)
|
||||
}
|
||||
|
||||
for _, jsQ := range j.Questions {
|
||||
questions = append(questions, dns.Question{
|
||||
Name: jsPropToString(jsQ, "Name"),
|
||||
Qtype: jsPropToUint16(jsQ, "Qtype"),
|
||||
Qclass: jsPropToUint16(jsQ, "Qclass"),
|
||||
})
|
||||
}
|
||||
|
||||
query := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: j.Header.Id,
|
||||
Response: j.Header.Response,
|
||||
Opcode: j.Header.Opcode,
|
||||
Authoritative: j.Header.Authoritative,
|
||||
Truncated: j.Header.Truncated,
|
||||
RecursionDesired: j.Header.RecursionDesired,
|
||||
RecursionAvailable: j.Header.RecursionAvailable,
|
||||
Zero: j.Header.Zero,
|
||||
AuthenticatedData: j.Header.AuthenticatedData,
|
||||
CheckingDisabled: j.Header.CheckingDisabled,
|
||||
Rcode: j.Header.Rcode,
|
||||
},
|
||||
Compress: j.Compress,
|
||||
Question: questions,
|
||||
Answer: answers,
|
||||
Ns: nameservers,
|
||||
Extra: extras,
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (j *JSQuery) UpdateHash() {
|
||||
j.refHash = j.NewHash()
|
||||
}
|
||||
|
||||
func (j *JSQuery) WasModified() bool {
|
||||
// check if any of the fields has been changed
|
||||
return j.NewHash() != j.refHash
|
||||
}
|
||||
|
||||
func (j *JSQuery) CheckIfModifiedAndUpdateHash() bool {
|
||||
// check if query was changed and update its hash
|
||||
newHash := j.NewHash()
|
||||
wasModified := j.refHash != newHash
|
||||
j.refHash = newHash
|
||||
return wasModified
|
||||
}
|
||||
961
modules/dns_proxy/dns_proxy_js_record.go
Normal file
961
modules/dns_proxy/dns_proxy_js_record.go
Normal file
|
|
@ -0,0 +1,961 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func NewJSResourceRecord(rr dns.RR) (jsRecord map[string]interface{}, err error) {
|
||||
header := rr.Header()
|
||||
|
||||
jsRecord = map[string]interface{}{
|
||||
"Header": map[string]interface{}{
|
||||
"Class": int64(header.Class),
|
||||
"Name": header.Name,
|
||||
"Rrtype": int64(header.Rrtype),
|
||||
"Ttl": int64(header.Ttl),
|
||||
},
|
||||
}
|
||||
|
||||
switch rr := rr.(type) {
|
||||
case *dns.A:
|
||||
jsRecord["A"] = rr.A.String()
|
||||
case *dns.AAAA:
|
||||
jsRecord["AAAA"] = rr.AAAA.String()
|
||||
case *dns.APL:
|
||||
jsPrefixes := make([]map[string]interface{}, len(rr.Prefixes))
|
||||
for i, v := range rr.Prefixes {
|
||||
jsPrefixes[i] = map[string]interface{}{
|
||||
"Negation": v.Negation,
|
||||
"Network": v.Network.String(),
|
||||
}
|
||||
}
|
||||
jsRecord["Prefixes"] = jsPrefixes
|
||||
case *dns.CNAME:
|
||||
jsRecord["Target"] = rr.Target
|
||||
case *dns.MB:
|
||||
jsRecord["Mb"] = rr.Mb
|
||||
case *dns.MD:
|
||||
jsRecord["Md"] = rr.Md
|
||||
case *dns.MF:
|
||||
jsRecord["Mf"] = rr.Mf
|
||||
case *dns.MG:
|
||||
jsRecord["Mg"] = rr.Mg
|
||||
case *dns.MR:
|
||||
jsRecord["Mr"] = rr.Mr
|
||||
case *dns.MX:
|
||||
jsRecord["Mx"] = rr.Mx
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.NULL:
|
||||
jsRecord["Data"] = rr.Data
|
||||
case *dns.SOA:
|
||||
jsRecord["Expire"] = int64(rr.Expire)
|
||||
jsRecord["Minttl"] = int64(rr.Minttl)
|
||||
jsRecord["Ns"] = rr.Ns
|
||||
jsRecord["Refresh"] = int64(rr.Refresh)
|
||||
jsRecord["Retry"] = int64(rr.Retry)
|
||||
jsRecord["Mbox"] = rr.Mbox
|
||||
jsRecord["Serial"] = int64(rr.Serial)
|
||||
case *dns.TXT:
|
||||
jsRecord["Txt"] = rr.Txt
|
||||
case *dns.SRV:
|
||||
jsRecord["Port"] = int64(rr.Port)
|
||||
jsRecord["Priority"] = int64(rr.Priority)
|
||||
jsRecord["Target"] = rr.Target
|
||||
jsRecord["Weight"] = int64(rr.Weight)
|
||||
case *dns.PTR:
|
||||
jsRecord["Ptr"] = rr.Ptr
|
||||
case *dns.NS:
|
||||
jsRecord["Ns"] = rr.Ns
|
||||
case *dns.DNAME:
|
||||
jsRecord["Target"] = rr.Target
|
||||
case *dns.AFSDB:
|
||||
jsRecord["Subtype"] = int64(rr.Subtype)
|
||||
jsRecord["Hostname"] = rr.Hostname
|
||||
case *dns.CAA:
|
||||
jsRecord["Flag"] = int64(rr.Flag)
|
||||
jsRecord["Tag"] = rr.Tag
|
||||
jsRecord["Value"] = rr.Value
|
||||
case *dns.HINFO:
|
||||
jsRecord["Cpu"] = rr.Cpu
|
||||
jsRecord["Os"] = rr.Os
|
||||
case *dns.MINFO:
|
||||
jsRecord["Email"] = rr.Email
|
||||
jsRecord["Rmail"] = rr.Rmail
|
||||
case *dns.ISDN:
|
||||
jsRecord["Address"] = rr.Address
|
||||
jsRecord["SubAddress"] = rr.SubAddress
|
||||
case *dns.KX:
|
||||
jsRecord["Exchanger"] = rr.Exchanger
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.LOC:
|
||||
jsRecord["Altitude"] = int64(rr.Altitude)
|
||||
jsRecord["HorizPre"] = int64(rr.HorizPre)
|
||||
jsRecord["Latitude"] = int64(rr.Latitude)
|
||||
jsRecord["Longitude"] = int64(rr.Longitude)
|
||||
jsRecord["Size"] = int64(rr.Size)
|
||||
jsRecord["Version"] = int64(rr.Version)
|
||||
jsRecord["VertPre"] = int64(rr.VertPre)
|
||||
case *dns.SSHFP:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["FingerPrint"] = rr.FingerPrint
|
||||
jsRecord["Type"] = int64(rr.Type)
|
||||
case *dns.TLSA:
|
||||
jsRecord["Certificate"] = rr.Certificate
|
||||
jsRecord["MatchingType"] = int64(rr.MatchingType)
|
||||
jsRecord["Selector"] = int64(rr.Selector)
|
||||
jsRecord["Usage"] = int64(rr.Usage)
|
||||
case *dns.CERT:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["Certificate"] = rr.Certificate
|
||||
jsRecord["KeyTag"] = int64(rr.KeyTag)
|
||||
jsRecord["Type"] = int64(rr.Type)
|
||||
case *dns.DS:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["Digest"] = rr.Digest
|
||||
jsRecord["DigestType"] = int64(rr.DigestType)
|
||||
jsRecord["KeyTag"] = int64(rr.KeyTag)
|
||||
case *dns.NAPTR:
|
||||
jsRecord["Order"] = int64(rr.Order)
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
jsRecord["Flags"] = rr.Flags
|
||||
jsRecord["Service"] = rr.Service
|
||||
jsRecord["Regexp"] = rr.Regexp
|
||||
jsRecord["Replacement"] = rr.Replacement
|
||||
case *dns.RRSIG:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["Expiration"] = int64(rr.Expiration)
|
||||
jsRecord["Inception"] = int64(rr.Inception)
|
||||
jsRecord["KeyTag"] = int64(rr.KeyTag)
|
||||
jsRecord["Labels"] = int64(rr.Labels)
|
||||
jsRecord["OrigTtl"] = int64(rr.OrigTtl)
|
||||
jsRecord["Signature"] = rr.Signature
|
||||
jsRecord["SignerName"] = rr.SignerName
|
||||
jsRecord["TypeCovered"] = int64(rr.TypeCovered)
|
||||
case *dns.NSEC:
|
||||
jsRecord["NextDomain"] = rr.NextDomain
|
||||
jsRecord["TypeBitMap"] = uint16ArrayToInt64Array(rr.TypeBitMap)
|
||||
case *dns.NSEC3:
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Hash"] = int64(rr.Hash)
|
||||
jsRecord["HashLength"] = int64(rr.HashLength)
|
||||
jsRecord["Iterations"] = int64(rr.Iterations)
|
||||
jsRecord["NextDomain"] = rr.NextDomain
|
||||
jsRecord["Salt"] = rr.Salt
|
||||
jsRecord["SaltLength"] = int64(rr.SaltLength)
|
||||
jsRecord["TypeBitMap"] = uint16ArrayToInt64Array(rr.TypeBitMap)
|
||||
case *dns.NSEC3PARAM:
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Hash"] = int64(rr.Hash)
|
||||
jsRecord["Iterations"] = int64(rr.Iterations)
|
||||
jsRecord["Salt"] = rr.Salt
|
||||
jsRecord["SaltLength"] = int64(rr.SaltLength)
|
||||
case *dns.TKEY:
|
||||
jsRecord["Algorithm"] = rr.Algorithm
|
||||
jsRecord["Error"] = int64(rr.Error)
|
||||
jsRecord["Expiration"] = int64(rr.Expiration)
|
||||
jsRecord["Inception"] = int64(rr.Inception)
|
||||
jsRecord["Key"] = rr.Key
|
||||
jsRecord["KeySize"] = int64(rr.KeySize)
|
||||
jsRecord["Mode"] = int64(rr.Mode)
|
||||
jsRecord["OtherData"] = rr.OtherData
|
||||
jsRecord["OtherLen"] = int64(rr.OtherLen)
|
||||
case *dns.TSIG:
|
||||
jsRecord["Algorithm"] = rr.Algorithm
|
||||
jsRecord["Error"] = int64(rr.Error)
|
||||
jsRecord["Fudge"] = int64(rr.Fudge)
|
||||
jsRecord["MACSize"] = int64(rr.MACSize)
|
||||
jsRecord["MAC"] = rr.MAC
|
||||
jsRecord["OrigId"] = int64(rr.OrigId)
|
||||
jsRecord["OtherData"] = rr.OtherData
|
||||
jsRecord["OtherLen"] = int64(rr.OtherLen)
|
||||
jsRecord["TimeSigned"] = int64(rr.TimeSigned)
|
||||
case *dns.IPSECKEY:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["GatewayAddr"] = rr.GatewayAddr.String()
|
||||
jsRecord["GatewayHost"] = rr.GatewayHost
|
||||
jsRecord["GatewayType"] = int64(rr.GatewayType)
|
||||
jsRecord["Precedence"] = int64(rr.Precedence)
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
case *dns.KEY:
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Protocol"] = int64(rr.Protocol)
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
case *dns.CDS:
|
||||
jsRecord["KeyTag"] = int64(rr.KeyTag)
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["DigestType"] = int64(rr.DigestType)
|
||||
jsRecord["Digest"] = rr.Digest
|
||||
case *dns.CDNSKEY:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Protocol"] = int64(rr.Protocol)
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
case *dns.NID:
|
||||
jsRecord["NodeID"] = rr.NodeID
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.L32:
|
||||
jsRecord["Locator32"] = rr.Locator32.String()
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.L64:
|
||||
jsRecord["Locator64"] = rr.Locator64
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.LP:
|
||||
jsRecord["Fqdn"] = rr.Fqdn
|
||||
jsRecord["Preference"] = int16(rr.Preference)
|
||||
case *dns.GPOS:
|
||||
jsRecord["Altitude"] = rr.Altitude
|
||||
jsRecord["Latitude"] = rr.Latitude
|
||||
jsRecord["Longitude"] = rr.Longitude
|
||||
case *dns.RP:
|
||||
jsRecord["Mbox"] = rr.Mbox
|
||||
jsRecord["Txt"] = rr.Txt
|
||||
case *dns.RKEY:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Protocol"] = int64(rr.Protocol)
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
case *dns.SMIMEA:
|
||||
jsRecord["Certificate"] = rr.Certificate
|
||||
jsRecord["MatchingType"] = int64(rr.MatchingType)
|
||||
jsRecord["Selector"] = int64(rr.Selector)
|
||||
jsRecord["Usage"] = int64(rr.Usage)
|
||||
case *dns.AMTRELAY:
|
||||
jsRecord["GatewayAddr"] = rr.GatewayAddr.String()
|
||||
jsRecord["GatewayHost"] = rr.GatewayHost
|
||||
jsRecord["GatewayType"] = int64(rr.GatewayType)
|
||||
jsRecord["Precedence"] = int64(rr.Precedence)
|
||||
case *dns.AVC:
|
||||
jsRecord["Txt"] = rr.Txt
|
||||
case *dns.URI:
|
||||
jsRecord["Priority"] = int64(rr.Priority)
|
||||
jsRecord["Weight"] = int64(rr.Weight)
|
||||
jsRecord["Target"] = rr.Target
|
||||
case *dns.EUI48:
|
||||
jsRecord["Address"] = rr.Address
|
||||
case *dns.EUI64:
|
||||
jsRecord["Address"] = rr.Address
|
||||
case *dns.GID:
|
||||
jsRecord["Gid"] = int64(rr.Gid)
|
||||
case *dns.UID:
|
||||
jsRecord["Uid"] = int64(rr.Uid)
|
||||
case *dns.UINFO:
|
||||
jsRecord["Uinfo"] = rr.Uinfo
|
||||
case *dns.SPF:
|
||||
jsRecord["Txt"] = rr.Txt
|
||||
case *dns.HTTPS:
|
||||
jsRecord["Priority"] = int64(rr.Priority)
|
||||
jsRecord["Target"] = rr.Target
|
||||
kvs := rr.Value
|
||||
var jsKvs []map[string]interface{}
|
||||
for _, kv := range kvs {
|
||||
jsKv, err := NewJSSVCBKeyValue(kv)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
jsKvs = append(jsKvs, jsKv)
|
||||
}
|
||||
jsRecord["Value"] = jsKvs
|
||||
case *dns.SVCB:
|
||||
jsRecord["Priority"] = int64(rr.Priority)
|
||||
jsRecord["Target"] = rr.Target
|
||||
kvs := rr.Value
|
||||
jsKvs := make([]map[string]interface{}, len(kvs))
|
||||
for i, kv := range kvs {
|
||||
jsKv, err := NewJSSVCBKeyValue(kv)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
jsKvs[i] = jsKv
|
||||
}
|
||||
jsRecord["Value"] = jsKvs
|
||||
case *dns.ZONEMD:
|
||||
jsRecord["Digest"] = rr.Digest
|
||||
jsRecord["Hash"] = int64(rr.Hash)
|
||||
jsRecord["Scheme"] = int64(rr.Scheme)
|
||||
jsRecord["Serial"] = int64(rr.Serial)
|
||||
case *dns.CSYNC:
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Serial"] = int64(rr.Serial)
|
||||
jsRecord["TypeBitMap"] = uint16ArrayToInt64Array(rr.TypeBitMap)
|
||||
case *dns.OPENPGPKEY:
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
case *dns.TALINK:
|
||||
jsRecord["NextName"] = rr.NextName
|
||||
jsRecord["PreviousName"] = rr.PreviousName
|
||||
case *dns.NINFO:
|
||||
jsRecord["ZSData"] = rr.ZSData
|
||||
case *dns.DHCID:
|
||||
jsRecord["Digest"] = rr.Digest
|
||||
case *dns.DNSKEY:
|
||||
jsRecord["Flags"] = int64(rr.Flags)
|
||||
jsRecord["Protocol"] = int64(rr.Protocol)
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
case *dns.HIP:
|
||||
jsRecord["Hit"] = rr.Hit
|
||||
jsRecord["HitLength"] = int64(rr.HitLength)
|
||||
jsRecord["PublicKey"] = rr.PublicKey
|
||||
jsRecord["PublicKeyAlgorithm"] = int64(rr.PublicKeyAlgorithm)
|
||||
jsRecord["PublicKeyLength"] = int64(rr.PublicKeyLength)
|
||||
jsRecord["RendezvousServers"] = rr.RendezvousServers
|
||||
case *dns.OPT:
|
||||
options := rr.Option
|
||||
jsOptions := make([]map[string]interface{}, len(options))
|
||||
for i, option := range options {
|
||||
jsOption, err := NewJSEDNS0(option)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
jsOptions[i] = jsOption
|
||||
}
|
||||
jsRecord["Option"] = jsOptions
|
||||
case *dns.NIMLOC:
|
||||
jsRecord["Locator"] = rr.Locator
|
||||
case *dns.EID:
|
||||
jsRecord["Endpoint"] = rr.Endpoint
|
||||
case *dns.NXT:
|
||||
jsRecord["NextDomain"] = rr.NextDomain
|
||||
jsRecord["TypeBitMap"] = uint16ArrayToInt64Array(rr.TypeBitMap)
|
||||
case *dns.PX:
|
||||
jsRecord["Mapx400"] = rr.Mapx400
|
||||
jsRecord["Map822"] = rr.Map822
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.SIG:
|
||||
jsRecord["Algorithm"] = int64(rr.Algorithm)
|
||||
jsRecord["Expiration"] = int64(rr.Expiration)
|
||||
jsRecord["Inception"] = int64(rr.Inception)
|
||||
jsRecord["KeyTag"] = int64(rr.KeyTag)
|
||||
jsRecord["Labels"] = int64(rr.Labels)
|
||||
jsRecord["OrigTtl"] = int64(rr.OrigTtl)
|
||||
jsRecord["Signature"] = rr.Signature
|
||||
jsRecord["SignerName"] = rr.SignerName
|
||||
jsRecord["TypeCovered"] = int64(rr.TypeCovered)
|
||||
case *dns.RT:
|
||||
jsRecord["Host"] = rr.Host
|
||||
jsRecord["Preference"] = int64(rr.Preference)
|
||||
case *dns.NSAPPTR:
|
||||
jsRecord["Ptr"] = rr.Ptr
|
||||
case *dns.X25:
|
||||
jsRecord["PSDNAddress"] = rr.PSDNAddress
|
||||
case *dns.RFC3597:
|
||||
jsRecord["Rdata"] = rr.Rdata
|
||||
// case *dns.ATMA:
|
||||
// case *dns.WKS:
|
||||
// case *dns.DOA:
|
||||
// case *dns.SINK:
|
||||
default:
|
||||
if header.Rrtype == dns.TypeNone {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("error creating JSResourceRecord: unknown type: %d", header.Rrtype)
|
||||
}
|
||||
|
||||
return jsRecord, nil
|
||||
}
|
||||
|
||||
func ToRR(jsRecord map[string]interface{}) (rr dns.RR, err error) {
|
||||
jsHeader := jsPropToMap(jsRecord, "Header")
|
||||
|
||||
header := dns.RR_Header{
|
||||
Class: jsPropToUint16(jsHeader, "Class"),
|
||||
Name: jsPropToString(jsHeader, "Name"),
|
||||
Rrtype: jsPropToUint16(jsHeader, "Rrtype"),
|
||||
Ttl: jsPropToUint32(jsHeader, "Ttl"),
|
||||
}
|
||||
|
||||
switch header.Rrtype {
|
||||
case dns.TypeNone:
|
||||
break
|
||||
case dns.TypeA:
|
||||
rr = &dns.A{
|
||||
Hdr: header,
|
||||
A: net.ParseIP(jsPropToString(jsRecord, "A")),
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
rr = &dns.AAAA{
|
||||
Hdr: header,
|
||||
AAAA: net.ParseIP(jsPropToString(jsRecord, "AAAA")),
|
||||
}
|
||||
case dns.TypeAPL:
|
||||
jsPrefixes := jsRecord["Prefixes"].([]map[string]interface{})
|
||||
prefixes := make([]dns.APLPrefix, len(jsPrefixes))
|
||||
for i, jsPrefix := range jsPrefixes {
|
||||
jsNetwork := jsPrefix["Network"].(string)
|
||||
_, network, err := net.ParseCIDR(jsNetwork)
|
||||
if err != nil {
|
||||
log.Error("error parsing CIDR: %s", jsNetwork)
|
||||
continue
|
||||
}
|
||||
prefixes[i] = dns.APLPrefix{
|
||||
Negation: jsPrefix["Negation"].(bool),
|
||||
Network: *network,
|
||||
}
|
||||
}
|
||||
rr = &dns.APL{
|
||||
Hdr: header,
|
||||
Prefixes: prefixes,
|
||||
}
|
||||
case dns.TypeCNAME:
|
||||
rr = &dns.CNAME{
|
||||
Hdr: header,
|
||||
Target: jsPropToString(jsRecord, "Target"),
|
||||
}
|
||||
case dns.TypeMB:
|
||||
rr = &dns.MB{
|
||||
Hdr: header,
|
||||
Mb: jsPropToString(jsRecord, "Mb"),
|
||||
}
|
||||
case dns.TypeMD:
|
||||
rr = &dns.MD{
|
||||
Hdr: header,
|
||||
Md: jsPropToString(jsRecord, "Md"),
|
||||
}
|
||||
case dns.TypeMF:
|
||||
rr = &dns.MF{
|
||||
Hdr: header,
|
||||
Mf: jsPropToString(jsRecord, "Mf"),
|
||||
}
|
||||
case dns.TypeMG:
|
||||
rr = &dns.MG{
|
||||
Hdr: header,
|
||||
Mg: jsPropToString(jsRecord, "Mg"),
|
||||
}
|
||||
case dns.TypeMR:
|
||||
rr = &dns.MR{
|
||||
Hdr: header,
|
||||
Mr: jsPropToString(jsRecord, "Mr"),
|
||||
}
|
||||
case dns.TypeMX:
|
||||
rr = &dns.MX{
|
||||
Hdr: header,
|
||||
Mx: jsPropToString(jsRecord, "Mx"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeNULL:
|
||||
rr = &dns.NULL{
|
||||
Hdr: header,
|
||||
Data: jsPropToString(jsRecord, "Data"),
|
||||
}
|
||||
case dns.TypeSOA:
|
||||
rr = &dns.SOA{
|
||||
Hdr: header,
|
||||
Expire: jsPropToUint32(jsRecord, "Expire"),
|
||||
Mbox: jsPropToString(jsRecord, "Mbox"),
|
||||
Minttl: jsPropToUint32(jsRecord, "Minttl"),
|
||||
Ns: jsPropToString(jsRecord, "Ns"),
|
||||
Refresh: jsPropToUint32(jsRecord, "Refresh"),
|
||||
Retry: jsPropToUint32(jsRecord, "Retry"),
|
||||
Serial: jsPropToUint32(jsRecord, "Serial"),
|
||||
}
|
||||
case dns.TypeTXT:
|
||||
rr = &dns.TXT{
|
||||
Hdr: header,
|
||||
Txt: jsPropToStringArray(jsRecord, "Txt"),
|
||||
}
|
||||
case dns.TypeSRV:
|
||||
rr = &dns.SRV{
|
||||
Hdr: header,
|
||||
Port: jsPropToUint16(jsRecord, "Port"),
|
||||
Priority: jsPropToUint16(jsRecord, "Priority"),
|
||||
Target: jsPropToString(jsRecord, "Target"),
|
||||
Weight: jsPropToUint16(jsRecord, "Weight"),
|
||||
}
|
||||
case dns.TypePTR:
|
||||
rr = &dns.PTR{
|
||||
Hdr: header,
|
||||
Ptr: jsPropToString(jsRecord, "Ptr"),
|
||||
}
|
||||
case dns.TypeNS:
|
||||
rr = &dns.NS{
|
||||
Hdr: header,
|
||||
Ns: jsPropToString(jsRecord, "Ns"),
|
||||
}
|
||||
case dns.TypeDNAME:
|
||||
rr = &dns.DNAME{
|
||||
Hdr: header,
|
||||
Target: jsPropToString(jsRecord, "Target"),
|
||||
}
|
||||
case dns.TypeAFSDB:
|
||||
rr = &dns.AFSDB{
|
||||
Hdr: header,
|
||||
Hostname: jsPropToString(jsRecord, "Hostname"),
|
||||
Subtype: jsPropToUint16(jsRecord, "Subtype"),
|
||||
}
|
||||
case dns.TypeCAA:
|
||||
rr = &dns.CAA{
|
||||
Hdr: header,
|
||||
Flag: jsPropToUint8(jsRecord, "Flag"),
|
||||
Tag: jsPropToString(jsRecord, "Tag"),
|
||||
Value: jsPropToString(jsRecord, "Value"),
|
||||
}
|
||||
case dns.TypeHINFO:
|
||||
rr = &dns.HINFO{
|
||||
Hdr: header,
|
||||
Cpu: jsPropToString(jsRecord, "Cpu"),
|
||||
Os: jsPropToString(jsRecord, "Os"),
|
||||
}
|
||||
case dns.TypeMINFO:
|
||||
rr = &dns.MINFO{
|
||||
Hdr: header,
|
||||
Email: jsPropToString(jsRecord, "Email"),
|
||||
Rmail: jsPropToString(jsRecord, "Rmail"),
|
||||
}
|
||||
case dns.TypeISDN:
|
||||
rr = &dns.ISDN{
|
||||
Hdr: header,
|
||||
Address: jsPropToString(jsRecord, "Address"),
|
||||
SubAddress: jsPropToString(jsRecord, "SubAddress"),
|
||||
}
|
||||
case dns.TypeKX:
|
||||
rr = &dns.KX{
|
||||
Hdr: header,
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
Exchanger: jsPropToString(jsRecord, "Exchanger"),
|
||||
}
|
||||
case dns.TypeLOC:
|
||||
rr = &dns.LOC{
|
||||
Hdr: header,
|
||||
Version: jsPropToUint8(jsRecord, "Version"),
|
||||
Size: jsPropToUint8(jsRecord, "Size"),
|
||||
HorizPre: jsPropToUint8(jsRecord, "HorizPre"),
|
||||
VertPre: jsPropToUint8(jsRecord, "VertPre"),
|
||||
Latitude: jsPropToUint32(jsRecord, "Latitude"),
|
||||
Longitude: jsPropToUint32(jsRecord, "Longitude"),
|
||||
Altitude: jsPropToUint32(jsRecord, "Altitude"),
|
||||
}
|
||||
case dns.TypeSSHFP:
|
||||
rr = &dns.SSHFP{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
FingerPrint: jsPropToString(jsRecord, "FingerPrint"),
|
||||
Type: jsPropToUint8(jsRecord, "Type"),
|
||||
}
|
||||
case dns.TypeTLSA:
|
||||
rr = &dns.TLSA{
|
||||
Hdr: header,
|
||||
Certificate: jsPropToString(jsRecord, "Certificate"),
|
||||
MatchingType: jsPropToUint8(jsRecord, "MatchingType"),
|
||||
Selector: jsPropToUint8(jsRecord, "Selector"),
|
||||
Usage: jsPropToUint8(jsRecord, "Usage"),
|
||||
}
|
||||
case dns.TypeCERT:
|
||||
rr = &dns.CERT{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Certificate: jsPropToString(jsRecord, "Certificate"),
|
||||
KeyTag: jsPropToUint16(jsRecord, "KeyTag"),
|
||||
Type: jsPropToUint16(jsRecord, "Type"),
|
||||
}
|
||||
case dns.TypeDS:
|
||||
rr = &dns.DS{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Digest: jsPropToString(jsRecord, "Digest"),
|
||||
DigestType: jsPropToUint8(jsRecord, "DigestType"),
|
||||
KeyTag: jsPropToUint16(jsRecord, "KeyTag"),
|
||||
}
|
||||
case dns.TypeNAPTR:
|
||||
rr = &dns.NAPTR{
|
||||
Hdr: header,
|
||||
Flags: jsPropToString(jsRecord, "Flags"),
|
||||
Order: jsPropToUint16(jsRecord, "Order"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
Regexp: jsPropToString(jsRecord, "Regexp"),
|
||||
Replacement: jsPropToString(jsRecord, "Replacement"),
|
||||
Service: jsPropToString(jsRecord, "Service"),
|
||||
}
|
||||
case dns.TypeRRSIG:
|
||||
rr = &dns.RRSIG{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Expiration: jsPropToUint32(jsRecord, "Expiration"),
|
||||
Inception: jsPropToUint32(jsRecord, "Inception"),
|
||||
KeyTag: jsPropToUint16(jsRecord, "KeyTag"),
|
||||
Labels: jsPropToUint8(jsRecord, "Labels"),
|
||||
OrigTtl: jsPropToUint32(jsRecord, "OrigTtl"),
|
||||
Signature: jsPropToString(jsRecord, "Signature"),
|
||||
SignerName: jsPropToString(jsRecord, "SignerName"),
|
||||
TypeCovered: jsPropToUint16(jsRecord, "TypeCovered"),
|
||||
}
|
||||
case dns.TypeNSEC:
|
||||
rr = &dns.NSEC{
|
||||
Hdr: header,
|
||||
NextDomain: jsPropToString(jsRecord, "NextDomain"),
|
||||
TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"),
|
||||
}
|
||||
case dns.TypeNSEC3:
|
||||
rr = &dns.NSEC3{
|
||||
Hdr: header,
|
||||
Flags: jsPropToUint8(jsRecord, "Flags"),
|
||||
Hash: jsPropToUint8(jsRecord, "Hash"),
|
||||
HashLength: jsPropToUint8(jsRecord, "HashLength"),
|
||||
Iterations: jsPropToUint16(jsRecord, "Iterations"),
|
||||
NextDomain: jsPropToString(jsRecord, "NextDomain"),
|
||||
Salt: jsPropToString(jsRecord, "Salt"),
|
||||
SaltLength: jsPropToUint8(jsRecord, "SaltLength"),
|
||||
TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"),
|
||||
}
|
||||
case dns.TypeNSEC3PARAM:
|
||||
rr = &dns.NSEC3PARAM{
|
||||
Hdr: header,
|
||||
Flags: jsPropToUint8(jsRecord, "Flags"),
|
||||
Hash: jsPropToUint8(jsRecord, "Hash"),
|
||||
Iterations: jsPropToUint16(jsRecord, "Iterations"),
|
||||
Salt: jsPropToString(jsRecord, "Salt"),
|
||||
SaltLength: jsPropToUint8(jsRecord, "SaltLength"),
|
||||
}
|
||||
case dns.TypeTKEY:
|
||||
rr = &dns.TKEY{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToString(jsRecord, "Algorithm"),
|
||||
Error: jsPropToUint16(jsRecord, "Error"),
|
||||
Expiration: jsPropToUint32(jsRecord, "Expiration"),
|
||||
Inception: jsPropToUint32(jsRecord, "Inception"),
|
||||
Key: jsPropToString(jsRecord, "Key"),
|
||||
KeySize: jsPropToUint16(jsRecord, "KeySize"),
|
||||
Mode: jsPropToUint16(jsRecord, "Mode"),
|
||||
OtherData: jsPropToString(jsRecord, "OtherData"),
|
||||
OtherLen: jsPropToUint16(jsRecord, "OtherLen"),
|
||||
}
|
||||
case dns.TypeTSIG:
|
||||
rr = &dns.TSIG{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToString(jsRecord, "Algorithm"),
|
||||
Error: jsPropToUint16(jsRecord, "Error"),
|
||||
Fudge: jsPropToUint16(jsRecord, "Fudge"),
|
||||
MACSize: jsPropToUint16(jsRecord, "MACSize"),
|
||||
MAC: jsPropToString(jsRecord, "MAC"),
|
||||
OrigId: jsPropToUint16(jsRecord, "OrigId"),
|
||||
OtherData: jsPropToString(jsRecord, "OtherData"),
|
||||
OtherLen: jsPropToUint16(jsRecord, "OtherLen"),
|
||||
TimeSigned: jsPropToUint64(jsRecord, "TimeSigned"),
|
||||
}
|
||||
case dns.TypeIPSECKEY:
|
||||
rr = &dns.IPSECKEY{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
GatewayAddr: net.IP(jsPropToString(jsRecord, "GatewayAddr")),
|
||||
GatewayHost: jsPropToString(jsRecord, "GatewayHost"),
|
||||
GatewayType: jsPropToUint8(jsRecord, "GatewayType"),
|
||||
Precedence: jsPropToUint8(jsRecord, "Precedence"),
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
}
|
||||
case dns.TypeKEY:
|
||||
rr = &dns.KEY{
|
||||
DNSKEY: dns.DNSKEY{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Flags: jsPropToUint16(jsRecord, "Flags"),
|
||||
Protocol: jsPropToUint8(jsRecord, "Protocol"),
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
},
|
||||
}
|
||||
case dns.TypeCDS:
|
||||
rr = &dns.CDS{
|
||||
DS: dns.DS{
|
||||
Hdr: header,
|
||||
KeyTag: jsPropToUint16(jsRecord, "KeyTag"),
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
DigestType: jsPropToUint8(jsRecord, "DigestType"),
|
||||
Digest: jsPropToString(jsRecord, "Digest"),
|
||||
},
|
||||
}
|
||||
case dns.TypeCDNSKEY:
|
||||
rr = &dns.CDNSKEY{
|
||||
DNSKEY: dns.DNSKEY{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Flags: jsPropToUint16(jsRecord, "Flags"),
|
||||
Protocol: jsPropToUint8(jsRecord, "Protocol"),
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
},
|
||||
}
|
||||
case dns.TypeNID:
|
||||
rr = &dns.NID{
|
||||
Hdr: header,
|
||||
NodeID: jsPropToUint64(jsRecord, "NodeID"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeL32:
|
||||
rr = &dns.L32{
|
||||
Hdr: header,
|
||||
Locator32: net.IP(jsPropToString(jsRecord, "Locator32")),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeL64:
|
||||
rr = &dns.L64{
|
||||
Hdr: header,
|
||||
Locator64: jsPropToUint64(jsRecord, "Locator64"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeLP:
|
||||
rr = &dns.LP{
|
||||
Hdr: header,
|
||||
Fqdn: jsPropToString(jsRecord, "Fqdn"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeGPOS:
|
||||
rr = &dns.GPOS{
|
||||
Hdr: header,
|
||||
Altitude: jsPropToString(jsRecord, "Altitude"),
|
||||
Latitude: jsPropToString(jsRecord, "Latitude"),
|
||||
Longitude: jsPropToString(jsRecord, "Longitude"),
|
||||
}
|
||||
case dns.TypeRP:
|
||||
rr = &dns.RP{
|
||||
Hdr: header,
|
||||
Mbox: jsPropToString(jsRecord, "Mbox"),
|
||||
Txt: jsPropToString(jsRecord, "Txt"),
|
||||
}
|
||||
case dns.TypeRKEY:
|
||||
rr = &dns.RKEY{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Flags: jsPropToUint16(jsRecord, "Flags"),
|
||||
Protocol: jsPropToUint8(jsRecord, "Protocol"),
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
}
|
||||
case dns.TypeSMIMEA:
|
||||
rr = &dns.SMIMEA{
|
||||
Hdr: header,
|
||||
Certificate: jsPropToString(jsRecord, "Certificate"),
|
||||
MatchingType: jsPropToUint8(jsRecord, "MatchingType"),
|
||||
Selector: jsPropToUint8(jsRecord, "Selector"),
|
||||
Usage: jsPropToUint8(jsRecord, "Usage"),
|
||||
}
|
||||
case dns.TypeAMTRELAY:
|
||||
rr = &dns.AMTRELAY{
|
||||
Hdr: header,
|
||||
GatewayAddr: net.IP(jsPropToString(jsRecord, "GatewayAddr")),
|
||||
GatewayHost: jsPropToString(jsRecord, "GatewayHost"),
|
||||
GatewayType: jsPropToUint8(jsRecord, "GatewayType"),
|
||||
Precedence: jsPropToUint8(jsRecord, "Precedence"),
|
||||
}
|
||||
case dns.TypeAVC:
|
||||
rr = &dns.AVC{
|
||||
Hdr: header,
|
||||
Txt: jsPropToStringArray(jsRecord, "Txt"),
|
||||
}
|
||||
case dns.TypeURI:
|
||||
rr = &dns.URI{
|
||||
Hdr: header,
|
||||
Priority: jsPropToUint16(jsRecord, "Priority"),
|
||||
Weight: jsPropToUint16(jsRecord, "Weight"),
|
||||
Target: jsPropToString(jsRecord, "Target"),
|
||||
}
|
||||
case dns.TypeEUI48:
|
||||
rr = &dns.EUI48{
|
||||
Hdr: header,
|
||||
Address: jsPropToUint64(jsRecord, "Address"),
|
||||
}
|
||||
case dns.TypeEUI64:
|
||||
rr = &dns.EUI64{
|
||||
Hdr: header,
|
||||
Address: jsPropToUint64(jsRecord, "Address"),
|
||||
}
|
||||
case dns.TypeGID:
|
||||
rr = &dns.GID{
|
||||
Hdr: header,
|
||||
Gid: jsPropToUint32(jsRecord, "Gid"),
|
||||
}
|
||||
case dns.TypeUID:
|
||||
rr = &dns.UID{
|
||||
Hdr: header,
|
||||
Uid: jsPropToUint32(jsRecord, "Uid"),
|
||||
}
|
||||
case dns.TypeUINFO:
|
||||
rr = &dns.UINFO{
|
||||
Hdr: header,
|
||||
Uinfo: jsPropToString(jsRecord, "Uinfo"),
|
||||
}
|
||||
case dns.TypeSPF:
|
||||
rr = &dns.SPF{
|
||||
Hdr: header,
|
||||
Txt: jsPropToStringArray(jsRecord, "Txt"),
|
||||
}
|
||||
case dns.TypeHTTPS:
|
||||
jsKvs := jsPropToMapArray(jsRecord, "Value")
|
||||
var kvs []dns.SVCBKeyValue
|
||||
for _, jsKv := range jsKvs {
|
||||
kv, err := ToSVCBKeyValue(jsKv)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
kvs = append(kvs, kv)
|
||||
}
|
||||
rr = &dns.HTTPS{
|
||||
SVCB: dns.SVCB{
|
||||
Hdr: header,
|
||||
Priority: jsPropToUint16(jsRecord, "Priority"),
|
||||
Target: jsPropToString(jsRecord, "Target"),
|
||||
Value: kvs,
|
||||
},
|
||||
}
|
||||
case dns.TypeSVCB:
|
||||
jsKvs := jsPropToMapArray(jsRecord, "Value")
|
||||
var kvs []dns.SVCBKeyValue
|
||||
for _, jsKv := range jsKvs {
|
||||
kv, err := ToSVCBKeyValue(jsKv)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
kvs = append(kvs, kv)
|
||||
}
|
||||
rr = &dns.SVCB{
|
||||
Hdr: header,
|
||||
Priority: jsPropToUint16(jsRecord, "Priority"),
|
||||
Target: jsPropToString(jsRecord, "Target"),
|
||||
Value: kvs,
|
||||
}
|
||||
case dns.TypeZONEMD:
|
||||
rr = &dns.ZONEMD{
|
||||
Hdr: header,
|
||||
Digest: jsPropToString(jsRecord, "Digest"),
|
||||
Hash: jsPropToUint8(jsRecord, "Hash"),
|
||||
Scheme: jsPropToUint8(jsRecord, "Scheme"),
|
||||
Serial: jsPropToUint32(jsRecord, "Serial"),
|
||||
}
|
||||
case dns.TypeCSYNC:
|
||||
rr = &dns.CSYNC{
|
||||
Hdr: header,
|
||||
Flags: jsPropToUint16(jsRecord, "Flags"),
|
||||
Serial: jsPropToUint32(jsRecord, "Serial"),
|
||||
TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"),
|
||||
}
|
||||
case dns.TypeOPENPGPKEY:
|
||||
rr = &dns.OPENPGPKEY{
|
||||
Hdr: header,
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
}
|
||||
case dns.TypeTALINK:
|
||||
rr = &dns.TALINK{
|
||||
Hdr: header,
|
||||
NextName: jsPropToString(jsRecord, "NextName"),
|
||||
PreviousName: jsPropToString(jsRecord, "PreviousName"),
|
||||
}
|
||||
case dns.TypeNINFO:
|
||||
rr = &dns.NINFO{
|
||||
Hdr: header,
|
||||
ZSData: jsPropToStringArray(jsRecord, "ZSData"),
|
||||
}
|
||||
case dns.TypeDHCID:
|
||||
rr = &dns.DHCID{
|
||||
Hdr: header,
|
||||
Digest: jsPropToString(jsRecord, "Digest"),
|
||||
}
|
||||
case dns.TypeDNSKEY:
|
||||
rr = &dns.DNSKEY{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Flags: jsPropToUint16(jsRecord, "Flags"),
|
||||
Protocol: jsPropToUint8(jsRecord, "Protocol"),
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
}
|
||||
case dns.TypeHIP:
|
||||
rr = &dns.HIP{
|
||||
Hdr: header,
|
||||
Hit: jsPropToString(jsRecord, "Hit"),
|
||||
HitLength: jsPropToUint8(jsRecord, "HitLength"),
|
||||
PublicKey: jsPropToString(jsRecord, "PublicKey"),
|
||||
PublicKeyAlgorithm: jsPropToUint8(jsRecord, "PublicKeyAlgorithm"),
|
||||
PublicKeyLength: jsPropToUint16(jsRecord, "PublicKeyLength"),
|
||||
RendezvousServers: jsPropToStringArray(jsRecord, "RendezvousServers"),
|
||||
}
|
||||
case dns.TypeOPT:
|
||||
jsOptions := jsPropToMapArray(jsRecord, "Option")
|
||||
var options []dns.EDNS0
|
||||
for _, jsOption := range jsOptions {
|
||||
option, err := ToEDNS0(jsOption)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
options = append(options, option)
|
||||
}
|
||||
rr = &dns.OPT{
|
||||
Hdr: header,
|
||||
Option: options,
|
||||
}
|
||||
case dns.TypeNIMLOC:
|
||||
rr = &dns.NIMLOC{
|
||||
Hdr: header,
|
||||
Locator: jsPropToString(jsRecord, "Locator"),
|
||||
}
|
||||
case dns.TypeEID:
|
||||
rr = &dns.EID{
|
||||
Hdr: header,
|
||||
Endpoint: jsPropToString(jsRecord, "Endpoint"),
|
||||
}
|
||||
case dns.TypeNXT:
|
||||
rr = &dns.NXT{
|
||||
NSEC: dns.NSEC{
|
||||
Hdr: header,
|
||||
NextDomain: jsPropToString(jsRecord, "NextDomain"),
|
||||
TypeBitMap: jsPropToUint16Array(jsRecord, "TypeBitMap"),
|
||||
},
|
||||
}
|
||||
case dns.TypePX:
|
||||
rr = &dns.PX{
|
||||
Hdr: header,
|
||||
Mapx400: jsPropToString(jsRecord, "Mapx400"),
|
||||
Map822: jsPropToString(jsRecord, "Map822"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeSIG:
|
||||
rr = &dns.SIG{
|
||||
RRSIG: dns.RRSIG{
|
||||
Hdr: header,
|
||||
Algorithm: jsPropToUint8(jsRecord, "Algorithm"),
|
||||
Expiration: jsPropToUint32(jsRecord, "Expiration"),
|
||||
Inception: jsPropToUint32(jsRecord, "Inception"),
|
||||
KeyTag: jsPropToUint16(jsRecord, "KeyTag"),
|
||||
Labels: jsPropToUint8(jsRecord, "Labels"),
|
||||
OrigTtl: jsPropToUint32(jsRecord, "OrigTtl"),
|
||||
Signature: jsPropToString(jsRecord, "Signature"),
|
||||
SignerName: jsPropToString(jsRecord, "SignerName"),
|
||||
TypeCovered: jsPropToUint16(jsRecord, "TypeCovered"),
|
||||
},
|
||||
}
|
||||
case dns.TypeRT:
|
||||
rr = &dns.RT{
|
||||
Hdr: header,
|
||||
Host: jsPropToString(jsRecord, "Host"),
|
||||
Preference: jsPropToUint16(jsRecord, "Preference"),
|
||||
}
|
||||
case dns.TypeNSAPPTR:
|
||||
rr = &dns.NSAPPTR{
|
||||
Hdr: header,
|
||||
Ptr: jsPropToString(jsRecord, "Ptr"),
|
||||
}
|
||||
case dns.TypeX25:
|
||||
rr = &dns.X25{
|
||||
Hdr: header,
|
||||
PSDNAddress: jsPropToString(jsRecord, "PSDNAddress"),
|
||||
}
|
||||
// case dns.TypeATMA:
|
||||
// case dns.TypeWKS:
|
||||
// case dns.TypeDOA:
|
||||
// case dns.TypeSINK:
|
||||
default:
|
||||
if rdata, ok := jsRecord["Rdata"].(string); ok {
|
||||
rr = &dns.RFC3597{
|
||||
Hdr: header,
|
||||
Rdata: rdata,
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("error converting to dns.RR: unknown type: %d", header.Rrtype)
|
||||
}
|
||||
}
|
||||
|
||||
return rr, nil
|
||||
}
|
||||
208
modules/dns_proxy/dns_proxy_js_record_edns0.go
Normal file
208
modules/dns_proxy/dns_proxy_js_record_edns0.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func NewJSEDNS0(e dns.EDNS0) (jsEDNS0 map[string]interface{}, err error) {
|
||||
option := e.Option()
|
||||
|
||||
jsEDNS0 = map[string]interface{}{
|
||||
"Option": option,
|
||||
}
|
||||
|
||||
var jsVal map[string]interface{}
|
||||
|
||||
switch opt := e.(type) {
|
||||
case *dns.EDNS0_LLQ:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Error": opt.Error,
|
||||
"Id": opt.Id,
|
||||
"LeaseLife": opt.LeaseLife,
|
||||
"Opcode": opt.Opcode,
|
||||
"Version": opt.Version,
|
||||
}
|
||||
case *dns.EDNS0_UL:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Lease": opt.Lease,
|
||||
"KeyLease": opt.KeyLease,
|
||||
}
|
||||
case *dns.EDNS0_NSID:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Nsid": opt.Nsid,
|
||||
}
|
||||
case *dns.EDNS0_ESU:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Uri": opt.Uri,
|
||||
}
|
||||
case *dns.EDNS0_DAU:
|
||||
jsVal = map[string]interface{}{
|
||||
"AlgCode": opt.AlgCode,
|
||||
"Code": opt.Code,
|
||||
}
|
||||
case *dns.EDNS0_DHU:
|
||||
jsVal = map[string]interface{}{
|
||||
"AlgCode": opt.AlgCode,
|
||||
"Code": opt.Code,
|
||||
}
|
||||
case *dns.EDNS0_N3U:
|
||||
jsVal = map[string]interface{}{
|
||||
"AlgCode": opt.AlgCode,
|
||||
"Code": opt.Code,
|
||||
}
|
||||
case *dns.EDNS0_SUBNET:
|
||||
jsVal = map[string]interface{}{
|
||||
"Address": opt.Address.String(),
|
||||
"Code": opt.Code,
|
||||
"Family": opt.Family,
|
||||
"SourceNetmask": opt.SourceNetmask,
|
||||
"SourceScope": opt.SourceScope,
|
||||
}
|
||||
case *dns.EDNS0_EXPIRE:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Empty": opt.Empty,
|
||||
"Expire": opt.Expire,
|
||||
}
|
||||
case *dns.EDNS0_COOKIE:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Cookie": opt.Cookie,
|
||||
}
|
||||
case *dns.EDNS0_TCP_KEEPALIVE:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Length": opt.Length,
|
||||
"Timeout": opt.Timeout,
|
||||
}
|
||||
case *dns.EDNS0_PADDING:
|
||||
jsVal = map[string]interface{}{
|
||||
"Padding": string(opt.Padding),
|
||||
}
|
||||
case *dns.EDNS0_EDE:
|
||||
jsVal = map[string]interface{}{
|
||||
"ExtraText": opt.ExtraText,
|
||||
"InfoCode": opt.InfoCode,
|
||||
}
|
||||
case *dns.EDNS0_LOCAL:
|
||||
jsVal = map[string]interface{}{
|
||||
"Code": opt.Code,
|
||||
"Data": string(opt.Data),
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported EDNS0 option: %d", option)
|
||||
}
|
||||
|
||||
jsEDNS0["Value"] = jsVal
|
||||
|
||||
return jsEDNS0, nil
|
||||
}
|
||||
|
||||
func ToEDNS0(jsEDNS0 map[string]interface{}) (e dns.EDNS0, err error) {
|
||||
option := jsPropToUint16(jsEDNS0, "Option")
|
||||
|
||||
jsVal := jsPropToMap(jsEDNS0, "Value")
|
||||
|
||||
switch option {
|
||||
case dns.EDNS0LLQ:
|
||||
e = &dns.EDNS0_LLQ{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Error: jsPropToUint16(jsVal, "Error"),
|
||||
Id: jsPropToUint64(jsVal, "Id"),
|
||||
LeaseLife: jsPropToUint32(jsVal, "LeaseLife"),
|
||||
Opcode: jsPropToUint16(jsVal, "Opcode"),
|
||||
Version: jsPropToUint16(jsVal, "Version"),
|
||||
}
|
||||
case dns.EDNS0UL:
|
||||
e = &dns.EDNS0_UL{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Lease: jsPropToUint32(jsVal, "Lease"),
|
||||
KeyLease: jsPropToUint32(jsVal, "KeyLease"),
|
||||
}
|
||||
case dns.EDNS0NSID:
|
||||
e = &dns.EDNS0_NSID{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Nsid: jsPropToString(jsVal, "Nsid"),
|
||||
}
|
||||
case dns.EDNS0ESU:
|
||||
e = &dns.EDNS0_ESU{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Uri: jsPropToString(jsVal, "Uri"),
|
||||
}
|
||||
case dns.EDNS0DAU:
|
||||
e = &dns.EDNS0_DAU{
|
||||
AlgCode: jsPropToUint8Array(jsVal, "AlgCode"),
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
}
|
||||
case dns.EDNS0DHU:
|
||||
e = &dns.EDNS0_DHU{
|
||||
AlgCode: jsPropToUint8Array(jsVal, "AlgCode"),
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
}
|
||||
case dns.EDNS0N3U:
|
||||
e = &dns.EDNS0_N3U{
|
||||
AlgCode: jsPropToUint8Array(jsVal, "AlgCode"),
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
}
|
||||
case dns.EDNS0SUBNET:
|
||||
e = &dns.EDNS0_SUBNET{
|
||||
Address: net.ParseIP(jsPropToString(jsVal, "Address")),
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Family: jsPropToUint16(jsVal, "Family"),
|
||||
SourceNetmask: jsPropToUint8(jsVal, "SourceNetmask"),
|
||||
SourceScope: jsPropToUint8(jsVal, "SourceScope"),
|
||||
}
|
||||
case dns.EDNS0EXPIRE:
|
||||
if empty, ok := jsVal["Empty"].(bool); !ok {
|
||||
log.Error("invalid or missing EDNS0_EXPIRE.Empty bool value, skipping field.")
|
||||
e = &dns.EDNS0_EXPIRE{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Expire: jsPropToUint32(jsVal, "Expire"),
|
||||
}
|
||||
} else {
|
||||
e = &dns.EDNS0_EXPIRE{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Expire: jsPropToUint32(jsVal, "Expire"),
|
||||
Empty: empty,
|
||||
}
|
||||
}
|
||||
case dns.EDNS0COOKIE:
|
||||
e = &dns.EDNS0_COOKIE{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Cookie: jsPropToString(jsVal, "Cookie"),
|
||||
}
|
||||
case dns.EDNS0TCPKEEPALIVE:
|
||||
e = &dns.EDNS0_TCP_KEEPALIVE{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Length: jsPropToUint16(jsVal, "Length"),
|
||||
Timeout: jsPropToUint16(jsVal, "Timeout"),
|
||||
}
|
||||
case dns.EDNS0PADDING:
|
||||
e = &dns.EDNS0_PADDING{
|
||||
Padding: []byte(jsPropToString(jsVal, "Padding")),
|
||||
}
|
||||
case dns.EDNS0EDE:
|
||||
e = &dns.EDNS0_EDE{
|
||||
ExtraText: jsPropToString(jsVal, "ExtraText"),
|
||||
InfoCode: jsPropToUint16(jsVal, "InfoCode"),
|
||||
}
|
||||
case dns.EDNS0LOCALSTART, dns.EDNS0LOCALEND, 0x8000:
|
||||
// _DO = 0x8000
|
||||
e = &dns.EDNS0_LOCAL{
|
||||
Code: jsPropToUint16(jsVal, "Code"),
|
||||
Data: []byte(jsPropToString(jsVal, "Data")),
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported EDNS0 option: %d", option)
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
127
modules/dns_proxy/dns_proxy_js_record_svcb.go
Normal file
127
modules/dns_proxy/dns_proxy_js_record_svcb.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func NewJSSVCBKeyValue(kv dns.SVCBKeyValue) (map[string]interface{}, error) {
|
||||
key := kv.Key()
|
||||
|
||||
jsKv := map[string]interface{}{
|
||||
"Key": uint16(key),
|
||||
}
|
||||
|
||||
switch v := kv.(type) {
|
||||
case *dns.SVCBAlpn:
|
||||
jsKv["Alpn"] = v.Alpn
|
||||
case *dns.SVCBNoDefaultAlpn:
|
||||
break
|
||||
case *dns.SVCBECHConfig:
|
||||
jsKv["ECH"] = string(v.ECH)
|
||||
case *dns.SVCBPort:
|
||||
jsKv["Port"] = v.Port
|
||||
case *dns.SVCBIPv4Hint:
|
||||
ips := v.Hint
|
||||
jsIps := make([]string, len(ips))
|
||||
for i, ip := range ips {
|
||||
jsIps[i] = ip.String()
|
||||
}
|
||||
jsKv["Hint"] = jsIps
|
||||
case *dns.SVCBIPv6Hint:
|
||||
ips := v.Hint
|
||||
jsIps := make([]string, len(ips))
|
||||
for i, ip := range ips {
|
||||
jsIps[i] = ip.String()
|
||||
}
|
||||
jsKv["Hint"] = jsIps
|
||||
case *dns.SVCBDoHPath:
|
||||
jsKv["Template"] = v.Template
|
||||
case *dns.SVCBOhttp:
|
||||
break
|
||||
case *dns.SVCBMandatory:
|
||||
keys := v.Code
|
||||
jsKeys := make([]uint16, len(keys))
|
||||
for i, _key := range keys {
|
||||
jsKeys[i] = uint16(_key)
|
||||
}
|
||||
jsKv["Code"] = jsKeys
|
||||
default:
|
||||
return nil, fmt.Errorf("error creating JSSVCBKeyValue: unknown key: %d", key)
|
||||
}
|
||||
|
||||
return jsKv, nil
|
||||
}
|
||||
|
||||
func ToSVCBKeyValue(jsKv map[string]interface{}) (dns.SVCBKeyValue, error) {
|
||||
var kv dns.SVCBKeyValue
|
||||
|
||||
key := dns.SVCBKey(jsPropToUint16(jsKv, "Key"))
|
||||
|
||||
switch key {
|
||||
case dns.SVCB_ALPN:
|
||||
kv = &dns.SVCBAlpn{
|
||||
Alpn: jsPropToStringArray(jsKv, "Value"),
|
||||
}
|
||||
case dns.SVCB_NO_DEFAULT_ALPN:
|
||||
kv = &dns.SVCBNoDefaultAlpn{}
|
||||
case dns.SVCB_ECHCONFIG:
|
||||
kv = &dns.SVCBECHConfig{
|
||||
ECH: []byte(jsPropToString(jsKv, "Value")),
|
||||
}
|
||||
case dns.SVCB_PORT:
|
||||
kv = &dns.SVCBPort{
|
||||
Port: jsPropToUint16(jsKv, "Value"),
|
||||
}
|
||||
case dns.SVCB_IPV4HINT:
|
||||
jsIps := jsPropToStringArray(jsKv, "Value")
|
||||
var ips []net.IP
|
||||
for _, jsIp := range jsIps {
|
||||
ip := net.ParseIP(jsIp)
|
||||
if ip == nil {
|
||||
log.Error("error converting to SVCBKeyValue: invalid IPv4Hint IP: %s", jsIp)
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
kv = &dns.SVCBIPv4Hint{
|
||||
Hint: ips,
|
||||
}
|
||||
case dns.SVCB_IPV6HINT:
|
||||
jsIps := jsPropToStringArray(jsKv, "Value")
|
||||
var ips []net.IP
|
||||
for _, jsIp := range jsIps {
|
||||
ip := net.ParseIP(jsIp)
|
||||
if ip == nil {
|
||||
log.Error("error converting to SVCBKeyValue: invalid IPv6Hint IP: %s", jsIp)
|
||||
continue
|
||||
}
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
kv = &dns.SVCBIPv6Hint{
|
||||
Hint: ips,
|
||||
}
|
||||
case dns.SVCB_DOHPATH:
|
||||
kv = &dns.SVCBDoHPath{
|
||||
Template: jsPropToString(jsKv, "Value"),
|
||||
}
|
||||
case dns.SVCB_OHTTP:
|
||||
kv = &dns.SVCBOhttp{}
|
||||
case dns.SVCB_MANDATORY:
|
||||
v := jsPropToUint16Array(jsKv, "Value")
|
||||
keys := make([]dns.SVCBKey, len(v))
|
||||
for i, jsKey := range v {
|
||||
keys[i] = dns.SVCBKey(jsKey)
|
||||
}
|
||||
kv = &dns.SVCBMandatory{
|
||||
Code: keys,
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("error converting to dns.SVCBKeyValue: unknown key: %d", key)
|
||||
}
|
||||
|
||||
return kv, nil
|
||||
}
|
||||
123
modules/dns_proxy/dns_proxy_script.go
Normal file
123
modules/dns_proxy/dns_proxy_script.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package dns_proxy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/plugin"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/robertkrimen/otto"
|
||||
)
|
||||
|
||||
type DnsProxyScript struct {
|
||||
*plugin.Plugin
|
||||
|
||||
doOnRequest bool
|
||||
doOnResponse bool
|
||||
doOnCommand bool
|
||||
}
|
||||
|
||||
func LoadDnsProxyScript(path string, sess *session.Session) (err error, s *DnsProxyScript) {
|
||||
log.Debug("loading proxy script %s ...", path)
|
||||
|
||||
plug, err := plugin.Load(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// define session pointer
|
||||
if err = plug.Set("env", sess.Env.Data); err != nil {
|
||||
log.Error("Error while defining environment: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// define addSessionEvent function
|
||||
err = plug.Set("addSessionEvent", func(call otto.FunctionCall) otto.Value {
|
||||
if len(call.ArgumentList) < 2 {
|
||||
log.Error("Failed to execute 'addSessionEvent' in DNS proxy: 2 arguments required, but only %d given.", len(call.ArgumentList))
|
||||
return otto.FalseValue()
|
||||
}
|
||||
ottoTag := call.Argument(0)
|
||||
if !ottoTag.IsString() {
|
||||
log.Error("Failed to execute 'addSessionEvent' in DNS proxy: first argument must be a string.")
|
||||
return otto.FalseValue()
|
||||
}
|
||||
tag := strings.TrimSpace(ottoTag.String())
|
||||
if tag == "" {
|
||||
log.Error("Failed to execute 'addSessionEvent' in DNS proxy: tag cannot be empty.")
|
||||
return otto.FalseValue()
|
||||
}
|
||||
data := call.Argument(1)
|
||||
sess.Events.Add(tag, data)
|
||||
return otto.TrueValue()
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Error while defining addSessionEvent function: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// run onLoad if defined
|
||||
if plug.HasFunc("onLoad") {
|
||||
if _, err = plug.Call("onLoad"); err != nil {
|
||||
log.Error("Error while executing onLoad callback: %s", "\nTraceback:\n "+err.(*otto.Error).String())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s = &DnsProxyScript{
|
||||
Plugin: plug,
|
||||
doOnRequest: plug.HasFunc("onRequest"),
|
||||
doOnResponse: plug.HasFunc("onResponse"),
|
||||
doOnCommand: plug.HasFunc("onCommand"),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *DnsProxyScript) OnRequest(req *dns.Msg, clientIP string) (jsreq, jsres *JSQuery) {
|
||||
if s.doOnRequest {
|
||||
jsreq := NewJSQuery(req, clientIP)
|
||||
jsres := NewJSQuery(req, clientIP)
|
||||
|
||||
if _, err := s.Call("onRequest", jsreq, jsres); err != nil {
|
||||
log.Error("%s", err)
|
||||
return nil, nil
|
||||
} else if jsreq.CheckIfModifiedAndUpdateHash() {
|
||||
return jsreq, nil
|
||||
} else if jsres.CheckIfModifiedAndUpdateHash() {
|
||||
return nil, jsres
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DnsProxyScript) OnResponse(req, res *dns.Msg, clientIP string) (jsreq, jsres *JSQuery) {
|
||||
if s.doOnResponse {
|
||||
jsreq := NewJSQuery(req, clientIP)
|
||||
jsres := NewJSQuery(res, clientIP)
|
||||
|
||||
if _, err := s.Call("onResponse", jsreq, jsres); err != nil {
|
||||
log.Error("%s", err)
|
||||
return nil, nil
|
||||
} else if jsres.CheckIfModifiedAndUpdateHash() {
|
||||
return nil, jsres
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *DnsProxyScript) OnCommand(cmd string) bool {
|
||||
if s.doOnCommand {
|
||||
if ret, err := s.Call("onCommand", cmd); err != nil {
|
||||
log.Error("Error while executing onCommand callback: %+v", err)
|
||||
return false
|
||||
} else if v, ok := ret.(bool); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -7,9 +7,10 @@ import (
|
|||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/bettercap/bettercap/log"
|
||||
"github.com/bettercap/bettercap/packets"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/log"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/packets"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
|
|
@ -100,7 +101,7 @@ func (mod *DNSSpoofer) Configure() error {
|
|||
|
||||
if mod.Running() {
|
||||
return session.ErrAlreadyStarted(mod.Name())
|
||||
} else if mod.Handle, err = pcap.OpenLive(mod.Session.Interface.Name(), 65536, true, pcap.BlockForever); err != nil {
|
||||
} else if mod.Handle, err = network.Capture(mod.Session.Interface.Name()); err != nil {
|
||||
return err
|
||||
} else if err = mod.Handle.SetBPFFilter("udp"); err != nil {
|
||||
return err
|
||||
|
|
@ -192,6 +193,13 @@ func DnsReply(s *session.Session, TTL uint32, pkt gopacket.Packet, peth *layers.
|
|||
|
||||
answers := make([]layers.DNSResourceRecord, 0)
|
||||
for _, q := range req.Questions {
|
||||
// do not include types we can't handle and that are not needed
|
||||
// for successful spoofing anyway
|
||||
// ref: https://github.com/bettercap/bettercap/issues/843
|
||||
if q.Type.String() == "Unknown" {
|
||||
continue
|
||||
}
|
||||
|
||||
answers = append(answers,
|
||||
layers.DNSResourceRecord{
|
||||
Name: []byte(q.Name),
|
||||
|
|
@ -231,7 +239,7 @@ func DnsReply(s *session.Session, TTL uint32, pkt gopacket.Packet, peth *layers.
|
|||
|
||||
err, raw = packets.Serialize(ð, &ip6, &udp, &dns)
|
||||
if err != nil {
|
||||
log.Error("error serializing packet: %s.", err)
|
||||
log.Error("error serializing ipv6 packet: %s.", err)
|
||||
return "", ""
|
||||
}
|
||||
} else {
|
||||
|
|
@ -252,7 +260,7 @@ func DnsReply(s *session.Session, TTL uint32, pkt gopacket.Packet, peth *layers.
|
|||
|
||||
err, raw = packets.Serialize(ð, &ip4, &udp, &dns)
|
||||
if err != nil {
|
||||
log.Error("error serializing packet: %s.", err)
|
||||
log.Error("error serializing ipv4 packet: %s.", err)
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/evilsocket/islazy/fs"
|
||||
"github.com/evilsocket/islazy/str"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package events_stream
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ package events_stream
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/bettercap/bettercap/modules/net_sniff"
|
||||
"github.com/bettercap/bettercap/modules/syn_scan"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/modules/net_sniff"
|
||||
"github.com/bettercap/bettercap/v2/modules/syn_scan"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
|
||||
|
|
@ -119,6 +120,10 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
|
|||
mod.viewBLEEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "hid.") {
|
||||
mod.viewHIDEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "can.") {
|
||||
mod.viewCANEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "gps.") {
|
||||
mod.viewGPSEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "mod.") {
|
||||
mod.viewModuleEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "net.sniff.") {
|
||||
|
|
@ -127,8 +132,12 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
|
|||
mod.viewSynScanEvent(output, e)
|
||||
} else if e.Tag == "update.available" {
|
||||
mod.viewUpdateEvent(output, e)
|
||||
} else {
|
||||
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
|
||||
} else if e.Tag == "gateway.change" {
|
||||
mod.viewGatewayEvent(output, e)
|
||||
} else if strings.HasPrefix(e.Tag, "zeroconf.") {
|
||||
mod.viewZeroConfEvent(output, e)
|
||||
} else if !strings.HasPrefix(e.Tag, "tick") && e.Tag != "session.started" && e.Tag != "session.stopped" {
|
||||
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e.Data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package events_stream
|
||||
|
|
@ -6,8 +7,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bettercap/bettercap/network"
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package events_stream
|
||||
|
||||
import (
|
||||
"github.com/bettercap/bettercap/session"
|
||||
"io"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
)
|
||||
|
||||
func (mod *EventsStream) viewBLEEvent(output io.Writer, e session.Event) {
|
||||
|
|
|
|||
98
modules/events_stream/events_view_can.go
Normal file
98
modules/events_stream/events_view_can.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package events_stream
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/modules/can"
|
||||
"github.com/bettercap/bettercap/v2/network"
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
func (mod *EventsStream) viewCANDeviceNew(output io.Writer, e session.Event) {
|
||||
dev := e.Data.(*network.CANDevice)
|
||||
fmt.Fprintf(output, "[%s] [%s] new CAN device %s (%s) detected.\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
tui.Bold(dev.Name),
|
||||
tui.Dim(dev.Description))
|
||||
}
|
||||
|
||||
func (mod *EventsStream) viewCANRawMessage(output io.Writer, e session.Event) {
|
||||
msg := e.Data.(can.Message)
|
||||
|
||||
fmt.Fprintf(output, "[%s] [%s] %s <0x%x> (%s): %s\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
tui.Dim("raw"),
|
||||
msg.Frame.ID,
|
||||
tui.Dim(humanize.Bytes(uint64(msg.Frame.Length))),
|
||||
hex.EncodeToString(msg.Frame.Data[:msg.Frame.Length]))
|
||||
}
|
||||
|
||||
func (mod *EventsStream) viewCANDBCMessage(output io.Writer, e session.Event) {
|
||||
msg := e.Data.(can.Message)
|
||||
src := ""
|
||||
if msg.Source != nil && msg.Source.Name != "" {
|
||||
src = fmt.Sprintf(" from %s", msg.Source.Name)
|
||||
}
|
||||
|
||||
fmt.Fprintf(output, "[%s] [%s] (dbc) <0x%x> %s (%s)%s:\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
msg.Frame.ID,
|
||||
msg.Name,
|
||||
tui.Dim(humanize.Bytes(uint64(msg.Frame.Length))),
|
||||
tui.Bold(src))
|
||||
|
||||
for name, value := range msg.Signals {
|
||||
fmt.Fprintf(output, " %s : %s\n", name, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (mod *EventsStream) viewCANOBDMessage(output io.Writer, e session.Event) {
|
||||
msg := e.Data.(can.Message)
|
||||
obd2 := msg.OBD2
|
||||
|
||||
if obd2.Type == can.OBD2MessageTypeRequest {
|
||||
fmt.Fprintf(output, "[%s] [%s] %s : %s > %s\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
tui.Yellow("obd2.request"),
|
||||
obd2.Service, obd2.PID)
|
||||
} else {
|
||||
|
||||
fmt.Fprintf(output, "[%s] [%s] %s : %s > %s > %s : 0x%x\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
tui.Yellow("obd2.response"),
|
||||
tui.Bold(msg.Source.Name),
|
||||
obd2.Service, obd2.PID,
|
||||
obd2.Data)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (mod *EventsStream) viewCANEvent(output io.Writer, e session.Event) {
|
||||
if e.Tag == "can.device.new" {
|
||||
mod.viewCANDeviceNew(output, e)
|
||||
} else if e.Tag == "can.message" {
|
||||
msg := e.Data.(can.Message)
|
||||
if msg.OBD2 != nil {
|
||||
// OBD-2 PID
|
||||
mod.viewCANOBDMessage(output, e)
|
||||
} else if msg.Name != "" {
|
||||
// parsed from DBC
|
||||
mod.viewCANDBCMessage(output, e)
|
||||
} else {
|
||||
// raw unparsed frame
|
||||
mod.viewCANRawMessage(output, e)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
|
||||
}
|
||||
}
|
||||
23
modules/events_stream/events_view_gateway.go
Normal file
23
modules/events_stream/events_view_gateway.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package events_stream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
func (mod *EventsStream) viewGatewayEvent(output io.Writer, e session.Event) {
|
||||
change := e.Data.(session.GatewayChange)
|
||||
|
||||
fmt.Fprintf(output, "[%s] [%s] %s gateway changed: '%s' (%s) -> '%s' (%s)\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Red(e.Tag),
|
||||
string(change.Type),
|
||||
change.Prev.IP,
|
||||
change.Prev.MAC,
|
||||
change.New.IP,
|
||||
change.New.MAC)
|
||||
}
|
||||
24
modules/events_stream/events_view_gps.go
Normal file
24
modules/events_stream/events_view_gps.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package events_stream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/bettercap/bettercap/v2/session"
|
||||
"github.com/evilsocket/islazy/tui"
|
||||
)
|
||||
|
||||
func (mod *EventsStream) viewGPSEvent(output io.Writer, e session.Event) {
|
||||
if e.Tag == "gps.new" {
|
||||
gps := e.Data.(session.GPS)
|
||||
|
||||
fmt.Fprintf(output, "[%s] [%s] latitude:%f longitude:%f quality:%s satellites:%d altitude:%f\n",
|
||||
e.Time.Format(mod.timeFormat),
|
||||
tui.Green(e.Tag),
|
||||
gps.Latitude,
|
||||
gps.Longitude,
|
||||
gps.FixQuality,
|
||||
gps.NumSatellites,
|
||||
gps.Altitude)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue