Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb778dade5 | ||
|
|
1da6232bf9 | ||
|
|
43b911d3e2 | ||
|
|
1fadfd0bb2 | ||
|
|
41eedec438 | ||
|
|
30fc09bfca | ||
|
|
4626de8d34 | ||
|
|
b761c66091 | ||
|
|
a1f69f5ecc | ||
|
|
1acae6a5e6 | ||
|
|
66b29ac1a2 | ||
|
|
ead3afeaaf | ||
|
|
5f91021b82 | ||
|
|
0f8605b0f9 | ||
|
|
62188833f7 | ||
|
|
beb2f60e0f | ||
|
|
3def1f28ab | ||
|
|
38b371caa2 | ||
|
|
1364a38c9e |
23
.github/workflows/pylint.yml
vendored
Normal file
23
.github/workflows/pylint.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Pylint
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint
|
||||
- name: Analysing the code with pylint
|
||||
run: |
|
||||
pylint $(git ls-files '*.py')
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
build
|
||||
*.uf2
|
||||
mpy-cross*
|
||||
adafruit-circuitpython-bundle*
|
||||
|
||||
399
.pylintrc
Normal file
399
.pylintrc
Normal file
@@ -0,0 +1,399 @@
|
||||
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the ignore-list. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the ignore-list. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint.
|
||||
jobs=1
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint.extensions.no_self_use
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once).You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
# disable=import-error,raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,deprecated-str-translate-call
|
||||
disable=raw-checker-failed,bad-inline-option,locally-disabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,import-error,pointless-string-statement,unspecified-encoding
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio).You can also give a reporter class, eg
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
# notes=FIXME,XXX,TODO
|
||||
notes=FIXME,XXX
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=board
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid to define new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expectedly
|
||||
# not used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
# expected-line-ending-format=
|
||||
expected-line-ending-format=LF
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=12
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Regular expression matching correct argument names
|
||||
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct attribute names
|
||||
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma
|
||||
bad-names=foo,bar,baz,toto,tutu,tata
|
||||
|
||||
# Regular expression matching correct class attribute names
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Regular expression matching correct class names
|
||||
# class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
class-rgx=[A-Z_][a-zA-Z0-9_]+$
|
||||
|
||||
# Regular expression matching correct constant names
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Regular expression matching correct function names
|
||||
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma
|
||||
# good-names=i,j,k,ex,Run,_
|
||||
good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name
|
||||
include-naming-hint=no
|
||||
|
||||
# Regular expression matching correct inline iteration names
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Regular expression matching correct method names
|
||||
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
# Regular expression matching correct module names
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct variable names
|
||||
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled)
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled)
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,__new__,setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
# max-attributes=7
|
||||
max-attributes=11
|
||||
|
||||
# Maximum number of boolean expressions in a if statement
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=1
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception"
|
||||
overgeneral-exceptions=builtins.Exception
|
||||
@@ -5,6 +5,8 @@ PwnPi Amora is a wireless keystroke injection tool built on the Raspberry Pi Pic
|
||||
The project makes an attempt to provide a fully featured web IDE for building
|
||||
and deploying keystroke injection scripts.
|
||||
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
Check out the [wiki](https://github.com/lavafroth/pwnpi-amora/wiki/Quick-Start) for getting started.
|
||||
|
||||
BIN
assets/preview.png
Normal file
BIN
assets/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
94
build.py
Normal file → Executable file
94
build.py
Normal file → Executable file
@@ -1,14 +1,30 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Builder script to compile .py files to .mpy bytecode using mpy-cross
|
||||
"""
|
||||
|
||||
import glob
|
||||
from subprocess import PIPE, Popen
|
||||
from errno import ENOTDIR
|
||||
from os import listdir, makedirs
|
||||
from os.path import join, splitext
|
||||
from shutil import copytree, copy
|
||||
from errno import ENOTDIR
|
||||
SRC = 'src'
|
||||
DST = 'build'
|
||||
from shutil import copy, copytree, rmtree
|
||||
from subprocess import PIPE, Popen
|
||||
|
||||
SRC = "src"
|
||||
DST = "build"
|
||||
|
||||
|
||||
def cp(src, dst):
|
||||
def recursive_copy(src: str, dst: str):
|
||||
"""
|
||||
Copy a file or directory from src to dst.
|
||||
|
||||
Parameters:
|
||||
src (str): The path of the source file or directory.
|
||||
dst (str): The path of the destination file or directory.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
copytree(src, dst)
|
||||
except OSError as exc:
|
||||
@@ -18,27 +34,53 @@ def cp(src, dst):
|
||||
raise
|
||||
|
||||
|
||||
def to_compile(s: str):
|
||||
name, ext = splitext(s)
|
||||
if name not in ("code", "boot") and ext == ".py":
|
||||
return name
|
||||
def to_compile(name: str) -> str:
|
||||
"""
|
||||
Check if a given file is a Python source file that needs to be compiled.
|
||||
|
||||
Parameters:
|
||||
name (str): The name of the file.
|
||||
|
||||
Returns:
|
||||
str: The name of the file without the extension if it need compilation,
|
||||
otherwise None.
|
||||
"""
|
||||
base, ext = splitext(name)
|
||||
if base not in ("code", "boot") and ext == ".py":
|
||||
return base
|
||||
return None
|
||||
|
||||
|
||||
makedirs(DST, exist_ok=True)
|
||||
mpy_cross_bin = join(".", glob.glob("mpy-cross.static*")[0])
|
||||
def main():
|
||||
"""
|
||||
Use mpy-cross to compile .py files to .mpy bytecode
|
||||
"""
|
||||
|
||||
for entry in listdir(SRC):
|
||||
src_path = join(SRC, entry)
|
||||
if name := to_compile(entry):
|
||||
Popen([
|
||||
mpy_cross_bin,
|
||||
"-o",
|
||||
join(DST, f'{name}.mpy'),
|
||||
src_path,
|
||||
],
|
||||
stdout=PIPE,
|
||||
).communicate()
|
||||
else:
|
||||
dst_path = join(DST, entry)
|
||||
cp(src_path, dst_path)
|
||||
# Remove the build directory if it exists, then create it again
|
||||
rmtree(DST, ignore_errors=True)
|
||||
makedirs(DST, exist_ok=True)
|
||||
makedirs(join(DST, "lib"), exist_ok=True)
|
||||
|
||||
# Find the path of the mpy-cross binary
|
||||
mpy_cross_bin = join(".", glob.glob("mpy-cross.static*")[0])
|
||||
|
||||
# Process each entry in the source directory
|
||||
for entry in listdir(SRC):
|
||||
src_path = join(SRC, entry)
|
||||
# If the entry is a Python source file that needs to be compiled
|
||||
if name := to_compile(entry):
|
||||
# Compile the file using mpy-cross
|
||||
with Popen(
|
||||
[mpy_cross_bin, "-o",
|
||||
join(DST, "lib", f"{name}.mpy"), src_path],
|
||||
stdout=PIPE,
|
||||
) as process:
|
||||
process.communicate()
|
||||
else:
|
||||
# Copy the file or directory to the build directory
|
||||
dst_path = join(DST, entry)
|
||||
recursive_copy(src_path, dst_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
36
src/api.py
36
src/api.py
@@ -1,29 +1,40 @@
|
||||
import logs
|
||||
"""
|
||||
Handler code to interact with the backend for each incoming web request
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
from ducky import run_script_file, run_script
|
||||
|
||||
from adafruit_httpserver import JSONResponse, Request
|
||||
|
||||
import logs
|
||||
from ducky import run_script, run_script_file
|
||||
|
||||
|
||||
def create(path, contents=b""):
|
||||
with open(path, "wb") as h:
|
||||
h.write(contents)
|
||||
"""
|
||||
Create a new payload file, optionally with content to write to it.
|
||||
"""
|
||||
with open(path, "wb") as file:
|
||||
file.write(contents)
|
||||
|
||||
|
||||
def handle(body, response):
|
||||
def handle(request: Request):
|
||||
"""
|
||||
Handle all the API requests from the web interface like
|
||||
create, load, store, delete and run.
|
||||
"""
|
||||
body = request.json()
|
||||
action = body["action"]
|
||||
if action == "list":
|
||||
response.send(json.dumps(os.listdir("payloads")))
|
||||
return
|
||||
return JSONResponse(request, os.listdir("payloads"))
|
||||
|
||||
if action == "logs":
|
||||
response.send(logs.consume())
|
||||
return
|
||||
return JSONResponse(request, logs.consume())
|
||||
|
||||
filename = body.get("filename")
|
||||
path = f"payloads/{filename}"
|
||||
if action == "load":
|
||||
with open(path) as h:
|
||||
response.send(json.dumps({"contents": h.read()}))
|
||||
with open(path) as file:
|
||||
return JSONResponse(request, {"contents": file.read()})
|
||||
elif action == "store":
|
||||
create(path, body["contents"].encode())
|
||||
elif action == "delete":
|
||||
@@ -35,3 +46,4 @@ def handle(body, response):
|
||||
run_script_file(path)
|
||||
elif contents := body["contents"]:
|
||||
run_script(contents)
|
||||
return JSONResponse(request, {})
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
Disable concurrent write protections on the storage.
|
||||
This is especially necessary for dev builds since we need to change the files
|
||||
on the board for hot reloading.
|
||||
"""
|
||||
|
||||
import storage
|
||||
|
||||
storage.remount("/", readonly=False, disable_concurrent_write_protection=True)
|
||||
|
||||
89
src/code.py
89
src/code.py
@@ -1,52 +1,63 @@
|
||||
import microcontroller
|
||||
import wifi
|
||||
import socketpool
|
||||
"""
|
||||
The entrypoint for our circuitpython board.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import json
|
||||
|
||||
import microcontroller
|
||||
import socketpool
|
||||
import wifi
|
||||
from adafruit_httpserver import POST, FileResponse, Request, Server
|
||||
from ducky import run_boot_script
|
||||
|
||||
from api import handle
|
||||
from adafruit_httpserver.server import HTTPServer
|
||||
from adafruit_httpserver.request import HTTPRequest
|
||||
from adafruit_httpserver.response import HTTPResponse
|
||||
from adafruit_httpserver.methods import HTTPMethod
|
||||
from adafruit_httpserver.mime_type import MIMEType
|
||||
|
||||
async def setup_server():
|
||||
"""
|
||||
Begin a wifi access point defined by the SSID and PASSWORD environment
|
||||
variables.
|
||||
Spawn a socketpool on this interface.
|
||||
Serve the web interface over this socketpool indefinitely using an HTTP
|
||||
server.
|
||||
"""
|
||||
wifi.radio.start_ap(ssid=os.getenv("SSID"), password=os.getenv("PASSWORD"))
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = Server(pool)
|
||||
|
||||
@server.route("/")
|
||||
def base(request: Request):
|
||||
return FileResponse(request, "index.html", root_path="/static")
|
||||
|
||||
@server.route("/main.css")
|
||||
def css(request: Request):
|
||||
return FileResponse(request, "main.css", root_path="/static")
|
||||
|
||||
@server.route("/script.js")
|
||||
def javascript(request: Request):
|
||||
return FileResponse(request, "script.js", root_path="/static")
|
||||
|
||||
@server.route("/api", POST)
|
||||
def api(request: Request):
|
||||
return handle(request)
|
||||
|
||||
server.serve_forever(str(wifi.radio.ipv4_address_ap))
|
||||
|
||||
|
||||
async def main():
|
||||
wifi.radio.start_ap(ssid=os.getenv("SSID"), password=os.getenv("PASSWORD"))
|
||||
pool = socketpool.SocketPool(wifi.radio)
|
||||
server = HTTPServer(pool)
|
||||
|
||||
@server.route("/")
|
||||
def base(request: HTTPRequest):
|
||||
with HTTPResponse(request, content_type=MIMEType.TYPE_HTML) as response:
|
||||
response.send_file("static/index.html")
|
||||
|
||||
@server.route("/main.css")
|
||||
def css(request: HTTPRequest):
|
||||
with HTTPResponse(request, content_type=MIMEType.TYPE_CSS) as response:
|
||||
response.send_file("static/main.css")
|
||||
|
||||
@server.route("/script.js")
|
||||
def js(request: HTTPRequest):
|
||||
with HTTPResponse(request, content_type=MIMEType.TYPE_JS) as response:
|
||||
response.send_file("static/script.js")
|
||||
|
||||
@server.route("/api", HTTPMethod.POST)
|
||||
def api(request: HTTPRequest):
|
||||
with HTTPResponse(request, content_type=MIMEType.TYPE_JSON) as response:
|
||||
handle(json.loads(request.body), response)
|
||||
|
||||
server.start(str(wifi.radio.ipv4_address_ap))
|
||||
while True:
|
||||
try:
|
||||
server.poll()
|
||||
except OSError as error:
|
||||
print(error)
|
||||
"""
|
||||
Asynchronously run the boot script while setting
|
||||
the server up for the web interface.
|
||||
"""
|
||||
await asyncio.gather(
|
||||
run_boot_script(),
|
||||
setup_server()
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
# For some reason, wifi.stop_ap is not implemented.
|
||||
except NotImplementedError:
|
||||
microcontroller.reset()
|
||||
|
||||
112
src/ducky.py
112
src/ducky.py
@@ -1,90 +1,132 @@
|
||||
"""
|
||||
Logic to interpret and execute user defined ducky script payloads.
|
||||
"""
|
||||
import time
|
||||
|
||||
import usb_hid
|
||||
from adafruit_hid.keyboard import Keyboard
|
||||
|
||||
# comment out these lines for non_US keyboards
|
||||
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS as KeyboardLayout
|
||||
from adafruit_hid.keycode import Keycode
|
||||
from board import LED
|
||||
|
||||
from logs import info, warn
|
||||
|
||||
# uncomment these lines for non_US keyboards
|
||||
# replace LANG with appropriate language
|
||||
# from keyboard_layout_win_LANG import KeyboardLayout
|
||||
# from keycode_win_LANG import Keycode
|
||||
|
||||
import time
|
||||
from board import LED
|
||||
from logs import log
|
||||
|
||||
kbd = Keyboard(usb_hid.devices)
|
||||
layout = KeyboardLayout(kbd)
|
||||
|
||||
|
||||
def delay(millis):
|
||||
"""
|
||||
Sleep, do absolutely nothing.
|
||||
"""
|
||||
time.sleep(float(millis) / 1000)
|
||||
|
||||
|
||||
def prefix_checker(line: str):
|
||||
"""
|
||||
Returns a function that checks if line begins with
|
||||
any of the prefixes supplied to it.
|
||||
|
||||
Syntax sugar so that we can later use it in conditional
|
||||
statements like if something := checker("foo", "bar")
|
||||
"""
|
||||
|
||||
def checker(*prefixes):
|
||||
for prefix in prefixes:
|
||||
if line.startswith(prefix):
|
||||
return line[len(prefix) + 1:]
|
||||
return None
|
||||
|
||||
return checker
|
||||
|
||||
|
||||
def press_keys(line: str):
|
||||
"""
|
||||
Press all the keys and then release them.
|
||||
Really useful for keyboard shortcuts like Meta+R.
|
||||
"""
|
||||
# loop on each key filtering empty values
|
||||
for key in filter(None, line.split(" ")):
|
||||
key = key.upper()
|
||||
for key in filter(None, line.upper().split(" ")):
|
||||
if command_keycode := Keycode.__dict__.get(key):
|
||||
# If this is a valid key, send its keycode
|
||||
kbd.press(command_keycode)
|
||||
continue
|
||||
# If it's not a known key name, log it for diagnosis
|
||||
log(f"warning: unknown key: <{key}>")
|
||||
kbd.release_all()
|
||||
warn(f"unknown key: <{key}>")
|
||||
kbd.release_all()
|
||||
|
||||
|
||||
def repeat(contents: str, times: int):
|
||||
"""
|
||||
If the contents supplied is not empty or None,
|
||||
repeat those ducky script lines `times` times.
|
||||
"""
|
||||
if not contents:
|
||||
return
|
||||
for _ in range(times):
|
||||
run_script(contents)
|
||||
|
||||
|
||||
def run_script(contents):
|
||||
"""
|
||||
Interpret the ducky script and execute it line by line
|
||||
"""
|
||||
default_delay = 0
|
||||
previous_line = None
|
||||
for line in filter(None, contents.splitlines()):
|
||||
line = line.rstrip()
|
||||
after = prefix_checker(line)
|
||||
# we only run a command once by default
|
||||
run_n_times = 1
|
||||
if repeat := after("REPEAT"):
|
||||
if not previous_line:
|
||||
continue
|
||||
run_n_times = int(repeat)
|
||||
line = previous_line
|
||||
|
||||
for _ in range(run_n_times):
|
||||
if after("REM"):
|
||||
continue
|
||||
if (millis := after("DELAY")) is not None:
|
||||
if millis == "":
|
||||
millis = default_delay
|
||||
delay(millis)
|
||||
elif message := after("PRINT"):
|
||||
log(message)
|
||||
elif path := after("IMPORT"):
|
||||
run_script_file(path)
|
||||
elif millis := after("DEFAULT_DELAY", "DEFAULTDELAY"):
|
||||
default_delay = int(millis) * 10
|
||||
elif after("LED") is not None:
|
||||
LED.value ^= True
|
||||
elif string := after("STRING"):
|
||||
layout.write(string)
|
||||
else:
|
||||
press_keys(line)
|
||||
if times := after("REPEAT"):
|
||||
repeat(previous_line, int(times))
|
||||
|
||||
elif after("REM"):
|
||||
continue
|
||||
elif (millis := after("DELAY")) is not None:
|
||||
delay(millis or default_delay)
|
||||
elif message := after("PRINT"):
|
||||
info(message)
|
||||
elif path := after("IMPORT"):
|
||||
run_script_file(path)
|
||||
elif millis := after("DEFAULT_DELAY", "DEFAULTDELAY"):
|
||||
default_delay = int(millis)
|
||||
elif after("LED") is not None:
|
||||
LED.value ^= True
|
||||
elif string := after("STRING"):
|
||||
layout.write(string)
|
||||
else:
|
||||
press_keys(line)
|
||||
|
||||
previous_line = line
|
||||
delay(default_delay)
|
||||
|
||||
|
||||
def run_script_file(path: str):
|
||||
"""
|
||||
Try reading and running a ducky script from the supplied path.
|
||||
"""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
run_script(handle.read())
|
||||
except OSError as e:
|
||||
log(f"warning: unable to open file {path}: {e}")
|
||||
except OSError as error:
|
||||
warn(f"unable to open file {path}: {error}")
|
||||
|
||||
|
||||
async def run_boot_script():
|
||||
"""
|
||||
If a script with the name 'boot.dd' exists,
|
||||
run it without user interaction on boot.
|
||||
"""
|
||||
try:
|
||||
with open("payloads/boot.dd", "r", encoding="utf-8") as handle:
|
||||
run_script(handle.read())
|
||||
except OSError:
|
||||
info("boot script does not exist, skipping its execution")
|
||||
|
||||
24
src/logs.py
24
src/logs.py
@@ -1,12 +1,30 @@
|
||||
import json
|
||||
"""
|
||||
A very bare-bones logging implementation
|
||||
for the bottom pane of the Web UI.
|
||||
"""
|
||||
|
||||
logs = []
|
||||
|
||||
|
||||
def consume() -> str:
|
||||
dump = json.dumps(logs)
|
||||
"""
|
||||
Convert all the log entries from the module's global mutable
|
||||
list to json return them, clearing the list after the dump.
|
||||
"""
|
||||
dump = logs.copy()
|
||||
logs.clear()
|
||||
return dump
|
||||
|
||||
|
||||
def log(message: str):
|
||||
def info(message: str):
|
||||
"""
|
||||
Add a log entry with the message prepended with the info marker
|
||||
"""
|
||||
logs.append("info: " + message)
|
||||
|
||||
|
||||
def warn(message: str):
|
||||
"""
|
||||
Add a log entry with the message prepended with the warning marker
|
||||
"""
|
||||
logs.append("warning: " + message)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="files">
|
||||
<div class="hide files">
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<div class="documents icon"></div>
|
||||
@@ -19,7 +19,14 @@
|
||||
<div class="add icon"></div>
|
||||
</div>
|
||||
<div class="editorarea">
|
||||
<input class="title" placeholder="new file"></input>
|
||||
<div class="title-bar">
|
||||
<input class="title" placeholder="new file"></input>
|
||||
<button class="title-btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-check" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea class="editor"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,8 +13,9 @@ body {
|
||||
}
|
||||
|
||||
.container {
|
||||
height: calc(70vh);
|
||||
height: 70vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -36,14 +37,17 @@ body {
|
||||
height: calc(30vh - 2rem);
|
||||
background: #000;
|
||||
padding: 1rem;
|
||||
overflow: scroll;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.files {
|
||||
background: #111;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25vw;
|
||||
width: 0;
|
||||
overflow: scroll;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entry {
|
||||
@@ -52,6 +56,10 @@ body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.run {
|
||||
background: url('data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUgNC45ODk1MUM1IDQuMDE4MzUgNSAzLjUzMjc3IDUuMjAyNDkgMy4yNjUxQzUuMzc4ODkgMy4wMzE5MSA1LjY0ODUyIDIuODg3NjEgNS45NDA0IDIuODcwMThDNi4yNzU0NCAyLjg1MDE3IDYuNjc5NDYgMy4xMTk1MyA3LjQ4NzUyIDMuNjU4MjNMMTguMDAzMSAxMC42Njg2QzE4LjY3MDggMTEuMTEzNyAxOS4wMDQ2IDExLjMzNjMgMTkuMTIwOSAxMS42MTY4QzE5LjIyMjcgMTEuODYyMSAxOS4yMjI3IDEyLjEzNzcgMTkuMTIwOSAxMi4zODNDMTkuMDA0NiAxMi42NjM1IDE4LjY3MDggMTIuODg2IDE4LjAwMzEgMTMuMzMxMkw3LjQ4NzUyIDIwLjM0MTVDNi42Nzk0NiAyMC44ODAyIDYuMjc1NDQgMjEuMTQ5NiA1Ljk0MDQgMjEuMTI5NkM1LjY0ODUyIDIxLjExMjIgNS4zNzg4OSAyMC45Njc5IDUuMjAyNDkgMjAuNzM0N0M1IDIwLjQ2NyA1IDE5Ljk4MTQgNSAxOS4wMTAzVjQuOTg5NTFaIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PC9zdmc+Cg==');
|
||||
}
|
||||
@@ -63,12 +71,18 @@ body {
|
||||
|
||||
.delete {
|
||||
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTkuMDM2IDcuOTc2YS43NS43NSAwIDAgMC0xLjA2IDEuMDZMMTAuOTM5IDEybC0yLjk2MyAyLjk2M2EuNzUuNzUgMCAxIDAgMS4wNiAxLjA2TDEyIDEzLjA2bDIuOTYzIDIuOTY0YS43NS43NSAwIDAgMCAxLjA2MS0xLjA2TDEzLjA2MSAxMmwyLjk2My0yLjk2NGEuNzUuNzUgMCAxIDAtMS4wNi0xLjA2TDEyIDEwLjkzOSA5LjAzNiA3Ljk3NloiIGZpbGw9IiNmZmYiPjwvcGF0aD48cGF0aCBkPSJNMTIgMWM2LjA3NSAwIDExIDQuOTI1IDExIDExcy00LjkyNSAxMS0xMSAxMVMxIDE4LjA3NSAxIDEyIDUuOTI1IDEgMTIgMVpNMi41IDEyYTkuNSA5LjUgMCAwIDAgOS41IDkuNSA5LjUgOS41IDAgMCAwIDkuNS05LjVBOS41IDkuNSAwIDAgMCAxMiAyLjUgOS41IDkuNSAwIDAgMCAyLjUgMTJaIiBmaWxsPSIjZmZmIj48L3BhdGg+PC9zdmc+Cg==');
|
||||
margin: 0.15rem 0 0 0;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.icon {
|
||||
min-width: 1rem;
|
||||
min-height: 1rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon:active {
|
||||
background-color: #444;
|
||||
transition: 0.25s background-color;
|
||||
}
|
||||
|
||||
.add {
|
||||
@@ -78,22 +92,36 @@ body {
|
||||
.editorarea {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.editorarea > .title {
|
||||
padding: 0.5rem 1rem;
|
||||
.editorarea > .title-bar {
|
||||
color: #888;
|
||||
background: #222;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-bottom: #444 solid 0.1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.editorarea > .title-bar > .title {
|
||||
outline: none;
|
||||
border: none;
|
||||
background: #222;
|
||||
flex-grow: 1;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.editorarea > .title-bar > .title-btn {
|
||||
border: none;
|
||||
background: #222;
|
||||
padding: 0 0.4rem 0 0.4rem;
|
||||
}
|
||||
|
||||
.editor {
|
||||
background: #222;
|
||||
border: none;
|
||||
min-width: max-content;
|
||||
resize: none;
|
||||
height: calc(70vh - 3rem);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -123,3 +151,9 @@ body {
|
||||
.editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
.show {
|
||||
width: calc(100vw - 3rem);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ const logs = document.querySelector('.logs')
|
||||
const documents_icon = document.querySelector('.documents')
|
||||
const run_icon = document.querySelector('.run')
|
||||
const add_icon = document.querySelector('.add')
|
||||
const title = document.querySelector('.editorarea > .title')
|
||||
const title = document.querySelector('.editorarea > .title-bar > .title')
|
||||
const title_button = document.querySelector('.editorarea > .title-bar > .title-btn')
|
||||
let timer
|
||||
|
||||
function doApi(message) {
|
||||
@@ -34,7 +35,8 @@ editor.addEventListener('keyup', (_) => {
|
||||
function reload_logs() {
|
||||
doApi({'action':'logs'}).then(r => r.json()).then(body => {
|
||||
body.map(entry => {
|
||||
logs.innerHTML += entry + '<br />'
|
||||
logs.innerText += entry + '\n'
|
||||
logs.scrollTo(0, logs.scrollHeight)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -66,10 +68,15 @@ function reload_listing() {
|
||||
})
|
||||
}
|
||||
|
||||
function create_file() {
|
||||
title.readOnly = true
|
||||
doApi({"action": "create", "filename": title.value})
|
||||
}
|
||||
|
||||
title_button.addEventListener('click', create_file);
|
||||
title.addEventListener('keypress', (e) => {
|
||||
if (e.keyCode==13) {
|
||||
title.readOnly = true
|
||||
doApi({"action": "create", "filename": title.value})
|
||||
create_file()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -81,14 +88,8 @@ add_icon.addEventListener('click', () => {
|
||||
})
|
||||
|
||||
documents_icon.addEventListener('click', () => {
|
||||
const classList = files.classList
|
||||
if (classList.contains("show")) {
|
||||
files.classList.replace('show', 'hide')
|
||||
} else if (classList.contains("hide")) {
|
||||
files.classList.replace('hide', 'show')
|
||||
} else {
|
||||
files.classList.add('hide')
|
||||
}
|
||||
files.classList.toggle("show");
|
||||
files.classList.toggle("hide");
|
||||
});
|
||||
|
||||
run_icon.addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user