Compare commits

...

663 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
Simone Margaritelli
4690a23ace releasing v2.31.1 2021-05-22 15:43:51 +02:00
Simone Margaritelli
0e2fd008e4 fix: fixed a bug in arp.spoof that caused targets not to be spoofed if not previously known to the attacker computer in terms of Mac address 2021-05-22 15:39:41 +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
Simone Margaritelli
badd13181d fix: removed broken test 2021-05-14 16:16:58 +02:00
Simone Margaritelli
b9cc36b6b6 fix: fixed core test 2021-05-14 15:54:31 +02:00
Simone Margaritelli
dd71ccf416 fix: updated gatt library (fixes #861) 2021-05-14 15:44:12 +02:00
Simone Margaritelli
dfe64ee4db fix: showing the entire error message when a command fails 2021-05-14 15:26:50 +02:00
Simone Margaritelli
82dd30c777 fix: less verbose logging 2021-05-14 15:21:47 +02:00
Simone Margaritelli
f42dcb72d5 fix: using newer macOS image on travis to avoid timeouts due to homebrew compilation forced by EOL OS (fixes #865) 2021-05-14 14:51:42 +02:00
Simone Margaritelli
4fc84f2907 new: new arp.spoof.skip_restore option (fixes #874) 2021-05-11 12:20:10 +02:00
Simone Margaritelli
8c00207e7e new: gps.new event now reports GPS data changes as they occur (fixes #878) 2021-05-11 12:02:26 +02:00
☸️
831020983c
Merge pull request #873 from buffermet/master
revert changes from #723 that prevented HTTP response header spoofing
2021-05-02 01:00:41 +10:00
☸️
821ce9aea2
revert changes from #723 2021-05-02 00:56:38 +10:00
Simone Margaritelli
c38de3a511 fix: support for negative numbers for decimal parameters (closes #866) 2021-04-21 14:02:36 +02:00
Simone Margaritelli
568c166fe1 releasing v2.31.0 2021-04-17 17:31:15 +02:00
Simone Margaritelli
ee14e96963 misc: small fix or general refactoring i did not bother commenting 2021-04-17 17:04:53 +02:00
Simone Margaritelli
05a185434b misc: small fix or general refactoring i did not bother commenting 2021-04-17 16:54:26 +02:00
Simone Margaritelli
e3078c7136 misc: small fix or general refactoring i did not bother commenting 2021-04-17 16:53:14 +02:00
Simone Margaritelli
131aa846b6 misc: added osx tests for tagless travis builds 2021-04-17 16:32:48 +02:00
Simone Margaritelli
22c95c0c4d misc: small fix or general refactoring i did not bother commenting 2021-04-17 00:19:05 +02:00
Simone Margaritelli
6206fc1b61
Delete codeql-analysis.yml 2021-04-12 20:10:03 +02:00
Simone Margaritelli
af85f9292a
Merge pull request #863 from jfcg/master
Create codeql-analysis.yml
2021-04-12 17:07:31 +02:00
Simone Margaritelli
80f7428afe fix: fixed a 'ble.recon off' panic on linux 2021-04-12 16:39:05 +02:00
Simone Margaritelli
eb384d67c1 misc: moved example script in dedicated repo 2021-04-12 14:36:27 +02:00
Simone Margaritelli
421df5035f misc: small fix or general refactoring i did not bother commenting 2021-04-12 14:25:37 +02:00
Serhat Şevki Dinçer
fee99c4116
Create codeql-analysis.yml 2021-04-12 15:24:12 +03:00
Simone Margaritelli
f2b6d9b708 misc: moved graph module to experimental branch for now 2021-04-12 14:09:03 +02:00
Simone Margaritelli
4dac3b9373 fix: handle disconnection (nil gateway) in routes monitor 2021-04-12 12:39:19 +02:00
Simone Margaritelli
c9ae0f360e misc: small fix or general refactoring i did not bother commenting 2021-04-11 01:26:29 +02:00
Simone Margaritelli
c47e3f6195 new: gateway.change event for MITM monitoring 2021-04-10 22:59:03 +02:00
Simone Margaritelli
43a93fd866 fix: refactored routing logic (fixes #701) 2021-04-10 21:55:00 +02:00
Simone Margaritelli
88a83192ef fix: do not trigger deauth events for frames sent by client stations or unknown access points 2021-04-10 14:48:30 +02:00
Simone Margaritelli
bc7d1d9663 misc: small fix or general refactoring i did not bother commenting 2021-04-09 16:31:13 +02:00
Simone Margaritelli
71ac5bb264 new: node context shows on click on the graphpage 2021-04-09 16:17:26 +02:00
Simone Margaritelli
93b7e7f2ed new: exposing the graph object to js 2021-04-09 13:46:33 +02:00
Simone Margaritelli
0042b77c36 fix: do not emit graph if empty 2021-04-09 12:45:06 +02:00
Simone Margaritelli
1d306e6cd2 fix: do not override req.Hostname in http proxy module script (fixes #678) 2021-04-09 12:42:24 +02:00
Simone Margaritelli
2b4188bb52 misc: small fix or general refactoring i did not bother commenting 2021-04-09 02:20:19 +02:00
Simone Margaritelli
3c506b7809 misc: small fix or general refactoring i did not bother commenting 2021-04-09 00:14:52 +02:00
Simone Margaritelli
d5fb7b6754 misc: small fix or general refactoring i did not bother commenting 2021-04-08 22:38:28 +02:00
Simone Margaritelli
6393dc1ea5 new: bot sending graphs 2021-04-08 22:13:57 +02:00
Simone Margaritelli
1be487843b new: graph.to_json 2021-04-08 20:54:55 +02:00
Simone Margaritelli
e465f9b145 misc: small fix or general refactoring i did not bother commenting 2021-04-08 19:06:10 +02:00
Simone Margaritelli
71634058a7 misc: several improvements to the graph module 2021-04-08 18:41:30 +02:00
Simone Margaritelli
5b8cb9a82c fix: check ssid probes for non printable characters 2021-04-07 17:58:18 +02:00
Simone Margaritelli
db275429c2 new: graph module 2021-04-07 17:04:42 +02:00
Simone Margaritelli
6aa8f45d20 misc: using script to detect karma attacks 2021-04-07 17:04:28 +02:00
Simone Margaritelli
bfe307ffe6 new: ticker now broadcasts a tick event 2021-04-07 17:03:47 +02:00
Simone Margaritelli
31b06638d8 fix: fixed a panic in net.show.meta when rendeing open ports 2021-04-07 13:16:20 +02:00
Simone Margaritelli
2dcfea02ce misc 2021-04-07 00:53:23 +02:00
Simone Margaritelli
662f5fb973 fix: don't print wifi.client.probe we generate 2021-04-07 00:52:57 +02:00
Simone Margaritelli
906969f1b3 new: wifi.probe to send fake client probe requests 2021-04-07 00:36:38 +02:00
Simone Margaritelli
8827a2af84 new: session scripts can now include other scripts via require('file') 2021-04-04 17:17:37 +02:00
Simone Margaritelli
2b1ff7d59f fix: fixed vm locking 2021-04-04 15:58:48 +02:00
Simone Margaritelli
3c20f2c9aa
misc: small fix or general refactoring i did not bother commenting 2021-04-04 15:32:15 +02:00
Simone Margaritelli
fb7bed9b6b
misc: small fix or general refactoring i did not bother commenting 2021-04-04 15:29:49 +02:00
Simone Margaritelli
40727063ec new: new -script allows to run JS code to instrument session 2021-04-04 15:15:32 +02:00
Simone Margaritelli
d5e5abcb9b fix: using static url for qemu 2021-04-04 01:28:15 +02:00
Simone Margaritelli
c2be8a440b fix: fixed net.probe mdns parsing from ipv6 2021-04-04 00:47:54 +02:00
Simone Margaritelli
fad6172b40
misc: small fix or general refactoring i did not bother commenting 2021-04-04 00:31:18 +02:00
Simone Margaritelli
b0f7e764dc fix: keep track of ipv6 packets 2021-04-04 00:30:09 +02:00
Simone Margaritelli
4d5e930e6c
misc: small fix or general refactoring i did not bother commenting 2021-04-04 00:06:34 +02:00
Simone Margaritelli
a6d5d5d048 new: implemented icmpv6 rogue router advertisement 2021-04-04 00:03:11 +02:00
Simone Margaritelli
c1520206a5
misc: small fix or general refactoring i did not bother commenting 2021-04-03 23:08:00 +02:00
Simone Margaritelli
57436a811c new: experimental ipv6 ndp spoofer (closes #851) 2021-04-03 22:55:03 +02:00
Simone Margaritelli
cbc1432358 new: net.sniff now supports ipv6 2021-04-03 19:20:12 +02:00
Simone Margaritelli
bef4c6abaa new: basic ipv6 support 2021-04-03 18:42:14 +02:00
Simone Margaritelli
d0b5c34763 new: module parameters now accept <iface-name> that will be resolved to the interface IP address 2021-04-03 16:41:55 +02:00
Simone Margaritelli
16891c4048
misc: small fix or general refactoring i did not bother commenting 2021-04-01 22:13:17 +02:00
Simone Margaritelli
6b821d2577
misc: small fix or general refactoring i did not bother commenting 2021-04-01 22:12:41 +02:00
Simone Margaritelli
240c4c3219 new: detection and parsing of deauthentication frames as wifi.deauthentication events 2021-03-31 00:47:56 +02:00
Simone Margaritelli
cea53b969e fix: collect additional frames for stations with key material (ref #810) 2021-03-30 23:19:06 +02:00
Simone Margaritelli
c68c88030d
misc: small fix or general refactoring i did not bother commenting 2021-03-30 11:59:13 +02:00
Simone Margaritelli
0d17ba3573
misc: small fix or general refactoring i did not bother commenting 2021-03-30 11:56:41 +02:00
Simone Margaritelli
6dd86c44fa fix: using iw if available to get supported wifi frequencies (fixes #743) 2021-03-29 17:55:33 +02:00
Simone Margaritelli
c4bbc129b6 fix: returning error when neither iw or iwconfig are found 2021-03-29 17:25:15 +02:00
Simone Margaritelli
67a0063ee4 fix: updated gatt library which fixes some linux issues 2021-03-23 19:29:36 +01:00
Simone Margaritelli
4f5f89b6e1 fix: do not add unhandled dns types to dns spoofing packets (closes #843) 2021-03-23 19:22:05 +01:00
Simone Margaritelli
d63122bab3 new: new -caplets-path argument to specify an alternative caplets base path (closes #850) 2021-03-23 19:05:58 +01:00
Simone Margaritelli
161124a3f4 releasing v2.30.2 2021-03-14 21:47:35 +01:00
Simone Margaritelli
b64dffb53f fix: fixed compilation issue on windows 2021-03-14 21:47:22 +01:00
Simone Margaritelli
3e12bb0290 releasing v2.30.1 2021-03-14 21:36:48 +01:00
Simone Margaritelli
1fea9f02fb
Merge pull request #848 from caquino/caquino/travis-fix
Travis and builds fixes and updates
2021-03-14 21:33:58 +01:00
Cassiano Aquino
9a10f0bf2a
change qemu url
update raspbian and golang

upgrade go from 1.15 to 1.16

upgrade libusb and npcap

add debug to npcap install

try nmap instead

change to nmap

remove deprecated sudo

move from npcap to winpcap
2021-03-14 20:25:47 +00:00
Simone Margaritelli
6392e21c40 misc: added git:changelog to release script 2021-03-14 16:07:50 +01:00
Simone Margaritelli
b3da4e30f7 fix: updated OUI list (closes #833) 2021-03-14 16:06:16 +01:00
Simone Margaritelli
ff91392866 new: any.proxy.src_port now supports multiple ports and port ranges (closes #845) 2021-03-14 16:03:09 +01:00
Simone Margaritelli
21aa14fd2f
misc: small fix or general refactoring i did not bother commenting 2021-03-14 15:42:09 +01:00
Simone Margaritelli
80a3e71193 docs: filled SECURITY.md 2021-03-14 15:41:15 +01:00
Simone Margaritelli
7c4da8a550
Merge pull request #829 from anonymous0011/patch-1
Create SECURITY.md
2021-03-14 15:38:18 +01:00
Simone Margaritelli
30f2fb0df1
Merge pull request #844 from drautb/rpi-dev
Enable packet proxy on ARM architectures
2021-03-14 15:35:54 +01:00
Simone Margaritelli
c1b93b8238 releasing v2.30 2021-03-12 15:02:07 +01:00
Simone Margaritelli
bd016d9388 new: replaced changelog and release scripts with stork script 2021-03-12 15:01:47 +01:00
Ben Draut
e0ff16f9f1 Enable packet proxy on ARM architectures
I couldn't find any explanation as to why the packet proxy isn't
enabled on ARM targets.

I've been successfully building/using it on a Raspberry Pi for the
last couple weeks. Is there a reason it shouldn't be enabled?

(The diff makes it look complicated, but I just copied
`packet_proxy_linux_amd64.go` to `packet_proxy_linux.go` and
then deleted `packet_proxy_linux_amd64.go`.)
2021-03-10 18:29:36 -07:00
Simone Margaritelli
08ae5e0920
Merge pull request #841 from drautb/master
Add start/stop callbacks to packet proxy
2021-03-10 17:58:00 +01:00
Ben Draut
32eee7d94b Fix bug in target parsing
When a MAC address with uppercase letters was provided, parsing would
return an error because the parsing logic would only attempt to remove
normalized versions (all lowercase) from the target list. This would
leave the address with uppercase letters in the target list, which it
would then try to interpet as an Alias. This fixes the bug by using
the original address form when removing it from the target list.
2021-03-09 20:30:48 -07:00
Ben Draut
ce5c5eb592 Add start/stop callbacks to packet proxy
This adds support for two additional functions in go plugins in the
`packet_proxy` module:

* `func OnStart() int`
* `func OnStop()`

These will be called when the packet proxy module is turned on and
off, respectively.
2021-03-05 13:07:21 -07:00
Simone Margaritelli
17799c0357 fix: updated readline, using syscall package instead of constants (fixes #776) 2021-02-28 16:18:32 +01:00
Simone Margaritelli
50c1505fd7
Merge pull request #830 from HarshCasper/master
Potential Code-Refactor and Bug Fixes
2021-02-20 10:34:07 +01:00
Harsh Mishra
e01cbfbe2e
Delete .deepsource.toml 2021-02-20 15:02:44 +05:30
Harsh Bardhan Mishra
df806e60da
Merge pull request #2 from HarshCasper/deepsource-fix-a44a7647
Remove unnecessary comparison with bool
2021-02-01 14:30:12 +05:30
deepsource-autofix[bot]
6591de4b63
Remove unnecessary comparison with bool 2021-02-01 08:57:11 +00:00
Harsh Bardhan Mishra
91386cc725
Merge pull request #1 from HarshCasper/deepsource-fix-dd2afe80
Remove unnecessary guard around `delete`
2021-02-01 14:18:51 +05:30
deepsource-autofix[bot]
a26b3f366b
Remove unnecessary guard around delete 2021-02-01 08:47:34 +00:00
DeepSource Bot
c0e9f8cbdd Add .deepsource.toml 2021-02-01 08:44:39 +00:00
anonymous0011
e4414b7a45
Create SECURITY.md 2021-02-01 01:49:01 +01:00
Simone Margaritelli
3ac520c744 fix: better phrasing (tnx @nieldk) 2021-01-27 17:11:15 +01:00
Simone Margaritelli
ac9c8d3865 fix: added sasl authentication support for the c2 module 2021-01-27 11:12:50 +01:00
Simone Margaritelli
583a54c194 new: new c2 module, first draft 2021-01-27 00:17:25 +01:00
Simone Margaritelli
35dbb8a368 Releasing v2.29 2021-01-20 11:41:33 +01:00
Simone Margaritelli
fef32192be
Merge pull request #812 from bonedaddy/master
Fix Issue 811
2021-01-15 17:56:07 +01:00
bonedaddy
07f7483ba3
network: remove mutex lock that breaks webui and api 2021-01-12 20:58:30 -08:00
Simone Margaritelli
3cfbcd900d misc: added openwrt makefile by DeathCamel58 for reference 2021-01-09 00:19:41 +01:00
Simone Margaritelli
cf7d06b30d misc: updated the version of go used to compile releases 2021-01-09 00:13:48 +01:00
Simone Margaritelli
2610d4b1e4 fix: do not close serial port if nil (fixes #805) 2021-01-08 23:45:08 +01:00
Simone Margaritelli
ffe20c5357
Merge pull request #801 from bonedaddy/lock-copy
Fix Lock Copy Warnings / Go Vet General Fixes
2021-01-06 10:54:09 +01:00
Simone Margaritelli
e6ecd6504f
Merge pull request #799 from bonedaddy/wifi#lock-optimize
WiFi Network Locking Optimizations
2021-01-06 10:51:36 +01:00
bonedaddy
05b8e3065e
go vet fixes 2020-12-24 17:31:40 -08:00
bonedaddy
10817d574c
wifi.go: dont claim read lock until it is needed 2020-12-23 13:22:48 -08:00
bonedaddy
08cad808ef
fix slice memory allocation optimization 2020-12-22 17:29:21 -08:00
bonedaddy
ac4b1f6e9e
network: optimize wifi locking and include memory allocation optimization 2020-12-22 17:10:33 -08:00
☸️
8acf81f61d
Merge pull request #781 from buffermet/master
Remove proxy-side TLD spoofing.
2020-10-15 01:06:52 +10:00
buffermet
3a2db2918a Remove proxy-side TLD spoofing. 2020-10-15 00:49:42 +10:00
☸️
69715137da
Merge pull request #778 from buffermet/master
Fix Content-Type parsing, improve HTTP header parsing.
2020-10-13 16:57:45 +10:00
buffermet
0a0cefc5d8 Fix content type parsing error, improve regexp search performance, strip header names and values. 2020-10-04 16:35:28 +10:00
buffermet
dd08976e8b Update HTTP header regexp selector. 2020-10-03 02:00:41 +10:00
Simone Margaritelli
6bf46c7ff6 misc: removed useless badges from the README 2020-09-25 16:52:36 +02:00
Simone Margaritelli
6f9f1954cb new: gps module can use both serial and gpsd (based on pr #680 from @fheylis) 2020-09-25 16:46:45 +02:00
Simone Margaritelli
a02f355926 misc: updated dependencies 2020-09-25 16:10:26 +02:00
Simone Margaritelli
6dba5d2bb9
Merge pull request #759 from dafyk/patch-1
Set Content-Type for PAC and WPAD file
2020-09-25 15:49:37 +02:00
Da-FyK
d3a46a6332
Set Content-Type for PAC and WPAD file
For Proxy Auto-Configuration (PAC) or Web Proxy Auto-Discovery (WPAD) to work correctly HTTP server needs to send "application/x-ns-proxy-autoconfig" Content-Type header. I've hardoced "proxy.pac" and "wpad.dat" because i am not golang coder and i dont know how to make it configurable. I hope somebody finds this usefull too and can make better PR.
2020-07-25 02:44:43 +02:00
Simone Margaritelli
e795caa334 Releasing v2.28 2020-07-03 14:30:03 +02:00
Simone Margaritelli
e3846cf416
Merge pull request #703 from FrankSpierings/skipacquired
Prevent deauth/assoc for AP's that have already been captured
2020-07-03 14:26:17 +02:00
Simone Margaritelli
b1381568d0
Merge pull request #752 from denandz/master
Make domain matches in the dns.spoof module case insensitive
2020-07-03 14:25:03 +02:00
Simone Margaritelli
b53b5b08d6
Merge pull request #723 from Petitoto/sslstrip
Fix sslstrip & some related issues in http(s).proxy and dns.spoof
2020-07-03 14:23:52 +02:00
DoI
ef27a79ec3 Make domain matches in the dns.spoof module case insensitive 2020-07-02 19:33:47 +12:00
Simone Margaritelli
6725a2aa53
Merge pull request #726 from guanicoe/patch-1
Update mysql_server.go
2020-05-15 15:13:11 +02:00
Petitoto
62e253ee8b Fix conflict with last commit 2020-05-12 16:42:33 +01:00
Petitoto
090ba11e5a Add merged pull requests 2020-05-12 16:38:13 +01:00
guanicoe
6fabe025a3
Update mysql_server.go
typo
2020-05-04 10:58:19 +00:00
Simone Margaritelli
1957d34ca2
Merge pull request #720 from alrs/ip-equal
modules/arp_spoof: use net.IP to compare addresses
2020-05-04 12:57:32 +02:00
Simone Margaritelli
9aab1295ca
Merge pull request #722 from stef03/https-proxy-client-ip
Fix problem with the client ip in https.proxy
2020-05-04 12:57:01 +02:00
Petitoto
40c7203d1f Fix sslstrip & some related issues in http(s).proxy and dns.spoof 2020-04-20 13:35:32 +01:00
stefan_hofbauer
a01e058d82 Fix problem with the client ip in https.proxy as described in https://github.com/bettercap/caplets/issues/45 2020-04-19 15:50:45 +02:00
Lars Lehtonen
bc05ed56fc
modules/arp_spoof: use net.IP to compare addresses 2020-04-14 09:22:58 -07:00
Simone Margaritelli
318029c022 Releasing v2.27.1 2020-04-13 17:35:11 +02:00
Simone Margaritelli
e00ba3268c
Merge pull request #715 from dadav/fix/websocket-ping
Increase timeout when ping is sent
2020-04-09 17:15:58 +02:00
dadav
24a1e34237 otherwise a read timeout will occasionally occure 2020-04-08 21:32:23 +02:00
Simone Margaritelli
f4abf62f76 misc: infosec is a circus, most of them are clowns 2020-04-08 10:58:46 +02:00
Simone Margaritelli
f5fb86da48 Merge branch 'master' of github.com:bettercap/bettercap 2020-04-08 10:50:22 +02:00
Simone Margaritelli
81b1cae131 Releasing v2.27 2020-04-08 10:49:46 +02:00
Simone Margaritelli
877600fec1
Merge pull request #705 from buffermet/master
add dns.spoof.ttl env variable
2020-04-08 10:47:26 +02:00
Simone Margaritelli
1cf74121a3
Merge pull request #706 from boolooper/master
Improve code and fix race conditions
2020-04-08 10:46:55 +02:00
Simone Margaritelli
61d9316cad fix: logging error when read from websocket fails 2020-04-08 10:36:04 +02:00
Simone Margaritelli
a39a0018eb
Merge pull request #709 from skooch/skooch-txpower-fix
Fix iw txpower syntax
2020-03-28 17:40:35 +01:00
skooch
3612e767d7
Update iw txpower syntax to only use int
This is probably due to a bug in iw, we do this because if we include "mBm", the strtol() that iw does has a check on endptr that returns 2, even if the txpower is valid.
2020-03-20 16:07:43 +11:00
skooch
140f33109f
Fix iw txpower syntax
The syntax used to set the txpower is incorrect and misses the keyword "fixed", this results in wireless adapter drivers that only support iw failing to initialize within bettercap.
This bug was introduced in commit bettercap/bettercap@2f3390cf36 which was a fix for issue bettercap/bettercap#657 due to maybe the code not being tested extensively.
2020-03-19 14:30:27 +11:00
Hasibul Hasan Anik
b253e6b4df Remove unnecessary variable assignment 2020-03-05 13:02:30 +06:00
Hasibul Hasan Anik
3b57b0cb38 Remove nil check for empty map. 2020-03-05 13:01:46 +06:00
Hasibul Hasan Anik
8c3f60641e Remove unnecessary and empty if block.
The if block itself is empty. Calling recover is enough to recover from panic
2020-03-05 13:01:17 +06:00
Hasibul Hasan Anik
050bd28511 Kepp sync.WorkerGroup.Add() outside of goroutine
The workergroup should be added before starting the worker.
The worker routine itself should not start the worker. It causes race condition.
2020-03-05 12:58:22 +06:00
Hasibul Hasan Anik
1fee1f718d Remove unnecessary fmt.Sprintf
the tui.Bold function already returning a string.
There has not need to convert it ot string by using fmt.Sprintf
2020-03-05 12:56:04 +06:00
buffermet
51dfd86898
Update dns_spoof.go 2020-03-05 08:44:12 +10:00
buffermet
2f14254c4c
Update dns_spoof.go 2020-03-05 08:43:21 +10:00
buffermet
466105a1af
Update dns_spoof.go 2020-03-05 08:39:07 +10:00
buffermet
03951d9d01
Update dns_spoof.go 2020-03-05 08:35:42 +10:00
buffermet
e4682168df
add dns.spoof.ttl env variable 2020-03-05 08:34:45 +10:00
Frank Spierings
a0a0963cd5 Implemented a way to not send deauthentication and/or association packets to AP's for which key material was already acquired 2020-02-28 12:05:23 +01:00
Simone Margaritelli
8ae28f4b3d
Merge pull request #700 from alnaeemi/master
Correcting content-length for stripped response body
2020-02-20 09:11:57 +01:00
mo
58b31d351f Correcting content-length for stripped response body 2020-02-13 18:08:44 -06:00
Simone Margaritelli
bb1f6cd0e8 new: added new http.proxy.redirect and https.proxy.redirect parameters to optionally disable iptables port redirection 2020-01-23 15:48:57 +01:00
Simone Margaritelli
9bf0139181
Merge pull request #669 from coderobe/patch-setrfmon
modules/wifi: Fix handle activation when monitor device is already set up
2020-01-17 11:48:01 +01:00
Robin Broda
15db10ad45 modules/wifi: Fix handle activation when monitor device is already set up 2020-01-16 23:51:57 +01:00
Simone Margaritelli
0e4f752ce4
Merge pull request #668 from coderobe/patch-1
modules/wifi: fix SetSnapLen error message text
2020-01-16 15:37:39 +01:00
Robin B
524e91af3d
modules/wifi: fix SetSnapLen error message text 2019-12-07 16:08:09 +01:00
Simone Margaritelli
2f3390cf36 fix: using iw instead of iwconfig whenever possible (fixes #657) 2019-11-25 11:59:04 +01:00
Simone Margaritelli
83c6cde152 fix: fixed a bug with wifi.recon.channel clear when wifi.interface is nil (fixes #661) 2019-11-25 11:38:38 +01:00
evilsocket
f9865299b3
Merge pull request #651 from alrs/tls-swap-err-returns
tls: Swap Error Returns
2019-11-25 11:33:10 +01:00
evilsocket
eb8bf4639b
Merge pull request #652 from alrs/caplet-error-returns
caplets: Swap Error Returns
2019-11-25 11:32:48 +01:00
evilsocket
114ac90ce0
Merge pull request #653 from alrs/ble-swap-error-returns
modules/ble: Swap Error Return
2019-11-25 11:32:36 +01:00
Lars Lehtonen
c980a7b4b2
modules/ble: swap error returns 2019-11-13 17:08:41 -08:00
Lars Lehtonen
07459424fb
caplets: Swap Error Returns 2019-11-13 15:39:20 -08:00
Lars Lehtonen
372c2d6428
tls: fix CertConfigFromModule() return order 2019-11-13 14:31:55 -08:00
Lars Lehtonen
7d7ab1937e
tls: fix CreateCertificate() return order 2019-11-13 14:31:48 -08:00
evilsocket
63d5ce7118
Merge pull request #648 from nipsufn/bettercap-644
Dockerfile: fix issue #644 and following problems
2019-11-13 02:23:01 +01:00
evilsocket
6fd7827eea
Merge pull request #650 from alrs/fix-events-stream-err
modules/events_stream: fix dropped error
2019-11-13 02:22:34 +01:00
Lars Lehtonen
fb0c2df643
modules/events_stream: fix dropped error 2019-11-11 21:58:31 -08:00
Simone Margaritelli
9c3790764a fix: fixed gateway regexp for macOS (closes #645) 2019-11-11 17:27:42 +01:00
nipsufn
a642a19b5f Dockerfile: Use go modules instead of third party dependency tool (fe7e103387) 2019-11-06 22:02:45 +01:00
nipsufn
d42621aa59 Dockerfile: fix caplets 2019-11-06 21:33:28 +01:00
nipsufn
cc9baaca1b Adjust Dockerfile for changes introduced in e06b832911 2019-11-06 21:03:02 +01:00
nipsufn
4b4bd128ce Fix https://github.com/bettercap/bettercap/issues/644 as described in https://github.com/golang/dep/issues/2055#issuecomment-456782205 2019-11-06 20:42:54 +01:00
evilsocket
6755d8c880
Merge pull request #646 from sten13/feature/sniff-basic-auth
Feature/sniff basic auth
2019-11-04 12:42:58 +01:00
Stephan Neuhaus
20a46151de Merge branch 'feature/sniff-basic-auth' of github.com:sten13/bettercap into feature/sniff-basic-auth 2019-11-01 11:15:41 +01:00
Stephan Neuhaus
a88c9078b3 View HTTP Basic authorization credentials when sniffing
Undid changes in events_view_http.go

Undid more changed to events_view_http.go

Undid more changed to events_view_http.go

Vew HTTP Basic authnoriyation credentials when sniffing

Undid changes in events_view_http.go

View HTTP Basic authorization credentials when sniffing

Undid changes in events_view_http.go

Undid more changed to events_view_http.go

Undid more changed to events_view_http.go

Vew HTTP Basic authnoriyation credentials when sniffing

Undid changes in events_view_http.go

Undid more changes
2019-11-01 11:15:21 +01:00
Stephan Neuhaus
3e1c024824 Undid more changes 2019-11-01 11:13:05 +01:00
Stephan Neuhaus
d388cf0f1a Merge branch 'feature/sniff-basic-auth' of github.com:sten13/bettercap into feature/sniff-basic-auth 2019-11-01 11:10:30 +01:00
Stephan Neuhaus
a3b80fba74 View HTTP Basic authorization credentials when sniffing
Undid changes in events_view_http.go

Undid more changed to events_view_http.go

Undid more changed to events_view_http.go

Vew HTTP Basic authnoriyation credentials when sniffing

Undid changes in events_view_http.go
2019-11-01 11:09:56 +01:00
Stephan Neuhaus
a400b3a766 Fixed conflict 2019-11-01 11:07:37 +01:00
Stephan Neuhaus
00778f1c80 View HTTP Basic authorization credentials when sniffing
Undid changes in events_view_http.go

Undid more changed to events_view_http.go

Undid more changed to events_view_http.go
2019-11-01 11:04:19 +01:00
Stephan Neuhaus
76c1e41f70 Undid changes in events_view_http.go 2019-11-01 10:59:00 +01:00
Stephan Neuhaus
d21793fc8f Vew HTTP Basic authnoriyation credentials when sniffing 2019-11-01 10:55:45 +01:00
Simone Margaritelli
e51e097e43 Releasing v2.26.1 2019-10-26 10:16:55 +02:00
Simone Margaritelli
4069887cf6 fix: do not save dummy/invalid half handshakes 2019-10-26 10:16:40 +02:00
Simone Margaritelli
5a6a7143f2 Releasing v2.26 2019-10-18 15:39:39 +02:00
Simone Margaritelli
176017a1f7
misc: small fix or general refactoring i did not bother commenting 2019-10-18 15:39:20 +02:00
Simone Margaritelli
9b8354d72c go get -u 2019-10-18 15:33:05 +02:00
Simone Margaritelli
266edb0631
misc: small fix or general refactoring i did not bother commenting 2019-10-18 15:27:49 +02:00
Simone Margaritelli
eef9b64d01 Merge branch 'rumpelsepp-master' 2019-10-18 15:23:49 +02:00
Simone Margaritelli
b8ff8a00e8 Merge branch 'master' of git://github.com/rumpelsepp/bettercap into rumpelsepp-master 2019-10-18 15:23:25 +02:00
Simone Margaritelli
ec9c203618 added deploy key for travis-ci 2019-10-18 15:06:20 +02:00
evilsocket
cc6a417869
Merge pull request #639 from caquino/master
New build system using travis-ci
2019-10-18 15:00:11 +02:00
Cassiano Aquino
a3a7cf07e2
new travis configuration (#1)
* New builder
2019-10-18 11:04:41 +01:00
evilsocket
3fee68fa37
Create FUNDING.yml 2019-10-09 16:04:03 +02:00
Simone Margaritelli
6d02fa8f38 new: updated build script to generate a linux/armv6l image (rpi0w+raspbian) 2019-10-04 20:43:40 +02:00
Simone Margaritelli
5b350154b0 fix: fixed naming for android armv7l builds 2019-10-04 20:23:13 +02:00
Simone Margaritelli
8c424765f6 merge 2019-09-28 17:43:31 +02:00
Simone Margaritelli
caba6e1952 new: wifi.client.probe.ap.filter and wifi.client.probe.sta.filter actions to filter wifi client probes 2019-09-28 17:43:07 +02:00
evilsocket
f1ef4bcb35
Merge pull request #630 from ns3777k/578-concurrent-read-write-meta
lock meta mutex during marshaling
2019-09-27 14:01:44 +02:00
Nikita Safonov
74ada7e865 lock meta mutex during marshaling 2019-09-26 16:52:31 +03:00
Simone Margaritelli
d0e311e283 Releasing v2.25 2019-09-26 14:30:01 +02:00
Simone Margaritelli
5ff8e3e4fa misc: added .idea to .gitignore 2019-09-26 13:58:31 +02:00
evilsocket
12a11ef19d
new: wifi.min.rssi, wifi.ap.ttl and wifi.sta.ttl changes are now applied in realtime 2019-09-15 15:10:56 +02:00
evilsocket
53b0d81f20
misc: small fix or general refactoring i did not bother commenting 2019-09-10 12:45:33 +02:00
evilsocket
9dd6145a39
misc: small fix or general refactoring i did not bother commenting 2019-09-08 16:48:32 +02:00
evilsocket
8ac2c0163b
misc: small fix or general refactoring i did not bother commenting 2019-09-08 16:47:24 +02:00
evilsocket
2e2a5248b4
new: ble module is now available for macOS 2019-09-08 16:39:27 +02:00
evilsocket
29c571cf16
fix: updated dependencies (fixes some issues with BLE) 2019-09-08 16:36:01 +02:00
evilsocket
f14470c8f6
new: new hid.ttl parameter (fixes #560) 2019-09-08 16:19:13 +02:00
evilsocket
8ec91c9206
new: implemented ble.timeout and ble.ttl parameters (ref #560) 2019-09-08 16:13:46 +02:00
evilsocket
4cba4f9ff2
fix: handling panics while decoding packets (fixes #612) 2019-09-07 18:11:15 +02:00
evilsocket
4c2dd1b411
misc: small fix or general refactoring i did not bother commenting 2019-09-07 17:58:19 +02:00
evilsocket
32e1bf8a7b
new: new wifi.ap.ttl and wifi.sta.ttl parameters 2019-09-07 17:53:59 +02:00
evilsocket
e3ed2ca5aa
Merge pull request #618 from realgam3/realgam3_features
features & bugfixes
2019-09-07 13:31:36 +02:00
realgam3
4181cd1c42 Trying not to invent the wheel 2019-09-06 18:05:07 +03:00
realgam3
2eb00ee11a Fix user home dir when using sudo on linux 2019-09-06 17:56:36 +03:00
realgam3
c46bb905b9 Added caplets windows compatibility 2019-09-06 17:25:54 +03:00
realgam3
11d2756283 Added dropCallback to drop packets instead of just changing it 2019-09-06 02:31:48 +03:00
realgam3
709232dba2 Added HTTPRequest to otto runtime 2019-09-05 13:50:17 +03:00
evilsocket
ff3add0fe2
misc: small fix or general refactoring i did not bother commenting 2019-08-22 13:48:52 -04:00
evilsocket
a79ed9b4d4
misc: small fix or general refactoring i did not bother commenting 2019-08-22 13:36:40 -04:00
evilsocket
2aa6fea92c
misc: small fix or general refactoring i did not bother commenting 2019-08-22 13:35:18 -04:00
evilsocket
da565afa9a
new: new wifi.handshakes.aggregate parameter to control how handshakes get saved 2019-08-22 13:21:52 -04:00
evilsocket
672a9f2706
fix: saving half handshakes 2019-08-19 15:59:43 -04:00
evilsocket
3d31bf3712
new: reporting if wifi handshakes are full or half 2019-08-19 13:56:18 -04:00
evilsocket
9e9b984fec
new: added support for half WPA handshakes (https://hashcat.net/forum/thread-6745-post-36007.html) 2019-08-17 22:33:26 -04:00
evilsocket
b57661a097
misc: small fix or general refactoring i did not bother commenting 2019-08-17 14:17:08 -04:00
evilsocket
33797b120e
Merge pull request #601 from yungtravla/master
add boolean for dumping HTTP bodies in hex format
2019-08-17 14:01:18 -04:00
evilsocket
8ae7f79b4f
Merge pull request #603 from dandare100/master
Added beacon packet to handshake cap file for PMKID assoc attack
2019-08-17 14:00:41 -04:00
evilsocket
f89d8b0144
Merge pull request #610 from ge0rg/hid_sleep
HID: implement ducky SLEEP command
2019-08-17 13:57:54 -04:00
evilsocket
209f6878c4
Merge pull request #611 from ge0rg/hid_menu
HID: add MENU key to generic keymap
2019-08-17 13:57:29 -04:00
Georg Lukas
ba76631379 HID: add MENU key to generic keymap 2019-08-15 17:28:27 +02:00
Georg Lukas
d2e449370c HID: implement ducky SLEEP command 2019-08-15 17:26:05 +02:00
root
5302f7f3f3 Added beacon packet to handshake cap file for PMKID assoc attack 2019-08-04 19:44:09 +02:00
yungtravla
5ab45693c5
add boolean for dumping HTTP bodies in hex format 2019-08-01 17:45:16 +10:00
yungtravla
3d6e28ea45
add boolean for dumping HTTP bodies in hex format 2019-08-01 17:42:47 +10:00
Stefan Tatschner
6406885928 Update broken tests 2019-06-30 22:22:36 +02:00
Stefan Tatschner
d22befab45 Update .travis to reflect Makefile changes 2019-06-30 22:14:56 +02:00
Stefan Tatschner
e06b832911 Fix broken test invokation in Makefile
The Makefile did call the test suite in a broken way. It always reported
passed tests, since the exit code was always 0.
2019-06-30 22:12:47 +02:00
Stefan Tatschner
fe7e103387 Use go modules instead of third party dependency tool
We have go modules now built in. Let's use it!
2019-06-30 22:11:23 +02:00
evilsocket
4a96bf641a
Releasing v2.24.1 2019-06-22 15:12:17 +02:00
evilsocket
c26f3ea1bb
fix: setting RemoteAddr field of a proxied request 2019-06-13 13:56:31 +02:00
evilsocket
1f3c009d15
fix: fixed a nil pointer dereference in the http banner grabber of syn.scan 2019-05-07 13:35:01 +02:00
evilsocket
23d8305e85
Merge pull request #566 from Matrix86/master
Fix on http proxy
2019-05-02 18:52:02 +02:00
Gianluca
be62757efa fix: http proxy modules couldn't handle properly requests with port number in the URL. 2019-05-02 18:25:19 +02:00
evilsocket
f8566d6020
fix: fixed a nil pointer dereference when wifi.show is called but the wifi module is not running (fixes #562) 2019-05-01 12:27:52 +02:00
evilsocket
3a4d730fce
fix: updated dependencies (fixes #561) 2019-05-01 12:21:53 +02:00
evilsocket
e8578e829c
Merge pull request #556 from djerfy/add-docker-image-latest
Add docker image latest
2019-04-28 12:14:10 +02:00
Jérémy CHABERNAUD
ae02658ebc
Add docker image latest 2019-04-27 23:24:48 +02:00
evilsocket
67eef05892
fix: logs when the api.rest http2 stream is closed are now debug logs 2019-04-25 18:16:35 +02:00
evilsocket
5e58a393b5
fix: fixed release script to update stable docker image (fixes #553) 2019-04-24 20:15:49 +02:00
evilsocket
95bc9b9d78
fix: fix an alignment issue for atomic ops on arm 2019-04-23 14:14:23 +02:00
evilsocket
a910a3729e
misc: small fix or general refactoring i did not bother commenting 2019-04-22 17:16:29 +02:00
evilsocket
b868408c17
misc: small fix or general refactoring i did not bother commenting 2019-04-22 16:29:58 +02:00
evilsocket
2bd768f065
new: net.probe is now be able to actively discover mDNS services 2019-04-22 15:39:23 +02:00
evilsocket
45951d2f82
new: implemented mDNS server / spoofer (closes #542) 2019-04-22 13:53:32 +02:00
evilsocket
385c8e3926
new: added dns CHAOS banner grabber to syn.scan 2019-04-22 12:33:48 +02:00
evilsocket
8cb330562b
misc: small fix or general refactoring i did not bother commenting 2019-04-22 12:02:37 +02:00
evilsocket
4e397a71d5
misc: small fix or general refactoring i did not bother commenting 2019-04-22 12:02:12 +02:00
evilsocket
30d9415d8c
misc: small fix or general refactoring i did not bother commenting 2019-04-22 11:47:36 +02:00
evilsocket
b8d9179def
misc: small fix or general refactoring i did not bother commenting 2019-04-22 11:13:24 +02:00
evilsocket
8c41e048d5
misc: small fix or general refactoring i did not bother commenting 2019-04-22 11:02:53 +02:00
evilsocket
8d66e68fc2
misc: small fix or general refactoring i did not bother commenting 2019-04-22 11:02:16 +02:00
evilsocket
df8ebd2525
misc: small fix or general refactoring i did not bother commenting 2019-04-21 20:53:27 +02:00
evilsocket
cd249687da
fix: syn.scanner now uses a dedicated pcap handle to prevent deadlocks and improve performances 2019-04-21 20:37:41 +02:00
evilsocket
8257d25ff3
fix: api.rest and https.server certificates are now correctly generated with IsCA to false 2019-04-21 19:55:53 +02:00
evilsocket
070708c307
new: improved syn.scan module performances when scanning multiple addresses 2019-04-21 16:26:37 +02:00
evilsocket
aea68460c8
new: syn.scan will now perform basic tcp banner grabbing 2019-04-21 15:45:32 +02:00
evilsocket
5a62546c50
fix: made BLE module less verbose by switching some of the logs to debug ones 2019-04-21 14:00:43 +02:00
evilsocket
aa3f4366a2
misc: small fix or general refactoring i did not bother commenting 2019-04-21 13:47:41 +02:00
evilsocket
0eb34e61fe
misc: updated dependencies 2019-04-21 13:47:08 +02:00
evilsocket
64d316af5b
fix: fixed compilation issue related to mdlayher/raw dependency (ref #468) 2019-04-21 13:37:53 +02:00
evilsocket
7a80dc2b49
Merge pull request #548 from stefanoj3/minor_improvements
Minor improvements
2019-04-21 13:26:33 +02:00
Stefano Gabryel
0e857e4467 Minor improvements to prevent unecessaries allocations 2019-04-21 07:26:54 +02:00
Stefano Gabryel
8f761dd76c Fixing ignored error in trigger list 2019-04-21 07:26:54 +02:00
evilsocket
29bf0d94f1
misc: set gps module default baud rate to 4800bps 2019-04-19 15:37:55 +02:00
evilsocket
936e0f740a
misc: small fix or general refactoring i did not bother commenting 2019-04-18 12:46:37 +02:00
evilsocket
2593f9160d
misc: small fix or general refactoring i did not bother commenting 2019-04-18 12:46:11 +02:00
evilsocket
5f973629d3
fix: fixing CORS headers only if sslstrip is enabled (fixes #543) 2019-04-18 12:44:50 +02:00
evilsocket
1a6faa9f66
misc: updated gatt library to fix an invalid memory access bug 2019-04-18 10:56:10 +02:00
evilsocket
9aaa13b6f6
Merge pull request #541 from ShySec/master
goroutine references address overwritten in loop; pass-by-value instead
2019-04-14 10:34:03 +03:00
kelson
01d33415a6 goroutine references address overwritten in loop; pass-by-value 2019-04-13 17:26:02 -04:00
evilsocket
4dadc3f41b
Merge pull request #538 from yungtravla/master
return more endpoint information with req.Client
2019-04-10 13:56:38 +03:00
yungtravla
62d46f5045
fix NewHash function 2019-04-10 20:27:17 +10:00
yungtravla
2b4b58462c
return more endpoint information with req.Client 2019-04-10 19:54:40 +10:00
evilsocket
ee809b6083
new: exporting hardware resources usage from api.rest 2019-04-09 12:08:30 +03:00
evilsocket
973c88fd31
misc: small fix or general refactoring i did not bother commenting 2019-04-09 10:56:15 +03:00
evilsocket
5dd248cef8
misc: small fix or general refactoring i did not bother commenting 2019-04-09 10:52:48 +03:00
evilsocket
0b430dd488
new: syn.scan can now resolve port services too 2019-04-09 10:52:15 +03:00
evilsocket
9c381a449d
misc: small fix or general refactoring i did not bother commenting 2019-04-08 11:44:57 +03:00
evilsocket
28063ff7c0
fix: fixed a lock issue on the wifi modules (fixes #535) 2019-04-06 17:01:00 +02:00
evilsocket
6e90b3d26c
Merge pull request #533 from jmg-duarte/patch-1
Typo fix
2019-04-06 16:02:27 +02:00
José Duarte
bca0b13cbf
Update README.md 2019-04-04 10:03:26 +01:00
evilsocket
1f37381fde
fix: when net.sniff is sniffing a mDNS hostname, it'll update the endpoint field 2019-04-04 10:01:49 +02:00
evilsocket
126cb7febf
misc: decoupled session record loading to external package 2019-04-03 10:26:54 +02:00
evilsocket
b743b26dde
misc: decoupled session record reader from the modules 2019-04-03 09:39:28 +02:00
evilsocket
36e5fe8bdb
fix: modules that require net.recon can now specify it as a dependency 2019-04-02 00:06:43 +02:00
evilsocket
5ef330f80b
misc: small fix or general refactoring i did not bother commenting 2019-03-30 18:19:34 +01:00
evilsocket
f9ebb1a77e
fix 2019-03-30 17:33:02 +01:00
evilsocket
460bd9b159
new: api.rest.record.clock parameter to decide delay per sample 2019-03-30 17:32:53 +01:00
evilsocket
0113286b4f
fix: gracefully handling hid receiver disconnection 2019-03-30 16:27:56 +01:00
evilsocket
afe300cd8a
fix: gracefully handling wifi device disconnection 2019-03-30 16:17:26 +01:00
evilsocket
54116f7fbe
fix: fixed replay time computation using actual dates instead of the assumption of one frame per second 2019-03-30 13:59:08 +01:00
evilsocket
50d01429cd
new: added Updated field to session.GPS 2019-03-30 00:26:38 +01:00
evilsocket
2f53e40f98
Revert "misc: keeping frames in memory as compressed"
This reverts commit 054d8a5a3e.
2019-03-29 21:52:03 +01:00
evilsocket
054d8a5a3e
misc: keeping frames in memory as compressed 2019-03-29 21:45:43 +01:00
evilsocket
5fb4cd6a5a
misc: small fix or general refactoring i did not bother commenting 2019-03-29 21:23:08 +01:00
evilsocket
1eec682aeb
new: new ble.device parameter to set HCI index (closes #519) 2019-03-29 20:16:41 +01:00
evilsocket
c3f0e3598b
misc: small fix or general refactoring i did not bother commenting 2019-03-29 20:03:46 +01:00
evilsocket
ec28399677
fix: updated gatt library with latest fixes 2019-03-29 19:52:24 +01:00
evilsocket
4d5876db2f
misc: small fix or general refactoring i did not bother commenting 2019-03-29 19:45:14 +01:00
evilsocket
fdc26ca3aa
misc: reporting session replay loading progress as api.rest state object 2019-03-29 19:31:20 +01:00
evilsocket
a411607a57
misc: added loading boolean flag to api.rest state object 2019-03-29 18:00:48 +01:00
evilsocket
0a31ac8167
new: implemented api.rest.record and api.rest.replay 2019-03-29 16:20:31 +01:00
evilsocket
4713d25ea7
misc: small fix or general refactoring i did not bother commenting 2019-03-27 16:47:28 +01:00
2947 changed files with 402014 additions and 446096 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

12
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: evilsocket
patreon: evilsocket
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

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.

38
.github/ISSUE_TEMPLATE/default_issue.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: General Issue
about: Write a general issue or bug report.
---
# Prerequisites
Please, before creating this issue make sure that you read the [README](https://github.com/bettercap/bettercap/blob/master/README.md), that you are running the [latest stable version](https://github.com/bettercap/bettercap/releases) and that you already searched [other issues](https://github.com/bettercap/bettercap/issues?q=is%3Aopen+is%3Aissue+label%3Abug) to see if your problem or request was already reported.
! PLEASE REMOVE THIS PART AND LEAVE ONLY THE FOLLOWING SECTIONS IN YOUR REPORT !
---
*Description of the bug or feature request*
### Environment
Please provide:
* Bettercap version you are using ( `bettercap -version` ).
* OS version and architecture you are using.
* Go version if building from sources.
* Command line arguments you are using.
* Caplet code you are using or the interactive session commands.
* **Full debug output** while reproducing the issue ( `bettercap -debug ...` ).
### Steps to Reproduce
1. *First Step*
2. *Second Step*
3. *and so on...*
**Expected behavior:** *What you expected to happen*
**Actual behavior:** *What actually happened*
--
**♥ ANY INCOMPLETE REPORT WILL BE CLOSED RIGHT AWAY ♥**

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

6
.gitignore vendored
View file

@ -1,6 +1,7 @@
*.sw* *.sw*
*.tar.gz *.tar.gz
*.prof* *.prof*
_example/config.js
pcaps pcaps
build build
bettercap bettercap
@ -12,3 +13,8 @@ snap/
stage/ stage/
/snap /snap
.idea
/cover.out
/can-data
/test*.yml
/zerochaos.js

View file

@ -1,36 +0,0 @@
sudo: false
language: go
go:
# - 1.9.x
- 1.10.x
- 1.11.x
- master
matrix:
# It's ok if our code fails on unstable development versions of Go.
allow_failures:
- go: master
# Don't wait for tip tests to finish. Mark the test run green if the
# tests pass on the stable versions of Go.
fast_finish: true
notifications:
email: false
git:
depth: 3
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y libpcap-dev libnetfilter-queue-dev
install:
- make deps
script:
- make test
after_success:
- bash <(curl -s https://codecov.io/bash)

20
Dockerfile vendored
View file

@ -1,15 +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 go get -u github.com/golang/dep/...
RUN make deps
RUN make RUN make
# get caplets # get caplets
@ -18,10 +14,10 @@ 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 /go/src/github.com/bettercap/bettercap/caplets /app/ COPY --from=build-env /usr/local/share/bettercap/caplets /app/
WORKDIR /app WORKDIR /app
EXPOSE 80 443 53 5300 8080 8081 8082 8083 8000 EXPOSE 80 443 53 5300 8080 8081 8082 8083 8000

343
Gopkg.lock generated
View file

@ -1,343 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:4132a4623657c2ba93a7cf83dccc6869b3e3bb91dc2afefa7c7032e10ceeaa12"
name = "github.com/adrianmo/go-nmea"
packages = ["."]
pruneopts = "UT"
revision = "a32116e4989e2b0e17c057ee378b4d5246add74e"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:1d692605b66a4fbe2a3de27131619e8e2c88ccf142ee43a7a8c902339942e0cc"
name = "github.com/antchfx/jsonquery"
packages = ["."]
pruneopts = "UT"
revision = "a2896be8c82bb2229d1cf26204863180e34b2b31"
[[projects]]
branch = "master"
digest = "1:7e4a9eaa42cb3b4cd48bd0cd2072a029fe25a47af20cfac529ea8c365f8559ac"
name = "github.com/antchfx/xpath"
packages = ["."]
pruneopts = "UT"
revision = "c8489ed3251e7d55ec2b7f18a2bc3a9a7222f0af"
[[projects]]
branch = "master"
digest = "1:a2c142e6c2aa1c71796c748bbe42d224e23d6638fd5b3ae153e70a4b08a8da4e"
name = "github.com/bettercap/gatt"
packages = [
".",
"linux",
"linux/cmd",
"linux/evt",
"linux/gioctl",
"linux/socket",
"linux/util",
"xpc",
]
pruneopts = "UT"
revision = "277ee0d0ef94d26e3190252c59fa34dde0df4f26"
[[projects]]
branch = "master"
digest = "1:c93fdd8820c13c2e2000d3064c510dde1397edca5ca1533fd15943402dab92b0"
name = "github.com/bettercap/nrf24"
packages = ["."]
pruneopts = "UT"
revision = "aa37e6d0e0eb125cee9ec71ed694db2ad58b509a"
[[projects]]
digest = "1:b95738a1e6ace058b5b8544303c0871fc01d224ef0d672f778f696265d0f2917"
name = "github.com/bettercap/readline"
packages = ["."]
pruneopts = "UT"
revision = "62c6fe6193755f722b8b8788aa7357be55a50ff1"
version = "v1.4"
[[projects]]
branch = "master"
digest = "1:8efd09ca363b01b7dca5baf091d65473df5f08f107b7c3fcd93c605189e031ed"
name = "github.com/chifflier/nfqueue-go"
packages = ["nfqueue"]
pruneopts = "UT"
revision = "61ca646babef3bd4dea1deb610bfb0005c0a1298"
[[projects]]
branch = "master"
digest = "1:6f9339c912bbdda81302633ad7e99a28dfa5a639c864061f1929510a9a64aa74"
name = "github.com/dustin/go-humanize"
packages = ["."]
pruneopts = "UT"
revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
[[projects]]
branch = "master"
digest = "1:f77360df85b686035d0bff3b3988b29eb7ce415328e37e82cc5ec8439822911c"
name = "github.com/elazarl/goproxy"
packages = ["."]
pruneopts = "UT"
revision = "2ce16c963a8ac5bd6af851d4877e38701346983f"
[[projects]]
digest = "1:5247f5757ba31623c464db149dc272a37604516d8fbae1561b36e0d7cee070a5"
name = "github.com/evilsocket/islazy"
packages = [
"data",
"fs",
"log",
"ops",
"plugin",
"str",
"tui",
"zip",
]
pruneopts = "UT"
revision = "c5c7a41bb1c20e6df409825ed24af8de5fb7fb70"
version = "v1.10.4"
[[projects]]
branch = "master"
digest = "1:e5e45557e1871c967a6ccaa5b95d1233a2c01ab00615621825d1aca7383dc022"
name = "github.com/gobwas/glob"
packages = [
".",
"compiler",
"match",
"syntax",
"syntax/ast",
"syntax/lexer",
"util/runes",
"util/strings",
]
pruneopts = "UT"
revision = "e7a84e9525fe90abcda167b604e483cc959ad4aa"
[[projects]]
digest = "1:51bee9f1987dcdb9f9a1b4c20745d78f6bf6f5f14ad4e64ca883eb64df4c0045"
name = "github.com/google/go-github"
packages = ["github"]
pruneopts = "UT"
revision = "e48060a28fac52d0f1cb758bc8b87c07bac4a87d"
version = "v15.0.0"
[[projects]]
digest = "1:a63cff6b5d8b95638bfe300385d93b2a6d9d687734b863da8e09dc834510a690"
name = "github.com/google/go-querystring"
packages = ["query"]
pruneopts = "UT"
revision = "44c6ddd0a2342c386950e880b658017258da92fc"
version = "v1.0.0"
[[projects]]
digest = "1:b23296076e13a960263285b98907623e5d45f12fc405b14da19c6afa2a113deb"
name = "github.com/google/gopacket"
packages = [
".",
"layers",
"pcap",
"pcapgo",
]
pruneopts = "UT"
revision = "v1.1.16"
[[projects]]
branch = "master"
digest = "1:2ef895ea08a0af10ad6f1e1faf631c82fa5413dcf0ada93eb62ab5ad02df4979"
name = "github.com/google/gousb"
packages = ["."]
pruneopts = "UT"
revision = "d0c05ab7f70d6f6dc60ecd184517cb5e25860657"
[[projects]]
digest = "1:ca59b1175189b3f0e9f1793d2c350114be36eaabbe5b9f554b35edee1de50aea"
name = "github.com/gorilla/mux"
packages = ["."]
pruneopts = "UT"
revision = "a7962380ca08b5a188038c69871b8d3fbdf31e89"
version = "v1.7.0"
[[projects]]
digest = "1:7b5c6e2eeaa9ae5907c391a91c132abfd5c9e8a784a341b5625e750c67e6825d"
name = "github.com/gorilla/websocket"
packages = ["."]
pruneopts = "UT"
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
version = "v1.4.0"
[[projects]]
branch = "master"
digest = "1:6480de9b8abc75bfb06947e139aa07429dfed37f95a258e90865c4c84a9e188b"
name = "github.com/inconshreveable/go-vhost"
packages = ["."]
pruneopts = "UT"
revision = "06d84117953b22058c096b49a429ebd4f3d3d97b"
[[projects]]
branch = "master"
digest = "1:45da610fe81bb89b25555d0111fa338dcdbf2346afdb2b7296c8c49e554a9d32"
name = "github.com/jpillora/go-tld"
packages = ["."]
pruneopts = "UT"
revision = "f16ca3b7b383d3f0373109cac19147de3e8ae2d1"
[[projects]]
digest = "1:4701b2acabe16722ecb1e387d39741a29269386bfc4ba6283ecda362d289eff1"
name = "github.com/malfunkt/iprange"
packages = ["."]
pruneopts = "UT"
revision = "3a31f5ed42d2d8a1fc46f1be91fd693bdef2dd52"
version = "v0.9.0"
[[projects]]
digest = "1:2fa7b0155cd54479a755c629de26f888a918e13f8857a2c442205d825368e084"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "3a70a971f94a22f2fa562ffcc7a0eb45f5daf045"
version = "v0.1.1"
[[projects]]
digest = "1:3bb9c8451d199650bfd303e0068d86f135952fead374ad87c09a9b8a2cc4bd7c"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "369ecd8cea9851e459abb67eb171853e3986591e"
version = "v0.0.6"
[[projects]]
branch = "master"
digest = "1:0e2c7e1de0daaa759dac2b7feb90fdc944f1d23a6e0c8c20502cb635bcd2aaba"
name = "github.com/mdlayher/dhcp6"
packages = [
".",
"dhcp6opts",
"internal/buffer",
]
pruneopts = "UT"
revision = "775147d26a880f77f6e06d1a28fbeb2ec4fec013"
[[projects]]
branch = "master"
digest = "1:34fe44dd2bbe5723068e0a7a266847965a88297d383fe611e0358e556d82de09"
name = "github.com/mdlayher/raw"
packages = ["."]
pruneopts = "UT"
revision = "480b93709cce56651807d3fdeb260a5a7c4e2d5f"
[[projects]]
branch = "master"
digest = "1:2b32af4d2a529083275afc192d1067d8126b578c7a9613b26600e4df9c735155"
name = "github.com/mgutz/ansi"
packages = ["."]
pruneopts = "UT"
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
digest = "1:17bc403348b60bd01bfd2e507fcb23463e76f4b1f433d50b0872b8219df1250d"
name = "github.com/mgutz/logxi"
packages = ["v1"]
pruneopts = "UT"
revision = "aebf8a7d67ab4625e0fd4a665766fef9a709161b"
version = "v1"
[[projects]]
digest = "1:cf31692c14422fa27c83a05292eb5cbe0fb2775972e8f1f8446a71549bd8980b"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = "UT"
revision = "ba968bfe8b2f7e042a574c888954fccecfa385b4"
version = "v0.8.1"
[[projects]]
branch = "master"
digest = "1:dbfe572cc258e5bcf54cb650a06d90edd0da04e42ca1ed909cc1d49f00011c63"
name = "github.com/robertkrimen/otto"
packages = [
".",
"ast",
"dbg",
"file",
"parser",
"registry",
"token",
]
pruneopts = "UT"
revision = "15f95af6e78dcd2030d8195a138bd88d4f403546"
[[projects]]
branch = "master"
digest = "1:52b21e6be25049834aea5ecdde35d723c00fbdad3ea0357f2072dfb105836e02"
name = "github.com/tarm/serial"
packages = ["."]
pruneopts = "UT"
revision = "98f6abe2eb07edd42f6dfa2a934aea469acc29b7"
[[projects]]
branch = "master"
digest = "1:b45576bdf553b4c64ff798345b3256e49a157f8b480d29c8c0a89f09119d6c5a"
name = "golang.org/x/net"
packages = ["bpf"]
pruneopts = "UT"
revision = "16b79f2e4e95ea23b2bf9903c9809ff7b013ce85"
[[projects]]
branch = "master"
digest = "1:a2bc6cb1f4e1d0b512e1d47d392ead580006b5bdade6ffde16271759fe609b34"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "b6889370fb1098ed892bd3400d189bb6a3355813"
[[projects]]
digest = "1:9935525a8c49b8434a0b0a54e1980e94a6fae73aaff45c5d33ba8dff69de123e"
name = "gopkg.in/sourcemap.v1"
packages = [
".",
"base64vlq",
]
pruneopts = "UT"
revision = "6e83acea0053641eff084973fee085f0c193c61a"
version = "v1.0.5"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/adrianmo/go-nmea",
"github.com/antchfx/jsonquery",
"github.com/bettercap/gatt",
"github.com/bettercap/nrf24",
"github.com/bettercap/readline",
"github.com/chifflier/nfqueue-go/nfqueue",
"github.com/dustin/go-humanize",
"github.com/elazarl/goproxy",
"github.com/evilsocket/islazy/data",
"github.com/evilsocket/islazy/fs",
"github.com/evilsocket/islazy/log",
"github.com/evilsocket/islazy/ops",
"github.com/evilsocket/islazy/plugin",
"github.com/evilsocket/islazy/str",
"github.com/evilsocket/islazy/tui",
"github.com/evilsocket/islazy/zip",
"github.com/gobwas/glob",
"github.com/google/go-github/github",
"github.com/google/gopacket",
"github.com/google/gopacket/layers",
"github.com/google/gopacket/pcap",
"github.com/google/gopacket/pcapgo",
"github.com/gorilla/mux",
"github.com/gorilla/websocket",
"github.com/inconshreveable/go-vhost",
"github.com/jpillora/go-tld",
"github.com/malfunkt/iprange",
"github.com/mdlayher/dhcp6",
"github.com/mdlayher/dhcp6/dhcp6opts",
"github.com/robertkrimen/otto",
"github.com/tarm/serial",
]
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -1,75 +0,0 @@
[[constraint]]
name = "github.com/evilsocket/islazy"
version = "1.9.1"
[[constraint]]
branch = "master"
name = "github.com/bettercap/gatt"
[[constraint]]
name = "github.com/bettercap/readline"
version = "1.4.0"
[[constraint]]
branch = "master"
name = "github.com/chifflier/nfqueue-go"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-humanize"
[[constraint]]
branch = "master"
name = "github.com/elazarl/goproxy"
[[constraint]]
branch = "master"
name = "github.com/gobwas/glob"
[[constraint]]
name = "github.com/google/go-github"
version = "15.0.0"
[[constraint]]
name = "github.com/google/gopacket"
revision = "v1.1.16"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.1"
[[constraint]]
name = "github.com/gorilla/websocket"
version = "1.2.0"
[[constraint]]
branch = "master"
name = "github.com/inconshreveable/go-vhost"
[[constraint]]
branch = "master"
name = "github.com/jpillora/go-tld"
[[constraint]]
name = "github.com/malfunkt/iprange"
version = "0.9.0"
[[constraint]]
branch = "master"
name = "github.com/mdlayher/dhcp6"
[[constraint]]
branch = "master"
name = "github.com/robertkrimen/otto"
[[constraint]]
branch = "master"
name = "github.com/tarm/serial"
[[constraint]]
name = "github.com/adrianmo/go-nmea"
version = "1.1.0"
[prune]
go-tests = true
unused-packages = true

View file

@ -1,33 +0,0 @@
# 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 REMOVE THIS PART AND LEAVE ONLY THE FOLLOWING SECTIONS IN YOUR REPORT !
---
*Description of the bug or feature request*
### Environment
Please provide:
* Bettercap version you are using ( `bettercap -version` ).
* OS version and architecture you are using.
* Go version if building from sources.
* Command line arguments you are using.
* Caplet code you are using or the interactive session commands.
* **Full debug output** while reproducing the issue ( `bettercap -debug ...` ).
### Steps to Reproduce
1. *First Step*
2. *Second Step*
3. *and so on...*
**Expected behavior:** *What you expected to happen*
**Actual behavior:** *What actually happened*
--
**♥ ANY INCOMPLETE REPORT WILL BE CLOSED RIGHT AWAY ♥**

View file

@ -1,62 +1,42 @@
TARGET=bettercap TARGET ?= bettercap
PACKAGES=core firewall log modules network packets session tls PACKAGES ?= core firewall log modules network packets session tls
PREFIX ?= /usr/local
GO ?= go
all: deps build all: build
deps: godep golint gofmt gomegacheck
@dep ensure
build_with_race_detector: resources
@go build -race -o $(TARGET) .
build: resources build: resources
@go build -o $(TARGET) . $(GO) build $(GOFLAGS) -o $(TARGET) .
build_with_race_detector: resources
$(GO) build $(GOFLAGS) -race -o $(TARGET) .
resources: network/manuf.go resources: network/manuf.go
network/manuf.go: network/manuf.go:
@python ./network/make_manuf.py @python3 ./network/make_manuf.py
clean:
@rm -rf $(TARGET)
@rm -rf build
install: install:
@mkdir -p /usr/local/share/bettercap/caplets @mkdir -p $(DESTDIR)$(PREFIX)/share/bettercap/caplets
@cp bettercap /usr/local/bin/ @cp bettercap $(DESTDIR)$(PREFIX)/bin/
docker: docker:
@docker build -t bettercap:latest . @docker build -t bettercap:latest .
# Go 1.9 doesn't support test coverage on multiple packages, while test:
# Go 1.10 does, let's keep it 1.9 compatible in order not to break $(GO) test -covermode=atomic -coverprofile=cover.out ./...
# travis
test: deps
@echo "mode: atomic" > coverage.profile
@for pkg in $(PACKAGES); do \
go fmt ./$$pkg ; \
go vet ./$$pkg ; \
megacheck ./$$pkg ; \
touch $$pkg.profile ; \
go test -race ./$$pkg -coverprofile=$$pkg.profile -covermode=atomic; \
tail -n +2 $$pkg.profile >> coverage.profile && rm -rf $$pkg.profile ; \
done
html_coverage: test html_coverage: test
@go tool cover -html=coverage.profile -o coverage.profile.html $(GO) tool cover -html=cover.out -o cover.out.html
benchmark: server_deps benchmark: server_deps
@go test ./... -v -run=doNotRunTests -bench=. -benchmem $(GO) test -v -run=doNotRunTests -bench=. -benchmem ./...
# tools fmt:
godep: $(GO) fmt -s -w $(PACKAGES)
@go get -u github.com/golang/dep/...
golint: clean:
@go get -u golang.org/x/lint/golint $(RM) $(TARGET)
$(RM) -r build
gomegacheck: .PHONY: all build build_with_race_detector resources install docker test html_coverage benchmark fmt clean
@go get honnef.co/go/tools/cmd/megacheck
gofmt:
gofmt -s -w $(PACKAGES)

View file

@ -1,45 +1,53 @@
<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://goreportcard.com/report/github.com/bettercap/bettercap"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/bettercap/bettercap?style=flat-square&fuckgithubcache=1"></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://codecov.io/gh/bettercap/bettercap"><img alt="Code Coverage" src="https://img.shields.io/codecov/c/github/bettercap/bettercap/master.svg?style=flat-square"></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)
## 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 and DHCPv6 spoofers** for MITM attacks on IP based networks. * **ARP, DNS, NDP and DHCPv6 spoofers** for MITM attacks on IPv4 and IPv6 based networks.
* **Proxies at packet level, TCP level and HTTP/HTTPS** application level fully scriptable with easy to implement **javascript plugins**. * **Proxies at packet level, TCP level and HTTP/HTTPS** application level fully scriptable with easy to implement **javascript plugins**.
* A powerful **network sniffer** for **credentials harvesting** which can also be used as a **network protocol fuzzer**. * A powerful **network sniffer** for **credentials harvesting** which can also be used as a **network protocol fuzzer**.
* A very fast port scanner. * A very fast port scanner.
* A powerful [REST API](https://www.bettercap.org/modules/core/api.rest/) with support for asynchronous events notification on websocket to orchestrate your attacks easily. * A powerful [REST API](https://www.bettercap.org/modules/core/api.rest/) with support for asynchronous events notification on websocket to orchestrate your attacks easily.
* **A very convenient [web UI](https://www.bettercap.org/usage/#web-ui).**
* [More!](https://www.bettercap.org/modules/) * [More!](https://www.bettercap.org/modules/)
## About the 1.x Legacy Version ## Contributors
While the first version (up to 1.6.2) of bettercap was implemented in Ruby and only offered basic MITM, sniffing and proxying capabilities, the 2.x is a complete reimplementation using the [Go programming language](https://golang.org/). <a href="https://github.com/bettercap/bettercap/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bettercap/bettercap" alt="bettercap project contributors" />
This ground-up rewrite offered several advantages: </a>
* bettercap can now be distributed as a **single binary** with very few dependencies, for basically **any OS and any architecture**.
* 1.x proxies, altough highly optimized and event based, **[used to bottleneck the entire network](https://en.wikipedia.org/wiki/Global_interpreter_lock)** when performing a MITM attack, while the new version adds almost no overhead.
* Due to such **performance and functional limitations**, most of the features that the 2.x version is offering were simply impossible to implement properly (read as: without killing the entire network ... or your computer).
For this reason, **any version prior to 2.x is considered deprecated** and any type of support has been dropped in favor of the new implementation. An archived copy of the legacy documentation is [available here](https://www.bettercap.org/legacy/), however **it is strongly suggested to upgrade**.
## Documentation and Examples
The project is documented [here](https://www.bettercap.org/).
## 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](https://starchart.cc/bettercap/bettercap.svg)](https://starchart.cc/bettercap/bettercap)

9
SECURITY.md Normal file
View file

@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
Feature updates and security fixes are streamlined only to the latest version, make sure to check [the release page](https://github.com/bettercap/bettercap/releases) periodically.
## Reporting a Vulnerability
For non critical bugs and vulnerabilities feel free to open an issue and tag `@evilsocket`, for more severe reports send an email to `evilsocket AT gmail DOT com`.

122
build.sh
View file

@ -1,122 +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_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" ]]; then
build_linux_amd64 && create_archive bettercap_linux_amd64_$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_arm_$VERSION.zip
fi
sha256sum * > checksums.txt
echo
echo
du -sh *
cd --

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

View file

@ -3,30 +3,52 @@ package caplets
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"
"github.com/mitchellh/go-homedir"
) )
const ( const (
EnvVarName = "CAPSPATH" EnvVarName = "CAPSPATH"
Suffix = ".cap" Suffix = ".cap"
InstallArchive = "https://github.com/bettercap/caplets/archive/master.zip" InstallArchive = "https://github.com/bettercap/caplets/archive/master.zip"
InstallBase = "/usr/local/share/bettercap/"
) )
func getDefaultInstallBase() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("ALLUSERSPROFILE"), "bettercap")
}
return "/usr/local/share/bettercap/"
}
func getUserHomeDir() string {
usr, _ := homedir.Dir()
return usr
}
var ( var (
InstallBase = ""
InstallPathArchive = ""
InstallPath = ""
ArchivePath = ""
LoadPaths = []string(nil)
)
func Setup(base string) error {
InstallBase = base
InstallPathArchive = filepath.Join(InstallBase, "caplets-master") InstallPathArchive = filepath.Join(InstallBase, "caplets-master")
InstallPath = filepath.Join(InstallBase, "caplets") InstallPath = filepath.Join(InstallBase, "caplets")
ArchivePath = filepath.Join(os.TempDir(), "caplets.zip")
LoadPaths = []string{ LoadPaths = []string{
"./", "./",
"./caplets/", "./caplets/",
InstallPath, InstallPath,
filepath.Join(getUserHomeDir(), "caplets"),
} }
)
func init() { for _, path := range str.SplitBy(str.Trim(os.Getenv(EnvVarName)), string(os.PathListSeparator)) {
for _, path := range str.SplitBy(str.Trim(os.Getenv(EnvVarName)), ":") {
if path = str.Trim(path); len(path) > 0 { if path = str.Trim(path); len(path) > 0 {
LoadPaths = append(LoadPaths, path) LoadPaths = append(LoadPaths, path)
} }
@ -35,4 +57,11 @@ func init() {
for i, path := range LoadPaths { for i, path := range LoadPaths {
LoadPaths[i], _ = filepath.Abs(path) LoadPaths[i], _ = filepath.Abs(path)
} }
return nil
}
func init() {
// init with defaults
Setup(getDefaultInstallBase())
} }

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

View file

@ -25,10 +25,10 @@ func List() []*Caplet {
for _, fileName := range append(files, files2...) { for _, fileName := range append(files, files2...) {
if _, err := os.Stat(fileName); err == nil { if _, err := os.Stat(fileName); err == nil {
base := strings.Replace(fileName, searchPath+"/", "", -1) base := strings.Replace(fileName, searchPath+string(os.PathSeparator), "", -1)
base = strings.Replace(base, Suffix, "", -1) base = strings.Replace(base, Suffix, "", -1)
if err, caplet := Load(base); err != nil { if caplet, err := Load(base); err != nil {
fmt.Fprintf(os.Stderr, "wtf: %v\n", err) fmt.Fprintf(os.Stderr, "wtf: %v\n", err)
} else { } else {
caplets = append(caplets, caplet) caplets = append(caplets, caplet)
@ -44,12 +44,12 @@ func List() []*Caplet {
return caplets return caplets
} }
func Load(name string) (error, *Caplet) { func Load(name string) (*Caplet, error) {
cacheLock.Lock() cacheLock.Lock()
defer cacheLock.Unlock() defer cacheLock.Unlock()
if caplet, found := cache[name]; found { if caplet, found := cache[name]; found {
return nil, caplet return caplet, nil
} }
baseName := name baseName := name
@ -58,7 +58,7 @@ func Load(name string) (error, *Caplet) {
name += Suffix name += Suffix
} }
if name[0] != '/' { if !filepath.IsAbs(name) {
for _, path := range LoadPaths { for _, path := range LoadPaths {
names = append(names, filepath.Join(path, name)) names = append(names, filepath.Join(path, name))
} }
@ -76,7 +76,7 @@ func Load(name string) (error, *Caplet) {
cache[name] = cap cache[name] = cap
if reader, err := fs.LineReader(fileName); err != nil { if reader, err := fs.LineReader(fileName); err != nil {
return fmt.Errorf("error reading caplet %s: %v", fileName, err), nil return nil, fmt.Errorf("error reading caplet %s: %v", fileName, err)
} else { } else {
for line := range reader { for line := range reader {
cap.Code = append(cap.Code, line) cap.Code = append(cap.Code, line)
@ -103,9 +103,8 @@ func Load(name string) (error, *Caplet) {
} }
} }
return nil, cap return cap, nil
} }
} }
return nil, fmt.Errorf("caplet %s not found", name)
return fmt.Errorf("caplet %s not found", name), nil
} }

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

@ -1,64 +0,0 @@
#!/bin/bash
NEW=()
FIXES=()
MISC=()
echo "@ Fetching remote tags ..."
# git fetch --tags > /dev/null
CURTAG=$(git describe --tags --abbrev=0)
OUTPUT=$(git log $CURTAG..HEAD --oneline)
IFS=$'\n' LINES=($OUTPUT)
for LINE in "${LINES[@]}"; do
LINE=$(echo "$LINE" | sed -E "s/^[[:xdigit:]]+\s+//")
if [[ $LINE = *"new:"* ]]; then
LINE=$(echo "$LINE" | sed -E "s/^new: //")
NEW+=("$LINE")
elif [[ $LINE = *"fix:"* ]]; then
LINE=$(echo "$LINE" | sed -E "s/^fix: //")
FIXES+=("$LINE")
elif [[ $LINE != *"i did not bother commenting"* ]] && [[ $LINE != *"Merge "* ]]; then
echo "MISC LINE =$LINE"
LINE=$(echo "$LINE" | sed -E "s/^[a-z]+: //")
MISC+=("$LINE")
fi
done
echo
echo "Changelog"
echo "==="
if [ -n "$NEW" ]; then
echo
echo "**New Features**"
echo
for l in "${NEW[@]}"
do
echo "* $l"
done
fi
if [ -n "$FIXES" ]; then
echo
echo "**Fixes**"
echo
for l in "${FIXES[@]}"
do
echo "* $l"
done
fi
if [ -n "$MISC" ]; then
echo
echo "**Misc**"
echo
for l in "${MISC[@]}"
do
echo "* $l"
done
fi
echo

View file

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

View file

@ -1,16 +1,16 @@
package core package core
import ( import (
"fmt"
"os/exec" "os/exec"
"sort" "sort"
"github.com/bettercap/bettercap/v2/log"
"github.com/evilsocket/islazy/str" "github.com/evilsocket/islazy/str"
) )
func UniqueInts(a []int, sorted bool) []int { func UniqueInts(a []int, sorted bool) []int {
tmp := make(map[int]bool) tmp := make(map[int]bool, len(a))
uniq := make([]int, 0) uniq := make([]int, 0, len(a))
for _, n := range a { for _, n := range a {
tmp[n] = true tmp[n] = true
@ -34,24 +34,19 @@ func HasBinary(executable string) bool {
return true return true
} }
func ExecSilent(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 "", err return str.Trim(string(raw)), err
} else { } else {
return str.Trim(string(raw)), nil return str.Trim(string(raw)), nil
} }
} }
func Exec(executable string, args []string) (string, error) {
out, err := ExecSilent(executable, args)
if err != nil {
fmt.Printf("ERROR for '%s %s': %s\n", executable, args, err)
}
return out, err
}

View file

@ -1,13 +1,10 @@
package core package core
import ( import (
"bytes"
"io"
"os" "os"
"testing" "testing"
"github.com/evilsocket/islazy/fs" "github.com/evilsocket/islazy/fs"
"github.com/evilsocket/islazy/str"
) )
func hasInt(a []int, v int) bool { func hasInt(a []int, v int) bool {
@ -81,85 +78,6 @@ func TestCoreUniqueIntsSorted(t *testing.T) {
} }
} }
func sameStrings(a []string, b []string) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
func TestCoreExec(t *testing.T) {
var units = []struct {
exec string
args []string
out string
err string
stdout string
}{
{"foo", []string{}, "", `exec: "foo": executable file not found in $PATH`, `ERROR for 'foo []': exec: "foo": executable file not found in $PATH`},
{"ps", []string{"-someinvalidflag"}, "", "exit status 1", "ERROR for 'ps [-someinvalidflag]': exit status 1"},
{"true", []string{}, "", "", ""},
{"head", []string{"/path/to/file/that/does/not/exist"}, "", "exit status 1", "ERROR for 'head [/path/to/file/that/does/not/exist]': exit status 1"},
}
for _, u := range units {
var buf bytes.Buffer
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
gotOut, gotErr := ExecSilent(u.exec, u.args)
w.Close()
io.Copy(&buf, r)
os.Stdout = oldStdout
gotStdout := str.Trim(buf.String())
if gotOut != u.out {
t.Fatalf("expected output '%s', got '%s'", u.out, gotOut)
} else if u.err == "" && gotErr != nil {
t.Fatalf("expected no error, got '%s'", gotErr)
} else if u.err != "" && gotErr == nil {
t.Fatalf("expected error '%s', got none", u.err)
} else if u.err != "" && gotErr != nil && gotErr.Error() != u.err {
t.Fatalf("expected error '%s', got '%s'", u.err, gotErr)
} else if gotStdout != "" {
t.Fatalf("expected empty stdout, got '%s'", gotStdout)
}
}
for _, u := range units {
var buf bytes.Buffer
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
gotOut, gotErr := Exec(u.exec, u.args)
w.Close()
io.Copy(&buf, r)
os.Stdout = oldStdout
gotStdout := str.Trim(buf.String())
if gotOut != u.out {
t.Fatalf("expected output '%s', got '%s'", u.out, gotOut)
} else if u.err == "" && gotErr != nil {
t.Fatalf("expected no error, got '%s'", gotErr)
} else if u.err != "" && gotErr == nil {
t.Fatalf("expected error '%s', got none", u.err)
} else if u.err != "" && gotErr != nil && gotErr.Error() != u.err {
t.Fatalf("expected error '%s', got '%s'", u.err, gotErr)
} else if gotStdout != u.stdout {
t.Fatalf("expected stdout '%s', got '%s'", u.stdout, gotStdout)
}
}
}
func TestCoreExists(t *testing.T) { func TestCoreExists(t *testing.T) {
var units = []struct { var units = []struct {
what string what string
@ -179,3 +97,144 @@ func TestCoreExists(t *testing.T) {
} }
} }
} }
func TestHasBinary(t *testing.T) {
tests := []struct {
name string
executable string
expected bool
}{
{
name: "common shell",
executable: "sh",
expected: true,
},
{
name: "echo command",
executable: "echo",
expected: true,
},
{
name: "non-existent binary",
executable: "this-binary-definitely-does-not-exist-12345",
expected: false,
},
{
name: "empty string",
executable: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := HasBinary(tt.executable)
if got != tt.expected {
t.Errorf("HasBinary(%q) = %v, want %v", tt.executable, got, tt.expected)
}
})
}
}
func TestExec(t *testing.T) {
tests := []struct {
name string
executable string
args []string
wantError bool
contains string
}{
{
name: "echo with args",
executable: "echo",
args: []string{"hello", "world"},
wantError: false,
contains: "hello world",
},
{
name: "echo empty",
executable: "echo",
args: []string{},
wantError: false,
contains: "",
},
{
name: "non-existent command",
executable: "this-command-does-not-exist-12345",
args: []string{},
wantError: true,
contains: "",
},
{
name: "true command",
executable: "true",
args: []string{},
wantError: false,
contains: "",
},
{
name: "false command",
executable: "false",
args: []string{},
wantError: true,
contains: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Skip platform-specific commands if not available
if !HasBinary(tt.executable) && !tt.wantError {
t.Skipf("%s not found in PATH", tt.executable)
}
output, err := Exec(tt.executable, tt.args)
if tt.wantError {
if err == nil {
t.Errorf("Exec(%q, %v) expected error but got none", tt.executable, tt.args)
}
} else {
if err != nil {
t.Errorf("Exec(%q, %v) unexpected error: %v", tt.executable, tt.args, err)
}
if tt.contains != "" && output != tt.contains {
t.Errorf("Exec(%q, %v) = %q, want %q", tt.executable, tt.args, output, tt.contains)
}
}
})
}
}
func TestExecWithOutput(t *testing.T) {
// Test that Exec properly captures and trims output
if HasBinary("printf") {
output, err := Exec("printf", []string{" hello world \n"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if output != "hello world" {
t.Errorf("expected trimmed output 'hello world', got %q", output)
}
}
}
func BenchmarkUniqueInts(b *testing.B) {
// Create a slice with duplicates
input := make([]int, 1000)
for i := 0; i < 1000; i++ {
input[i] = i % 100 // This creates 10 duplicates of each number 0-99
}
b.Run("unsorted", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = UniqueInts(input, false)
}
})
b.Run("sorted", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = UniqueInts(input, true)
}
})
}

View file

@ -1,6 +1,8 @@
package core package core
import "flag" import (
"flag"
)
type Options struct { type Options struct {
InterfaceName *string InterfaceName *string
@ -16,6 +18,9 @@ type Options struct {
Commands *string Commands *string
CpuProfile *string CpuProfile *string
MemProfile *string MemProfile *string
CapletsPath *string
Script *string
PcapBufSize *int
} }
func ParseOptions() (Options, error) { func ParseOptions() (Options, error) {
@ -33,6 +38,9 @@ func ParseOptions() (Options, error) {
Commands: flag.String("eval", "", "Run one or more commands separated by ; in the interactive session, used to set variables via command line."), Commands: flag.String("eval", "", "Run one or more commands separated by ; in the interactive session, used to set variables via command line."),
CpuProfile: flag.String("cpu-profile", "", "Write cpu profile `file`."), CpuProfile: flag.String("cpu-profile", "", "Write cpu profile `file`."),
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."),
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"
) )
@ -41,7 +41,7 @@ func Make(iface *network.Endpoint) FirewallManager {
} }
func (f PfFirewall) sysCtlRead(param string) (string, error) { func (f PfFirewall) sysCtlRead(param string) (string, error) {
if out, err := core.ExecSilent("sysctl", []string{param}); err != nil { if out, err := core.Exec("sysctl", []string{param}); err != nil {
return "", err return "", err
} else if m := sysCtlParser.FindStringSubmatch(out); len(m) == 3 && m[1] == param { } else if m := sysCtlParser.FindStringSubmatch(out); len(m) == 3 && m[1] == param {
return m[2], nil return m[2], nil
@ -52,7 +52,7 @@ func (f PfFirewall) sysCtlRead(param string) (string, error) {
func (f PfFirewall) sysCtlWrite(param string, value string) (string, error) { func (f PfFirewall) sysCtlWrite(param string, value string) (string, error) {
args := []string{"-w", fmt.Sprintf("%s=%s", param, value)} args := []string{"-w", fmt.Sprintf("%s=%s", param, value)}
_, err := core.ExecSilent("sysctl", args) _, err := core.Exec("sysctl", args)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -115,9 +115,9 @@ func (f PfFirewall) generateRule(r *Redirection) string {
func (f *PfFirewall) enable(enabled bool) { func (f *PfFirewall) enable(enabled bool) {
f.enabled = enabled f.enabled = enabled
if enabled { if enabled {
core.ExecSilent("pfctl", []string{"-e"}) core.Exec("pfctl", []string{"-e"})
} else { } else {
core.ExecSilent("pfctl", []string{"-d"}) core.Exec("pfctl", []string{"-d"})
} }
} }
@ -139,7 +139,7 @@ func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error {
f.enable(true) f.enable(true)
// load the rule // load the rule
if _, err := core.ExecSilent("pfctl", []string{"-f", f.filename}); err != nil { if _, err := core.Exec("pfctl", []string{"-f", f.filename}); err != nil {
return err return err
} }
} else { } else {

View file

@ -4,27 +4,32 @@ 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/fs"
"github.com/evilsocket/islazy/str" "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
} }
const ( const (
IPV4ForwardingFile = "/proc/sys/net/ipv4/ip_forward" IPV4ForwardingFile = "/proc/sys/net/ipv4/ip_forward"
IPV6ForwardingFile = "/proc/sys/net/ipv6/conf/all/forwarding"
) )
func Make(iface *network.Endpoint) FirewallManager { 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),
} }
@ -61,15 +66,32 @@ func (f LinuxFirewall) IsForwardingEnabled() bool {
} }
func (f LinuxFirewall) EnableForwarding(enabled bool) error { func (f LinuxFirewall) EnableForwarding(enabled bool) error {
return f.enableFeature(IPV4ForwardingFile, enabled) if err := f.enableFeature(IPV4ForwardingFile, enabled); err != nil {
return err
}
if fs.Exists(IPV6ForwardingFile) {
return f.enableFeature(IPV6ForwardingFile, enabled)
}
f.restore = true
return nil
} }
func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []string) { 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",
@ -78,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{
@ -89,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),
} }
} }
@ -100,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 {
@ -109,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 {
@ -121,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
} }
} }
@ -130,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()
}
}

67
go.mod Normal file
View file

@ -0,0 +1,67 @@
module github.com/bettercap/bettercap/v2
go 1.23.0
toolchain go1.24.4
require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/adrianmo/go-nmea v1.10.0
github.com/antchfx/jsonquery v1.3.6
github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/dustin/go-humanize v1.0.1
github.com/elazarl/goproxy v1.7.2
github.com/evilsocket/islazy v1.11.0
github.com/florianl/go-nfqueue/v2 v2.0.0
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe
github.com/google/go-github v17.0.0+incompatible
github.com/google/gopacket v1.1.19
github.com/google/gousb v1.1.3
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-bexpr v0.1.14
github.com/inconshreveable/go-vhost v1.0.0
github.com/jpillora/go-tld v1.2.1
github.com/malfunkt/iprange v0.9.0
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b
github.com/miekg/dns v1.1.67
github.com/mitchellh/go-homedir v1.1.0
github.com/phin1x/go-ipp v1.6.1
github.com/robertkrimen/otto v0.5.1
github.com/stratoberry/go-gpsd v1.3.0
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
go.einride.tech/can v0.14.0
golang.org/x/net v0.42.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/antchfx/xpath v1.3.4 // indirect
github.com/chzyer/logex v1.2.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/kr/binarydist v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab // indirect
github.com/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
)

192
go.sum Normal file
View file

@ -0,0 +1,192 @@
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/adrianmo/go-nmea v1.10.0 h1:L1aYaebZ4cXFCoXNSeDeQa0tApvSKvIbqMsK+iaRiCo=
github.com/adrianmo/go-nmea v1.10.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg=
github.com/antchfx/jsonquery v1.3.6 h1:TaSfeAh7n6T11I74bsZ1FswreIfrbJ0X+OyLflx6mx4=
github.com/antchfx/jsonquery v1.3.6/go.mod h1:fGzSGJn9Y826Qd3pC8Wx45avuUwpkePsACQJYy+58BU=
github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/antchfx/xpath v1.3.4 h1:1ixrW1VnXd4HurCj7qnqnR0jo14g8JMe20Fshg1Vgz4=
github.com/antchfx/xpath v1.3.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0 h1:HiFUGV/7eGWG/YJAf9HcKOUmxIj+7LVzC8zD57VX1qo=
github.com/bettercap/gatt v0.0.0-20240808115956-ec4935e8c4a0/go.mod h1:oafnPgaBI4gqJiYkueCyR4dqygiWGXTGOE0gmmAVeeQ=
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb h1:JWAAJk4ny+bT3VrtcX+e7mcmWtWUeUM0xVcocSAUuWc=
github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb/go.mod h1:g6WiaSRgMTiChuk7jYyFSEtpgaw1F0wAsBfspG3bu0M=
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf h1:pwGPRc5PIp4KCF9QbKn0iLVMhfigUMw4IzGZEZ81m1I=
github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf/go.mod h1:03rWiUf60r1miMVzMEtgtkq7RdZniecZFw3/Zgvyxcs=
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3 h1:pC4ZAk7UtDIbrRKzMMiIL1TVkiKlgtgcJodqKB53Rl4=
github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3/go.mod h1:kqVwnx6DKuOHMZcBnzsgp2Lq2JZHDtFtm92b5hxdRaM=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/evilsocket/islazy v1.11.0 h1:B5w6uuS6ki6iDG+aH/RFeoMb8ijQh/pGabewqp2UeJ0=
github.com/evilsocket/islazy v1.11.0/go.mod h1:muYH4x5MB5YRdkxnrOtrXLIBX6LySj1uFIqys94LKdo=
github.com/florianl/go-nfqueue/v2 v2.0.0 h1:NTCxS9b0GSbHkWv1a7oOvZn679fsyDkaSkRvOYpQ9Oo=
github.com/florianl/go-nfqueue/v2 v2.0.0/go.mod h1:M2tBLIj62QpwqjwV0qfcjqGOqP3qiTuXr2uSRBXH9Qk=
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe h1:8P+/htb3mwwpeGdJg69yBF/RofK7c6Fjz5Ypa/bTqbY=
github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o=
github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-bexpr v0.1.14 h1:uKDeyuOhWhT1r5CiMTjdVY4Aoxdxs6EtwgTGnlosyp4=
github.com/hashicorp/go-bexpr v0.1.14/go.mod h1:gN7hRKB3s7yT+YvTdnhZVLTENejvhlkZ8UE4YVBS+Q8=
github.com/inconshreveable/go-vhost v1.0.0 h1:IK4VZTlXL4l9vz2IZoiSFbYaaqUW7dXJAiPriUN5Ur8=
github.com/inconshreveable/go-vhost v1.0.0/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jpillora/go-tld v1.2.1 h1:kDKOkmXLlskqjcvNs7w5XHLep7c8WM7Xd4HQjxllVMk=
github.com/jpillora/go-tld v1.2.1/go.mod h1:plzIl7xr5UWKGy7R+giuv+L/nOjrPjsoWxy/ST9OBUk=
github.com/kr/binarydist v0.1.0 h1:6kAoLA9FMMnNGSehX0s1PdjbEaACznAv/W219j2uvyo=
github.com/kr/binarydist v0.1.0/go.mod h1:DY7S//GCoz1BCd0B0EVrinCKAZN3pXe+MDaIZbXQVgM=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/malfunkt/iprange v0.9.0 h1:VCs0PKLUPotNVQTpVNszsut4lP7OCGNBwX+lOYBrnVQ=
github.com/malfunkt/iprange v0.9.0/go.mod h1:TRGqO/f95gh3LOndUGTL46+W0GXA91WTqyZ0Quwvt4U=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b h1:r12blE3QRYlW1WBiBEe007O6NrTb/P54OjR5d4WLEGk=
github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b/go.mod h1:p4K2+UAoap8Jzsadsxc0KG0OZjmmCthTPUyZqAVkjBY=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab h1:n8cgpHzJ5+EDyDri2s/GC7a9+qK3/YEGnBsd0uS/8PY=
github.com/mgutz/logxi v0.0.0-20161027140823-aebf8a7d67ab/go.mod h1:y1pL58r5z2VvAjeG1VLGc8zOQgSOzbKN7kMHPvFXJ+8=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/phin1x/go-ipp v1.6.1 h1:oxJXi92BO2FZhNcG3twjnxKFH1liTQ46vbbZx+IN/80=
github.com/phin1x/go-ipp v1.6.1/go.mod h1:GZwyNds6grdLi2xRBX22Cvt7Dh7ITWsML0bjrqBF5uo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
github.com/stratoberry/go-gpsd v1.3.0 h1:JxJOEC4SgD0QY65AE7B1CtJtweP73nqJghZeLNU9J+c=
github.com/stratoberry/go-gpsd v1.3.0/go.mod h1:nVf/vTgfYxOMxiQdy9BtJjojbFRtG8H3wNula++VgkU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64 h1:l/T7dYuJEQZOwVOpjIXr1180aM9PZL/d1MnMVIxefX4=
github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64/go.mod h1:Q1NAJOuRdQCqN/VIWdnaaEhV8LpeO2rtlBP7/iDJNII=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.einride.tech/can v0.14.0 h1:OkQ0jsjCk4ijgTMjD43V1NKQyDztpX7Vo/NrvmnsAXE=
go.einride.tech/can v0.14.0/go.mod h1:615YuRGnWfndMGD+f3Ud1sp1xJLP1oj14dKRtb2CXDQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
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.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-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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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-20190412213103-97732733099d/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

29
js/crypto.go Normal file
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
}

164
js/data.go Normal file
View file

@ -0,0 +1,164 @@
package js
import (
"bytes"
"compress/gzip"
"encoding/base64"
"github.com/robertkrimen/otto"
)
func textEncode(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("textEncode: expected 1 argument, %d given instead.", argc)
}
arg := argv[0]
if (!arg.IsString()) {
return ReportError("textEncode: single argument must be a string.")
}
encoded := []byte(arg.String())
vm := otto.New()
v, err := vm.ToValue(encoded)
if err != nil {
return ReportError("textEncode: could not convert to []uint8: %s", err.Error())
}
return v
}
func textDecode(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("textDecode: expected 1 argument, %d given instead.", argc)
}
arg, err := argv[0].Export()
if err != nil {
return ReportError("textDecode: could not export argument value: %s", err.Error())
}
byteArr, ok := arg.([]uint8)
if !ok {
return ReportError("textDecode: single argument must be of type []uint8.")
}
decoded := string(byteArr)
v, err := otto.ToValue(decoded)
if err != nil {
return ReportError("textDecode: could not convert to string: %s", err.Error())
}
return v
}
func btoa(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("btoa: expected 1 argument, %d given instead.", argc)
}
arg := argv[0]
if (!arg.IsString()) {
return ReportError("btoa: single argument must be a string.")
}
encoded := base64.StdEncoding.EncodeToString([]byte(arg.String()))
v, err := otto.ToValue(encoded)
if err != nil {
return ReportError("btoa: could not convert to string: %s", err.Error())
}
return v
}
func atob(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("atob: expected 1 argument, %d given instead.", argc)
}
arg := argv[0]
if (!arg.IsString()) {
return ReportError("atob: single argument must be a string.")
}
decoded, err := base64.StdEncoding.DecodeString(arg.String())
if err != nil {
return ReportError("atob: could not decode string: %s", err.Error())
}
v, err := otto.ToValue(string(decoded))
if err != nil {
return ReportError("atob: could not convert to string: %s", err.Error())
}
return v
}
func gzipCompress(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("gzipCompress: expected 1 argument, %d given instead.", argc)
}
arg := argv[0]
if (!arg.IsString()) {
return ReportError("gzipCompress: single argument must be a string.")
}
uncompressedBytes := []byte(arg.String())
var writerBuffer bytes.Buffer
gzipWriter := gzip.NewWriter(&writerBuffer)
_, err := gzipWriter.Write(uncompressedBytes)
if err != nil {
return ReportError("gzipCompress: could not compress data: %s", err.Error())
}
gzipWriter.Close()
compressedBytes := writerBuffer.Bytes()
v, err := otto.ToValue(string(compressedBytes))
if err != nil {
return ReportError("gzipCompress: could not convert to string: %s", err.Error())
}
return v
}
func gzipDecompress(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("gzipDecompress: expected 1 argument, %d given instead.", argc)
}
compressedBytes := []byte(argv[0].String())
readerBuffer := bytes.NewBuffer(compressedBytes)
gzipReader, err := gzip.NewReader(readerBuffer)
if err != nil {
return ReportError("gzipDecompress: could not create gzip reader: %s", err.Error())
}
var decompressedBuffer bytes.Buffer
_, err = decompressedBuffer.ReadFrom(gzipReader)
if err != nil {
return ReportError("gzipDecompress: could not decompress data: %s", err.Error())
}
decompressedBytes := decompressedBuffer.Bytes()
v, err := otto.ToValue(string(decompressedBytes))
if err != nil {
return ReportError("gzipDecompress: could not convert to string: %s", err.Error())
}
return v
}

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

70
js/fs.go Normal file
View file

@ -0,0 +1,70 @@
package js
import (
"github.com/robertkrimen/otto"
"io/ioutil"
)
func readDir(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("readDir: expected 1 argument, %d given instead.", argc)
}
path := argv[0].String()
dir, err := ioutil.ReadDir(path)
if err != nil {
return ReportError("Could not read directory %s: %s", path, err)
}
entry_list := []string{}
for _, file := range dir {
entry_list = append(entry_list, file.Name())
}
v, err := otto.Otto.ToValue(*call.Otto, entry_list)
if err != nil {
return ReportError("Could not convert to array: %s", err)
}
return v
}
func readFile(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return ReportError("readFile: expected 1 argument, %d given instead.", argc)
}
filename := argv[0].String()
raw, err := ioutil.ReadFile(filename)
if err != nil {
return ReportError("Could not read file %s: %s", filename, err)
}
v, err := otto.ToValue(string(raw))
if err != nil {
return ReportError("Could not convert to string: %s", err)
}
return v
}
func writeFile(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 2 {
return ReportError("writeFile: expected 2 arguments, %d given instead.", argc)
}
filename := argv[0].String()
data := argv[1].String()
err := ioutil.WriteFile(filename, []byte(data), 0644)
if err != nil {
return ReportError("Could not write %d bytes to %s: %s", len(data), filename, err)
}
return otto.NullValue()
}

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

155
js/http.go Normal file
View file

@ -0,0 +1,155 @@
package js
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/robertkrimen/otto"
)
type httpPackage struct {
}
type httpResponse struct {
Error error
Response *http.Response
Raw []byte
Body string
JSON interface{}
}
func (c httpPackage) Encode(s string) string {
return url.QueryEscape(s)
}
func (c httpPackage) Request(method string, uri string,
headers map[string]string,
form map[string]string,
json string) httpResponse {
var reader io.Reader
if form != nil {
data := url.Values{}
for k, v := range form {
data.Set(k, v)
}
reader = bytes.NewBufferString(data.Encode())
} else if json != "" {
reader = strings.NewReader(json)
}
req, err := http.NewRequest(method, uri, reader)
if err != nil {
return httpResponse{Error: err}
}
if form != nil {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
} else if json != "" {
req.Header.Set("Content-Type", "application/json")
}
for name, value := range headers {
req.Header.Add(name, value)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return httpResponse{Error: err}
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return httpResponse{Error: err}
}
res := httpResponse{
Response: resp,
Raw: raw,
Body: string(raw),
}
if resp.StatusCode != http.StatusOK {
res.Error = fmt.Errorf("%s", resp.Status)
}
return res
}
func (c httpPackage) Get(url string, headers map[string]string) httpResponse {
return c.Request("GET", url, headers, nil, "")
}
func (c httpPackage) PostForm(url string, headers map[string]string, form map[string]string) httpResponse {
return c.Request("POST", url, headers, form, "")
}
func (c httpPackage) PostJSON(url string, headers map[string]string, json string) httpResponse {
return c.Request("POST", url, headers, nil, json)
}
func httpRequest(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc < 2 {
return ReportError("httpRequest: expected 2 or more, %d given instead.", argc)
}
method := argv[0].String()
url := argv[1].String()
client := &http.Client{}
req, err := http.NewRequest(method, url, nil)
if argc >= 3 {
data := argv[2].String()
req, err = http.NewRequest(method, url, bytes.NewBuffer([]byte(data)))
if err != nil {
return ReportError("Could create request to url %s: %s", url, err)
}
if argc > 3 {
headers := argv[3].Object()
for _, key := range headers.Keys() {
v, err := headers.Get(key)
if err != nil {
return ReportError("Could add header %s to request: %s", key, err)
}
req.Header.Add(key, v.String())
}
}
} else if err != nil {
return ReportError("Could create request to url %s: %s", url, err)
}
resp, err := client.Do(req)
if err != nil {
return ReportError("Could not request url %s: %s", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return ReportError("Could not read response: %s", err)
}
object, err := otto.New().Object("({})")
if err != nil {
return ReportError("Could not create response object: %s", err)
}
err = object.Set("body", string(body))
if err != nil {
return ReportError("Could not populate response object: %s", err)
}
v, err := otto.ToValue(object)
if err != nil {
return ReportError("Could not convert to object: %s", err)
}
return v
}

45
js/init.go Normal file
View file

@ -0,0 +1,45 @@
package js
import (
"github.com/evilsocket/islazy/log"
"github.com/evilsocket/islazy/plugin"
"github.com/robertkrimen/otto"
)
var NullValue = otto.Value{}
func ReportError(format string, args ...interface{}) otto.Value {
log.Error(format, args...)
return NullValue
}
func init() {
// TODO: refactor this in packages
plugin.Defines["readDir"] = readDir
plugin.Defines["readFile"] = readFile
plugin.Defines["writeFile"] = writeFile
plugin.Defines["log"] = flog
plugin.Defines["log_debug"] = log_debug
plugin.Defines["log_info"] = log_info
plugin.Defines["log_warn"] = log_warn
plugin.Defines["log_error"] = log_error
plugin.Defines["log_fatal"] = log_fatal
plugin.Defines["Crypto"] = map[string]interface{}{
"sha1": cryptoSha1,
}
plugin.Defines["btoa"] = btoa
plugin.Defines["atob"] = atob
plugin.Defines["gzipCompress"] = gzipCompress
plugin.Defines["gzipDecompress"] = gzipDecompress
plugin.Defines["textEncode"] = textEncode
plugin.Defines["textDecode"] = textDecode
plugin.Defines["httpRequest"] = httpRequest
plugin.Defines["http"] = httpPackage{}
plugin.Defines["random"] = randomPackage{}
}

48
js/log.go Normal file
View file

@ -0,0 +1,48 @@
package js
import (
"github.com/evilsocket/islazy/log"
"github.com/robertkrimen/otto"
)
func flog(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Info("%s", v.String())
}
return otto.Value{}
}
func log_debug(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Debug("%s", v.String())
}
return otto.Value{}
}
func log_info(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Info("%s", v.String())
}
return otto.Value{}
}
func log_warn(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Warning("%s", v.String())
}
return otto.Value{}
}
func log_error(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Error("%s", v.String())
}
return otto.Value{}
}
func log_fatal(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Fatal("%s", v.String())
}
return otto.Value{}
}

27
js/random.go Normal file
View file

@ -0,0 +1,27 @@
package js
import (
"math/rand"
"net"
"github.com/bettercap/bettercap/v2/network"
)
type randomPackage struct {
}
func (c randomPackage) String(size int, charset string) string {
runes := []rune(charset)
nrunes := len(runes)
buf := make([]rune, size)
for i := range buf {
buf[i] = runes[rand.Intn(nrunes)]
}
return string(buf)
}
func (c randomPackage) Mac() string {
hw := make([]byte, 6)
rand.Read(hw)
return network.NormalizeMac(net.HardwareAddr(hw).String())
}

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

@ -1,13 +1,20 @@
package any_proxy package any_proxy
import ( import (
"github.com/bettercap/bettercap/firewall" "fmt"
"github.com/bettercap/bettercap/session" "strconv"
"strings"
"github.com/bettercap/bettercap/v2/firewall"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/str"
) )
type AnyProxy struct { type AnyProxy struct {
session.SessionModule session.SessionModule
Redirection *firewall.Redirection // not using map[int]*firewall.Redirection to preserve order
ports []int
redirections []*firewall.Redirection
} }
func NewAnyProxy(s *session.Session) *AnyProxy { func NewAnyProxy(s *session.Session) *AnyProxy {
@ -25,9 +32,11 @@ func NewAnyProxy(s *session.Session) *AnyProxy {
"(TCP|UDP)", "(TCP|UDP)",
"Proxy protocol.")) "Proxy protocol."))
mod.AddParam(session.NewIntParameter("any.proxy.src_port", mod.AddParam(session.NewStringParameter("any.proxy.src_port",
"80", "80",
"Remote port to redirect when the module is activated.")) "",
"Remote port to redirect when the module is activated, "+
"also supported a comma separated list of ports and/or port-ranges."))
mod.AddParam(session.NewStringParameter("any.proxy.src_address", mod.AddParam(session.NewStringParameter("any.proxy.src_address",
"", "",
@ -36,7 +45,7 @@ func NewAnyProxy(s *session.Session) *AnyProxy {
mod.AddParam(session.NewStringParameter("any.proxy.dst_address", 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",
@ -72,7 +81,7 @@ func (mod *AnyProxy) Author() string {
func (mod *AnyProxy) Configure() error { func (mod *AnyProxy) Configure() error {
var err error var err error
var srcPort int var srcPorts string
var dstPort int var dstPort int
var iface string var iface string
var protocol string var protocol string
@ -85,8 +94,6 @@ func (mod *AnyProxy) Configure() error {
return err return err
} else if err, protocol = mod.StringParam("any.proxy.protocol"); err != nil { } else if err, protocol = mod.StringParam("any.proxy.protocol"); err != nil {
return err return err
} else if err, srcPort = mod.IntParam("any.proxy.src_port"); err != nil {
return err
} else if err, dstPort = mod.IntParam("any.proxy.dst_port"); err != nil { } else if err, dstPort = mod.IntParam("any.proxy.dst_port"); err != nil {
return err return err
} else if err, srcAddress = mod.StringParam("any.proxy.src_address"); err != nil { } else if err, srcAddress = mod.StringParam("any.proxy.src_address"); err != nil {
@ -95,27 +102,71 @@ func (mod *AnyProxy) Configure() error {
return err return err
} }
if err, srcPorts = mod.StringParam("any.proxy.src_port"); err != nil {
return err
} else {
var ports []int
// srcPorts can be a single port, a list of ports or a list of ranges, or a mix.
for _, token := range str.Comma(str.Trim(srcPorts)) {
if p, err := strconv.Atoi(token); err == nil {
// simple case, integer port
ports = append(ports, p)
} else if strings.Contains(token, "-") {
// port range
if parts := strings.Split(token, "-"); len(parts) == 2 {
if from, err := strconv.Atoi(str.Trim(parts[0])); err != nil {
return fmt.Errorf("invalid start port %s: %s", parts[0], err)
} else if from < 1 || from > 65535 {
return fmt.Errorf("port %s out of valid range", parts[0])
} else if to, err := strconv.Atoi(str.Trim(parts[1])); err != nil {
return fmt.Errorf("invalid end port %s: %s", parts[1], err)
} else if to < 1 || to > 65535 {
return fmt.Errorf("port %s out of valid range", parts[1])
} else if from > to {
return fmt.Errorf("start port should be lower than end port")
} else {
for p := from; p <= to; p++ {
ports = append(ports, p)
}
}
} else {
return fmt.Errorf("can't parse '%s' as range", token)
}
} else {
return fmt.Errorf("can't parse '%s' as port or range", token)
}
}
// after parsing and validation, create a redirection per source port
mod.ports = ports
mod.redirections = nil
for _, port := range mod.ports {
redir := firewall.NewRedirection(iface,
protocol,
port,
dstAddress,
dstPort)
if srcAddress != "" {
redir.SrcAddress = srcAddress
}
mod.redirections = append(mod.redirections, redir)
}
}
if !mod.Session.Firewall.IsForwardingEnabled() { if !mod.Session.Firewall.IsForwardingEnabled() {
mod.Info("Enabling forwarding.") mod.Info("Enabling forwarding.")
mod.Session.Firewall.EnableForwarding(true) mod.Session.Firewall.EnableForwarding(true)
} }
mod.Redirection = firewall.NewRedirection(iface, for _, redir := range mod.redirections {
protocol, if err := mod.Session.Firewall.EnableRedirection(redir, true); err != nil {
srcPort, return err
dstAddress, }
dstPort) mod.Info("applied redirection %s", redir.String())
if srcAddress != "" {
mod.Redirection.SrcAddress = srcAddress
} }
if err := mod.Session.Firewall.EnableRedirection(mod.Redirection, true); err != nil {
return err
}
mod.Info("Applied redirection %s", mod.Redirection.String())
return nil return nil
} }
@ -128,13 +179,11 @@ func (mod *AnyProxy) Start() error {
} }
func (mod *AnyProxy) Stop() error { func (mod *AnyProxy) Stop() error {
if mod.Redirection != nil { for _, redir := range mod.redirections {
mod.Info("Disabling redirection %s", mod.Redirection.String()) mod.Info("disabling redirection %s", redir.String())
if err := mod.Session.Firewall.EnableRedirection(mod.Redirection, false); err != nil { if err := mod.Session.Firewall.EnableRedirection(redir, false); err != nil {
return err return err
} }
mod.Redirection = nil
} }
return mod.SetRunning(false, func() {}) return mod.SetRunning(false, func() {})
} }

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

@ -4,10 +4,13 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
"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/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -26,6 +29,17 @@ type RestAPI struct {
useWebsocket bool useWebsocket bool
upgrader websocket.Upgrader upgrader websocket.Upgrader
quit chan bool quit chan bool
recClock int
recording bool
recTime int
loading bool
replaying bool
recordFileName string
recordWait *sync.WaitGroup
record *recording.Archive
recStarted time.Time
recStopped time.Time
} }
func NewRestAPI(s *session.Session) *RestAPI { func NewRestAPI(s *session.Session) *RestAPI {
@ -39,8 +53,28 @@ func NewRestAPI(s *session.Session) *RestAPI {
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
}, },
recClock: 1,
recording: false,
recTime: 0,
loading: false,
replaying: false,
recordFileName: "",
recordWait: &sync.WaitGroup{},
record: nil,
} }
mod.State.Store("recording", &mod.recording)
mod.State.Store("rec_clock", &mod.recClock)
mod.State.Store("replaying", &mod.replaying)
mod.State.Store("loading", &mod.loading)
mod.State.Store("load_progress", 0)
mod.State.Store("rec_time", &mod.recTime)
mod.State.Store("rec_filename", &mod.recordFileName)
mod.State.Store("rec_frames", 0)
mod.State.Store("rec_cur_frame", 0)
mod.State.Store("rec_started", &mod.recStarted)
mod.State.Store("rec_stopped", &mod.recStopped)
mod.AddParam(session.NewStringParameter("api.rest.address", mod.AddParam(session.NewStringParameter("api.rest.address",
"127.0.0.1", "127.0.0.1",
session.IPv4Validator, session.IPv4Validator,
@ -56,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."))
@ -93,6 +127,34 @@ func NewRestAPI(s *session.Session) *RestAPI {
return mod.Stop() return mod.Stop()
})) }))
mod.AddParam(session.NewIntParameter("api.rest.record.clock",
"1",
"Number of seconds to wait while recording with api.rest.record between one sample and the next one."))
mod.AddHandler(session.NewModuleHandler("api.rest.record off", "",
"Stop recording the session.",
func(args []string) error {
return mod.stopRecording()
}))
mod.AddHandler(session.NewModuleHandler("api.rest.record FILENAME", `api\.rest\.record (.+)`,
"Start polling the rest API periodically recording each sample in a compressed file that can be later replayed.",
func(args []string) error {
return mod.startRecording(args[0])
}))
mod.AddHandler(session.NewModuleHandler("api.rest.replay off", "",
"Stop replaying the recorded session.",
func(args []string) error {
return mod.stopReplay()
}))
mod.AddHandler(session.NewModuleHandler("api.rest.replay FILENAME", `api\.rest\.replay (.+)`,
"Start the rest API module in replay mode using FILENAME as the recorded session file, will revert to normal mode once the replay is over.",
func(args []string) error {
return mod.startReplay(args[0])
}))
return mod return mod
} }
@ -151,7 +213,7 @@ func (mod *RestAPI) Configure() error {
if mod.isTLS() { if mod.isTLS() {
if !fs.Exists(mod.certFile) || !fs.Exists(mod.keyFile) { if !fs.Exists(mod.certFile) || !fs.Exists(mod.keyFile) {
err, cfg := tls.CertConfigFromModule("api.rest", mod.SessionModule) cfg, err := tls.CertConfigFromModule("api.rest", mod.SessionModule)
if err != nil { if err != nil {
return err return err
} }
@ -159,7 +221,7 @@ func (mod *RestAPI) Configure() error {
mod.Debug("%+v", cfg) mod.Debug("%+v", cfg)
mod.Info("generating TLS key to %s", mod.keyFile) mod.Info("generating TLS key to %s", mod.keyFile)
mod.Info("generating TLS certificate to %s", mod.certFile) mod.Info("generating TLS certificate to %s", mod.certFile)
if err := tls.Generate(cfg, mod.certFile, mod.keyFile); err != nil { if err := tls.Generate(cfg, mod.certFile, mod.keyFile, false); err != nil {
return err return err
} }
} else { } else {
@ -168,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()
@ -205,7 +268,9 @@ func (mod *RestAPI) Configure() error {
} }
func (mod *RestAPI) Start() error { func (mod *RestAPI) Start() error {
if err := mod.Configure(); err != nil { if mod.replaying {
return fmt.Errorf("the api is currently in replay mode, run api.rest.replay off before starting it")
} else if err := mod.Configure(); err != nil {
return err return err
} }
@ -229,6 +294,12 @@ func (mod *RestAPI) Start() error {
} }
func (mod *RestAPI) Stop() error { func (mod *RestAPI) Stop() error {
if mod.recording {
mod.stopRecording()
} else if mod.replaying {
mod.stopReplay()
}
return mod.SetRunning(false, func() { return mod.SetRunning(false, func() {
go func() { go func() {
mod.quit <- true mod.quit <- true

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"`
} }
@ -36,7 +41,7 @@ func (mod *RestAPI) setAuthFailed(w http.ResponseWriter, r *http.Request) {
func (mod *RestAPI) toJSON(w http.ResponseWriter, o interface{}) { func (mod *RestAPI) toJSON(w http.ResponseWriter, o interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(o); err != nil { if err := json.NewEncoder(w).Encode(o); err != nil {
mod.Error("error while encoding object to JSON: %v", err) mod.Debug("error while encoding object to JSON: %v", err)
} }
} }
@ -64,8 +69,68 @@ func (mod *RestAPI) checkAuth(r *http.Request) bool {
return true return true
} }
func (mod *RestAPI) patchFrame(buf []byte) (frame map[string]interface{}, err error) {
// this is ugly but necessary: since we're replaying, the
// api.rest state object is filled with *old* values (the
// recorded ones), but the UI needs updated values at least
// of that in order to understand that a replay is going on
// and where we are at it. So we need to parse the record
// back into a session object and update only the api.rest.state
frame = make(map[string]interface{})
if err = json.Unmarshal(buf, &frame); err != nil {
return
}
for _, i := range frame["modules"].([]interface{}) {
m := i.(map[string]interface{})
if m["name"] == "api.rest" {
state := m["state"].(map[string]interface{})
mod.State.Range(func(key interface{}, value interface{}) bool {
state[key.(string)] = value
return true
})
break
}
}
return
}
func (mod *RestAPI) showSession(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showSession(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I) if mod.replaying {
if !mod.record.Session.Over() {
from := mod.record.Session.Index() - 1
q := r.URL.Query()
vals := q["from"]
if len(vals) > 0 {
if n, err := strconv.Atoi(vals[0]); err == nil {
from = n
}
}
mod.record.Session.SetFrom(from)
mod.Debug("replaying session %d of %d from %s",
mod.record.Session.Index(),
mod.record.Session.Frames(),
mod.recordFileName)
mod.State.Store("rec_frames", mod.record.Session.Frames())
mod.State.Store("rec_cur_frame", mod.record.Session.Index())
buf := mod.record.Session.Next()
if frame, err := mod.patchFrame(buf); err != nil {
mod.Error("%v", err)
} else {
mod.toJSON(w, frame)
return
}
} else {
mod.stopReplay()
}
}
mod.toJSON(w, mod.Session)
} }
func (mod *RestAPI) showBLE(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showBLE(w http.ResponseWriter, r *http.Request) {
@ -73,8 +138,8 @@ func (mod *RestAPI) showBLE(w http.ResponseWriter, r *http.Request) {
mac := strings.ToLower(params["mac"]) mac := strings.ToLower(params["mac"])
if mac == "" { if mac == "" {
mod.toJSON(w, session.I.BLE) mod.toJSON(w, mod.Session.BLE)
} else if dev, found := session.I.BLE.Get(mac); found { } else if dev, found := mod.Session.BLE.Get(mac); found {
mod.toJSON(w, dev) mod.toJSON(w, dev)
} else { } else {
http.Error(w, "Not Found", 404) http.Error(w, "Not Found", 404)
@ -86,8 +151,8 @@ func (mod *RestAPI) showHID(w http.ResponseWriter, r *http.Request) {
mac := strings.ToLower(params["mac"]) mac := strings.ToLower(params["mac"])
if mac == "" { if mac == "" {
mod.toJSON(w, session.I.HID) mod.toJSON(w, mod.Session.HID)
} else if dev, found := session.I.HID.Get(mac); found { } else if dev, found := mod.Session.HID.Get(mac); found {
mod.toJSON(w, dev) mod.toJSON(w, dev)
} else { } else {
http.Error(w, "Not Found", 404) http.Error(w, "Not Found", 404)
@ -95,19 +160,19 @@ func (mod *RestAPI) showHID(w http.ResponseWriter, r *http.Request) {
} }
func (mod *RestAPI) showEnv(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showEnv(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.Env) mod.toJSON(w, mod.Session.Env)
} }
func (mod *RestAPI) showGateway(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showGateway(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.Gateway) mod.toJSON(w, mod.Session.Gateway)
} }
func (mod *RestAPI) showInterface(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showInterface(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.Interface) mod.toJSON(w, mod.Session.Interface)
} }
func (mod *RestAPI) showModules(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showModules(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.Modules) mod.toJSON(w, mod.Session.Modules)
} }
func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) {
@ -115,8 +180,8 @@ func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) {
mac := strings.ToLower(params["mac"]) mac := strings.ToLower(params["mac"])
if mac == "" { if mac == "" {
mod.toJSON(w, session.I.Lan) mod.toJSON(w, mod.Session.Lan)
} else if host, found := session.I.Lan.Get(mac); found { } else if host, found := mod.Session.Lan.Get(mac); found {
mod.toJSON(w, host) mod.toJSON(w, host)
} else { } else {
http.Error(w, "Not Found", 404) http.Error(w, "Not Found", 404)
@ -124,15 +189,15 @@ func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) {
} }
func (mod *RestAPI) showOptions(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showOptions(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.Options) mod.toJSON(w, mod.Session.Options)
} }
func (mod *RestAPI) showPackets(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showPackets(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.Queue) mod.toJSON(w, mod.Session.Queue)
} }
func (mod *RestAPI) showStartedAt(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showStartedAt(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, session.I.StartedAt) mod.toJSON(w, mod.Session.StartedAt)
} }
func (mod *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) {
@ -140,10 +205,10 @@ func (mod *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) {
mac := strings.ToLower(params["mac"]) mac := strings.ToLower(params["mac"])
if mac == "" { if mac == "" {
mod.toJSON(w, session.I.WiFi) mod.toJSON(w, mod.Session.WiFi)
} else if station, found := session.I.WiFi.Get(mac); found { } else if station, found := mod.Session.WiFi.Get(mac); found {
mod.toJSON(w, station) mod.toJSON(w, station)
} else if client, found := session.I.WiFi.GetClient(mac); found { } else if client, found := mod.Session.WiFi.GetClient(mac); found {
mod.toJSON(w, client) mod.toJSON(w, client)
} else { } else {
http.Error(w, "Not Found", 404) http.Error(w, "Not Found", 404)
@ -160,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)
@ -167,45 +236,80 @@ 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 {
events := make([]session.Event, 0)
for _, e := range mod.Session.Events.Sorted() {
if mod.Session.EventsIgnoreList.Ignored(e) == false {
events = append(events, e)
}
}
nevents := len(events)
nmax := nevents
n := nmax
if limit > 0 && limit < nmax {
n = limit
}
return events[nevents-n:]
} }
func (mod *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) {
var err error q := r.URL.Query()
if mod.replaying {
if !mod.record.Events.Over() {
from := mod.record.Events.Index() - 1
vals := q["from"]
if len(vals) > 0 {
if n, err := strconv.Atoi(vals[0]); err == nil {
from = n
}
}
mod.record.Events.SetFrom(from)
mod.Debug("replaying events %d of %d from %s",
mod.record.Events.Index(),
mod.record.Events.Frames(),
mod.recordFileName)
buf := mod.record.Events.Next()
if _, err := w.Write(buf); err != nil {
mod.Error("%v", err)
} else {
return
}
} else {
mod.stopReplay()
}
}
if mod.useWebsocket { if mod.useWebsocket {
mod.startStreamingEvents(w, r) mod.startStreamingEvents(w, r)
} else { } else {
events := make([]session.Event, 0)
for _, e := range session.I.Events.Sorted() {
if mod.Session.EventsIgnoreList.Ignored(e) == false {
events = append(events, e)
}
}
nevents := len(events)
nmax := nevents
n := nmax
q := r.URL.Query()
vals := q["n"] vals := q["n"]
limit := 0
if len(vals) > 0 { if len(vals) > 0 {
n, err = strconv.Atoi(q["n"][0]) if n, err := strconv.Atoi(q["n"][0]); err == nil {
if err == nil { limit = n
if n > nmax {
n = nmax
}
} else {
n = nmax
} }
} }
mod.toJSON(w, events[nevents-n:]) mod.toJSON(w, mod.getEvents(limit))
} }
} }
func (mod *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) {
session.I.Events.Clear() mod.Session.Events.Clear()
} }
func (mod *RestAPI) corsRoute(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) corsRoute(w http.ResponseWriter, r *http.Request) {
@ -227,10 +331,10 @@ func (mod *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) {
return return
} }
session.I.Lock() mod.Session.Lock()
defer session.I.Unlock() defer mod.Session.Unlock()
path := r.URL.String() path := r.URL.Path
switch { switch {
case path == "/api/session": case path == "/api/session":
mod.showSession(w, r) mod.showSession(w, r)
@ -289,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)
@ -297,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)
@ -336,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,118 @@
package api_rest
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/bettercap/recording"
"github.com/evilsocket/islazy/fs"
)
var (
errNotRecording = errors.New("not recording")
)
func (mod *RestAPI) errAlreadyRecording() error {
return fmt.Errorf("the module is already recording to %s", mod.recordFileName)
}
func (mod *RestAPI) recordState() error {
mod.Session.Lock()
defer mod.Session.Unlock()
session := new(bytes.Buffer)
encoder := json.NewEncoder(session)
if err := encoder.Encode(mod.Session); err != nil {
return err
}
events := new(bytes.Buffer)
encoder = json.NewEncoder(events)
if err := encoder.Encode(mod.getEvents(0)); err != nil {
return err
}
return mod.record.NewState(session.Bytes(), events.Bytes())
}
func (mod *RestAPI) recorder() {
clock := time.Duration(mod.recClock) * time.Second
mod.recTime = 0
mod.recording = true
mod.replaying = false
mod.record = recording.New(mod.recordFileName)
mod.Info("started recording to %s (clock %s) ...", mod.recordFileName, clock)
mod.recordWait.Add(1)
defer mod.recordWait.Done()
tick := time.NewTicker(1 * time.Second)
lastSampled := time.Time{}
for range tick.C {
if !mod.recording {
break
}
mod.recTime++
if time.Since(lastSampled) >= clock {
lastSampled = time.Now()
if err := mod.recordState(); err != nil {
mod.Error("error while recording: %s", err)
mod.recording = false
break
}
}
}
mod.Info("stopped recording to %s ...", mod.recordFileName)
}
func (mod *RestAPI) startRecording(filename string) (err error) {
if mod.recording {
return mod.errAlreadyRecording()
} else if mod.replaying {
return mod.errAlreadyReplaying()
} else if err, mod.recClock = mod.IntParam("api.rest.record.clock"); err != nil {
return err
} else if mod.recordFileName, err = fs.Expand(filename); err != nil {
return err
}
// we need the api itself up and running
if !mod.Running() {
if err = mod.Start(); err != nil {
return err
}
}
go mod.recorder()
return nil
}
func (mod *RestAPI) stopRecording() error {
if !mod.recording {
return errNotRecording
}
mod.recording = false
mod.recordWait.Wait()
err := mod.record.Flush()
mod.recordFileName = ""
mod.record = nil
return err
}

View file

@ -0,0 +1,86 @@
package api_rest
import (
"errors"
"fmt"
"time"
"github.com/bettercap/recording"
"github.com/evilsocket/islazy/fs"
)
var (
errNotReplaying = errors.New("not replaying")
)
func (mod *RestAPI) errAlreadyReplaying() error {
return fmt.Errorf("the module is already replaying a session from %s", mod.recordFileName)
}
func (mod *RestAPI) startReplay(filename string) (err error) {
if mod.replaying {
return mod.errAlreadyReplaying()
} else if mod.recording {
return mod.errAlreadyRecording()
} else if mod.recordFileName, err = fs.Expand(filename); err != nil {
return err
}
mod.State.Store("load_progress", 0)
defer func() {
mod.State.Store("load_progress", 100.0)
}()
mod.loading = true
defer func() {
mod.loading = false
}()
mod.Info("loading %s ...", mod.recordFileName)
start := time.Now()
mod.record, err = recording.Load(mod.recordFileName, func(progress float64, done int, total int) {
mod.State.Store("load_progress", progress)
})
if err != nil {
return err
}
loadedIn := time.Since(start)
// we need the api itself up and running
if !mod.Running() {
if err := mod.Start(); err != nil {
return err
}
}
mod.recStarted = mod.record.Session.StartedAt()
mod.recStopped = mod.record.Session.StoppedAt()
duration := mod.recStopped.Sub(mod.recStarted)
mod.recTime = int(duration.Seconds())
mod.replaying = true
mod.recording = false
mod.Info("loaded %s of recording (%d frames) started at %s in %s, started replaying ...",
duration,
mod.record.Session.Frames(),
mod.recStarted,
loadedIn)
return nil
}
func (mod *RestAPI) stopReplay() error {
if !mod.replaying {
return errNotReplaying
}
mod.replaying = false
mod.Info("stopped replaying from %s ...", mod.recordFileName)
mod.recordFileName = ""
return nil
}

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"
) )
@ -40,6 +40,7 @@ func (mod *RestAPI) streamEvent(ws *websocket.Conn, event session.Event) error {
func (mod *RestAPI) sendPing(ws *websocket.Conn) error { func (mod *RestAPI) sendPing(ws *websocket.Conn) error {
ws.SetWriteDeadline(time.Now().Add(writeWait)) ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.SetReadDeadline(time.Now().Add(pongWait))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
mod.Error("Error while writing websocket ping message: %s", err) mod.Error("Error while writing websocket ping message: %s", err)
return err return err
@ -95,7 +96,7 @@ func (mod *RestAPI) streamReader(ws *websocket.Conn) {
for { for {
_, _, err := ws.ReadMessage() _, _, err := ws.ReadMessage()
if err != nil { if err != nil {
mod.Debug("Closing websocket reader.") mod.Warning("error reading message from websocket: %v", err)
break break
} }
} }
@ -105,12 +106,12 @@ func (mod *RestAPI) startStreamingEvents(w http.ResponseWriter, r *http.Request)
ws, err := mod.upgrader.Upgrade(w, r, nil) ws, err := mod.upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok { if _, ok := err.(websocket.HandshakeError); !ok {
mod.Error("Error while updating api.rest connection to websocket: %s", err) mod.Error("error while updating api.rest connection to websocket: %s", err)
} }
return return
} }
mod.Debug("Websocket streaming started for %s", r.RemoteAddr) mod.Debug("websocket streaming started for %s", r.RemoteAddr)
go mod.streamWriter(ws, w, r) go mod.streamWriter(ws, w, r)
mod.streamReader(ws) mod.streamReader(ws)

View file

@ -3,26 +3,28 @@ package arp_spoof
import ( import (
"bytes" "bytes"
"net" "net"
"strings"
"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"
) )
type ArpSpoofer struct { type ArpSpoofer struct {
session.SessionModule session.SessionModule
addresses []net.IP addresses []net.IP
macs []net.HardwareAddr macs []net.HardwareAddr
wAddresses []net.IP wAddresses []net.IP
wMacs []net.HardwareAddr wMacs []net.HardwareAddr
fullDuplex bool fullDuplex bool
internal bool internal bool
ban bool ban bool
waitGroup *sync.WaitGroup skipRestore bool
waitGroup *sync.WaitGroup
} }
func NewArpSpoofer(s *session.Session) *ArpSpoofer { func NewArpSpoofer(s *session.Session) *ArpSpoofer {
@ -35,9 +37,12 @@ func NewArpSpoofer(s *session.Session) *ArpSpoofer {
ban: false, ban: false,
internal: false, internal: false,
fullDuplex: false, fullDuplex: false,
skipRestore: false,
waitGroup: &sync.WaitGroup{}, waitGroup: &sync.WaitGroup{},
} }
mod.SessionModule.Requires("net.recon")
mod.AddParam(session.NewStringParameter("arp.spoof.targets", session.ParamSubnet, "", "Comma separated list of IP addresses, MAC addresses or aliases to spoof, also supports nmap style IP ranges.")) mod.AddParam(session.NewStringParameter("arp.spoof.targets", session.ParamSubnet, "", "Comma separated list of IP addresses, MAC addresses or aliases to spoof, also supports nmap style IP ranges."))
mod.AddParam(session.NewStringParameter("arp.spoof.whitelist", "", "", "Comma separated list of IP addresses, MAC addresses or aliases to skip while spoofing.")) mod.AddParam(session.NewStringParameter("arp.spoof.whitelist", "", "", "Comma separated list of IP addresses, MAC addresses or aliases to skip while spoofing."))
@ -50,6 +55,20 @@ func NewArpSpoofer(s *session.Session) *ArpSpoofer {
"false", "false",
"If true, both the targets and the gateway will be attacked, otherwise only the target (if the router has ARP spoofing protections in place this will make the attack fail).")) "If true, both the targets and the gateway will be attacked, otherwise only the target (if the router has ARP spoofing protections in place this will make the attack fail)."))
noRestore := session.NewBoolParameter("arp.spoof.skip_restore",
"false",
"If set to true, targets arp cache won't be restored when spoofing is stopped.")
mod.AddObservableParam(noRestore, func(v string) {
if strings.ToLower(v) == "true" || v == "1" {
mod.skipRestore = true
mod.Warning("arp cache restoration after spoofing disabled")
} else {
mod.skipRestore = false
mod.Debug("arp cache restoration after spoofing enabled")
}
})
mod.AddHandler(session.NewModuleHandler("arp.spoof on", "", mod.AddHandler(session.NewModuleHandler("arp.spoof on", "",
"Start ARP spoofer.", "Start ARP spoofer.",
func(args []string) error { func(args []string) error {
@ -169,20 +188,24 @@ func (mod *ArpSpoofer) Start() error {
} }
func (mod *ArpSpoofer) unSpoof() error { func (mod *ArpSpoofer) unSpoof() error {
nTargets := len(mod.addresses) + len(mod.macs) if !mod.skipRestore {
mod.Info("restoring ARP cache of %d targets.", nTargets) nTargets := len(mod.addresses) + len(mod.macs)
mod.arpSpoofTargets(mod.Session.Gateway.IP, mod.Session.Gateway.HW, false, false) mod.Info("restoring ARP cache of %d targets.", nTargets)
mod.arpSpoofTargets(mod.Session.Gateway.IP, mod.Session.Gateway.HW, false, false)
if mod.internal { if mod.internal {
list, _ := iprange.ParseList(mod.Session.Interface.CIDR()) list, _ := iprange.ParseList(mod.Session.Interface.CIDR())
neighbours := list.Expand() neighbours := list.Expand()
for _, address := range neighbours { for _, address := range neighbours {
if !mod.Session.Skip(address) { if !mod.Session.Skip(address) {
if realMAC, err := mod.Session.FindMAC(address, false); err == nil { if realMAC, err := mod.Session.FindMAC(address, false); err == nil {
mod.arpSpoofTargets(address, realMAC, false, false) mod.arpSpoofTargets(address, realMAC, false, false)
}
} }
} }
} }
} else {
mod.Warning("arp cache restoration is disabled")
} }
return nil return nil
@ -250,7 +273,7 @@ func (mod *ArpSpoofer) arpSpoofTargets(saddr net.IP, smac net.HardwareAddr, chec
isSpoofing := false isSpoofing := false
// are we spoofing the gateway IP? // are we spoofing the gateway IP?
if bytes.Equal(saddr, gwIP) { if net.IP.Equal(saddr, gwIP) {
isGW = true isGW = true
// are we restoring the original MAC of the gateway? // are we restoring the original MAC of the gateway?
if !bytes.Equal(smac, gwHW) { if !bytes.Equal(smac, gwHW) {
@ -258,47 +281,51 @@ func (mod *ArpSpoofer) arpSpoofTargets(saddr net.IP, smac net.HardwareAddr, chec
} }
} }
for ip, mac := range mod.getTargets(probe) { if targets := mod.getTargets(probe); len(targets) == 0 {
if check_running && !mod.Running() { mod.Warning("could not find spoof targets")
return } else {
} else if mod.isWhitelisted(ip, mac) { for ip, mac := range targets {
mod.Debug("%s (%s) is whitelisted, skipping from spoofing loop.", ip, mac) if check_running && !mod.Running() {
continue return
} else if saddr.String() == ip { } else if mod.isWhitelisted(ip, mac) {
continue mod.Debug("%s (%s) is whitelisted, skipping from spoofing loop.", ip, mac)
} continue
} else if saddr.String() == ip {
rawIP := net.ParseIP(ip) continue
if err, pkt := packets.NewARPReply(saddr, smac, rawIP, mac); err != nil {
mod.Error("error while creating ARP spoof packet for %s: %s", ip, err)
} else {
mod.Debug("sending %d bytes of ARP packet to %s:%s.", len(pkt), ip, mac.String())
mod.Session.Queue.Send(pkt)
}
if mod.fullDuplex && isGW {
err := error(nil)
gwPacket := []byte(nil)
if isSpoofing {
mod.Debug("telling the gw we are %s", ip)
// we told the target we're te gateway, not let's tell the
// gateway that we are the target
if err, gwPacket = packets.NewARPReply(rawIP, ourHW, gwIP, gwHW); err != nil {
mod.Error("error while creating ARP spoof packet: %s", err)
}
} else {
mod.Debug("telling the gw %s is %s", ip, mac)
// send the gateway the original MAC of the target
if err, gwPacket = packets.NewARPReply(rawIP, mac, gwIP, gwHW); err != nil {
mod.Error("error while creating ARP spoof packet: %s", err)
}
} }
if gwPacket != nil { rawIP := net.ParseIP(ip)
mod.Debug("sending %d bytes of ARP packet to the gateway", len(gwPacket)) if err, pkt := packets.NewARPReply(saddr, smac, rawIP, mac); err != nil {
if err = mod.Session.Queue.Send(gwPacket); err != nil { mod.Error("error while creating ARP spoof packet for %s: %s", ip, err)
mod.Error("error while sending packet: %v", err) } else {
mod.Debug("sending %d bytes of ARP packet to %s:%s.", len(pkt), ip, mac.String())
mod.Session.Queue.Send(pkt)
}
if mod.fullDuplex && isGW {
err := error(nil)
gwPacket := []byte(nil)
if isSpoofing {
mod.Debug("telling the gw we are %s", ip)
// we told the target we're te gateway, not let's tell the
// gateway that we are the target
if err, gwPacket = packets.NewARPReply(rawIP, ourHW, gwIP, gwHW); err != nil {
mod.Error("error while creating ARP spoof packet: %s", err)
}
} else {
mod.Debug("telling the gw %s is %s", ip, mac)
// send the gateway the original MAC of the target
if err, gwPacket = packets.NewARPReply(rawIP, mac, gwIP, gwHW); err != nil {
mod.Error("error while creating ARP spoof packet: %s", err)
}
}
if gwPacket != nil {
mod.Debug("sending %d bytes of ARP packet to the gateway", len(gwPacket))
if err = mod.Session.Queue.Send(gwPacket); err != nil {
mod.Error("error while sending packet: %v", err)
}
} }
} }
} }

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

@ -0,0 +1,19 @@
package ble
import (
"github.com/bettercap/gatt"
)
func getClientOptions(deviceID int) []gatt.Option {
return []gatt.Option{
gatt.MacDeviceRole(gatt.CentralManager),
}
}
/*
var defaultBLEServerOptions = []gatt.Option{
gatt.MacDeviceRole(gatt.PeripheralManager),
}
*/

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,5 +1,5 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !darwin // +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
@ -9,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"
@ -20,12 +20,14 @@ import (
type BLERecon struct { type BLERecon struct {
session.SessionModule session.SessionModule
deviceId int
gattDevice gatt.Device gattDevice gatt.Device
currDevice *network.BLEDevice currDevice *network.BLEDevice
writeUUID *gatt.UUID writeUUID *gatt.UUID
writeData []byte writeData []byte
connected bool connected bool
connTimeout time.Duration connTimeout int
devTTL int
quit chan bool quit chan bool
done chan bool done chan bool
selector *utils.ViewSelector selector *utils.ViewSelector
@ -34,10 +36,12 @@ type BLERecon struct {
func NewBLERecon(s *session.Session) *BLERecon { func NewBLERecon(s *session.Session) *BLERecon {
mod := &BLERecon{ mod := &BLERecon{
SessionModule: session.NewSessionModule("ble.recon", s), SessionModule: session.NewSessionModule("ble.recon", s),
deviceId: -1,
gattDevice: nil, gattDevice: nil,
quit: make(chan bool), quit: make(chan bool),
done: make(chan bool), done: make(chan bool),
connTimeout: time.Duration(10) * time.Second, connTimeout: 5,
devTTL: 30,
currDevice: nil, currDevice: nil,
connected: false, connected: false,
} }
@ -77,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
@ -96,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)
@ -110,6 +114,18 @@ func NewBLERecon(s *session.Session) *BLERecon {
mod.AddHandler(write) mod.AddHandler(write)
mod.AddParam(session.NewIntParameter("ble.device",
fmt.Sprintf("%d", mod.deviceId),
"Index of the HCI device to use, -1 to autodetect."))
mod.AddParam(session.NewIntParameter("ble.timeout",
fmt.Sprintf("%d", mod.connTimeout),
"Connection timeout in seconds."))
mod.AddParam(session.NewIntParameter("ble.ttl",
fmt.Sprintf("%d", mod.devTTL),
"Seconds of inactivity for a device to be pruned."))
return mod return mod
} }
@ -142,11 +158,16 @@ func (mod *BLERecon) Configure() (err error) {
if mod.Running() { if mod.Running() {
return session.ErrAlreadyStarted(mod.Name()) return session.ErrAlreadyStarted(mod.Name())
} else if mod.gattDevice == nil { } else if mod.gattDevice == nil {
mod.Debug("initializing device ...") if err, mod.deviceId = mod.IntParam("ble.device"); err != nil {
return err
}
mod.Debug("initializing device (id:%d) ...", mod.deviceId)
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
} }
@ -160,14 +181,24 @@ func (mod *BLERecon) Configure() (err error) {
mod.gattDevice.Init(mod.onStateChanged) mod.gattDevice.Init(mod.onStateChanged)
} }
if err, mod.connTimeout = mod.IntParam("ble.timeout"); err != nil {
return err
} else if err, mod.devTTL = mod.IntParam("ble.ttl"); err != nil {
return err
}
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()
@ -194,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
@ -205,7 +238,8 @@ func (mod *BLERecon) Stop() error {
} }
func (mod *BLERecon) pruner() { func (mod *BLERecon) pruner() {
mod.Debug("started devices pruner ...") blePresentInterval := time.Duration(mod.devTTL) * time.Second
mod.Debug("started devices pruner with ttl %s", blePresentInterval)
for mod.Running() { for mod.Running() {
for _, dev := range mod.Session.BLE.Devices() { for _, dev := range mod.Session.BLE.Devices() {
@ -246,8 +280,9 @@ func (mod *BLERecon) enumAllTheThings(mac string) error {
mod.Info("connecting to %s ...", mac) mod.Info("connecting to %s ...", mac)
go func() { go func() {
time.Sleep(mod.connTimeout) time.Sleep(time.Duration(mod.connTimeout) * time.Second)
if mod.isEnumerating() && !mod.connected { if mod.isEnumerating() && !mod.connected {
mod.Warning("connection timeout")
mod.Session.Events.Add("ble.connection.timeout", mod.currDevice) mod.Session.Events.Add("ble.connection.timeout", mod.currDevice)
mod.onPeriphDisconnected(nil, nil) mod.onPeriphDisconnected(nil, nil)
} }

View file

@ -1,5 +1,5 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !darwin // +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
@ -13,7 +13,7 @@ func (mod *BLERecon) onStateChanged(dev gatt.Device, s gatt.State) {
switch s { switch s {
case gatt.StatePoweredOn: case gatt.StatePoweredOn:
if mod.currDevice == nil { if mod.currDevice == nil {
mod.Info("starting discovery ...") mod.Debug("starting discovery ...")
dev.Scan([]gatt.UUID{}, true) dev.Scan([]gatt.UUID{}, true)
} else { } else {
mod.Debug("current device was not cleaned: %v", mod.currDevice) mod.Debug("current device was not cleaned: %v", mod.currDevice)
@ -37,7 +37,7 @@ func (mod *BLERecon) onPeriphDisconnected(p gatt.Peripheral, err error) {
mod.Session.Events.Add("ble.device.disconnected", mod.currDevice) mod.Session.Events.Add("ble.device.disconnected", mod.currDevice)
mod.setCurrentDevice(nil) mod.setCurrentDevice(nil)
if mod.Running() { if mod.Running() {
mod.Info("device disconnected, restoring discovery.") mod.Debug("device disconnected, restoring discovery.")
mod.gattDevice.Scan([]gatt.UUID{}, true) mod.gattDevice.Scan([]gatt.UUID{}, true)
} }
} }
@ -54,7 +54,7 @@ func (mod *BLERecon) onPeriphConnected(p gatt.Peripheral, err error) {
mod.connected = true mod.connected = true
defer func(per gatt.Peripheral) { defer func(per gatt.Peripheral) {
mod.Info("disconnecting from %s ...", per.ID()) mod.Debug("disconnecting from %s ...", per.ID())
per.Device().CancelConnection(per) per.Device().CancelConnection(per)
mod.setCurrentDevice(nil) mod.setCurrentDevice(nil)
}(p) }(p)
@ -65,7 +65,7 @@ func (mod *BLERecon) onPeriphConnected(p gatt.Peripheral, err error) {
mod.Warning("failed to set MTU: %s", err) mod.Warning("failed to set MTU: %s", err)
} }
mod.Info("connected, enumerating all the things for %s!", p.ID()) mod.Debug("connected, enumerating all the things for %s!", p.ID())
services, err := p.DiscoverServices(nil) services, err := p.DiscoverServices(nil)
// https://github.com/bettercap/bettercap/issues/498 // https://github.com/bettercap/bettercap/issues/498
if err != nil && err.Error() != "success" { if err != nil && err.Error() != "success" {

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,22 +1,20 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !darwin // +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
import ( import (
"os"
"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"
) )
var ( var (
bleAliveInterval = time.Duration(5) * time.Second bleAliveInterval = time.Duration(5) * time.Second
blePresentInterval = time.Duration(30) * time.Second
) )
func (mod *BLERecon) getRow(dev *network.BLEDevice, withName bool) []string { func (mod *BLERecon) getRow(dev *network.BLEDevice, withName bool) []string {
@ -27,6 +25,7 @@ func (mod *BLERecon) getRow(dev *network.BLEDevice, withName bool) []string {
sinceSeen := time.Since(dev.LastSeen) sinceSeen := time.Since(dev.LastSeen)
lastSeen := dev.LastSeen.Format("15:04:05") lastSeen := dev.LastSeen.Format("15:04:05")
blePresentInterval := time.Duration(mod.devTTL) * time.Second
if sinceSeen <= bleAliveInterval { if sinceSeen <= bleAliveInterval {
lastSeen = tui.Bold(lastSeen) lastSeen = tui.Bold(lastSeen)
} else if sinceSeen > blePresentInterval { } else if sinceSeen > blePresentInterval {
@ -65,7 +64,7 @@ func (mod *BLERecon) doFilter(dev *network.BLEDevice) bool {
mod.selector.Expression.MatchString(dev.Vendor) mod.selector.Expression.MatchString(dev.Vendor)
} }
func (mod *BLERecon) doSelection() (err error, devices []*network.BLEDevice) { func (mod *BLERecon) doSelection() (devices []*network.BLEDevice, err error) {
if err = mod.selector.Update(); err != nil { if err = mod.selector.Update(); err != nil {
return return
} }
@ -128,7 +127,7 @@ func (mod *BLERecon) colNames(withName bool) []string {
} }
func (mod *BLERecon) Show() error { func (mod *BLERecon) Show() error {
err, devices := mod.doSelection() devices, err := mod.doSelection()
if err != nil { if err != nil {
return err return err
} }
@ -147,7 +146,7 @@ func (mod *BLERecon) Show() error {
} }
if len(rows) > 0 { if len(rows) > 0 {
tui.Table(os.Stdout, mod.colNames(hasName), rows) tui.Table(mod.Session.Events.Stdout, mod.colNames(hasName), rows)
mod.Session.Refresh() mod.Session.Refresh()
} }

View file

@ -1,16 +1,15 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !darwin // +build !windows,!freebsd,!openbsd,!netbsd
package ble package ble
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"os"
"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"
@ -332,7 +331,7 @@ func (mod *BLERecon) showServices(p gatt.Peripheral, services []*gatt.Service) {
if wantsToWrite && mod.writeUUID.Equal(ch.UUID()) { if wantsToWrite && mod.writeUUID.Equal(ch.UUID()) {
foundToWrite = true foundToWrite = true
if isWritable { if isWritable {
mod.Info("writing %d bytes to characteristics %s ...", len(mod.writeData), mod.writeUUID) mod.Debug("writing %d bytes to characteristics %s ...", len(mod.writeData), mod.writeUUID)
} else { } else {
mod.Warning("attempt to write %d bytes to non writable characteristics %s ...", len(mod.writeData), mod.writeUUID) mod.Warning("attempt to write %d bytes to non writable characteristics %s ...", len(mod.writeData), mod.writeUUID)
} }
@ -407,7 +406,7 @@ func (mod *BLERecon) showServices(p gatt.Peripheral, services []*gatt.Service) {
if wantsToWrite && !foundToWrite { if wantsToWrite && !foundToWrite {
mod.Error("writable characteristics %s not found.", mod.writeUUID) mod.Error("writable characteristics %s not found.", mod.writeUUID)
} else { } else {
tui.Table(os.Stdout, columns, rows) tui.Table(mod.Session.Events.Stdout, columns, rows)
mod.Session.Refresh() mod.Session.Refresh()
} }
} }

View file

@ -1,10 +1,10 @@
// +build !windows //go:build !windows && !freebsd && !openbsd && !netbsd
// +build !darwin // +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,26 +1,16 @@
// +build windows darwin //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 {
session.SessionModule session.SessionModule
} }
/*
// darwin
var defaultBLEClientOptions = []gatt.Option{
gatt.MacDeviceRole(gatt.CentralManager),
}
var defaultBLEServerOptions = []gatt.Option{
gatt.MacDeviceRole(gatt.PeripheralManager),
}
*/
func NewBLERecon(s *session.Session) *BLERecon { func NewBLERecon(s *session.Session) *BLERecon {
mod := &BLERecon{ mod := &BLERecon{
SessionModule: session.NewSessionModule("ble.recon", s), SessionModule: session.NewSessionModule("ble.recon", s),

386
modules/c2/c2.go Normal file
View file

@ -0,0 +1,386 @@
package c2
import (
"bytes"
"crypto/tls"
"fmt"
"strings"
"text/template"
"github.com/acarl005/stripansi"
"github.com/bettercap/bettercap/v2/modules/events_stream"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/log"
"github.com/evilsocket/islazy/str"
irc "github.com/thoj/go-ircevent"
)
type settings struct {
server string
tls bool
tlsVerify bool
nick string
user string
password string
saslUser string
saslPassword string
operator string
controlChannel string
eventsChannel string
outputChannel string
}
type C2 struct {
session.SessionModule
settings settings
stream *events_stream.EventsStream
templates map[string]*template.Template
channels map[string]string
client *irc.Connection
eventBus session.EventBus
quit chan bool
}
type eventContext struct {
Session *session.Session
Event session.Event
}
func NewC2(s *session.Session) *C2 {
mod := &C2{
SessionModule: session.NewSessionModule("c2", s),
stream: events_stream.NewEventsStream(s),
templates: make(map[string]*template.Template),
channels: make(map[string]string),
quit: make(chan bool),
settings: settings{
server: "localhost:6697",
tls: true,
tlsVerify: false,
nick: "bettercap",
user: "bettercap",
password: "password",
operator: "admin",
eventsChannel: "#events",
outputChannel: "#events",
controlChannel: "#events",
},
}
mod.AddParam(session.NewStringParameter("c2.server",
mod.settings.server,
"",
"IRC server address and port."))
mod.AddParam(session.NewBoolParameter("c2.server.tls",
"true",
"Enable TLS."))
mod.AddParam(session.NewBoolParameter("c2.server.tls.verify",
"false",
"Enable TLS certificate validation."))
mod.AddParam(session.NewStringParameter("c2.operator",
mod.settings.operator,
"",
"IRC nickname of the user allowed to run commands."))
mod.AddParam(session.NewStringParameter("c2.nick",
mod.settings.nick,
"",
"IRC nickname."))
mod.AddParam(session.NewStringParameter("c2.username",
mod.settings.user,
"",
"IRC username."))
mod.AddParam(session.NewStringParameter("c2.password",
mod.settings.password,
"",
"IRC server password."))
mod.AddParam(session.NewStringParameter("c2.sasl.username",
mod.settings.saslUser,
"",
"IRC SASL username."))
mod.AddParam(session.NewStringParameter("c2.sasl.password",
mod.settings.saslPassword,
"",
"IRC server SASL password."))
mod.AddParam(session.NewStringParameter("c2.channel.output",
mod.settings.outputChannel,
"",
"IRC channel to send commands output to."))
mod.AddParam(session.NewStringParameter("c2.channel.events",
mod.settings.eventsChannel,
"",
"IRC channel to send events to."))
mod.AddParam(session.NewStringParameter("c2.channel.control",
mod.settings.controlChannel,
"",
"IRC channel to receive commands from."))
mod.AddHandler(session.NewModuleHandler("c2 on", "",
"Start the C2 module.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("c2 off", "",
"Stop the C2 module.",
func(args []string) error {
return mod.Stop()
}))
mod.AddHandler(session.NewModuleHandler("c2.channel.set EVENT_TYPE CHANNEL",
"c2.channel.set ([^\\s]+) (.+)",
"Set a specific channel to report events of this type.",
func(args []string) error {
eventType := args[0]
channel := args[1]
mod.Debug("setting channel for event %s: %v", eventType, channel)
mod.channels[eventType] = channel
return nil
}))
mod.AddHandler(session.NewModuleHandler("c2.channel.clear EVENT_TYPE",
"c2.channel.clear ([^\\s]+)",
"Clear the channel to use for a specific event type.",
func(args []string) error {
eventType := args[0]
if _, found := mod.channels[args[0]]; found {
delete(mod.channels, eventType)
mod.Debug("cleared channel for %s", eventType)
} else {
return fmt.Errorf("channel for event %s not set", args[0])
}
return nil
}))
mod.AddHandler(session.NewModuleHandler("c2.template.set EVENT_TYPE TEMPLATE",
"c2.template.set ([^\\s]+) (.+)",
"Set the reporting template to use for a specific event type.",
func(args []string) error {
eventType := args[0]
eventTemplate := args[1]
parsed, err := template.New(eventType).Parse(eventTemplate)
if err != nil {
return err
}
mod.Debug("setting template for event %s: %v", eventType, parsed)
mod.templates[eventType] = parsed
return nil
}))
mod.AddHandler(session.NewModuleHandler("c2.template.clear EVENT_TYPE",
"c2.template.clear ([^\\s]+)",
"Clear the reporting template to use for a specific event type.",
func(args []string) error {
eventType := args[0]
if _, found := mod.templates[args[0]]; found {
delete(mod.templates, eventType)
mod.Debug("cleared template for %s", eventType)
} else {
return fmt.Errorf("template for event %s not set", args[0])
}
return nil
}))
mod.Session.Events.OnPrint(mod.onPrint)
return mod
}
func (mod *C2) Name() string {
return "c2"
}
func (mod *C2) Description() string {
return "A CnC module that connects to an IRC server for reporting and commands."
}
func (mod *C2) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *C2) Configure() (err error) {
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
}
if err, mod.settings.server = mod.StringParam("c2.server"); err != nil {
return err
} else if err, mod.settings.tls = mod.BoolParam("c2.server.tls"); err != nil {
return err
} else if err, mod.settings.tlsVerify = mod.BoolParam("c2.server.tls.verify"); err != nil {
return err
} else if err, mod.settings.nick = mod.StringParam("c2.nick"); err != nil {
return err
} else if err, mod.settings.user = mod.StringParam("c2.username"); err != nil {
return err
} else if err, mod.settings.password = mod.StringParam("c2.password"); err != nil {
return err
} else if err, mod.settings.saslUser = mod.StringParam("c2.sasl.username"); err != nil {
return err
} else if err, mod.settings.saslPassword = mod.StringParam("c2.sasl.password"); err != nil {
return err
} else if err, mod.settings.operator = mod.StringParam("c2.operator"); err != nil {
return err
} else if err, mod.settings.eventsChannel = mod.StringParam("c2.channel.events"); err != nil {
return err
} else if err, mod.settings.controlChannel = mod.StringParam("c2.channel.control"); err != nil {
return err
} else if err, mod.settings.outputChannel = mod.StringParam("c2.channel.output"); err != nil {
return err
}
mod.eventBus = mod.Session.Events.Listen()
mod.client = irc.IRC(mod.settings.nick, mod.settings.user)
if log.Level == log.DEBUG {
mod.client.VerboseCallbackHandler = true
mod.client.Debug = true
}
mod.client.Password = mod.settings.password
mod.client.UseTLS = mod.settings.tls
mod.client.TLSConfig = &tls.Config{
InsecureSkipVerify: !mod.settings.tlsVerify,
}
if mod.settings.saslUser != "" || mod.settings.saslPassword != "" {
mod.client.SASLLogin = mod.settings.saslUser
mod.client.SASLPassword = mod.settings.saslPassword
mod.client.UseSASL = true
}
mod.client.AddCallback("PRIVMSG", func(event *irc.Event) {
channel := event.Arguments[0]
message := event.Message()
from := event.Nick
if from != mod.settings.operator {
mod.client.Privmsg(event.Nick, "nope")
return
}
if channel != mod.settings.controlChannel && channel != mod.settings.nick {
mod.Debug("from:%s on:%s - '%s'", from, channel, message)
return
}
mod.Debug("from:%s on:%s - '%s'", from, channel, message)
parts := strings.SplitN(message, " ", 2)
cmd := parts[0]
args := ""
if len(parts) > 1 {
args = parts[1]
}
if cmd == "join" {
mod.client.Join(args)
} else if cmd == "part" {
mod.client.Part(args)
} else if cmd == "nick" {
mod.client.Nick(args)
} else if err = mod.Session.Run(message); err == nil {
} else {
mod.client.Privmsgf(event.Nick, "error: %v", stripansi.Strip(err.Error()))
}
})
mod.client.AddCallback("001", func(e *irc.Event) {
mod.Debug("got 101")
mod.client.Join(mod.settings.controlChannel)
mod.client.Join(mod.settings.outputChannel)
mod.client.Join(mod.settings.eventsChannel)
})
return mod.client.Connect(mod.settings.server)
}
func (mod *C2) onPrint(format string, args ...interface{}) {
if !mod.Running() {
return
}
msg := stripansi.Strip(str.Trim(fmt.Sprintf(format, args...)))
for _, line := range strings.Split(msg, "\n") {
mod.client.Privmsg(mod.settings.outputChannel, line)
}
}
func (mod *C2) onEvent(e session.Event) {
if mod.Session.EventsIgnoreList.Ignored(e) {
return
}
// default channel or event specific channel?
channel := mod.settings.eventsChannel
if custom, found := mod.channels[e.Tag]; found {
channel = custom
}
var out bytes.Buffer
if tpl, found := mod.templates[e.Tag]; found {
// use a custom template to render this event
if err := tpl.Execute(&out, eventContext{
Session: mod.Session,
Event: e,
}); err != nil {
fmt.Fprintf(&out, "%v", err)
}
} else {
// use the default view to render this event
mod.stream.Render(&out, e)
}
// make sure colors and in general bash escape sequences are removed
msg := stripansi.Strip(str.Trim(string(out.Bytes())))
mod.client.Privmsg(channel, msg)
}
func (mod *C2) Start() error {
if err := mod.Configure(); err != nil {
return err
}
return mod.SetRunning(true, func() {
mod.Info("started")
for mod.Running() {
var e session.Event
select {
case e = <-mod.eventBus:
mod.onEvent(e)
case <-mod.quit:
mod.Debug("got quit")
return
}
}
})
}
func (mod *C2) Stop() error {
return mod.SetRunning(false, func() {
mod.quit <- true
mod.Session.Events.Unlisten(mod.eventBus)
mod.client.Quit()
mod.client.Disconnect()
})
}

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"
@ -91,7 +91,7 @@ func (mod *CapletsModule) Show() error {
}) })
} }
tui.Table(os.Stdout, colNames, rows) tui.Table(mod.Session.Events.Stdout, colNames, rows)
return nil return nil
} }
@ -106,8 +106,8 @@ func (mod *CapletsModule) Paths() error {
rows = append(rows, []string{path}) rows = append(rows, []string{path})
} }
tui.Table(os.Stdout, colNames, rows) tui.Table(mod.Session.Events.Stdout, colNames, rows)
fmt.Printf("(paths can be customized by defining the %s environment variable)\n", tui.Bold(caplets.EnvVarName)) mod.Printf("(paths can be customized by defining the %s environment variable)\n", tui.Bold(caplets.EnvVarName))
return nil return nil
} }
@ -120,7 +120,7 @@ func (mod *CapletsModule) Update() error {
} }
} }
out, err := os.Create("/tmp/caplets.zip") out, err := os.Create(caplets.ArchivePath)
if err != nil { if err != nil {
return err return err
} }
@ -140,7 +140,7 @@ func (mod *CapletsModule) Update() error {
mod.Info("installing caplets to %s ...", caplets.InstallPath) mod.Info("installing caplets to %s ...", caplets.InstallPath)
if _, err = zip.Unzip("/tmp/caplets.zip", caplets.InstallBase); err != nil { if _, err = zip.Unzip(caplets.ArchivePath, caplets.InstallBase); err != nil {
return err return err
} }

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"
@ -42,6 +43,8 @@ func NewDHCP6Spoofer(s *session.Session) *DHCP6Spoofer {
waitGroup: &sync.WaitGroup{}, waitGroup: &sync.WaitGroup{},
} }
mod.SessionModule.Requires("net.recon")
mod.AddParam(session.NewStringParameter("dhcp6.spoof.domains", mod.AddParam(session.NewStringParameter("dhcp6.spoof.domains",
"microsoft.com, google.com, facebook.com, apple.com, twitter.com", "microsoft.com, google.com, facebook.com, apple.com, twitter.com",
``, ``,
@ -81,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

@ -4,10 +4,13 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net" "net"
"strconv"
"sync" "sync"
"github.com/bettercap/bettercap/packets" "github.com/bettercap/bettercap/v2/log"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/v2/packets"
"github.com/bettercap/bettercap/v2/session"
"github.com/google/gopacket" "github.com/google/gopacket"
"github.com/google/gopacket/layers" "github.com/google/gopacket/layers"
@ -20,6 +23,7 @@ type DNSSpoofer struct {
session.SessionModule session.SessionModule
Handle *pcap.Handle Handle *pcap.Handle
Hosts Hosts Hosts Hosts
TTL uint32
All bool All bool
waitGroup *sync.WaitGroup waitGroup *sync.WaitGroup
pktSourceChan chan gopacket.Packet pktSourceChan chan gopacket.Packet
@ -31,9 +35,12 @@ func NewDNSSpoofer(s *session.Session) *DNSSpoofer {
Handle: nil, Handle: nil,
All: false, All: false,
Hosts: Hosts{}, Hosts: Hosts{},
TTL: 1024,
waitGroup: &sync.WaitGroup{}, waitGroup: &sync.WaitGroup{},
} }
mod.SessionModule.Requires("net.recon")
mod.AddParam(session.NewStringParameter("dns.spoof.hosts", mod.AddParam(session.NewStringParameter("dns.spoof.hosts",
"", "",
"", "",
@ -53,6 +60,11 @@ func NewDNSSpoofer(s *session.Session) *DNSSpoofer {
"false", "false",
"If true the module will reply to every DNS request, otherwise it will only reply to the one targeting the local pc.")) "If true the module will reply to every DNS request, otherwise it will only reply to the one targeting the local pc."))
mod.AddParam(session.NewStringParameter("dns.spoof.ttl",
"1024",
"^[0-9]+$",
"TTL of spoofed DNS replies."))
mod.AddHandler(session.NewModuleHandler("dns.spoof on", "", mod.AddHandler(session.NewModuleHandler("dns.spoof on", "",
"Start the DNS spoofer in the background.", "Start the DNS spoofer in the background.",
func(args []string) error { func(args []string) error {
@ -82,13 +94,14 @@ func (mod DNSSpoofer) Author() string {
func (mod *DNSSpoofer) Configure() error { func (mod *DNSSpoofer) Configure() error {
var err error var err error
var ttl string
var hostsFile string var hostsFile string
var domains []string var domains []string
var address net.IP var address net.IP
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
@ -100,6 +113,8 @@ func (mod *DNSSpoofer) Configure() error {
return err return err
} else if err, hostsFile = mod.StringParam("dns.spoof.hosts"); err != nil { } else if err, hostsFile = mod.StringParam("dns.spoof.hosts"); err != nil {
return err return err
} else if err, ttl = mod.StringParam("dns.spoof.ttl"); err != nil {
return err
} }
mod.Hosts = Hosts{} mod.Hosts = Hosts{}
@ -129,26 +144,27 @@ func (mod *DNSSpoofer) Configure() error {
mod.Session.Firewall.EnableForwarding(true) mod.Session.Firewall.EnableForwarding(true)
} }
_ttl, _ := strconv.Atoi(ttl)
mod.TTL = uint32(_ttl)
return nil return nil
} }
func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp *layers.UDP, domain string, address net.IP, req *layers.DNS, target net.HardwareAddr) { func DnsReply(s *session.Session, TTL uint32, pkt gopacket.Packet, peth *layers.Ethernet, pudp *layers.UDP, domain string, address net.IP, req *layers.DNS, target net.HardwareAddr) (string, string) {
redir := fmt.Sprintf("(->%s)", address.String()) redir := fmt.Sprintf("(->%s)", address.String())
who := target.String() who := target.String()
if t, found := mod.Session.Lan.Get(target.String()); found { if t, found := s.Lan.Get(target.String()); found {
who = t.String() who = t.String()
} }
mod.Info("sending spoofed DNS reply for %s %s to %s.", tui.Red(domain), tui.Dim(redir), tui.Bold(who))
var err error var err error
var src, dst net.IP var src, dst net.IP
nlayer := pkt.NetworkLayer() nlayer := pkt.NetworkLayer()
if nlayer == nil { if nlayer == nil {
mod.Debug("missing network layer skipping packet.") log.Debug("missing network layer skipping packet.")
return return "", ""
} }
var eType layers.EthernetType var eType layers.EthernetType
@ -177,12 +193,19 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp
answers := make([]layers.DNSResourceRecord, 0) answers := make([]layers.DNSResourceRecord, 0)
for _, q := range req.Questions { for _, q := range req.Questions {
// do not include types we can't handle and that are not needed
// for successful spoofing anyway
// ref: https://github.com/bettercap/bettercap/issues/843
if q.Type.String() == "Unknown" {
continue
}
answers = append(answers, answers = append(answers,
layers.DNSResourceRecord{ layers.DNSResourceRecord{
Name: []byte(q.Name), Name: []byte(q.Name),
Type: q.Type, Type: q.Type,
Class: q.Class, Class: q.Class,
TTL: 1024, TTL: TTL,
IP: address, IP: address,
}) })
} }
@ -216,8 +239,8 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp
err, raw = packets.Serialize(&eth, &ip6, &udp, &dns) err, raw = packets.Serialize(&eth, &ip6, &udp, &dns)
if err != nil { if err != nil {
mod.Error("error serializing packet: %s.", err) log.Error("error serializing ipv6 packet: %s.", err)
return return "", ""
} }
} else { } else {
ip4 := layers.IPv4{ ip4 := layers.IPv4{
@ -237,15 +260,18 @@ func (mod *DNSSpoofer) dnsReply(pkt gopacket.Packet, peth *layers.Ethernet, pudp
err, raw = packets.Serialize(&eth, &ip4, &udp, &dns) err, raw = packets.Serialize(&eth, &ip4, &udp, &dns)
if err != nil { if err != nil {
mod.Error("error serializing packet: %s.", err) log.Error("error serializing ipv4 packet: %s.", err)
return return "", ""
} }
} }
mod.Debug("sending %d bytes of packet ...", len(raw)) log.Debug("sending %d bytes of packet ...", len(raw))
if err := mod.Session.Queue.Send(raw); err != nil { if err := s.Queue.Send(raw); err != nil {
mod.Error("error sending packet: %s", err) log.Error("error sending packet: %s", err)
return "", ""
} }
return redir, who
} }
func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) { func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) {
@ -263,7 +289,10 @@ func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) {
for _, q := range dns.Questions { for _, q := range dns.Questions {
qName := string(q.Name) qName := string(q.Name)
if address := mod.Hosts.Resolve(qName); address != nil { if address := mod.Hosts.Resolve(qName); address != nil {
mod.dnsReply(pkt, eth, udp, qName, address, dns, eth.SrcMAC) redir, who := DnsReply(mod.Session, mod.TTL, pkt, eth, udp, qName, address, dns, eth.SrcMAC)
if redir != "" && who != "" {
mod.Info("sending spoofed DNS reply for %s %s to %s.", tui.Red(qName), tui.Dim(redir), tui.Bold(who))
}
break break
} else { } else {
mod.Debug("skipping domain %s", qName) mod.Debug("skipping domain %s", qName)

View file

@ -22,7 +22,8 @@ type HostEntry struct {
} }
func (e HostEntry) Matches(host string) bool { func (e HostEntry) Matches(host string) bool {
return e.Host == host || strings.HasSuffix(host, e.Suffix) || (e.Expr != nil && e.Expr.Match(host)) lowerHost := strings.ToLower(host)
return e.Host == lowerHost || strings.HasSuffix(lowerHost, e.Suffix) || (e.Expr != nil && e.Expr.Match(lowerHost))
} }
type Hosts []HostEntry type Hosts []HostEntry
@ -46,7 +47,7 @@ func NewHostEntry(host string, address net.IP) HostEntry {
return entry return entry
} }
func HostsFromFile(filename string,defaultAddress net.IP) (err error, entries []HostEntry) { func HostsFromFile(filename string, defaultAddress net.IP) (err error, entries []HostEntry) {
input, err := os.Open(filename) input, err := os.Open(filename)
if err != nil { if err != nil {
return return

View file

@ -0,0 +1,61 @@
package events_stream
import (
"fmt"
"github.com/evilsocket/islazy/zip"
"os"
"time"
)
func (mod *EventsStream) doRotation() {
if mod.output == os.Stdout {
return
} else if !mod.rotation.Enabled {
return
}
output, isFile := mod.output.(*os.File)
if !isFile {
return
}
mod.rotation.Lock()
defer mod.rotation.Unlock()
doRotate := false
if info, err := output.Stat(); err == nil {
if mod.rotation.How == "size" {
doRotate = float64(info.Size()) >= float64(mod.rotation.Period*1024*1024)
} else if mod.rotation.How == "time" {
doRotate = info.ModTime().Unix()%int64(mod.rotation.Period) == 0
}
}
if doRotate {
var err error
name := fmt.Sprintf("%s-%s", mod.outputName, time.Now().Format(mod.rotation.Format))
if err := output.Close(); err != nil {
mod.Printf("could not close log for rotation: %s\n", err)
return
}
if err := os.Rename(mod.outputName, name); err != nil {
mod.Printf("could not rename %s to %s: %s\n", mod.outputName, name, err)
} else if mod.rotation.Compress {
zipName := fmt.Sprintf("%s.zip", name)
if err = zip.Files(zipName, []string{name}); err != nil {
mod.Printf("error creating %s: %s", zipName, err)
} else if err = os.Remove(name); err != nil {
mod.Printf("error deleting %s: %s", name, err)
}
}
mod.output, err = os.OpenFile(mod.outputName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
mod.Printf("could not open %s: %s", mod.outputName, err)
}
}
}

View file

@ -2,12 +2,13 @@ package events_stream
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"strconv" "strconv"
"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"
@ -27,7 +28,7 @@ type EventsStream struct {
session.SessionModule session.SessionModule
timeFormat string timeFormat string
outputName string outputName string
output *os.File output io.Writer
rotation rotation rotation rotation
triggerList *TriggerList triggerList *TriggerList
waitFor string waitFor string
@ -36,6 +37,7 @@ type EventsStream struct {
quit chan bool quit chan bool
dumpHttpReqs bool dumpHttpReqs bool
dumpHttpResp bool dumpHttpResp bool
dumpFormatHex bool
} }
func NewEventsStream(s *session.Session) *EventsStream { func NewEventsStream(s *session.Session) *EventsStream {
@ -148,13 +150,13 @@ func NewEventsStream(s *session.Session) *EventsStream {
"Print the list of filters used to ignore events.", "Print the list of filters used to ignore events.",
func(args []string) error { func(args []string) error {
if mod.Session.EventsIgnoreList.Empty() { if mod.Session.EventsIgnoreList.Empty() {
fmt.Printf("Ignore filters list is empty.\n") mod.Printf("Ignore filters list is empty.\n")
} else { } else {
mod.Session.EventsIgnoreList.RLock() mod.Session.EventsIgnoreList.RLock()
defer mod.Session.EventsIgnoreList.RUnlock() defer mod.Session.EventsIgnoreList.RUnlock()
for _, filter := range mod.Session.EventsIgnoreList.Filters() { for _, filter := range mod.Session.EventsIgnoreList.Filters() {
fmt.Printf(" '%s'\n", string(filter)) mod.Printf(" '%s'\n", string(filter))
} }
} }
return nil return nil
@ -214,18 +216,22 @@ func NewEventsStream(s *session.Session) *EventsStream {
"false", "false",
"If true all HTTP responses will be dumped.")) "If true all HTTP responses will be dumped."))
mod.AddParam(session.NewBoolParameter("events.stream.http.format.hex",
"true",
"If true dumped HTTP bodies will be in hexadecimal format."))
return mod return mod
} }
func (mod EventsStream) Name() string { func (mod *EventsStream) Name() string {
return "events.stream" return "events.stream"
} }
func (mod EventsStream) Description() string { func (mod *EventsStream) Description() string {
return "Print events as a continuous stream." return "Print events as a continuous stream."
} }
func (mod EventsStream) Author() string { func (mod *EventsStream) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>" return "Simone Margaritelli <evilsocket@gmail.com>"
} }
@ -237,6 +243,9 @@ func (mod *EventsStream) Configure() (err error) {
mod.output = os.Stdout mod.output = os.Stdout
} else if mod.outputName, err = fs.Expand(output); err == nil { } else if mod.outputName, err = fs.Expand(output); err == nil {
mod.output, err = os.OpenFile(mod.outputName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) mod.output, err = os.OpenFile(mod.outputName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
} }
} }
@ -258,6 +267,8 @@ func (mod *EventsStream) Configure() (err error) {
return err return err
} else if err, mod.dumpHttpResp = mod.BoolParam("events.stream.http.response.dump"); err != nil { } else if err, mod.dumpHttpResp = mod.BoolParam("events.stream.http.response.dump"); err != nil {
return err return err
} else if err, mod.dumpFormatHex = mod.BoolParam("events.stream.http.format.hex"); err != nil {
return err
} }
return err return err
@ -312,7 +323,7 @@ func (mod *EventsStream) Show(limit int) error {
} }
if numSelected := len(selected); numSelected > 0 { if numSelected := len(selected); numSelected > 0 {
fmt.Println() mod.Printf("\n")
for i := range selected { for i := range selected {
mod.View(selected[numSelected-1-i], false) mod.View(selected[numSelected-1-i], false)
} }
@ -350,7 +361,9 @@ func (mod *EventsStream) Stop() error {
return mod.SetRunning(false, func() { return mod.SetRunning(false, func() {
mod.quit <- true mod.quit <- true
if mod.output != os.Stdout { if mod.output != os.Stdout {
mod.output.Close() if fp, ok := mod.output.(*os.File); ok {
fp.Close()
}
} }
}) })
} }

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