Compare commits

...

329 commits

Author SHA1 Message Date
evilsocket
4ec2753fad releasing version 2.41.4
Some checks failed
Build and Push Docker Images / docker (push) Has been cancelled
Linux tests / build (1.24.x, ubuntu-latest) (push) Has been cancelled
macOS tests / build (1.24.x, macos-latest) (push) Has been cancelled
Windows tests / build (1.24.x, windows-latest) (push) Has been cancelled
2025-08-18 19:15:44 +02:00
evilsocket
42da612113 hotfix: hotfix 2 for tcp.proxy 2025-08-18 19:14:05 +02:00
evilsocket
fc65cde728 releasing version 2.41.3 2025-08-18 17:08:42 +02:00
evilsocket
cc475ddfba hotfix: fixed tcp_proxy onData bug 2025-08-18 17:08:14 +02:00
evilsocket
cfc6d55462 misc: removed bogus test 2025-08-18 15:25:26 +02:00
evilsocket
ccf4fa09e2 releasing version 2.41.2 2025-08-18 15:10:45 +02:00
evilsocket
1e235181aa fix: fixed tcp.proxy onData return value bug (fixes #788) 2025-08-18 15:01:34 +02:00
Simone Margaritelli
453c417e92
Merge pull request #1218 from kkrypt0nn/master
Some checks failed
Build and Push Docker Images / docker (push) Has been cancelled
Linux tests / build (1.24.x, ubuntu-latest) (push) Has been cancelled
macOS tests / build (1.24.x, macos-latest) (push) Has been cancelled
Windows tests / build (1.24.x, windows-latest) (push) Has been cancelled
feat: Add default username and password for API
2025-08-09 13:48:07 +02:00
Krypton
d1925cd926
fix: Consistency between HTTP(S) servers 2025-08-08 18:46:06 +02:00
Krypton
d60d4612f2
feat: Add default username and password for API 2025-08-08 18:34:31 +02:00
Simone Margaritelli
8bd6052851
Merge pull request #1217 from bettercap/dependabot/github_actions/actions/download-artifact-5
Some checks are pending
Build and Push Docker Images / docker (push) Waiting to run
Linux tests / build (1.24.x, ubuntu-latest) (push) Waiting to run
macOS tests / build (1.24.x, macos-latest) (push) Waiting to run
Windows tests / build (1.24.x, windows-latest) (push) Waiting to run
build(deps): bump actions/download-artifact from 4 to 5
2025-08-08 16:06:45 +02:00
Simone Margaritelli
a23ba5fcba
Merge pull request #1210 from kkrypt0nn/master
fix: `OnPacket` proxy plugin callback signature check
2025-08-08 16:06:26 +02:00
dependabot[bot]
be76c0a7da
build(deps): bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 04:52:30 +00:00
Krypton
faee64a2c0 fix: Consistency and small typo 2025-07-16 21:01:15 +02:00
Krypton
0f68fcca8b fix: Small typo in ticker off description 2025-07-15 22:10:28 +02:00
Krypton
5a6a5fbbdf fix: Callback signature check 2025-07-15 21:19:00 +02:00
evilsocket
fa7e95c420 releasing version 2.41.1
Some checks failed
Build and Push Docker Images / docker (push) Has been cancelled
Linux tests / build (1.24.x, ubuntu-latest) (push) Has been cancelled
macOS tests / build (1.24.x, macos-latest) (push) Has been cancelled
Windows tests / build (1.24.x, windows-latest) (push) Has been cancelled
2025-07-15 11:52:58 +02:00
evilsocket
ad102afa2f misc: small fix or general refactoring i did not bother commenting 2025-07-15 11:20:54 +02:00
Simone Margaritelli
c154546fba
Merge pull request #1209 from kkrypt0nn/master
fix: Print event data, not whole struct on non-special events
2025-07-15 11:14:57 +02:00
buffermet
db1b386326
Merge pull request #1160 from buffermet/master
Some checks are pending
Build and Push Docker Images / docker (push) Waiting to run
Linux tests / build (1.24.x, ubuntu-latest) (push) Waiting to run
macOS tests / build (1.24.x, macos-latest) (push) Waiting to run
Windows tests / build (1.24.x, windows-latest) (push) Waiting to run
Begin implementing JavaScript Crypto API, add textEncode and textDecode bindings, improve parsing and error handling.
2025-07-15 00:44:49 +02:00
Krypton
183837e216 fix: Print event data, not whole struct 2025-07-14 21:46:35 +02:00
evilsocket
0216ea69f9 misc: small fix or general refactoring i did not bother commenting
Some checks failed
Build and Push Docker Images / docker (push) Has been cancelled
Linux tests / build (1.24.x, ubuntu-latest) (push) Has been cancelled
macOS tests / build (1.24.x, macos-latest) (push) Has been cancelled
Windows tests / build (1.24.x, windows-latest) (push) Has been cancelled
2025-07-12 16:04:06 +02:00
evilsocket
fecd81118d fix: various unit tests fixes for windows 2025-07-12 16:03:23 +02:00
evilsocket
61891e86a3 fix: routing tables unit tests fix for linux 2025-07-12 15:53:35 +02:00
evilsocket
0b64530cea new: increased unit tests coverage considerably 2025-07-12 15:48:20 +02:00
evilsocket
39d9254462 misc: added contributors to readme 2025-07-12 12:13:23 +02:00
evilsocket
ceb5ecd12f misc: small fix or general refactoring i did not bother commenting 2025-07-12 12:08:00 +02:00
evilsocket
47077d877c new: updated docker image to newer golang version 2025-07-12 12:06:53 +02:00
evilsocket
414d18a6da new: queue handle is not passed to the packet proxy plugins in order to be able to drop/accept packets from within the callback (fixes #1202) 2025-07-12 11:59:55 +02:00
Simone Margaritelli
da2292fbb7
Merge pull request #1205 from bettercap/dependabot/github_actions/actions/setup-go-5
build(deps): bump actions/setup-go from 2 to 5
2025-07-12 11:49:27 +02:00
Simone Margaritelli
b331be47d6
Merge pull request #1207 from bettercap/dependabot/github_actions/docker/build-push-action-6
build(deps): bump docker/build-push-action from 5 to 6
2025-07-12 11:49:12 +02:00
Simone Margaritelli
0865d5af52
Merge pull request #1208 from bettercap/dependabot/github_actions/actions/checkout-4
build(deps): bump actions/checkout from 2 to 4
2025-07-12 11:48:51 +02:00
evilsocket
1c78ffa7be fix: refactored deprecated ioutil calls to io equivalents 2025-07-12 11:47:43 +02:00
dependabot[bot]
58da4b6fce
build(deps): bump actions/setup-go from 2 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 09:44:32 +00:00
dependabot[bot]
159f065058
build(deps): bump actions/checkout from 2 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 09:44:00 +00:00
Simone Margaritelli
3440e9999a
Merge pull request #1136 from BoboTiG/fix-ci-release-artifacts
fix(ci): Store release assets to the GitHub release
2025-07-12 11:43:06 +02:00
dependabot[bot]
d28692eef6
build(deps): bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 09:42:59 +00:00
Simone Margaritelli
2317c28062
Merge pull request #1137 from BoboTiG/feat-dependabot
feat(ci): Add Dependabot to keep GitHub actions up-to-date
2025-07-12 11:42:17 +02:00
evilsocket
5069224e64 new: bumped all dependencies 2025-07-12 11:41:41 +02:00
evilsocket
0356082947 misc: version bump for golang.org/x/net 2025-07-12 11:40:26 +02:00
evilsocket
84acb9556e fix: removed unused module (ref #1201) 2025-07-12 11:39:07 +02:00
evilsocket
aa819862eb fix: do not show empty zeroconf fields
Some checks failed
Build and Push Docker Images / docker (push) Has been cancelled
Linux tests / build (1.22.x, ubuntu-latest) (push) Has been cancelled
macOS tests / build (1.22.x, macos-latest) (push) Has been cancelled
Windows tests / build (1.22.x, windows-latest) (push) Has been cancelled
2025-07-10 13:02:11 +02:00
evilsocket
fed98adffa fix: gracefully handle packets that would crash gopacket (fixes #1184) 2025-07-10 12:55:07 +02:00
Simone Margaritelli
948756208a
Merge pull request #1172 from spameier/fix-Makefile
fix: put GOFLAGS in correct order
2025-06-01 13:44:34 +02:00
Simone Margaritelli
4f51c57dd4
Merge pull request #1195 from bettercap/otto_http_getHeaders
Add req.GetHeaders and res.GetHeaders, reduce overhead.
2025-06-01 13:37:20 +02:00
buffermet
04ed02f420 Reduce overhead. 2025-04-09 12:39:33 +02:00
buffermet
a53d561ddd Add req.GetHeaders and res.GetHeaders, reduce overhead. 2025-04-09 11:26:56 +02:00
Simone Margaritelli
84846b11dc
Merge pull request #1186 from nmurilo/master
Code review of 6GHz stuff
2025-03-31 20:14:42 +02:00
evilsocket
9ebd958218 fix: do not reset wifi channels if set before wifi module start 2025-03-27 13:34:35 +01:00
evilsocket
3a360e4622 new: wifi module reports current channel in state 2025-03-27 13:18:21 +01:00
evilsocket
7a2ecb15f6 fix: fixed net.sniff stats output for local packets flag 2025-03-27 08:03:51 +01:00
evilsocket
69b3daa5b9 new: net.sniffer.interface parameter to sniff from a different interface 2025-03-27 07:47:30 +01:00
evilsocket
2662831fab fix: removing bash escape sequences from stdout before sending it as api response 2025-03-27 04:56:39 +01:00
buffermet
6ff2839e15
Try to restore issue template. 2025-03-20 19:02:17 +01:00
buffermet
1303b8e0d1
Try to restore issue template. 2025-03-20 18:56:49 +01:00
buffermet
2c157d2c5c
Try to restore issue template. 2025-03-20 18:55:26 +01:00
buffermet
3608e76fb6
Try to restore issue template. 2025-03-20 18:54:17 +01:00
buffermet
862d2c0825
Try to restore issue template. 2025-03-20 18:53:25 +01:00
evilsocket
cdf870dd4f misc: small fix or general refactoring i did not bother commenting 2025-03-15 00:18:00 +01:00
Nelson Murilo
93554a8448
Update wifi.go 2025-03-13 16:19:51 -04:00
Nelson Murilo
6d75d9e8e2
Added 6GHz stuff 2025-03-13 16:18:51 -04:00
Nelson Murilo
f9ab25aa8b
Update wifi.go 2025-03-13 15:33:09 -04:00
Nelson Murilo
dd05670e1f
Update net_linux.go
Code Review
2025-03-13 14:03:10 -04:00
Simone Margaritelli
4320b98e80
Merge pull request #1173 from danf42/fix-issue-1170
Update Dockerfile
2025-03-13 14:08:14 +01:00
Simone Margaritelli
fc02767e72
Merge pull request #1176 from bettercap/reduce_overhead
Reduce overhead for proxied HTTP/DNS packets
2025-03-13 14:07:43 +01:00
buffermet
0ea15563b1
Move issue template. 2025-03-03 00:38:02 +01:00
buffermet
e9fee2f2fa
Update config.yml 2025-03-03 00:35:50 +01:00
buffermet
99e7f78a22
Create issue template config 2025-03-03 00:34:46 +01:00
buffermet
84db5ed9bf
Merge pull request #1182 from bettercap/fix_issue_template
Fix issue template
2025-03-03 00:30:24 +01:00
buffermet
fd1f3bc1d2
Delete deprecated issue template. 2025-03-02 23:29:15 +01:00
buffermet
053ca5be55
Create issue_template.md 2025-03-02 23:28:38 +01:00
buffermet
2c6f048cec
Merge pull request #1156 from bettercap/otto_onExit
Implement onExit otto function calls when quitting the session or modules.
2025-03-01 23:12:51 +01:00
buffermet
890b83501c
Merge pull request #1180 from bettercap/dns_proxy_fix
Fix JavaScript backwards compatible number conversion and EDNS0 record binding
2025-02-22 23:40:37 +01:00
buffermet
f8884da78c
Remove unused var 2025-02-22 19:48:34 +01:00
buffermet
0b6fade8fd
Remove unused var 2025-02-22 19:46:58 +01:00
buffermet
df91176308
Fix JavaScript backwards compatible number conversion 2025-02-22 19:33:29 +01:00
buffermet
5da2cd8d29
Fix JavaScript backwards compatible number conversion 2025-02-22 19:26:44 +01:00
buffermet
4eb923f972
Fix float64/int64 to uint64 conversion from JS environment 2025-02-16 13:53:54 +01:00
buffermet
876449e105
Fix backwards compatible uint64 conversion 2025-02-15 14:37:16 +01:00
buffermet
f3001aa565
misc 2025-02-15 11:57:08 +01:00
buffermet
1c657fdf53
Update http_proxy_script.go 2025-02-13 21:32:52 +01:00
buffermet
25c6339275
Update http_proxy_js_response.go 2025-02-13 21:32:26 +01:00
buffermet
5e97fbb6eb
Update http_proxy_js_request.go 2025-02-13 21:30:46 +01:00
buffermet
12556bc6be
Update dns_proxy_script.go 2025-02-13 21:29:57 +01:00
buffermet
c8c1072cc0
Update dns_proxy_js_query.go 2025-02-13 21:28:58 +01:00
buffermet
086eed49d5
Merge pull request #1175 from bettercap/dns_proxy_fix
Fix number to uint conversion in DNS proxy.
2025-02-13 21:23:00 +01:00
buffermet
d03d778e46
Fix number to uint conversion in DNS proxy. 2025-02-13 00:20:28 +01:00
Dan
0ea1dec113
Update Dockerfile
Add iw to docker image
2025-02-11 08:39:00 -05:00
spameier
63ff51efdf fix: put GOFLAGS in correct order 2025-02-08 10:39:23 +01:00
evilsocket
8eedf6d90c releasing version 2.41.0 2025-01-31 12:27:10 +01:00
evilsocket
61a9069e50 new: api.rest will return the stdout data after executing a session command 2025-01-09 02:55:51 +01:00
☸️
1d7a49a952
misc 2024-12-06 17:09:27 +01:00
☸️
243d3e7016
Fix error messages. 2024-12-05 15:33:04 +01:00
☸️
30257fd547
misc 2024-12-05 13:43:48 +01:00
buffermet
9ed0fadd24 Begin implementing JavaScript Crypto API, add basic Uint8Array methods. 2024-12-05 13:11:48 +01:00
☸️
3e8063c2c7
misc 2024-12-05 12:52:27 +01:00
☸️
3cea30a277
Improve parsing and error handling in js bindings. 2024-12-05 12:49:54 +01:00
☸️
fdca49678e
Implement DNS proxy script onExit call. 2024-11-23 16:02:28 +01:00
☸️
91f5213526
Implement HTTP proxy script onEvent call. 2024-11-23 15:56:54 +01:00
☸️
159aed5080
Implement session script onExit call. 2024-11-23 15:49:05 +01:00
Simone Margaritelli
c4e45b368d
Merge pull request #1150 from Sniffleupagus/patch-1
Improve backwards compatibility with getHandshakeFileFor
2024-11-20 14:32:39 +01:00
evilsocket
169b0cb8c9 new: added removeEventListener builtin function (closes #1139) 2024-11-20 14:30:17 +01:00
☸️
a7e4572416
Merge pull request #1153 from bettercap/1152-fix
Add IPv6 nil check for interface.
2024-11-16 18:07:57 +01:00
☸️
01a144d69b
Add IPv6 nil check for interface. 2024-11-16 10:19:30 +01:00
Sniffleupagus
cb5f7679d8
Improve backwards compatibility with getHandshakeFileFor
The getHandshakeFile function was using "path.Dir(shakesFileName)", which drops the last element of the path.  This is not backwards compatible with prior versions that used the variable as the dir name. In particular this change causes pwnagotchi to store handshakes in /root instead of /root/handshakes.
This commit checks for an existing directory at shakesFileName and will use that as the path instead of taking the parent directory of the path.
2024-11-13 11:33:25 -08:00
Simone Margaritelli
00854261a4
Merge pull request #1145 from bettercap/otto-session-events
Implement addSessionEvent function in HTTP proxy script env
2024-11-13 11:17:26 +01:00
Simone Margaritelli
906162e5fa
Merge pull request #1143 from buffermet/master
dns.proxy module
2024-11-13 11:17:03 +01:00
☸️
08e248e38c
Implement addSessionEvent function in DNS proxy script env 2024-10-23 13:30:01 +02:00
☸️
6d242022fb
Implement addSessionEvent function in HTTP proxy script env 2024-10-22 23:11:42 +02:00
buffermet
6de6de7418 Allow wildcard in blacklist. 2024-10-12 22:05:29 +02:00
buffermet
ccb2774814 Shrink code. 2024-10-12 21:50:26 +02:00
buffermet
40f3906115 Add blacklist and whitelist logic. 2024-10-12 21:47:02 +02:00
buffermet
27d245625c Remove redundant nil assignment. 2024-10-12 17:38:49 +02:00
buffermet
32995aada3 Merge branch 'master' of https://github.com/buffermet/bettercap 2024-10-12 17:35:08 +02:00
buffermet
fe9481cb42 Print JS property conversion errors. 2024-10-12 17:34:14 +02:00
☸️
d0d1029a5a
Merge branch 'bettercap:master' into master 2024-10-12 13:49:13 +02:00
buffermet
c5017ed020 Support RFC3597 generic/unknown record type if Rdata field is present. 2024-10-12 13:45:27 +02:00
evilsocket
88d813543a merge 2024-10-10 14:34:20 +02:00
evilsocket
55edafb33c new: implemented named tickers (ref #779) 2024-10-10 14:33:52 +02:00
buffermet
c5d93825bd Catch RR nil value, improve debug logs readability. 2024-10-09 20:07:22 +02:00
buffermet
43f1013f0d Add TLS support for DNS proxy, implement backwards compatible DNS record conversion. 2024-10-09 13:47:21 +02:00
buffermet
a49d561dce init dns.proxy 2024-10-04 03:00:54 +02:00
evilsocket
e190737c91 new: added known services descriptions from IANA 2024-10-01 14:46:59 +02:00
evilsocket
30c4c320a6 misc: more compact zerogod.show 2024-10-01 14:20:14 +02:00
evilsocket
7e1cb69071 misc 2024-10-01 00:37:54 +02:00
evilsocket
02871b0ae6 new: _http and _https zeroconf acceptors 2024-09-30 19:06:10 +02:00
Simone Margaritelli
ef69151a7f chore: added credits to grandcat package 2024-09-27 20:40:48 +02:00
Simone Margaritelli
ae466b702a fix: directly embedding ui assets (fixes #1135) 2024-09-27 20:23:57 +02:00
Simone Margaritelli
ea8e96c285 chore: removed ui submodule (ref #1135) 2024-09-27 20:18:18 +02:00
Mickaël Schoentgen
520592d1a5 feat(ci): Add Dependabot to keep GitHub actions up-to-date 2024-09-27 19:50:20 +02:00
Mickaël Schoentgen
3b4cdc60cb fix(ci): Store release assets to the GitHub release
Those changes fix several issues.

First, artifacts were not stored between jobs, so when publishing release assets, nothing was found.
It explains why the latest GitHub release assets list contains only ZIP'ed sources.

Secondly, the workflow matrix was not working as expected: for instance, Linux AMD64 was run alone
while both AMD64 and ARM64 were expected.

Thirdly, even if the Linux matrix is fixed, there is no official GitHub runner for ARM64 yet.
so this is disabled by default for now (I wanted to propose changes about the workflow, not to
fix all issues at once).
2024-09-27 19:40:46 +02:00
Simone Margaritelli
ba29bea0cd misc: small fix or general refactoring i did not bother commenting 2024-09-22 19:25:24 +02:00
Simone Margaritelli
209725d623 misc: small fix or general refactoring i did not bother commenting 2024-09-22 17:40:23 +02:00
Simone Margaritelli
d2f13a3293 misc: small fix or general refactoring i did not bother commenting 2024-09-22 15:34:37 +02:00
Simone Margaritelli
fabf3bb8e9 fix: prioritize longer and more explicit host names 2024-09-22 15:21:11 +02:00
Simone Margaritelli
5969acd55d misc: small fix or general refactoring i did not bother commenting 2024-09-22 15:05:43 +02:00
Simone Margaritelli
bd959586c5 misc: small fix or general refactoring i did not bother commenting 2024-09-22 15:03:10 +02:00
Simone Margaritelli
a234c20650 fix: better ipv6 detection logic 2024-09-22 15:03:06 +02:00
Simone Margaritelli
8446d66d12 misc: small fix or general refactoring i did not bother commenting 2024-09-22 14:00:39 +02:00
Simone Margaritelli
5652d15426 misc: small fix or general refactoring i did not bother commenting 2024-09-22 13:23:30 +02:00
Simone Margaritelli
7b4fc3d31d misc: small fix or general refactoring i did not bother commenting 2024-09-21 17:52:50 +02:00
Simone Margaritelli
26c532316a misc: small fix or general refactoring i did not bother commenting 2024-09-21 17:38:52 +02:00
Simone Margaritelli
2966153adf misc: small fix or general refactoring i did not bother commenting 2024-09-21 11:56:14 +02:00
Simone Margaritelli
76e094f687 misc: small fix or general refactoring i did not bother commenting 2024-09-20 15:48:20 +02:00
Simone Margaritelli
6af2de6de9 misc: small fix or general refactoring i did not bother commenting 2024-09-20 15:30:13 +02:00
Simone Margaritelli
17ba1be16c misc: small fix or general refactoring i did not bother commenting 2024-09-20 12:06:32 +02:00
Simone Margaritelli
b0a197b377 external resolver 2024-09-20 10:29:45 +02:00
Simone Margaritelli
e656a6cbfa misc: small fix or general refactoring i did not bother commenting 2024-09-19 22:24:45 +02:00
Simone Margaritelli
51a5b4ad6e misc: small fix or general refactoring i did not bother commenting 2024-09-19 21:49:02 +02:00
Simone Margaritelli
91d360327a it works! 2024-09-19 16:33:44 +02:00
Simone Margaritelli
67cc9680ed progress 2024-09-18 23:21:30 +02:00
Simone Margaritelli
756dc3d71a
Update README.md 2024-09-17 11:32:37 +02:00
Simone Margaritelli
531da20048 releasing version 2.40.0 2024-09-14 23:36:05 +02:00
Simone Margaritelli
acda32e304 releasing version 2.4.0 2024-09-13 13:22:41 +02:00
Simone Margaritelli
75478a21f6 misc: small fix or general refactoring i did not bother commenting 2024-09-13 12:43:27 +02:00
Simone Margaritelli
5bc9dd9259 fix: added better debug logging for core.Exec (fixes #1125) 2024-09-13 12:27:22 +02:00
Simone Margaritelli
97b4dcb46e new: added support for 29bit obd2 identifiers 2024-09-01 13:30:01 +02:00
Simone Margaritelli
c3999d6bb5 new: implemented can.obd2 builtin parser 2024-08-31 14:01:40 +02:00
evilsocket
cf6fba6151 chore: updated ui version 2024-08-29 11:16:12 +02:00
Simone Margaritelli
3775295a2c chore: updated ui submodule 2024-08-28 16:14:27 +02:00
Simone Margaritelli
b2035daf54 fix: fixed address reload on api.rest 2024-08-28 16:14:23 +02:00
Simone Margaritelli
00c5b2c9c6 chore: updated ui submodule 2024-08-28 16:00:26 +02:00
Simone Margaritelli
b1ac9cda7d chore: updated ui submodule 2024-08-28 12:11:17 +02:00
Simone Margaritelli
5786ffdaa9 chore: updated ui submodule 2024-08-28 11:40:38 +02:00
Simone Margaritelli
72afa07d28 new: can.fuzz now supports an optional size argument (thanks musafir) 2024-08-27 10:28:30 +02:00
Simone Margaritelli
1c56622cde fix: can.fuzz now expects an hexadecimal frame id (thanks musafir) 2024-08-27 09:25:40 +02:00
Simone Margaritelli
4c7599566c fix: stop can.dump reader when can.recon is stopped 2024-08-26 18:58:11 +02:00
Simone Margaritelli
c4c7b8c43d chore: updated ui submodule 2024-08-26 17:12:00 +02:00
Simone Margaritelli
bb847fcf8a new: can.dump reader will now sleep for the correct amount of time 2024-08-26 16:37:06 +02:00
Simone Margaritelli
7702207ee9 new: implemented can.dup and can.dump.inject to read a candump log file 2024-08-26 15:12:09 +02:00
Simone Margaritelli
840f819484 refact: refactored can dbc logic 2024-08-23 16:03:35 +02:00
Simone Margaritelli
31d93e7c39 new: added new wifi cipher suites and auth types 2024-08-23 10:53:17 +02:00
Simone Margaritelli
f0126c28fb new: added new wifi RSN parsing 2024-08-23 10:39:58 +02:00
Simone Margaritelli
26b2c300b8 fix: fixed a nil pointer dereference when wifi.show is executed before wifi.recon on 2024-08-23 10:27:08 +02:00
Simone Margaritelli
575022fac4 fix: fixed deprecation warning on macOS native code 2024-08-23 10:21:50 +02:00
Simone Margaritelli
a4c99df51e chore: updated ui 2024-08-22 11:01:13 +02:00
Simone Margaritelli
81e18d20b7 chore: updated ui 2024-08-22 10:42:56 +02:00
Simone Margaritelli
81adcc96e6 fix: fixed handshakes filename if wifi.aggregate is false 2024-08-22 10:28:42 +02:00
Simone Margaritelli
7d85483214 fix: expanding file path in file read api 2024-08-22 10:28:28 +02:00
Simone Margaritelli
ac2d333609 fix: initialize wifi module state correctly 2024-08-22 09:50:55 +02:00
Simone Margaritelli
9f61ec7f13 chore: fixed submodule url for github workflow 2024-08-21 17:42:54 +02:00
Simone Margaritelli
f3132cee34 chore: updated git workflows to init submodules 2024-08-21 17:36:52 +02:00
Simone Margaritelli
d8aeecb99f new: embedded ui 2024-08-21 17:33:47 +02:00
Simone Margaritelli
3ec7b01bed new: added CAN to session json object 2024-08-21 15:47:27 +02:00
Simone Margaritelli
0202028524 fix: do not allow wifi.recon if wifi.bruteforce is running 2024-08-19 15:27:53 +02:00
Simone Margaritelli
ef9a3ef85b misc: small fix or general refactoring i did not bother commenting 2024-08-18 15:45:35 +02:00
Simone Margaritelli
5cc7260ca9 misc: small fix or general refactoring i did not bother commenting 2024-08-18 15:42:38 +02:00
Simone Margaritelli
77ae56cc62 fix: added p2p_disabled=1 for wifi.bruteforce on linux (ref #1075) 2024-08-18 15:39:14 +02:00
Simone Margaritelli
1b91eb348b new: implemented wifi.bruteforce for linux (closes #1075) 2024-08-18 15:34:32 +02:00
Simone Margaritelli
d8e11287c6 fix: bring interface down for mac.changer module 2024-08-18 15:34:02 +02:00
Simone Margaritelli
08da91ed5c new: implemented wifi.bruteforce for darwin (ref #1075) 2024-08-18 13:44:12 +02:00
Simone Margaritelli
b0d56e4f5e fix: do not attempt mac lookup if gateway is empty 2024-08-18 13:43:43 +02:00
Simone Margaritelli
23e074b686 fix: do not report a routing error if the interface is disconnected 2024-08-18 13:43:19 +02:00
Simone Margaritelli
2d03782fe1 fix: make sure that wifi channels are unique and sorted 2024-08-17 13:37:48 +02:00
Simone Margaritelli
8d8af63577 chore: added WPA3 to readme 2024-08-17 13:14:08 +02:00
Simone Margaritelli
6a6e942ea4 chore: added github to funding 2024-08-17 12:44:39 +02:00
Simone Margaritelli
0ceb938f10 fix: WPA3 is now correcly identified and reported (fixes #1098) 2024-08-17 12:40:40 +02:00
Simone Margaritelli
6282fe3451 new: ble, can, hid and wifi modules will now set a custom prompt (closes #1117) 2024-08-17 12:10:38 +02:00
Simone Margaritelli
d9a91d393e new: implemented can.filter 2024-08-17 11:38:58 +02:00
Simone Margaritelli
e45c9cc053 docs: added can-bus to readme 2024-08-16 17:34:54 +02:00
Simone Margaritelli
cc66b6459f chore: removed unused files 2024-08-16 17:06:27 +02:00
Simone Margaritelli
c5d20220a1 fix: fixed github action macOS architecture and Windows build script 2024-08-16 16:59:24 +02:00
Simone Margaritelli
d733381322 misc: small fix or general refactoring i did not bother commenting 2024-08-16 16:54:00 +02:00
Simone Margaritelli
e1e8a0b78d fix: fixed windows build via msys2/setup-msys2@v2 2024-08-16 16:53:11 +02:00
Simone Margaritelli
9266ee942f misc: small fix or general refactoring i did not bother commenting 2024-08-16 16:48:46 +02:00
Simone Margaritelli
13cab7b637 fix: fixed binaries building action 2024-08-16 16:36:21 +02:00
Simone Margaritelli
f6192653ef misc: small fix or general refactoring i did not bother commenting 2024-08-16 16:27:10 +02:00
Simone Margaritelli
72b14502c3 fix: setting PKG_CONFIG_PATH on windows builds 2024-08-16 16:16:50 +02:00
Simone Margaritelli
235017c294 chore: removed armhf build 2024-08-16 16:00:11 +02:00
Simone Margaritelli
86e87ab656 fix: attempt to fix tests on windows 2024-08-16 15:59:54 +02:00
Simone Margaritelli
cdefa3c9d3 fix: attempt to fix tests on windows 2024-08-16 15:53:42 +02:00
Simone Margaritelli
ee944d9640 fix: attempt to fix tests on windows 2024-08-16 15:47:00 +02:00
Simone Margaritelli
fcf285aabb chore: more veborse errors in environment tests 2024-08-16 15:33:48 +02:00
Simone Margaritelli
7f22425dcc fix: fixed compilation on windows 2024-08-16 15:30:19 +02:00
Simone Margaritelli
1df51fd13a fix: fixed windows test failure in environment_test.go 2024-08-16 15:20:18 +02:00
Simone Margaritelli
afdc68f512 chore: refactored github workflows into separate files 2024-08-16 15:17:42 +02:00
Simone Margaritelli
9ab2e13f31 chore: refactored github workflows into separate files 2024-08-16 15:12:17 +02:00
Simone Margaritelli
6f1920f478 new: can.fuzz command 2024-08-16 13:30:26 +02:00
Simone Margaritelli
69744e6b63 fix: disable ble module for BSD (fixes #1115) 2024-08-09 18:56:41 +02:00
Simone Margaritelli
7636ca2808 new: gps.set to manually set/override gps coordinates (closes #915) 2024-08-09 18:42:08 +02:00
Simone Margaritelli
9d5c38c693 fix: fixed verbose gousb logging (fixes #969) 2024-08-09 18:28:01 +02:00
Simone Margaritelli
2659a559c9 fix: using proper v2 package suffix (fixes #727) 2024-08-09 18:19:21 +02:00
evilsocket
76e136a18e fix: fixed device index use for BLE module (fixes #994) 2024-08-09 18:00:08 +02:00
evilsocket
93de427f9a new: history file location can now be set via BETTERCAP_HISTORY env var (closes #627) 2024-08-09 17:27:06 +02:00
evilsocket
9e7fda751a fix: added packet_proxy_freebsd (fixes #1067) 2024-08-09 17:17:53 +02:00
evilsocket
fd05df613e new: implemented can.inject 2024-08-09 16:19:35 +02:00
evilsocket
5fe3ef3d52 new: new can module for CAN-bus 2024-08-09 15:42:03 +02:00
Simone Margaritelli
9937e797ae releasing version 2.33.0 2024-08-09 11:25:32 +02:00
Simone Margaritelli
780032b116 misc: small fix or general refactoring i did not bother commenting 2024-08-09 11:23:31 +02:00
Simone Margaritelli
41fa4cd850 new: using simpler release file 2024-08-09 11:22:08 +02:00
Simone Margaritelli
107c8fdf99 fix: fixed docker build 2024-08-09 11:14:36 +02:00
Simone Margaritelli
856c0d5a7d misc: small fix or general refactoring i did not bother commenting 2024-08-08 18:24:25 +02:00
Simone Margaritelli
0343e002a9 new: docker workflow 2024-08-08 18:23:41 +02:00
Simone Margaritelli
d68da2108d fix: dockerfile fixes 2024-08-08 18:17:32 +02:00
Simone Margaritelli
7605f4afa3 fix: replaced nfqueue package (fixes #1070) 2024-08-08 18:06:23 +02:00
Simone Margaritelli
dc621f5934 misc: small fix or general refactoring i did not bother commenting 2024-08-08 17:05:53 +02:00
Simone Margaritelli
3e16c6dad0 new: replaced travis with github actions (closes #1114) 2024-08-08 17:04:08 +02:00
Simone Margaritelli
dd71378ce7 misc: small fix or general refactoring i did not bother commenting 2024-08-08 16:57:54 +02:00
Simone Margaritelli
5d2c173d5e misc: small fix or general refactoring i did not bother commenting 2024-08-08 16:54:29 +02:00
Simone Margaritelli
632d703087 new: added github workflows (ref #1114) 2024-08-08 16:52:14 +02:00
Simone Margaritelli
8b24723c18 fix: added cover.out to .gitignore 2024-08-08 16:44:10 +02:00
Simone Margaritelli
9abf7c809a fix: if interface name has not been provided, avoid default to a tun interface 2024-08-08 16:42:52 +02:00
Simone Margaritelli
7beb27cfca fix: updated gatt library to include fix for #1095 2024-08-08 14:06:59 +02:00
Simone Margaritelli
b12ba7947b fix: workaround for PCAP_SET_RFMON issue (fixes #819, https://github.com/the-tcpdump-group/libpcap/issues/1041, https://github.com/the-tcpdump-group/libpcap/issues/1033) 2024-08-08 13:40:45 +02:00
Simone Margaritelli
06623ddfb9
Merge pull request #1085 from SkyperTHC/master
sslstrip fix & don't restore iptables/ip_forward on exit when bettercap did not change them.
2024-08-08 13:02:15 +02:00
Simone Margaritelli
2499d5147f fix: every wifi frame injection operation on macOS will print an error (ref #448) 2024-08-08 12:59:21 +02:00
Simone Margaritelli
5858743b6e new: updated mac vendor lookup with IEEE datasets 2024-08-08 12:47:59 +02:00
Simone Margaritelli
0dc5f66e27 fix: fixed wifi support on macOS (fixes pr #1100, fixes #1090, fixes #1076) 2024-08-08 12:10:28 +02:00
Simone Margaritelli
02fa241d06 misc: small fix or general refactoring i did not bother commenting 2024-08-07 22:08:50 +02:00
Simone Margaritelli
c2ab5f4756 reverted pr #1100 due to instability 2024-08-07 22:03:11 +02:00
Simone Margaritelli
7371a85828
Merge pull request #1107 from stefanb/go1.23-support
Bump golang.org/x/net for Go 1.23 compatibility
2024-08-07 21:51:28 +02:00
Simone Margaritelli
9b6694f565 mereg 2024-08-07 18:00:39 +02:00
Simone Margaritelli
6951fbb8dd fix: fixes cgo newline 2024-08-07 17:59:58 +02:00
Simone Margaritelli
9bf1474615
Merge pull request #1074 from konradh/ndp-spoof-unset-neighbour
Fix: Allow clearing ndp.spoof.neighbour to disable neighbor advertisements
2024-08-07 16:35:22 +02:00
Simone Margaritelli
9cd1609306
Merge pull request #1073 from konradh/ndp-spoof-router-lifetime
Add ndp.spoof.router_lifetime option
2024-08-07 16:31:29 +02:00
Simone Margaritelli
3df89fb7e5
Merge pull request #1100 from loks0n/fix-macos
fix: macos
2024-07-24 13:46:45 +02:00
Simone Margaritelli
74647db825
Update README.md 2024-07-13 16:42:20 +02:00
Štefan Baebler
474215ebd9
Bump golang.org/x/net for Go 1.23 compatibility
Fixes https://github.com/bettercap/bettercap/issues/1106
2024-06-30 14:40:03 +02:00
Simone Margaritelli
826f13e47a new: pushed new graph experimental module 2024-05-31 14:07:19 +02:00
Simone Margaritelli
ca2e257fbb
Merge pull request #1087 from testwill/file_close
fix: close cpu profile
2024-05-31 13:55:49 +02:00
Luke B. Silver
71822229a0 fix: macos 2024-05-24 22:04:46 +00:00
guoguangwu
043bd4593b fix: close cpu profile
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-04-03 13:51:10 +08:00
Root THC
a950d3b767 sslstrip fix 2024-03-29 17:40:16 +01:00
konradh
5af1be3356 fix: allow clearing ndp.spoof.neighbour to disable neighbor advertisements 2024-01-24 20:33:54 +01:00
konradh
4dc7bae48c new: new ndp.spoof.router_lifetime option 2024-01-24 20:26:43 +01:00
Simone Margaritelli
924ff5753d
Merge pull request #1005 from elleuc4/route_headings_fix
Skip line processing if routing headings weren't found yet.
2024-01-07 12:47:06 +01:00
Simone Margaritelli
ee35550f70
Merge pull request #1008 from jansramek/master
Fix getting of possible channels (darwin) #998
2023-07-25 14:35:25 +02:00
Simone Margaritelli
76a7820da5
Merge pull request #1024 from half-duplex/ndp-ban
ndp.spoof: add ndp.ban
2023-07-25 14:34:47 +02:00
Simone Margaritelli
bdc389eaee
Merge pull request #1023 from half-duplex/ndp-spoof-ip-fmt
ndp.spoof: fix "couldn't get MAC" format string
2023-07-25 14:34:17 +02:00
Simone Margaritelli
32d997ea5a
Merge pull request #1036 from ttdennis/master
Fix BLE name assignment
2023-07-25 14:33:56 +02:00
Dennis Heinze
786dacf8ca
Fix BLE name assignment 2023-06-07 12:06:13 +02:00
Trevor Bergeron
cdd483e698
ndp.spoof: add ndp.ban 2023-03-15 10:14:04 -04:00
Trevor Bergeron
27bae1cd3b
ndp.spoof: fix format string 2023-03-15 10:04:05 -04:00
Simone Margaritelli
e5f8c168c3 fix: enable both ipv4 and ipv6 forwarding 2023-02-01 18:32:31 +01:00
Simone Margaritelli
58ca59bc6f new: switching to github.com/stratoberry/go-gpsd (closes #938) 2023-01-16 13:37:35 +01:00
Jan Šrámek
8e4a00091e Update getting of possible channels (darwin)
- added regex to be able to parse system_profiler format of MacOS 13+
2022-12-29 19:43:54 +01:00
Luca
44e24204e5 Skip line if no route headings found yet 2022-12-26 01:31:56 +01:00
Simone Margaritelli
24a5dfe48f
Merge pull request #988 from tranzmatt/master
Fix go/alpine Docker version to correct build breakage
2022-11-17 13:07:05 +01:00
Matthew Clark
3f154cccd9 Fix go/alpine Docker version to correct build breakage 2022-10-11 11:50:06 -04:00
Simone Margaritelli
e224eea8c6 fix: give priority to iwlist as iw gives unsupported frequencies (fixes #881) 2022-06-13 17:08:26 +02:00
Simone Margaritelli
4f3f55f648 fix: fs related scripting functions will now resolve paths containing special characters 2022-06-13 16:55:41 +02:00
Simone Margaritelli
a4fb94ce68 Merge branch 'master' of github.com:bettercap/bettercap 2022-06-10 23:39:05 +02:00
Simone Margaritelli
11d96069ae new: added fileExists function to the session scripting runtime 2022-06-10 23:38:48 +02:00
Simone Margaritelli
c81db63a10
Merge pull request #956 from BenGardiner/ipv6_anyproxy_fixes
Ipv6 any.proxy fixes
2022-06-10 23:00:28 +02:00
Ben Gardiner
eaf2b96407
revert removal of default any.proxy.dst_address ipv4 address 2022-06-10 16:59:11 -04:00
Simone Margaritelli
28371084d3 new: added saveJSON function to the session scripting runtime 2022-06-10 22:39:29 +02:00
Simone Margaritelli
eff8135d99 new: added loadJSON function to the session scripting runtime 2022-06-10 22:27:13 +02:00
Simone Margaritelli
22de9d3d4f new: added session.stopped event (and fixed session.started event propagation) 2022-06-10 22:12:45 +02:00
Simone Margaritelli
fd160bf7ca
Merge pull request #953 from firefart/err
do not swallow err on settxpower
2022-06-10 20:55:17 +02:00
Ben Gardiner
628c0b79fb ndp.spoof: use validator for neighbour parameter, print targets on start, complain when a MAC can't be found (UDP thing doesn't always work) 2022-04-25 21:38:23 +00:00
Ben Gardiner
2bc3d871ef use ip6tables for any_proxy to ipv6 addresses 2022-04-25 21:36:03 +00:00
firefart
68924c34c4 do not swallow err on settxpower 2022-04-23 22:43:27 +02:00
Simone Margaritelli
ed4239fad5
Merge pull request #913 from PeterDaveHello/ImproveDockerfile
Remove `--update` and replace with `--no-cache` for `apk` in Dockerfile
2021-11-29 10:43:05 +01:00
Simone Margaritelli
b9a546ec9d
Merge pull request #923 from xrayid/master
fix: exclude disabled channels
2021-11-29 10:42:45 +01:00
Mikhail Markov
0193d13ca0
fix: exclude disabled channels 2021-10-28 01:06:03 +03:00
☸️
a20fb139f5
parse every IPv4 and IPv6 route 2021-09-21 05:38:37 +10:00
☸️
3bd813f545
fix: parse interface names in IPv6 routing tables 2021-09-21 05:08:01 +10:00
☸️
ac96bc8d2f
fix IPv6 routing table parsing 2021-09-21 00:40:55 +10:00
☸️
5389228034
also support 'Netif' reference in certain netstat builds 2021-09-20 20:03:46 +10:00
☸️
c6740a5750
fix automatic gateway detection for Linux 2021-09-20 19:18:43 +10:00
☸️
aba29e03f6
fix: revert back to parsing IPv4 address blocks in net.FindInterface 2021-09-20 17:12:12 +10:00
☸️
e255eba69f
simplify interface IPv4 address parsing 2021-09-20 16:52:53 +10:00
☸️
44a17602ed
fix: adopt new IPv4 parsing logic 2021-09-20 15:42:07 +10:00
☸️
eee94e993c
simplify IPv4 validators 2021-09-20 14:20:44 +10:00
☸️
7fd9d18625
fix macParser, MACValidator and IPv4Validator regexp selectors + add IPv4BlockValidator
This commit fixes the `network.macParser` and `network.MACValidator` regexp selectors which could validate invalid MAC addresses (e.g. `a🅱️c:d:e:f`).

It also introduces a new `IPv4BlockValidator` regexp selector which allows us to distinguish IPv4 address blocks from single IPv4 addresses, as the previous implementation of `IPv4Validator` would also validate IPv4 address blocks (e.g. `10.0.0.0/8`) whilst only being used to validate single IPv4 addresses.

Both IPv4RangeValidator and IPv4Validator were optimized to only match the IPv4 format.

### Validation examples

- `IPv4BlockValidator` validates `10.0.0.0/8`
- `IPv4RangeValidator` validates `10.0-255.0-255.0-255`
- `IPv4Validator` validates `10.0.0.0`
2021-09-20 14:04:21 +10:00
Peter Dave Hello
e81f36c582 Remove --update and replace with --no-cache for apk in Dockerfile
There is no need to use `--update` with `--no-cache`, only `--no-cache`
should be used when building a Docker image, as using `--no-cache` will
fetch the index every time and leave no local cache, so the index will
always be the latest and without temporary files remains in the image.
2021-09-12 18:26:59 +08:00
Simone Margaritelli
74e3303963 Merge branch 'master' of github.com:bettercap/bettercap 2021-08-21 14:59:51 +02:00
Simone Margaritelli
f10ccfb4c6 releasing v2.32.0 2021-08-21 14:59:44 +02:00
Simone Margaritelli
8b867c29ed
Merge pull request #905 from TheRealKeto/makefile/various-changes
Various changes and fixes
2021-08-21 14:49:45 +02:00
Simone Margaritelli
81ae731b9f new: new -pcap-buf-size option to set a custom pcap buffer size (closes #896) 2021-08-21 14:44:36 +02:00
Simone Margaritelli
59dce4ced6 new: centralized pcap capture configuration management 2021-08-21 14:21:36 +02:00
Simone Margaritelli
d0ecfd499f new: updated to go 1.16 2021-08-21 13:50:56 +02:00
Simone Margaritelli
0598272384
Merge pull request #901 from qq906907952/master
add two 802.11 attack
2021-08-21 12:43:22 +02:00
TheRealKeto
c78a67d439
Add DESTDIR variable
Signed-off-by: TheRealKeto <therealketo@gmail.com>
2021-08-08 00:19:47 -04:00
TheRealKeto
d7f95dc97d
Various changes and fixes
- Remove GOFLAGS variable; it's not needed
- Make GOFLAGS variable first on all Go related functions

Signed-off-by: TheRealKeto <therealketo@gmail.com>
2021-08-08 00:18:56 -04:00
Simone Margaritelli
754b6b3841
Merge pull request #895 from cmingxu/master
make import statement clean
2021-08-07 16:41:12 +02:00
Simone Margaritelli
cb8a87460b
Merge pull request #894 from Azrotronik/arp_spoof-patch
Fix arp.spoof not sending replies and hanging
2021-08-07 16:40:57 +02:00
ydx
ef2cd0063d add wifi fake authentication attack 2021-07-23 14:51:37 +08:00
ydx
c8ecaf99e0 add channel hopping attack 2021-07-23 14:49:29 +08:00
xuchunming1
e9dad78ec2 nothing but import format change
Signed-off-by: xuchunming1 <xuchunming@jd.com>
2021-07-01 15:37:33 +08:00
xuchunming1
9020c53820 make import statement clean
Signed-off-by: xuchunming1 <xuchunming@jd.com>
2021-07-01 15:18:12 +08:00
Azrotronik
0637451390
Fix arp.spoof not sending replies
Fixed arp.spoof not sending replies and timing out when asked to shut down.
2021-06-27 00:17:42 +00:00
Simone Margaritelli
c1770b3aa6
Merge pull request #892 from antipopp/master
UI hardcoded setup path changed to handle Windows installation
2021-06-22 09:29:58 +02:00
Francesco Cartier
f10159ec19 added Windows basepath to UI setup 2021-06-21 18:08:54 +02:00
Francesco Cartier
58f4214756 added Windows basepath to UI setup 2021-06-21 18:01:32 +02:00
Simone Margaritelli
118a348e3e
Merge pull request #882 from syylari/ch177-fix
WiFi frequency and channel mapping enchancements
2021-06-03 17:46:37 +02:00
Simone Margaritelli
6c2c0da22c fix: checking boundaries when parsing WPS vendor extensions (fixes #885) 2021-05-28 16:41:34 +02:00
syylari
9404620468 Support for ch177 2021-05-20 21:57:10 +03:00
syylari
daf2f943e2 Further tests for mapping dot11 frequencies to channels as ch177 was not discovered correctly based on freq 2021-05-20 21:55:35 +03:00
1932 changed files with 396525 additions and 86681 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
*.js linguist-vendored
/Dockerfile linguist-vendored
/release.py linguist-vendored
/**/*.js linguist-vendored

2
.github/FUNDING.yml vendored
View file

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] github: evilsocket
patreon: evilsocket patreon: evilsocket
open_collective: # Replace with a single Open Collective username open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View 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.

View file

@ -1,3 +1,8 @@
---
name: General Issue
about: Write a general issue or bug report.
---
# Prerequisites # 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. 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
View 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
View 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 }}

View 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
View 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
View 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
View 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

4
.gitignore vendored
View file

@ -14,3 +14,7 @@ stage/
/snap /snap
.idea .idea
/cover.out
/can-data
/test*.yml
/zerochaos.js

View file

@ -1,182 +0,0 @@
# Globals
language: go
go:
- 1.16.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://github.com/bettercap/buildutils/raw/main/qemu-user-static_5.2_dfsg-9_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
osx_image: xcode12.5
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/winpcap/WpdPack/Include -I/c/libusb/include/libusb-1.0"
- CGO_LDFLAGS="-L/c/winpcap/WpdPack/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.24/libusb-1.0.24.7z" -o "/c/libusb.7z"
- 7z x -y "/c/libusb.7z" -o"/c/libusb"
- cp builder/libusb.pc /c/pkg-config/libusb.pc
- cp builder/libusb.pc /c/pkg-config/libusb-1.0.pc
- choco install winpcap -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"
<<: *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
- name: OSX - tests
if: tag IS blank
os: osx
osx_image: xcode12.5
arch: amd64
allow_failures:
- go: master
fast_finish: true
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"
script:
- env GO111MODULE=on make test
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
View file

@ -1,13 +1,11 @@
# build stage # 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 WORKDIR $GOPATH/src/github.com/bettercap/bettercap
RUN apk add --no-cache --update bash iptables wireless-tools build-base libpcap-dev libusb-dev linux-headers libnetfilter_queue-dev git ADD . $GOPATH/src/github.com/bettercap/bettercap
WORKDIR $SRC_DIR
ADD . $SRC_DIR
RUN make RUN make
# get caplets # get caplets
@ -16,8 +14,8 @@ RUN git clone https://github.com/bettercap/caplets /usr/local/share/bettercap/ca
# final stage # final stage
FROM alpine FROM alpine
RUN apk add --update ca-certificates RUN apk add --no-cache ca-certificates
RUN apk add --no-cache --update bash iproute2 libpcap libusb-dev libnetfilter_queue wireless-tools 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 /go/src/github.com/bettercap/bettercap/bettercap /app/
COPY --from=build-env /usr/local/share/bettercap/caplets /app/ COPY --from=build-env /usr/local/share/bettercap/caplets /app/
WORKDIR /app WORKDIR /app

View file

@ -2,15 +2,14 @@ TARGET ?= bettercap
PACKAGES ?= core firewall log modules network packets session tls PACKAGES ?= core firewall log modules network packets session tls
PREFIX ?= /usr/local PREFIX ?= /usr/local
GO ?= go GO ?= go
GOFLAGS ?=
all: build all: build
build: resources build: resources
$(GO) $(GOFLAGS) build -o $(TARGET) . $(GO) build $(GOFLAGS) -o $(TARGET) .
build_with_race_detector: resources build_with_race_detector: resources
$(GO) $(GOFLAGS) build -race -o $(TARGET) . $(GO) build $(GOFLAGS) -race -o $(TARGET) .
resources: network/manuf.go resources: network/manuf.go
@ -18,20 +17,20 @@ network/manuf.go:
@python3 ./network/make_manuf.py @python3 ./network/make_manuf.py
install: install:
@mkdir -p $(PREFIX)/share/bettercap/caplets @mkdir -p $(DESTDIR)$(PREFIX)/share/bettercap/caplets
@cp bettercap $(PREFIX)/bin/ @cp bettercap $(DESTDIR)$(PREFIX)/bin/
docker: docker:
@docker build -t bettercap:latest . @docker build -t bettercap:latest .
test: test:
$(GO) $(GOFLAGS) test -covermode=atomic -coverprofile=cover.out ./... $(GO) test -covermode=atomic -coverprofile=cover.out ./...
html_coverage: test 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 benchmark: server_deps
$(GO) $(GOFLAGS) test -v -run=doNotRunTests -bench=. -benchmem ./... $(GO) test -v -run=doNotRunTests -bench=. -benchmem ./...
fmt: fmt:
$(GO) fmt -s -w $(PACKAGES) $(GO) fmt -s -w $(PACKAGES)

View file

@ -1,22 +1,34 @@
<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"> <p align="center">
<img alt="BetterCap" src="https://raw.githubusercontent.com/bettercap/media/master/logo.png" height="140" /> <img alt="BetterCap" src="https://raw.githubusercontent.com/bettercap/media/master/logo.png" height="140" />
<p align="center"> <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/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://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>
</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.
![UI](https://raw.githubusercontent.com/bettercap/media/master/ui-events.png) ![UI](https://raw.githubusercontent.com/bettercap/media/master/ui-events.png)
## Main Features ## 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. * **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). * 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. * Passive and active IP network hosts probing and recon.
* **ARP, DNS, NDP and DHCPv6 spoofers** for MITM attacks on IPv4 and IPv6 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**. * **Proxies at packet level, TCP level and HTTP/HTTPS** application level fully scriptable with easy to implement **javascript plugins**.
@ -26,9 +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).** * **A very convenient [web UI](https://www.bettercap.org/usage/#web-ui).**
* [More!](https://www.bettercap.org/modules/) * [More!](https://www.bettercap.org/modules/)
## Contributors
<a href="https://github.com/bettercap/bettercap/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bettercap/bettercap" alt="bettercap project contributors" />
</a>
## License ## 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 ## Stargazers over time

141
build.sh
View file

@ -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 --

View file

@ -1,73 +0,0 @@
#!/usr/bin/env bash
set -eu
PROGRAM="${1}"
shift
COMMAND="${*}"
IMAGE="https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2020-02-14/2020-02-13-raspbian-buster-lite.zip"
GOLANG="https://golang.org/dl/go1.16.2.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"

View file

@ -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
View 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)
}
}
}

308
caplets/env_test.go Normal file
View 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
View 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
}

View file

@ -2,7 +2,7 @@ package core
const ( const (
Name = "bettercap" Name = "bettercap"
Version = "2.31.1" Version = "2.41.4"
Author = "Simone 'evilsocket' Margaritelli" Author = "Simone 'evilsocket' Margaritelli"
Website = "https://bettercap.org/" Website = "https://bettercap.org/"
) )

View file

@ -4,6 +4,7 @@ import (
"os/exec" "os/exec"
"sort" "sort"
"github.com/bettercap/bettercap/v2/log"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"
) )
@ -36,10 +37,13 @@ func HasBinary(executable string) bool {
func Exec(executable string, args []string) (string, error) { func Exec(executable string, args []string) (string, error) {
path, err := exec.LookPath(executable) path, err := exec.LookPath(executable)
if err != nil { if err != nil {
log.Warning("executable %s not found in $PATH", executable)
return "", err return "", err
} }
raw, err := exec.Command(path, args...).CombinedOutput() 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 { if err != nil {
return str.Trim(string(raw)), err return str.Trim(string(raw)), err
} else { } else {

View file

@ -97,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)
}
})
}

View file

@ -1,6 +1,8 @@
package core package core
import "flag" import (
"flag"
)
type Options struct { type Options struct {
InterfaceName *string InterfaceName *string
@ -18,6 +20,7 @@ type Options struct {
MemProfile *string MemProfile *string
CapletsPath *string CapletsPath *string
Script *string Script *string
PcapBufSize *int
} }
func ParseOptions() (Options, error) { func ParseOptions() (Options, error) {
@ -37,6 +40,7 @@ func ParseOptions() (Options, error) {
MemProfile: flag.String("mem-profile", "", "Write memory profile to `file`."), MemProfile: flag.String("mem-profile", "", "Write memory profile to `file`."),
CapletsPath: flag.String("caplets-path", "", "Specify an alternative base path for caplets."), CapletsPath: flag.String("caplets-path", "", "Specify an alternative base path for caplets."),
Script: flag.String("script", "", "Load a session script."), 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() flag.Parse()

View file

@ -9,8 +9,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/bettercap/bettercap/core" "github.com/bettercap/bettercap/v2/core"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"
) )

View file

@ -4,17 +4,19 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"github.com/bettercap/bettercap/core" "github.com/bettercap/bettercap/v2/core"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/evilsocket/islazy/str"
"github.com/evilsocket/islazy/fs" "github.com/evilsocket/islazy/fs"
"github.com/evilsocket/islazy/str"
) )
type LinuxFirewall struct { type LinuxFirewall struct {
iface *network.Endpoint iface *network.Endpoint
forwarding bool forwarding bool
restore bool
redirections map[string]*Redirection redirections map[string]*Redirection
} }
@ -27,6 +29,7 @@ func Make(iface *network.Endpoint) FirewallManager {
firewall := &LinuxFirewall{ firewall := &LinuxFirewall{
iface: iface, iface: iface,
forwarding: false, forwarding: false,
restore: false,
redirections: make(map[string]*Redirection), redirections: make(map[string]*Redirection),
} }
@ -65,18 +68,30 @@ func (f LinuxFirewall) IsForwardingEnabled() bool {
func (f LinuxFirewall) EnableForwarding(enabled bool) error { func (f LinuxFirewall) EnableForwarding(enabled bool) error {
if err := f.enableFeature(IPV4ForwardingFile, enabled); err != nil { if err := f.enableFeature(IPV4ForwardingFile, enabled); err != nil {
return err return err
} else if fs.Exists(IPV6ForwardingFile) { }
if fs.Exists(IPV6ForwardingFile) {
return f.enableFeature(IPV6ForwardingFile, enabled) return f.enableFeature(IPV6ForwardingFile, enabled)
} }
f.restore = true
return nil return nil
} }
func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []string) { func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []string) {
action := "-A" action := "-A"
destination := ""
if !enabled { if !enabled {
action = "-D" action = "-D"
} }
if strings.Count(r.DstAddress, ":") < 2 {
destination = r.DstAddress
} else {
destination = fmt.Sprintf("[%s]", r.DstAddress)
}
if r.SrcAddress == "" { if r.SrcAddress == "" {
cmdLine = []string{ cmdLine = []string{
"-t", "nat", "-t", "nat",
@ -85,7 +100,7 @@ func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []
"-p", r.Protocol, "-p", r.Protocol,
"--dport", fmt.Sprintf("%d", r.SrcPort), "--dport", fmt.Sprintf("%d", r.SrcPort),
"-j", "DNAT", "-j", "DNAT",
"--to", fmt.Sprintf("%s:%d", r.DstAddress, r.DstPort), "--to", fmt.Sprintf("%s:%d", destination, r.DstPort),
} }
} else { } else {
cmdLine = []string{ cmdLine = []string{
@ -96,7 +111,7 @@ func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []
"-d", r.SrcAddress, "-d", r.SrcAddress,
"--dport", fmt.Sprintf("%d", r.SrcPort), "--dport", fmt.Sprintf("%d", r.SrcPort),
"-j", "DNAT", "-j", "DNAT",
"--to", fmt.Sprintf("%s:%d", r.DstAddress, r.DstPort), "--to", fmt.Sprintf("%s:%d", destination, r.DstPort),
} }
} }
@ -107,6 +122,13 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
cmdLine := f.getCommandLine(r, enabled) cmdLine := f.getCommandLine(r, enabled)
rkey := r.String() rkey := r.String()
_, found := f.redirections[rkey] _, found := f.redirections[rkey]
cmd := ""
if strings.Count(r.DstAddress, ":") < 2 {
cmd = "iptables"
} else {
cmd = "ip6tables"
}
if enabled { if enabled {
if found { if found {
@ -116,9 +138,9 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
f.redirections[rkey] = r f.redirections[rkey] = r
// accept all // 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 return err
} else if _, err := core.Exec("iptables", cmdLine); err != nil { } else if _, err := core.Exec(cmd, cmdLine); err != nil {
return err return err
} }
} else { } else {
@ -128,7 +150,7 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
delete(f.redirections, r.String()) delete(f.redirections, r.String())
if _, err := core.Exec("iptables", cmdLine); err != nil { if _, err := core.Exec(cmd, cmdLine); err != nil {
return err return err
} }
} }
@ -137,6 +159,9 @@ func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
} }
func (f LinuxFirewall) Restore() { func (f LinuxFirewall) Restore() {
if f.restore == false {
return
}
for _, r := range f.redirections { for _, r := range f.redirections {
if err := f.EnableRedirection(r, false); err != nil { if err := f.EnableRedirection(r, false); err != nil {
fmt.Printf("%s", err) fmt.Printf("%s", err)

View file

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/bettercap/bettercap/core" "github.com/bettercap/bettercap/v2/core"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
) )
type WindowsFirewall struct { type WindowsFirewall struct {

View 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()
}
}

82
go.mod
View file

@ -1,47 +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 ( require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/adrianmo/go-nmea v1.3.0 github.com/adrianmo/go-nmea v1.10.0
github.com/antchfx/jsonquery v1.1.4 github.com/antchfx/jsonquery v1.3.6
github.com/antchfx/xpath v1.1.11 // indirect github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0
github.com/bettercap/gatt v0.0.0-20210514133428-df6e615f2f67
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3 github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3
github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef github.com/cenkalti/backoff v2.2.1+incompatible
github.com/chzyer/logex v1.1.10 // indirect github.com/dustin/go-humanize v1.0.1
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect github.com/elazarl/goproxy v1.7.2
github.com/dustin/go-humanize v1.0.0 github.com/evilsocket/islazy v1.11.0
github.com/elazarl/goproxy v0.0.0-20210110162100-a92cc753f88e github.com/florianl/go-nfqueue/v2 v2.0.0
github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e // indirect
github.com/evilsocket/islazy v1.10.6
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/google/go-github v17.0.0+incompatible github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gopacket v1.1.19 github.com/google/gopacket v1.1.19
github.com/google/gousb v2.1.0+incompatible github.com/google/gousb v1.1.3
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.5.3
github.com/hashicorp/mdns v1.0.3 github.com/hashicorp/go-bexpr v0.1.14
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b github.com/inconshreveable/go-vhost v1.0.0
github.com/jpillora/go-tld v1.1.1 github.com/jpillora/go-tld v1.2.1
github.com/koppacetic/go-gpsd v0.4.0
github.com/kr/binarydist v0.1.0 // indirect
github.com/malfunkt/iprange v0.9.0 github.com/malfunkt/iprange v0.9.0
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b
github.com/miekg/dns v1.1.41 github.com/miekg/dns v1.1.67
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1 // indirect github.com/phin1x/go-ipp v1.6.1
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac 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/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 go.einride.tech/can v0.14.0
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect golang.org/x/net v0.42.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 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/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/pkg/errors v0.9.1 // 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 gopkg.in/sourcemap.v1 v1.0.5 // indirect
) )

177
go.sum
View file

@ -1,67 +1,68 @@
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 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/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/adrianmo/go-nmea v1.3.0 h1:BFrLRj/oIh+DYujIKpuQievq7X3NDHYq57kNgsfr2GY= github.com/adrianmo/go-nmea v1.10.0 h1:L1aYaebZ4cXFCoXNSeDeQa0tApvSKvIbqMsK+iaRiCo=
github.com/adrianmo/go-nmea v1.3.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg= github.com/adrianmo/go-nmea v1.10.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg=
github.com/antchfx/jsonquery v1.1.4 h1:+OlFO3QS9wjU0MKx9MgHm5f6o6hdd4e9mUTp0wTjxlM= github.com/antchfx/jsonquery v1.3.6 h1:TaSfeAh7n6T11I74bsZ1FswreIfrbJ0X+OyLflx6mx4=
github.com/antchfx/jsonquery v1.1.4/go.mod h1:cHs8r6Bymd8j6HI6Ej1IJbjahKvLBcIEh54dfmo+E9A= github.com/antchfx/jsonquery v1.3.6/go.mod h1:fGzSGJn9Y826Qd3pC8Wx45avuUwpkePsACQJYy+58BU=
github.com/antchfx/xpath v1.1.7/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.1.11 h1:WOFtK8TVAjLm3lbgqeP0arlHpvCEeTANeWZ/csPpJkQ= github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
github.com/antchfx/xpath v1.1.11/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bettercap/gatt v0.0.0-20210514133428-df6e615f2f67 h1:xzN6806c01hWTz8gjGsRjhOPlYj5/dNoZIR8CN9+O1c= github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0 h1:HiFUGV/7eGWG/YJAf9HcKOUmxIj+7LVzC8zD57VX1qo=
github.com/bettercap/gatt v0.0.0-20210514133428-df6e615f2f67/go.mod h1:oafnPgaBI4gqJiYkueCyR4dqygiWGXTGOE0gmmAVeeQ= 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 h1:JWAAJk4ny+bT3VrtcX+e7mcmWtWUeUM0xVcocSAUuWc=
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb/go.mod h1:g6WiaSRgMTiChuk7jYyFSEtpgaw1F0wAsBfspG3bu0M= github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb/go.mod h1:g6WiaSRgMTiChuk7jYyFSEtpgaw1F0wAsBfspG3bu0M=
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf h1:pwGPRc5PIp4KCF9QbKn0iLVMhfigUMw4IzGZEZ81m1I= 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/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 h1:pC4ZAk7UtDIbrRKzMMiIL1TVkiKlgtgcJodqKB53Rl4=
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3/go.mod h1:kqVwnx6DKuOHMZcBnzsgp2Lq2JZHDtFtm92b5hxdRaM= 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/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef/go.mod h1:xn8SYXvxzI99iSN8+Kh3wCvt2fhr27vPPf8ju9FwRS0= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20210110162100-a92cc753f88e h1:/cwV7t2xezilMljIftb7WlFtzGANRCnoOhPjtl2ifcs= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v0.0.0-20210110162100-a92cc753f88e/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e h1:CQn2/8fi3kmpT9BTiHEELgdxAOQNVZc9GoPA4qnQzrs= github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/florianl/go-nfqueue/v2 v2.0.0 h1:NTCxS9b0GSbHkWv1a7oOvZn679fsyDkaSkRvOYpQ9Oo=
github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo= github.com/florianl/go-nfqueue/v2 v2.0.0/go.mod h1:M2tBLIj62QpwqjwV0qfcjqGOqP3qiTuXr2uSRBXH9Qk=
github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe h1:8P+/htb3mwwpeGdJg69yBF/RofK7c6Fjz5Ypa/bTqbY= 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/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/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.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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 h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 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/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 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/gousb v2.1.0+incompatible h1:ApzMDjF3FeO219QwWybJxYfFhXQzPLOEy0o+w9k5DNI= github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o=
github.com/google/gousb v2.1.0+incompatible/go.mod h1:Tl4HdAs1ThE3gECkNwz+1MWicX6FXddhJEw7L8jRDiI= github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/mdns v1.0.3 h1:hPneYJlzSjxFBmUlnDGXRykxBZ++dQAJhU57gCO7TzI= github.com/hashicorp/go-bexpr v0.1.14 h1:uKDeyuOhWhT1r5CiMTjdVY4Aoxdxs6EtwgTGnlosyp4=
github.com/hashicorp/mdns v1.0.3/go.mod h1:P9sIDVQGUBr2GtS4qS2QCBdtgqP7TBt6d8looU5l5r4= github.com/hashicorp/go-bexpr v0.1.14/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8=
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE= github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8=
github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU= github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
github.com/jpillora/go-tld v1.1.1 h1:P1ZwtKDHBYYUl235R/D64cdBARfGYzEy1Hg2Ikir3FQ= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/jpillora/go-tld v1.1.1/go.mod h1:kitBxOF//DR5FxYeIGw+etdiiTIq5S7bx0dwy1GUNAk= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/koppacetic/go-gpsd v0.4.0 h1:/T3cRvi1ZsWbxCZPB9pPor0HjIw3HuD+MSvaxV5QqQ8= github.com/jpillora/go-tld v1.2.1 h1:kDKOkmXLlskqjcvNs7w5XHLep7c8WM7Xd4HQjxllVMk=
github.com/koppacetic/go-gpsd v0.4.0/go.mod h1:mhcLuh9X++WHepbL3jEmEwnx1OkQDepZMihv12RO4qk= 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 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM= github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
@ -72,75 +73,110 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/malfunkt/iprange v0.9.0 h1:VCs0PKLUPotNVQTpVNszsut4lP7OCGNBwX+lOYBrnVQ= 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/malfunkt/iprange v0.9.0/go.mod h1:TRGqO/f95gh3LOndUGTL46+W0GXA91WTqyZ0Quwvt4U=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 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.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 h1:r12blE3QRYlW1WBiBEe007O6NrTb/P54OjR5d4WLEGk=
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b/go.mod h1:p4K2+UAoap8Jzsadsxc0KG0OZjmmCthTPUyZqAVkjBY= github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b/go.mod h1:p4K2+UAoap8Jzsadsxc0KG0OZjmmCthTPUyZqAVkjBY=
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 h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 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 h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8= github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 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/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac h1:kYPjbEN6YPYWWHI6ky1J813KzIq/8+Wg4TO4xU7A/KU= github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 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-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
github.com/thoj/go-ircevent v0.0.0-20190807115034-8e7ce4b5a1eb/go.mod h1:I0ZT9x8wStY6VOxtNOrLpnDURFs7HS0z1e1vhuKUEVc= 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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 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/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
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-20190310074541-c10a0554eabf/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 h1:4qWs8cYYH6PoEFy4dfhDFgoMGkwAcETd+MmPdCPMzUc= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= 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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/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-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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.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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@ -149,5 +185,8 @@ gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 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= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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
View 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
}

View file

@ -4,27 +4,100 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/base64" "encoding/base64"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
) )
func btoa(call otto.FunctionCall) otto.Value { func textEncode(call otto.FunctionCall) otto.Value {
varValue := base64.StdEncoding.EncodeToString([]byte(call.Argument(0).String())) argv := call.ArgumentList
v, err := otto.ToValue(varValue) argc := len(argv)
if err != nil { if argc != 1 {
return ReportError("Could not convert to string: %s", varValue) 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 return v
} }
func atob(call otto.FunctionCall) otto.Value { func atob(call otto.FunctionCall) otto.Value {
varValue, err := base64.StdEncoding.DecodeString(call.Argument(0).String()) argv := call.ArgumentList
if err != nil { argc := len(argv)
return ReportError("Could not decode string: %s", call.Argument(0).String()) if argc != 1 {
return ReportError("atob: expected 1 argument, %d given instead.", argc)
} }
v, err := otto.ToValue(string(varValue))
if err != nil { arg := argv[0]
return ReportError("Could not convert to string: %s", varValue) 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 return v
} }
@ -35,7 +108,12 @@ func gzipCompress(call otto.FunctionCall) otto.Value {
return ReportError("gzipCompress: expected 1 argument, %d given instead.", argc) return ReportError("gzipCompress: expected 1 argument, %d given instead.", argc)
} }
uncompressedBytes := []byte(argv[0].String()) arg := argv[0]
if (!arg.IsString()) {
return ReportError("gzipCompress: single argument must be a string.")
}
uncompressedBytes := []byte(arg.String())
var writerBuffer bytes.Buffer var writerBuffer bytes.Buffer
gzipWriter := gzip.NewWriter(&writerBuffer) gzipWriter := gzip.NewWriter(&writerBuffer)
@ -49,7 +127,7 @@ func gzipCompress(call otto.FunctionCall) otto.Value {
v, err := otto.ToValue(string(compressedBytes)) v, err := otto.ToValue(string(compressedBytes))
if err != nil { if err != nil {
return ReportError("Could not convert to string: %s", err.Error()) return ReportError("gzipCompress: could not convert to string: %s", err.Error())
} }
return v return v
@ -79,7 +157,7 @@ func gzipDecompress(call otto.FunctionCall) otto.Value {
decompressedBytes := decompressedBuffer.Bytes() decompressedBytes := decompressedBuffer.Bytes()
v, err := otto.ToValue(string(decompressedBytes)) v, err := otto.ToValue(string(decompressedBytes))
if err != nil { if err != nil {
return ReportError("Could not convert to string: %s", err.Error()) return ReportError("gzipDecompress: could not convert to string: %s", err.Error())
} }
return v return v

514
js/data_test.go Normal file
View 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)
}
}

684
js/fs_test.go Normal file
View 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)
}
}

View file

@ -3,12 +3,12 @@ package js
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/robertkrimen/otto"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/robertkrimen/otto"
) )
type httpPackage struct { type httpPackage struct {
@ -26,7 +26,10 @@ func (c httpPackage) Encode(s string) string {
return url.QueryEscape(s) return url.QueryEscape(s)
} }
func (c httpPackage) Request(method string, uri string, headers map[string]string, form map[string]string, json string) httpResponse { func (c httpPackage) Request(method string, uri string,
headers map[string]string,
form map[string]string,
json string) httpResponse {
var reader io.Reader var reader io.Reader
if form != nil { if form != nil {
@ -60,7 +63,7 @@ func (c httpPackage) Request(method string, uri string, headers map[string]strin
} }
defer resp.Body.Close() defer resp.Body.Close()
raw, err := ioutil.ReadAll(resp.Body) raw, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return httpResponse{Error: err} return httpResponse{Error: err}
} }
@ -129,7 +132,7 @@ func httpRequest(call otto.FunctionCall) otto.Value {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return ReportError("Could not read response: %s", err) return ReportError("Could not read response: %s", err)
} }

View file

@ -3,7 +3,6 @@ package js
import ( import (
"github.com/evilsocket/islazy/log" "github.com/evilsocket/islazy/log"
"github.com/evilsocket/islazy/plugin" "github.com/evilsocket/islazy/plugin"
"github.com/robertkrimen/otto" "github.com/robertkrimen/otto"
) )
@ -28,10 +27,16 @@ func init() {
plugin.Defines["log_error"] = log_error plugin.Defines["log_error"] = log_error
plugin.Defines["log_fatal"] = log_fatal plugin.Defines["log_fatal"] = log_fatal
plugin.Defines["Crypto"] = map[string]interface{}{
"sha1": cryptoSha1,
}
plugin.Defines["btoa"] = btoa plugin.Defines["btoa"] = btoa
plugin.Defines["atob"] = atob plugin.Defines["atob"] = atob
plugin.Defines["gzipCompress"] = gzipCompress plugin.Defines["gzipCompress"] = gzipCompress
plugin.Defines["gzipDecompress"] = gzipDecompress plugin.Defines["gzipDecompress"] = gzipDecompress
plugin.Defines["textEncode"] = textEncode
plugin.Defines["textDecode"] = textDecode
plugin.Defines["httpRequest"] = httpRequest plugin.Defines["httpRequest"] = httpRequest
plugin.Defines["http"] = httpPackage{} plugin.Defines["http"] = httpPackage{}

View file

@ -3,7 +3,8 @@ package js
import ( import (
"math/rand" "math/rand"
"net" "net"
"github.com/bettercap/bettercap/network"
"github.com/bettercap/bettercap/v2/network"
) )
type randomPackage struct { type randomPackage struct {

307
js/random_test.go Normal file
View 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)
}
})
}
}

View file

@ -1,27 +1,39 @@
package log package log
import ( import (
"github.com/bettercap/bettercap/session" "github.com/evilsocket/islazy/log"
ll "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{}) { 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{}) { 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{}) { 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{}) { 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{}) { 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
View 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)
}
}

View file

@ -8,10 +8,10 @@ import (
"runtime" "runtime"
"github.com/bettercap/bettercap/core" "github.com/bettercap/bettercap/v2/core"
"github.com/bettercap/bettercap/log" "github.com/bettercap/bettercap/v2/log"
"github.com/bettercap/bettercap/modules" "github.com/bettercap/bettercap/v2/modules"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"

88
main_test.go Normal file
View 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 + ")"
}

View file

@ -2,11 +2,12 @@ package any_proxy
import ( import (
"fmt" "fmt"
"github.com/bettercap/bettercap/firewall"
"github.com/bettercap/bettercap/session"
"github.com/evilsocket/islazy/str"
"strconv" "strconv"
"strings" "strings"
"github.com/bettercap/bettercap/v2/firewall"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/str"
) )
type AnyProxy struct { type AnyProxy struct {
@ -44,7 +45,7 @@ func NewAnyProxy(s *session.Session) *AnyProxy {
mod.AddParam(session.NewStringParameter("any.proxy.dst_address", mod.AddParam(session.NewStringParameter("any.proxy.dst_address",
session.ParamIfaceAddress, session.ParamIfaceAddress,
session.IPv4Validator, "",
"Address where the proxy is listening.")) "Address where the proxy is listening."))
mod.AddParam(session.NewIntParameter("any.proxy.dst_port", mod.AddParam(session.NewIntParameter("any.proxy.dst_port",

View 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")
}
}

View file

@ -7,8 +7,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/bettercap/bettercap/tls" "github.com/bettercap/bettercap/v2/tls"
"github.com/bettercap/recording" "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.")) "Value of the Access-Control-Allow-Origin header of the API server."))
mod.AddParam(session.NewStringParameter("api.rest.username", mod.AddParam(session.NewStringParameter("api.rest.username",
"", "user",
"", "",
"API authentication username.")) "API authentication username."))
mod.AddParam(session.NewStringParameter("api.rest.password", mod.AddParam(session.NewStringParameter("api.rest.password",
"", "pass",
"", "",
"API authentication password.")) "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) mod.server.Addr = fmt.Sprintf("%s:%d", ip, port)
router := mux.NewRouter() router := mux.NewRouter()

View file

@ -5,17 +5,22 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/fs"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
var (
ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
)
type CommandRequest struct { type CommandRequest struct {
Command string `json:"cmd"` Command string `json:"cmd"`
} }
@ -220,6 +225,10 @@ func (mod *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400) http.Error(w, "Bad Request", 400)
} }
rescueStdout := os.Stdout
stdoutReader, stdoutWriter, _ := os.Pipe()
os.Stdout = stdoutWriter
for _, aCommand := range session.ParseCommands(cmd.Command) { for _, aCommand := range session.ParseCommands(cmd.Command) {
if err = mod.Session.Run(aCommand); err != nil { if err = mod.Session.Run(aCommand); err != nil {
http.Error(w, err.Error(), 400) 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 { 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) { 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 { if err != nil {
msg := fmt.Sprintf("invalid file upload: %s", err) msg := fmt.Sprintf("invalid file upload: %s", err)
mod.Warning(msg) mod.Warning(msg)
@ -387,7 +401,7 @@ func (mod *RestAPI) writeFile(fileName string, w http.ResponseWriter, r *http.Re
return return
} }
err = ioutil.WriteFile(fileName, data, 0666) err = os.WriteFile(fileName, data, 0666)
if err != nil { if err != nil {
msg := fmt.Sprintf("can't write to %s: %s", fileName, err) msg := fmt.Sprintf("can't write to %s: %s", fileName, err)
mod.Warning(msg) mod.Warning(msg)
@ -426,7 +440,14 @@ func (mod *RestAPI) fileRoute(w http.ResponseWriter, r *http.Request) {
return return
} }
var err error
fileName := r.URL.Query().Get("name") 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" { if fileName != "" && r.Method == "GET" {
mod.readFile(fileName, w, r) mod.readFile(fileName, w, r)

View 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")
}
}

View file

@ -6,7 +6,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )

View file

@ -7,9 +7,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/v2/packets"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/malfunkt/iprange" "github.com/malfunkt/iprange"
) )
@ -175,7 +175,7 @@ func (mod *ArpSpoofer) Start() error {
gwIP := mod.Session.Gateway.IP gwIP := mod.Session.Gateway.IP
myMAC := mod.Session.Interface.HW myMAC := mod.Session.Interface.HW
for mod.Running() { for mod.Running() {
mod.arpSpoofTargets(gwIP, myMAC, true, true) mod.arpSpoofTargets(gwIP, myMAC, true, false)
for _, address := range neighbours { for _, address := range neighbours {
if !mod.Session.Skip(address) { if !mod.Session.Skip(address) {
mod.arpSpoofTargets(address, myMAC, true, false) mod.arpSpoofTargets(address, myMAC, true, false)

View 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)
}
}

View file

@ -4,8 +4,10 @@ import (
"github.com/bettercap/gatt" "github.com/bettercap/gatt"
) )
var defaultBLEClientOptions = []gatt.Option{ func getClientOptions(deviceID int) []gatt.Option {
gatt.MacDeviceRole(gatt.CentralManager), return []gatt.Option{
gatt.MacDeviceRole(gatt.CentralManager),
}
} }
/* /*

View file

@ -5,9 +5,11 @@ import (
// "github.com/bettercap/gatt/linux/cmd" // "github.com/bettercap/gatt/linux/cmd"
) )
var defaultBLEClientOptions = []gatt.Option{ func getClientOptions(deviceID int) []gatt.Option {
gatt.LnxMaxConnections(255), return []gatt.Option{
gatt.LnxDeviceID(-1, true), gatt.LnxMaxConnections(255),
gatt.LnxDeviceID(deviceID, true),
}
} }
/* /*

View file

@ -1,4 +1,5 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
@ -8,9 +9,9 @@ import (
golog "log" golog "log"
"time" "time"
"github.com/bettercap/bettercap/modules/utils" "github.com/bettercap/bettercap/v2/modules/utils"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/bettercap/gatt" "github.com/bettercap/gatt"
@ -80,7 +81,7 @@ func NewBLERecon(s *session.Session) *BLERecon {
"Enumerate services and characteristics for the given BLE device.", "Enumerate services and characteristics for the given BLE device.",
func(args []string) error { func(args []string) error {
if mod.isEnumerating() { 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 mod.writeData = nil
@ -99,11 +100,11 @@ func NewBLERecon(s *session.Session) *BLERecon {
mac := network.NormalizeMac(args[0]) mac := network.NormalizeMac(args[0])
uuid, err := gatt.ParseUUID(args[1]) uuid, err := gatt.ParseUUID(args[1])
if err != nil { 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]) data, err := hex.DecodeString(args[2])
if err != nil { 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) return mod.writeBuffer(mac, uuid, data)
@ -166,7 +167,7 @@ func (mod *BLERecon) Configure() (err error) {
golog.SetFlags(0) golog.SetFlags(0)
golog.SetOutput(dummyWriter{mod}) 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) mod.Debug("error while creating new gatt device: %v", err)
return err return err
} }
@ -189,11 +190,15 @@ func (mod *BLERecon) Configure() (err error) {
return nil return nil
} }
const blePrompt = "{blb}{fw}BLE {fb}{reset} {bold}» {reset}"
func (mod *BLERecon) Start() error { func (mod *BLERecon) Start() error {
if err := mod.Configure(); err != nil { if err := mod.Configure(); err != nil {
return err return err
} }
mod.SetPrompt(blePrompt)
return mod.SetRunning(true, func() { return mod.SetRunning(true, func() {
go mod.pruner() go mod.pruner()
@ -220,6 +225,8 @@ func (mod *BLERecon) Start() error {
} }
func (mod *BLERecon) Stop() error { func (mod *BLERecon) Stop() error {
mod.SetPrompt(session.DefaultPrompt)
return mod.SetRunning(false, func() { return mod.SetRunning(false, func() {
mod.quit <- true mod.quit <- true
<-mod.done <-mod.done

View file

@ -1,4 +1,5 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble

View 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)
}
}

View file

@ -1,4 +1,5 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
@ -6,7 +7,7 @@ import (
"sort" "sort"
"time" "time"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/evilsocket/islazy/ops" "github.com/evilsocket/islazy/ops"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"

View file

@ -1,4 +1,5 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
@ -8,7 +9,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/gatt" "github.com/bettercap/gatt"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"

View file

@ -1,9 +1,10 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
import ( import (
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
) )
type ByBLERSSISorter []*network.BLEDevice type ByBLERSSISorter []*network.BLEDevice

View file

@ -1,9 +1,10 @@
// +build windows //go:build windows || freebsd || netbsd || openbsd
// +build windows freebsd netbsd openbsd
package ble package ble
import ( import (
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
) )
type BLERecon struct { type BLERecon struct {

View file

@ -4,14 +4,15 @@ import (
"bytes" "bytes"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"strings"
"text/template"
"github.com/acarl005/stripansi" "github.com/acarl005/stripansi"
"github.com/bettercap/bettercap/modules/events_stream" "github.com/bettercap/bettercap/v2/modules/events_stream"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/log" "github.com/evilsocket/islazy/log"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"
irc "github.com/thoj/go-ircevent" irc "github.com/thoj/go-ircevent"
"strings"
"text/template"
) )
type settings struct { type settings struct {

356
modules/c2/c2_test.go Normal file
View 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
View 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
View 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
}

View 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
})
}
}
}

View file

@ -0,0 +1,6 @@
package can
func (mod *CANModule) dbcLoad(name string) error {
// load as file
return mod.dbc.LoadFile(mod, name)
}

View 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
View 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
View 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
}

View 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
View 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
}

View 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
}

View 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 FuelAir 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
}

View 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
View 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
View 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
View 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"})
}
}

View file

@ -6,8 +6,8 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/bettercap/bettercap/caplets" "github.com/bettercap/bettercap/v2/caplets"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"

View file

@ -9,8 +9,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/packets"
"github.com/bettercap/bettercap/v2/session"
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
@ -83,7 +84,7 @@ func (mod *DHCP6Spoofer) Configure() error {
return session.ErrAlreadyStarted(mod.Name()) 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 return err
} }

View 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()
})
}

View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View file

@ -7,9 +7,10 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/bettercap/bettercap/log" "github.com/bettercap/bettercap/v2/log"
"github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/packets"
"github.com/bettercap/bettercap/v2/session"
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
@ -100,7 +101,7 @@ func (mod *DNSSpoofer) Configure() error {
if mod.Running() { if mod.Running() {
return session.ErrAlreadyStarted(mod.Name()) 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 return err
} else if err = mod.Handle.SetBPFFilter("udp"); err != nil { } else if err = mod.Handle.SetBPFFilter("udp"); err != nil {
return err return err

View file

@ -8,7 +8,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/fs" "github.com/evilsocket/islazy/fs"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"

View file

@ -1,7 +1,7 @@
package events_stream package events_stream
import ( import (
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )

View file

@ -6,11 +6,11 @@ import (
"os" "os"
"strings" "strings"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/bettercap/bettercap/modules/net_sniff" "github.com/bettercap/bettercap/v2/modules/net_sniff"
"github.com/bettercap/bettercap/modules/syn_scan" "github.com/bettercap/bettercap/v2/modules/syn_scan"
"github.com/google/go-github/github" "github.com/google/go-github/github"
@ -120,6 +120,8 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
mod.viewBLEEvent(output, e) mod.viewBLEEvent(output, e)
} else if strings.HasPrefix(e.Tag, "hid.") { } else if strings.HasPrefix(e.Tag, "hid.") {
mod.viewHIDEvent(output, e) mod.viewHIDEvent(output, e)
} else if strings.HasPrefix(e.Tag, "can.") {
mod.viewCANEvent(output, e)
} else if strings.HasPrefix(e.Tag, "gps.") { } else if strings.HasPrefix(e.Tag, "gps.") {
mod.viewGPSEvent(output, e) mod.viewGPSEvent(output, e)
} else if strings.HasPrefix(e.Tag, "mod.") { } else if strings.HasPrefix(e.Tag, "mod.") {
@ -132,8 +134,10 @@ func (mod *EventsStream) Render(output io.Writer, e session.Event) {
mod.viewUpdateEvent(output, e) mod.viewUpdateEvent(output, e)
} else if e.Tag == "gateway.change" { } else if e.Tag == "gateway.change" {
mod.viewGatewayEvent(output, e) mod.viewGatewayEvent(output, e)
} else if e.Tag != "tick" { } else if strings.HasPrefix(e.Tag, "zeroconf.") {
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e) 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)
} }
} }

View file

@ -1,3 +1,4 @@
//go:build !windows
// +build !windows // +build !windows
package events_stream package events_stream
@ -6,8 +7,8 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )

View file

@ -1,10 +1,12 @@
//go:build windows
// +build windows // +build windows
package events_stream package events_stream
import ( import (
"io" "io"
"github.com/bettercap/bettercap/session"
"github.com/bettercap/bettercap/v2/session"
) )
func (mod *EventsStream) viewBLEEvent(output io.Writer, e session.Event) { func (mod *EventsStream) viewBLEEvent(output io.Writer, e session.Event) {

View 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)
}
}

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )

View file

@ -4,8 +4,8 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )

View file

@ -11,8 +11,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/bettercap/bettercap/modules/net_sniff" "github.com/bettercap/bettercap/v2/modules/net_sniff"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )

View file

@ -2,12 +2,13 @@ package events_stream
import ( import (
"fmt" "fmt"
"github.com/bettercap/bettercap/modules/wifi"
"io" "io"
"strings" "strings"
"github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/v2/modules/wifi"
"github.com/bettercap/bettercap/session"
"github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/tui" "github.com/evilsocket/islazy/tui"
) )
@ -131,6 +132,16 @@ func (mod *EventsStream) viewWiFiDeauthEvent(output io.Writer, e session.Event)
deauth.RSSI) deauth.RSSI)
} }
func (mod *EventsStream) viewWiFiBruteforceEvent(output io.Writer, e session.Event) {
success := e.Data.(wifi.BruteforceSuccess)
fmt.Fprintf(output, "[%s] [%s] target='%s' password='%s' auth_in=%v\n",
e.Time.Format(mod.timeFormat),
tui.Green(tui.Bold(e.Tag)),
tui.Bold(success.Target),
tui.Bold(success.Password),
success.Elapsed)
}
func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) { func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) {
if strings.HasPrefix(e.Tag, "wifi.ap.") { if strings.HasPrefix(e.Tag, "wifi.ap.") {
mod.viewWiFiApEvent(output, e) mod.viewWiFiApEvent(output, e)
@ -142,6 +153,8 @@ func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) {
mod.viewWiFiHandshakeEvent(output, e) mod.viewWiFiHandshakeEvent(output, e)
} else if e.Tag == "wifi.client.new" || e.Tag == "wifi.client.lost" { } else if e.Tag == "wifi.client.new" || e.Tag == "wifi.client.lost" {
mod.viewWiFiClientEvent(output, e) mod.viewWiFiClientEvent(output, e)
} else if e.Tag == "wifi.bruteforce.success" {
mod.viewWiFiBruteforceEvent(output, e)
} else { } else {
fmt.Fprintf(output, "[%s] [%s] %#v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e) fmt.Fprintf(output, "[%s] [%s] %#v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
} }

View file

@ -0,0 +1,65 @@
package events_stream
import (
"fmt"
"io"
"strings"
"github.com/bettercap/bettercap/v2/modules/zerogod"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/ops"
"github.com/evilsocket/islazy/tui"
)
func (mod *EventsStream) viewZeroConfEvent(output io.Writer, e session.Event) {
if e.Tag == "zeroconf.service" {
event := e.Data.(zerogod.ServiceDiscoveryEvent)
fmt.Fprintf(output, "[%s] [%s] service %s detected for %s (%s):%d with %d records\n",
e.Time.Format(mod.timeFormat),
tui.Green(e.Tag),
tui.Bold(event.Service.ServiceInstanceName()),
event.Service.AddrIPv4,
tui.Dim(event.Service.HostName),
event.Service.Port,
len(event.Service.Text),
)
} else if e.Tag == "zeroconf.browsing" {
event := e.Data.(zerogod.BrowsingEvent)
source := event.Source
if event.Endpoint != nil {
source = event.Endpoint.ShortString()
}
services := make([]string, 0)
for _, q := range event.Services {
services = append(services, tui.Yellow(q))
}
instPart := ""
if len(event.Instances) > 0 {
instances := make([]string, 0)
for _, q := range event.Instances {
instances = append(instances, tui.Green(q))
}
instPart = fmt.Sprintf(" and instances %s", strings.Join(instances, ", "))
}
textPart := ""
if len(event.Text) > 0 {
textPart = fmt.Sprintf("\n text records: %s\n", strings.Join(event.Text, ", "))
}
fmt.Fprintf(output, "[%s] [%s] %s is browsing (%s) for services %s%s\n%s",
e.Time.Format(mod.timeFormat),
tui.Green(e.Tag),
source,
ops.Ternary(event.Query.QR, "RESPONSE", "QUERY"),
strings.Join(services, ", "),
instPart,
textPart,
)
} else {
fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
}
}

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