Compare commits

...

46 commits

Author SHA1 Message Date
Dan Halbert
c43147a016
Merge pull request #106 from dhalbert/update-ether-example
Update Ethernet library usage
2025-06-24 14:17:48 -04:00
Neradoc
ca65b59d66
Merge pull request #103 from Neradoc/work-when-no-ssl
Run on platforms without ssl
2025-06-24 19:24:06 +02:00
Dan Halbert
d9fb1e5343 update docs/starting_methods.rst 2025-06-24 13:22:48 -04:00
Dan Halbert
0f654784d8 Update Ethernet library usage 2025-06-24 13:10:22 -04:00
Dan Halbert
9ed25b875e
Remove previous from ssl import
The fix is good, but forgot to remove the non-guarded `from ssl import ...`
2025-06-24 13:01:59 -04:00
Neradoc
3c1b8e10bd enable running on platforms without ssl 2025-06-24 18:03:02 +02:00
foamyguy
b70106b17b update rtd.yml file
Signed-off-by: foamyguy <foamyguy@gmail.com>
2025-06-17 10:31:11 -05:00
foamyguy
b6a95f34a8
Merge pull request #104 from adafruit/use_ruff
change to ruff
2025-05-17 10:38:41 -05:00
foamyguy
cd39addea8 fix docs example highlights 2025-05-17 10:30:53 -05:00
foamyguy
fa365b0149 change to ruff 2025-05-16 16:11:43 +00:00
foamyguy
09e5431071
Merge pull request #101 from justmobilize/remove-secrets-usage
Remove secrets usage
2025-02-27 21:01:29 -06:00
Justin Myers
c8d958daaf Remove secrets usage 2025-02-27 15:40:25 -08:00
foamyguy
06820fbe03 add sphinx configuration to rtd.yaml
Signed-off-by: foamyguy <foamyguy@gmail.com>
2025-01-16 09:18:31 -06:00
Dan Halbert
529e0a8d1c
Merge pull request #100 from ncguk/patch-1
Fix a couple of typos in starting_methods.rst
2025-01-14 08:38:44 -05:00
ncguk
aea7b3809e
Fix a couple of typos in starting_methods.rst
Change 'socker pool' to 'socket pool' and 'more about the it here' to 'more about it here'.
2025-01-13 20:46:25 +00:00
Dan Halbert
bcef270a3d
Merge pull request #88 from michalpokusa/https-implementation
HTTPS implementation
2024-12-29 09:53:28 -05:00
michalpokusa
5f696e77e7 Fixed typos in examples.rst 2024-12-21 02:00:54 +01:00
michalpokusa
17364edf80 Last changes in docs and minor refactor/fixes in typing in Server method 2024-12-21 01:34:38 +01:00
michalpokusa
1bb84f9ee1 Merge branch 'main' into https-implementation 2024-12-20 21:43:10 +01:00
foamyguy
be94572e4d
Merge pull request #98 from michalpokusa/video-streaming-example
Custom response types docs and video streaming example
2024-12-04 16:33:32 -06:00
michalpokusa
27bf079b4d Added docs about the video streaming example and removed content_type parameter from XMixedReplaceResponse 2024-11-13 00:32:39 +00:00
michalpokusa
ec89785428 Added example code for video streaming using multipart/x-mixed-replace content type 2024-11-12 19:54:57 +00:00
michalpokusa
0e6180daf6 Fixed typo in multiple servers example 2024-11-12 01:05:06 +00:00
foamyguy
10cfca75d4 remove deprecated get_html_theme_path() call
Signed-off-by: foamyguy <foamyguy@gmail.com>
2024-10-07 14:47:47 -05:00
michalpokusa
ff165dd59c Added info about HTTPS support to README.md 2024-06-30 23:12:51 +00:00
michalpokusa
b2b01aaaa9 Minor refactor of some parts of docs 2024-05-01 02:42:27 +00:00
michalpokusa
14d6bef9e2 Added docs about SSL/TLS 2024-05-01 02:40:03 +00:00
michalpokusa
736471afe1 Fix: pylint CI 2024-05-01 02:03:17 +00:00
michalpokusa
3d4a64916d Fix and refactor of SSL handling for CPython 2024-05-01 01:57:27 +00:00
michalpokusa
d943404f72 Changed moment of starting debug timing handlers 2024-05-01 01:35:21 +00:00
michalpokusa
420ef62f8a Merge remote-tracking branch 'origin/main' into https-implementation 2024-04-30 23:31:00 +00:00
foamyguy
9b16e16f5b
Merge pull request #94 from michalpokusa/connection-manager-and-ap-examples
AP example and docs about ConnectionManager example
2024-04-15 10:53:31 -05:00
michalpokusa
aaa640e303 Added AP example and connection manager example to docs, updated docs and unified connection related examples 2024-04-03 23:28:30 +00:00
foamyguy
4cc9503303
Merge pull request #91 from DJDevon3/DJDevon3-CMSimpleTest
Add httpserver simpletest with connection manager
2024-04-01 10:18:36 -05:00
DJDevon3
863ac1e73c removed unused ssl_context
Changed ssid and password env's to match the simpletest_manual example.
2024-03-27 21:10:37 -04:00
DJDevon3
2f1c484c7b Add httpserver simpletest with connection manager
Only handles setup for socketpool. Coming from requests examples I'm more used to seeing connection manager handle the pool now. It might be good for consistency to role out connection manager for all examples but I'll leave that up to you.
2024-03-26 23:50:47 -04:00
michalpokusa
bc9c844e60 Fix: Setting Server._ssl_context to None if not using HTTPS 2024-03-05 23:42:06 +00:00
michalpokusa
1b9371a943 Added example of using HTTPS 2024-02-25 16:56:10 +00:00
foamyguy
dc9f83cd1a
Merge pull request #84 from michalpokusa/9.0.0-compatibility-and-better-typing
9.0.0 compatibility and better typing for sockets
2024-02-24 10:20:09 -06:00
michalpokusa
9b06f5a3e4 Modified Server to create and use SSLContext 2024-02-23 18:23:15 +00:00
michalpokusa
b00f70f1c4 Added validation for root_path in FileResponse 2024-02-21 04:44:46 +00:00
michalpokusa
b88bfa53ef Changes in repr of multiple classes 2024-02-21 04:43:57 +00:00
michalpokusa
f7d20b5fe6 Updated docs to use port 5000 2024-02-20 05:45:39 +00:00
michalpokusa
e49a5ed0b3 Changed default host to 0.0.0.0 and port to 5000 2024-02-20 00:48:34 +00:00
michalpokusa
2cc63fe366 Extracted creating server socket to separate method 2024-02-20 00:48:34 +00:00
michalpokusa
1c38bc8ca6 Added explicit typing for socketpool and socket with shared interface 2024-02-18 13:52:16 +00:00
49 changed files with 905 additions and 815 deletions

11
.gitattributes vendored Normal file
View file

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
.py text eol=lf
.rst text eol=lf
.txt text eol=lf
.yaml text eol=lf
.toml text eol=lf
.license text eol=lf
.md text eol=lf

View file

@ -1,42 +1,21 @@
# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò # SPDX-FileCopyrightText: 2024 Justin Myers for Adafruit Industries
# #
# SPDX-License-Identifier: Unlicense # SPDX-License-Identifier: Unlicense
repos: repos:
- repo: https://github.com/python/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/fsfe/reuse-tool
rev: v1.1.2
hooks:
- id: reuse
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/pycqa/pylint - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v2.17.4 rev: v0.3.4
hooks: hooks:
- id: pylint - id: ruff-format
name: pylint (library code) - id: ruff
types: [python] args: ["--fix"]
args: - repo: https://github.com/fsfe/reuse-tool
- --disable=consider-using-f-string rev: v3.0.1
exclude: "^(docs/|examples/|tests/|setup.py$)" hooks:
- id: pylint - id: reuse
name: pylint (example code)
description: Run pylint rules on "examples/*.py" files
types: [python]
files: "^examples/"
args:
- --disable=missing-docstring,invalid-name,consider-using-f-string,duplicate-code
- id: pylint
name: pylint (test code)
description: Run pylint rules on "tests/*.py" files
types: [python]
files: "^tests/"
args:
- --disable=missing-docstring,consider-using-f-string,duplicate-code

399
.pylintrc
View file

@ -1,399 +0,0 @@
# 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

View file

@ -8,8 +8,11 @@
# Required # Required
version: 2 version: 2
sphinx:
configuration: docs/conf.py
build: build:
os: ubuntu-20.04 os: ubuntu-lts-latest
tools: tools:
python: "3" python: "3"

View file

@ -17,9 +17,9 @@ Introduction
:alt: Build Status :alt: Build Status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
:target: https://github.com/psf/black :target: https://github.com/astral-sh/ruff
:alt: Code Style: Black :alt: Code Style: Ruff
HTTP Server for CircuitPython. HTTP Server for CircuitPython.
@ -32,6 +32,7 @@ HTTP Server for CircuitPython.
- Supports URL parameters and wildcard URLs. - Supports URL parameters and wildcard URLs.
- Supports HTTP Basic and Bearer Authentication on both server and route per level. - Supports HTTP Basic and Bearer Authentication on both server and route per level.
- Supports Websockets and Server-Sent Events. - Supports Websockets and Server-Sent Events.
- Limited support for HTTPS (only on selected microcontrollers with enough memory e.g. ESP32-S3).
Dependencies Dependencies

View file

@ -25,70 +25,70 @@ __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_HTTPServer.git"
from .authentication import ( from .authentication import (
Basic, Basic,
Token,
Bearer, Bearer,
Token,
check_authentication, check_authentication,
require_authentication, require_authentication,
) )
from .exceptions import ( from .exceptions import (
ServerStoppedError,
AuthenticationError, AuthenticationError,
BackslashInPathError,
FileNotExistsError,
InvalidPathError, InvalidPathError,
ParentDirectoryReferenceError, ParentDirectoryReferenceError,
BackslashInPathError, ServerStoppedError,
ServingFilesDisabledError, ServingFilesDisabledError,
FileNotExistsError,
) )
from .headers import Headers from .headers import Headers
from .methods import ( from .methods import (
GET, CONNECT,
POST,
PUT,
DELETE, DELETE,
PATCH, GET,
HEAD, HEAD,
OPTIONS, OPTIONS,
PATCH,
POST,
PUT,
TRACE, TRACE,
CONNECT,
) )
from .mime_types import MIMETypes from .mime_types import MIMETypes
from .request import QueryParams, FormData, Request from .request import FormData, QueryParams, Request
from .response import ( from .response import (
Response,
FileResponse,
ChunkedResponse, ChunkedResponse,
FileResponse,
JSONResponse, JSONResponse,
Redirect, Redirect,
Response,
SSEResponse, SSEResponse,
Websocket, Websocket,
) )
from .route import Route, as_route from .route import Route, as_route
from .server import ( from .server import (
Server,
NO_REQUEST,
CONNECTION_TIMED_OUT, CONNECTION_TIMED_OUT,
NO_REQUEST,
REQUEST_HANDLED_NO_RESPONSE, REQUEST_HANDLED_NO_RESPONSE,
REQUEST_HANDLED_RESPONSE_SENT, REQUEST_HANDLED_RESPONSE_SENT,
Server,
) )
from .status import ( from .status import (
Status,
SWITCHING_PROTOCOLS_101,
OK_200,
CREATED_201,
ACCEPTED_202, ACCEPTED_202,
NO_CONTENT_204,
PARTIAL_CONTENT_206,
MOVED_PERMANENTLY_301,
FOUND_302,
TEMPORARY_REDIRECT_307,
PERMANENT_REDIRECT_308,
BAD_REQUEST_400, BAD_REQUEST_400,
UNAUTHORIZED_401, CREATED_201,
FORBIDDEN_403, FORBIDDEN_403,
NOT_FOUND_404, FOUND_302,
METHOD_NOT_ALLOWED_405,
TOO_MANY_REQUESTS_429,
INTERNAL_SERVER_ERROR_500, INTERNAL_SERVER_ERROR_500,
METHOD_NOT_ALLOWED_405,
MOVED_PERMANENTLY_301,
NO_CONTENT_204,
NOT_FOUND_404,
NOT_IMPLEMENTED_501, NOT_IMPLEMENTED_501,
OK_200,
PARTIAL_CONTENT_206,
PERMANENT_REDIRECT_308,
SERVICE_UNAVAILABLE_503, SERVICE_UNAVAILABLE_503,
SWITCHING_PROTOCOLS_101,
TEMPORARY_REDIRECT_307,
TOO_MANY_REQUESTS_429,
UNAUTHORIZED_401,
Status,
) )

View file

@ -8,7 +8,7 @@
""" """
try: try:
from typing import Union, List from typing import List, Union
except ImportError: except ImportError:
pass pass
@ -40,15 +40,13 @@ class Token:
return f"{self.prefix} {self._value}" return f"{self.prefix} {self._value}"
class Bearer(Token): # pylint: disable=too-few-public-methods class Bearer(Token):
"""Represents HTTP Bearer Token Authentication.""" """Represents HTTP Bearer Token Authentication."""
prefix = "Bearer" prefix = "Bearer"
def check_authentication( def check_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> bool:
request: Request, auths: List[Union[Basic, Token, Bearer]]
) -> bool:
""" """
Returns ``True`` if request is authorized by any of the authentications, ``False`` otherwise. Returns ``True`` if request is authorized by any of the authentications, ``False`` otherwise.
@ -65,9 +63,7 @@ def check_authentication(
return any(auth_header == str(auth) for auth in auths) return any(auth_header == str(auth) for auth in auths)
def require_authentication( def require_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> None:
request: Request, auths: List[Union[Basic, Token, Bearer]]
) -> None:
""" """
Checks if the request is authorized and raises ``AuthenticationError`` if not. Checks if the request is authorized and raises ``AuthenticationError`` if not.

View file

@ -93,9 +93,7 @@ class Headers(_IFieldStorage):
return default return default
return header_value.split(";")[0].strip('" ') return header_value.split(";")[0].strip('" ')
def get_parameter( def get_parameter(self, name: str, parameter: str, default: str = None) -> Union[str, None]:
self, name: str, parameter: str, default: str = None
) -> Union[str, None]:
""" """
Returns the value of the given parameter for the given header name, or default if not found. Returns the value of the given parameter for the given header name, or default if not found.
@ -124,16 +122,12 @@ class Headers(_IFieldStorage):
def update(self, headers: Dict[str, str]): def update(self, headers: Dict[str, str]):
"""Updates the headers with the given dict.""" """Updates the headers with the given dict."""
return self._storage.update( return self._storage.update({key.lower(): [value] for key, value in headers.items()})
{key.lower(): [value] for key, value in headers.items()}
)
def copy(self): def copy(self):
"""Returns a copy of the headers.""" """Returns a copy of the headers."""
return Headers( return Headers(
"\r\n".join( "\r\n".join(f"{key}: {value}" for key in self.fields for value in self.get_list(key))
f"{key}: {value}" for key in self.fields for value in self.get_list(key)
)
) )
def __getitem__(self, name: str): def __getitem__(self, name: str):

View file

@ -8,11 +8,59 @@
""" """
try: try:
from typing import List, Dict, Union, Any from typing import Any, Dict, List, Tuple, Union
except ImportError: except ImportError:
pass pass
class _ISocket:
"""A class for typing necessary methods for a socket object."""
def accept(self) -> Tuple["_ISocket", Tuple[str, int]]: ...
def bind(self, address: Tuple[str, int]) -> None: ...
def setblocking(self, flag: bool) -> None: ...
def settimeout(self, value: "Union[float, None]") -> None: ...
def setsockopt(self, level: int, optname: int, value: int) -> None: ...
def listen(self, backlog: int) -> None: ...
def send(self, data: bytes) -> int: ...
def recv_into(self, buffer: memoryview, nbytes: int) -> int: ...
def close(self) -> None: ...
class _ISocketPool:
"""A class to typing necessary methods and properties for a socket pool object."""
AF_INET: int
SO_REUSEADDR: int
SOCK_STREAM: int
SOL_SOCKET: int
def socket(
self,
family: int = ...,
type: int = ...,
proto: int = ...,
) -> _ISocket: ...
def getaddrinfo(
self,
host: str,
port: int,
family: int = ...,
type: int = ...,
proto: int = ...,
flags: int = ...,
) -> Tuple[int, int, int, str, Tuple[str, int]]: ...
class _IFieldStorage: class _IFieldStorage:
"""Interface with shared methods for QueryParams, FormData and Headers.""" """Interface with shared methods for QueryParams, FormData and Headers."""
@ -62,7 +110,7 @@ class _IFieldStorage:
return key in self._storage return key in self._storage
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.__class__.__name__}({repr(self._storage)})" return f"<{self.__class__.__name__} {repr(self._storage)}>"
def _encode_html_entities(value: Union[str, None]) -> Union[str, None]: def _encode_html_entities(value: Union[str, None]) -> Union[str, None]:
@ -81,9 +129,7 @@ def _encode_html_entities(value: Union[str, None]) -> Union[str, None]:
class _IXSSSafeFieldStorage(_IFieldStorage): class _IXSSSafeFieldStorage(_IFieldStorage):
def get( def get(self, field_name: str, default: Any = None, *, safe=True) -> Union[Any, None]:
self, field_name: str, default: Any = None, *, safe=True
) -> Union[Any, None]:
if safe: if safe:
return _encode_html_entities(super().get(field_name, default)) return _encode_html_entities(super().get(field_name, default))
@ -92,9 +138,7 @@ class _IXSSSafeFieldStorage(_IFieldStorage):
def get_list(self, field_name: str, *, safe=True) -> List[Any]: def get_list(self, field_name: str, *, safe=True) -> List[Any]:
if safe: if safe:
return [ return [_encode_html_entities(value) for value in super().get_list(field_name)]
_encode_html_entities(value) for value in super().get_list(field_name)
]
_debug_warning_nonencoded_output() _debug_warning_nonencoded_output()
return super().get_list(field_name) return super().get_list(field_name)

View file

@ -7,7 +7,6 @@
* Author(s): Michał Pokusa * Author(s): Michał Pokusa
""" """
GET = "GET" GET = "GET"
POST = "POST" POST = "POST"

View file

@ -8,7 +8,7 @@
""" """
try: try:
from typing import List, Dict from typing import Dict, List
except ImportError: except ImportError:
pass pass

View file

@ -8,9 +8,7 @@
""" """
try: try:
from typing import List, Dict, Tuple, Union, Any, TYPE_CHECKING from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Union
from socket import socket
from socketpool import SocketPool
if TYPE_CHECKING: if TYPE_CHECKING:
from .server import Server from .server import Server
@ -20,8 +18,8 @@ except ImportError:
import json import json
from .headers import Headers from .headers import Headers
from .interfaces import _IFieldStorage, _IXSSSafeFieldStorage from .interfaces import _IFieldStorage, _ISocket, _IXSSSafeFieldStorage
from .methods import POST, PUT, PATCH, DELETE from .methods import DELETE, PATCH, POST, PUT
class QueryParams(_IXSSSafeFieldStorage): class QueryParams(_IXSSSafeFieldStorage):
@ -56,9 +54,7 @@ class QueryParams(_IXSSSafeFieldStorage):
def _add_field_value(self, field_name: str, value: str) -> None: def _add_field_value(self, field_name: str, value: str) -> None:
super()._add_field_value(field_name, value) super()._add_field_value(field_name, value)
def get( def get(self, field_name: str, default: str = None, *, safe=True) -> Union[str, None]:
self, field_name: str, default: str = None, *, safe=True
) -> Union[str, None]:
return super().get(field_name, default, safe=safe) return super().get(field_name, default, safe=safe)
def get_list(self, field_name: str, *, safe=True) -> List[str]: def get_list(self, field_name: str, *, safe=True) -> List[str]:
@ -94,9 +90,7 @@ class File:
content: Union[str, bytes] content: Union[str, bytes]
"""Content of the file.""" """Content of the file."""
def __init__( def __init__(self, filename: str, content_type: str, content: Union[str, bytes]) -> None:
self, filename: str, content_type: str, content: Union[str, bytes]
) -> None:
self.filename = filename self.filename = filename
self.content_type = content_type self.content_type = content_type
self.content = content self.content = content
@ -114,11 +108,7 @@ class File:
with open(file.filename, "wb") as f: with open(file.filename, "wb") as f:
f.write(file.content_bytes) f.write(file.content_bytes)
""" """
return ( return self.content.encode("utf-8") if isinstance(self.content, str) else self.content
self.content.encode("utf-8")
if isinstance(self.content, str)
else self.content
)
@property @property
def size(self) -> int: def size(self) -> int:
@ -127,11 +117,11 @@ class File:
def __repr__(self) -> str: def __repr__(self) -> str:
filename, content_type, size = ( filename, content_type, size = (
repr(self.filename), self.filename,
repr(self.content_type), self.content_type,
repr(self.size), self.size,
) )
return f"{self.__class__.__name__}({filename=}, {content_type=}, {size=})" return f"<{self.__class__.__name__} {filename=}, {content_type=}, {size=}>"
class Files(_IFieldStorage): class Files(_IFieldStorage):
@ -179,11 +169,11 @@ class FormData(_IXSSSafeFieldStorage):
@staticmethod @staticmethod
def _check_is_supported_content_type(content_type: str) -> None: def _check_is_supported_content_type(content_type: str) -> None:
return content_type in ( return content_type in {
"application/x-www-form-urlencoded", "application/x-www-form-urlencoded",
"multipart/form-data", "multipart/form-data",
"text/plain", "text/plain",
) }
def __init__(self, data: bytes, headers: Headers, *, debug: bool = False) -> None: def __init__(self, data: bytes, headers: Headers, *, debug: bool = False) -> None:
self._storage = {} self._storage = {}
@ -233,9 +223,7 @@ class FormData(_IXSSSafeFieldStorage):
# TODO: Other text content types (e.g. application/json) should be decoded as well and # TODO: Other text content types (e.g. application/json) should be decoded as well and
if filename is not None: if filename is not None:
self.files._add_field_value( # pylint: disable=protected-access self.files._add_field_value(field_name, File(filename, content_type, value))
field_name, File(filename, content_type, value)
)
else: else:
self._add_field_value(field_name, value) self._add_field_value(field_name, value)
@ -260,10 +248,10 @@ class FormData(_IXSSSafeFieldStorage):
def __repr__(self) -> str: def __repr__(self) -> str:
class_name = self.__class__.__name__ class_name = self.__class__.__name__
return f"{class_name}({repr(self._storage)}, files={repr(self.files._storage)})" return f"<{class_name} {repr(self._storage)}, files={repr(self.files._storage)}>"
class Request: # pylint: disable=too-many-instance-attributes class Request:
""" """
Incoming request, constructed from raw incoming bytes. Incoming request, constructed from raw incoming bytes.
It is passed as first argument to all route handlers. It is passed as first argument to all route handlers.
@ -274,7 +262,7 @@ class Request: # pylint: disable=too-many-instance-attributes
Server object that received the request. Server object that received the request.
""" """
connection: Union["SocketPool.Socket", "socket.socket"] connection: _ISocket
""" """
Socket object used to send and receive data on the connection. Socket object used to send and receive data on the connection.
""" """
@ -325,7 +313,7 @@ class Request: # pylint: disable=too-many-instance-attributes
def __init__( def __init__(
self, self,
server: "Server", server: "Server",
connection: Union["SocketPool.Socket", "socket.socket"], connection: _ISocket,
client_address: Tuple[str, int], client_address: Tuple[str, int],
raw_request: bytes = None, raw_request: bytes = None,
) -> None: ) -> None:
@ -367,9 +355,7 @@ class Request: # pylint: disable=too-many-instance-attributes
return { return {
name: value.strip('"') name: value.strip('"')
for name, value in [ for name, value in [cookie.strip().split("=", 1) for cookie in cookie_header.split(";")]
cookie.strip().split("=", 1) for cookie in cookie_header.split(";")
]
} }
@property @property
@ -443,7 +429,7 @@ class Request: # pylint: disable=too-many-instance-attributes
""" """
return ( return (
json.loads(self.body) json.loads(self.body)
if (self.body and self.method in (POST, PUT, PATCH, DELETE)) if (self.body and self.method in {POST, PUT, PATCH, DELETE})
else None else None
) )
@ -467,9 +453,7 @@ class Request: # pylint: disable=too-many-instance-attributes
) -> Tuple[str, str, QueryParams, str, Headers]: ) -> Tuple[str, str, QueryParams, str, Headers]:
"""Parse HTTP Start line to method, path, query_params and http_version.""" """Parse HTTP Start line to method, path, query_params and http_version."""
start_line, headers_string = ( start_line, headers_string = header_bytes.decode("utf-8").strip().split("\r\n", 1)
header_bytes.decode("utf-8").strip().split("\r\n", 1)
)
method, path, http_version = start_line.strip().split() method, path, http_version = start_line.strip().split()
@ -481,6 +465,10 @@ class Request: # pylint: disable=too-many-instance-attributes
return method, path, query_params, http_version, headers return method, path, query_params, http_version, headers
def __repr__(self) -> str:
path = self.path + (f"?{self.query_params}" if self.query_params else "")
return f'<{self.__class__.__name__} "{self.method} {path}">'
def _debug_unsupported_form_content_type(content_type: str) -> None: def _debug_unsupported_form_content_type(content_type: str) -> None:
"""Warns when an unsupported form content type is used.""" """Warns when an unsupported form content type is used."""

View file

@ -8,16 +8,14 @@
""" """
try: try:
from typing import Optional, Dict, Union, Tuple, Generator, Any from typing import Any, Dict, Generator, Optional, Tuple, Union
from socket import socket
from socketpool import SocketPool
except ImportError: except ImportError:
pass pass
import os
import json import json
import os
from binascii import b2a_base64 from binascii import b2a_base64
from errno import EAGAIN, ECONNRESET, ETIMEDOUT, ENOTCONN from errno import EAGAIN, ECONNRESET, ENOTCONN, ETIMEDOUT
try: try:
try: try:
@ -35,21 +33,22 @@ from .exceptions import (
FileNotExistsError, FileNotExistsError,
ParentDirectoryReferenceError, ParentDirectoryReferenceError,
) )
from .headers import Headers
from .interfaces import _ISocket
from .mime_types import MIMETypes from .mime_types import MIMETypes
from .request import Request from .request import Request
from .status import ( from .status import (
Status,
SWITCHING_PROTOCOLS_101,
OK_200,
MOVED_PERMANENTLY_301,
FOUND_302, FOUND_302,
TEMPORARY_REDIRECT_307, MOVED_PERMANENTLY_301,
OK_200,
PERMANENT_REDIRECT_308, PERMANENT_REDIRECT_308,
SWITCHING_PROTOCOLS_101,
TEMPORARY_REDIRECT_307,
Status,
) )
from .headers import Headers
class Response: # pylint: disable=too-few-public-methods class Response:
""" """
Response to a given `Request`. Use in `Server.route` handler functions. Response to a given `Request`. Use in `Server.route` handler functions.
@ -63,7 +62,7 @@ class Response: # pylint: disable=too-few-public-methods
return Response(request, body='Some content', content_type="text/plain") return Response(request, body='Some content', content_type="text/plain")
""" """
def __init__( # pylint: disable=too-many-arguments def __init__(
self, self,
request: Request, request: Request,
body: Union[str, bytes] = "", body: Union[str, bytes] = "",
@ -85,9 +84,7 @@ class Response: # pylint: disable=too-few-public-methods
self._request = request self._request = request
self._body = body self._body = body
self._status = status if isinstance(status, Status) else Status(*status) self._status = status if isinstance(status, Status) else Status(*status)
self._headers = ( self._headers = headers.copy() if isinstance(headers, Headers) else Headers(headers)
headers.copy() if isinstance(headers, Headers) else Headers(headers)
)
self._cookies = cookies.copy() if cookies else {} self._cookies = cookies.copy() if cookies else {}
self._content_type = content_type self._content_type = content_type
self._size = 0 self._size = 0
@ -99,13 +96,9 @@ class Response: # pylint: disable=too-few-public-methods
) -> None: ) -> None:
headers = self._headers.copy() headers = self._headers.copy()
response_message_header = ( response_message_header = f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
)
headers.setdefault( headers.setdefault("Content-Type", content_type or self._content_type or MIMETypes.DEFAULT)
"Content-Type", content_type or self._content_type or MIMETypes.DEFAULT
)
headers.setdefault("Content-Length", content_length) headers.setdefault("Content-Length", content_length)
headers.setdefault("Connection", "close") headers.setdefault("Connection", "close")
@ -117,14 +110,10 @@ class Response: # pylint: disable=too-few-public-methods
response_message_header += f"{header}: {value}\r\n" response_message_header += f"{header}: {value}\r\n"
response_message_header += "\r\n" response_message_header += "\r\n"
self._send_bytes( self._send_bytes(self._request.connection, response_message_header.encode("utf-8"))
self._request.connection, response_message_header.encode("utf-8")
)
def _send(self) -> None: def _send(self) -> None:
encoded_body = ( encoded_body = self._body.encode("utf-8") if isinstance(self._body, str) else self._body
self._body.encode("utf-8") if isinstance(self._body, str) else self._body
)
self._send_headers(len(encoded_body), self._content_type) self._send_headers(len(encoded_body), self._content_type)
self._send_bytes(self._request.connection, encoded_body) self._send_bytes(self._request.connection, encoded_body)
@ -132,7 +121,7 @@ class Response: # pylint: disable=too-few-public-methods
def _send_bytes( def _send_bytes(
self, self,
conn: Union["SocketPool.Socket", "socket.socket"], conn: _ISocket,
buffer: Union[bytes, bytearray, memoryview], buffer: Union[bytes, bytearray, memoryview],
): ):
bytes_sent: int = 0 bytes_sent: int = 0
@ -156,7 +145,7 @@ class Response: # pylint: disable=too-few-public-methods
pass pass
class FileResponse(Response): # pylint: disable=too-few-public-methods class FileResponse(Response):
""" """
Specialized version of `Response` class for sending files. Specialized version of `Response` class for sending files.
@ -174,7 +163,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
return FileResponse(request, filename='index.html', root_path='/www') return FileResponse(request, filename='index.html', root_path='/www')
""" """
def __init__( # pylint: disable=too-many-arguments def __init__(
self, self,
request: Request, request: Request,
filename: str = "index.html", filename: str = "index.html",
@ -217,6 +206,10 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
) )
self._filename = filename + "index.html" if filename.endswith("/") else filename self._filename = filename + "index.html" if filename.endswith("/") else filename
self._root_path = root_path or self._request.server.root_path self._root_path = root_path or self._request.server.root_path
if self._root_path is None:
raise ValueError("root_path must be provided in Server or in FileResponse")
self._full_file_path = self._combine_path(self._root_path, self._filename) self._full_file_path = self._combine_path(self._root_path, self._filename)
self._content_type = content_type or MIMETypes.get_for_filename(self._filename) self._content_type = content_type or MIMETypes.get_for_filename(self._filename)
self._file_length = self._get_file_length(self._full_file_path) self._file_length = self._get_file_length(self._full_file_path)
@ -240,7 +233,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
""" """
# Check for backslashes # Check for backslashes
if "\\" in file_path: # pylint: disable=anomalous-backslash-in-string if "\\" in file_path:
raise BackslashInPathError(file_path) raise BackslashInPathError(file_path)
# Check each component of the path for parent directory references # Check each component of the path for parent directory references
@ -273,7 +266,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
assert (st_mode & 0o170000) == 0o100000 # Check if it is a regular file assert (st_mode & 0o170000) == 0o100000 # Check if it is a regular file
return st_size return st_size
except (OSError, AssertionError): except (OSError, AssertionError):
raise FileNotExistsError(file_path) # pylint: disable=raise-missing-from raise FileNotExistsError(file_path)
def _send(self) -> None: def _send(self) -> None:
self._send_headers(self._file_length, self._content_type) self._send_headers(self._file_length, self._content_type)
@ -285,7 +278,7 @@ class FileResponse(Response): # pylint: disable=too-few-public-methods
self._close_connection() self._close_connection()
class ChunkedResponse(Response): # pylint: disable=too-few-public-methods class ChunkedResponse(Response):
""" """
Specialized version of `Response` class for sending data using chunked transfer encoding. Specialized version of `Response` class for sending data using chunked transfer encoding.
@ -305,7 +298,7 @@ class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
return ChunkedResponse(request, body, content_type="text/plain") return ChunkedResponse(request, body, content_type="text/plain")
""" """
def __init__( # pylint: disable=too-many-arguments def __init__(
self, self,
request: Request, request: Request,
body: Generator[Union[str, bytes], Any, Any], body: Generator[Union[str, bytes], Any, Any],
@ -353,7 +346,7 @@ class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
self._close_connection() self._close_connection()
class JSONResponse(Response): # pylint: disable=too-few-public-methods class JSONResponse(Response):
""" """
Specialized version of `Response` class for sending JSON data. Specialized version of `Response` class for sending JSON data.
@ -400,7 +393,7 @@ class JSONResponse(Response): # pylint: disable=too-few-public-methods
self._close_connection() self._close_connection()
class Redirect(Response): # pylint: disable=too-few-public-methods class Redirect(Response):
""" """
Specialized version of `Response` class for redirecting to another URL. Specialized version of `Response` class for redirecting to another URL.
@ -445,9 +438,7 @@ class Redirect(Response): # pylint: disable=too-few-public-methods
""" """
if status is not None and (permanent or preserve_method): if status is not None and (permanent or preserve_method):
raise ValueError( raise ValueError("Cannot specify both status and permanent/preserve_method argument")
"Cannot specify both status and permanent/preserve_method argument"
)
if status is None: if status is None:
if preserve_method: if preserve_method:
@ -463,7 +454,7 @@ class Redirect(Response): # pylint: disable=too-few-public-methods
self._close_connection() self._close_connection()
class SSEResponse(Response): # pylint: disable=too-few-public-methods class SSEResponse(Response):
""" """
Specialized version of `Response` class for sending Server-Sent Events. Specialized version of `Response` class for sending Server-Sent Events.
@ -497,7 +488,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
sse.close() sse.close()
""" """
def __init__( # pylint: disable=too-many-arguments def __init__(
self, self,
request: Request, request: Request,
headers: Union[Headers, Dict[str, str]] = None, headers: Union[Headers, Dict[str, str]] = None,
@ -520,11 +511,11 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
def _send(self) -> None: def _send(self) -> None:
self._send_headers() self._send_headers()
def send_event( # pylint: disable=too-many-arguments def send_event(
self, self,
data: str, data: str,
event: str = None, event: str = None,
id: int = None, # pylint: disable=redefined-builtin,invalid-name id: int = None,
retry: int = None, retry: int = None,
custom_fields: Dict[str, str] = None, custom_fields: Dict[str, str] = None,
) -> None: ) -> None:
@ -561,7 +552,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
self._close_connection() self._close_connection()
class Websocket(Response): # pylint: disable=too-few-public-methods class Websocket(Response):
""" """
Specialized version of `Response` class for creating a websocket connection. Specialized version of `Response` class for creating a websocket connection.
@ -632,7 +623,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
return b2a_base64(response_key.digest()).strip().decode() return b2a_base64(response_key.digest()).strip().decode()
def __init__( # pylint: disable=too-many-arguments def __init__(
self, self,
request: Request, request: Request,
headers: Union[Headers, Dict[str, str]] = None, headers: Union[Headers, Dict[str, str]] = None,
@ -690,7 +681,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
if fin != Websocket.FIN and opcode == Websocket.CONT: if fin != Websocket.FIN and opcode == Websocket.CONT:
return Websocket.CONT, None return Websocket.CONT, None
payload = bytes() payload = b""
if fin == Websocket.FIN and opcode == Websocket.CLOSE: if fin == Websocket.FIN and opcode == Websocket.CLOSE:
return Websocket.CLOSE, payload return Websocket.CLOSE, payload
@ -708,7 +699,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
length -= min(payload_length, length) length -= min(payload_length, length)
if has_mask: if has_mask:
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload)) payload = bytes(byte ^ mask[idx % 4] for idx, byte in enumerate(payload))
return opcode, payload return opcode, payload
@ -743,9 +734,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
if self.closed: if self.closed:
if fail_silently: if fail_silently:
return None return None
raise RuntimeError( raise RuntimeError("Websocket connection is closed, cannot receive messages")
"Websocket connection is closed, cannot receive messages"
)
try: try:
opcode, payload = self._read_frame() opcode, payload = self._read_frame()

View file

@ -8,7 +8,7 @@
""" """
try: try:
from typing import Callable, Iterable, Union, Tuple, Literal, Dict, TYPE_CHECKING from typing import TYPE_CHECKING, Callable, Dict, Iterable, Literal, Tuple, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from .response import Response from .response import Response
@ -52,9 +52,7 @@ class Route:
self._validate_path(path, append_slash) self._validate_path(path, append_slash)
self.path = path self.path = path
self.methods = ( self.methods = set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
)
self.handler = handler self.handler = handler
self.parameters_names = [ self.parameters_names = [
name[1:-1] for name in re.compile(r"/[^<>]*/?").split(path) if name != "" name[1:-1] for name in re.compile(r"/[^<>]*/?").split(path) if name != ""
@ -136,11 +134,11 @@ class Route:
return True, dict(zip(self.parameters_names, url_parameters_values)) return True, dict(zip(self.parameters_names, url_parameters_values))
def __repr__(self) -> str: def __repr__(self) -> str:
path = repr(self.path) path = self.path
methods = repr(self.methods) methods = self.methods
handler = repr(self.handler) handler = self.handler
return f"Route({path=}, {methods=}, {handler=})" return f"<Route {path=}, {methods=}, {handler=}>"
def as_route( def as_route(

View file

@ -8,9 +8,7 @@
""" """
try: try:
from typing import Callable, Protocol, Union, List, Tuple, Dict, Iterable from typing import Callable, Dict, Iterable, List, Tuple, Union
from socket import socket
from socketpool import SocketPool
except ImportError: except ImportError:
pass pass
@ -19,20 +17,36 @@ from sys import implementation
from time import monotonic, sleep from time import monotonic, sleep
from traceback import print_exception from traceback import print_exception
from .authentication import Basic, Token, Bearer, require_authentication from .authentication import Basic, Bearer, Token, require_authentication
from .exceptions import ( from .exceptions import (
ServerStoppedError,
AuthenticationError, AuthenticationError,
FileNotExistsError, FileNotExistsError,
InvalidPathError, InvalidPathError,
ServerStoppedError,
ServingFilesDisabledError, ServingFilesDisabledError,
) )
from .headers import Headers from .headers import Headers
from .interfaces import _ISocket, _ISocketPool
from .methods import GET, HEAD from .methods import GET, HEAD
from .request import Request from .request import Request
from .response import Response, FileResponse from .response import FileResponse, Response
from .route import Route from .route import Route
from .status import BAD_REQUEST_400, UNAUTHORIZED_401, FORBIDDEN_403, NOT_FOUND_404 from .status import BAD_REQUEST_400, FORBIDDEN_403, NOT_FOUND_404, UNAUTHORIZED_401
try:
from ssl import SSLContext, create_default_context
try: # ssl imports for C python
from ssl import (
CERT_NONE,
Purpose,
SSLError,
)
except ImportError:
pass
SSL_AVAILABLE = True
except ImportError:
SSL_AVAILABLE = False
NO_REQUEST = "no_request" NO_REQUEST = "no_request"
@ -40,8 +54,11 @@ CONNECTION_TIMED_OUT = "connection_timed_out"
REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response" REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response"
REQUEST_HANDLED_RESPONSE_SENT = "request_handled_response_sent" REQUEST_HANDLED_RESPONSE_SENT = "request_handled_response_sent"
# CircuitPython does not have these error codes
MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE = -30592
class Server: # pylint: disable=too-many-instance-attributes
class Server:
"""A basic socket-based HTTP server.""" """A basic socket-based HTTP server."""
host: str host: str
@ -53,8 +70,50 @@ class Server: # pylint: disable=too-many-instance-attributes
root_path: str root_path: str
"""Root directory to serve files from. ``None`` if serving files is disabled.""" """Root directory to serve files from. ``None`` if serving files is disabled."""
@staticmethod
def _validate_https_cert_provided(
certfile: Union[str, None], keyfile: Union[str, None]
) -> None:
if certfile is None or keyfile is None:
raise ValueError("Both certfile and keyfile must be specified for HTTPS")
@staticmethod
def _create_circuitpython_ssl_context(certfile: str, keyfile: str) -> SSLContext:
ssl_context = create_default_context()
ssl_context.load_verify_locations(cadata="")
ssl_context.load_cert_chain(certfile, keyfile)
return ssl_context
@staticmethod
def _create_cpython_ssl_context(certfile: str, keyfile: str) -> SSLContext:
ssl_context = create_default_context(purpose=Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(certfile, keyfile)
ssl_context.verify_mode = CERT_NONE
ssl_context.check_hostname = False
return ssl_context
@classmethod
def _create_ssl_context(cls, certfile: str, keyfile: str) -> SSLContext:
return (
cls._create_circuitpython_ssl_context(certfile, keyfile)
if implementation.name == "circuitpython"
else cls._create_cpython_ssl_context(certfile, keyfile)
)
def __init__( def __init__(
self, socket_source: Protocol, root_path: str = None, *, debug: bool = False self,
socket_source: _ISocketPool,
root_path: str = None,
*,
https: bool = False,
certfile: str = None,
keyfile: str = None,
debug: bool = False,
) -> None: ) -> None:
"""Create a server, and get it ready to run. """Create a server, and get it ready to run.
@ -62,17 +121,33 @@ class Server: # pylint: disable=too-many-instance-attributes
in CircuitPython or the `socket` module in CPython. in CircuitPython or the `socket` module in CPython.
:param str root_path: Root directory to serve files from :param str root_path: Root directory to serve files from
:param bool debug: Enables debug messages useful during development :param bool debug: Enables debug messages useful during development
:param bool https: If True, the server will use HTTPS
:param str certfile: Path to the certificate file, required if ``https`` is True
:param str keyfile: Path to the private key file, required if ``https`` is True
""" """
self._auths = []
self._buffer = bytearray(1024) self._buffer = bytearray(1024)
self._timeout = 1 self._timeout = 1
self._auths = []
self._routes: "List[Route]" = [] self._routes: "List[Route]" = []
self.headers = Headers()
self._socket_source = socket_source self._socket_source = socket_source
self._sock = None self._sock = None
self.headers = Headers()
self.host, self.port = None, None self.host, self.port = None, None
self.root_path = root_path self.root_path = root_path
if root_path in ["", "/"] and debug: self.https = https
if https:
if not SSL_AVAILABLE:
raise NotImplementedError("SSL not available on this platform")
self._validate_https_cert_provided(certfile, keyfile)
self._ssl_context = self._create_ssl_context(certfile, keyfile)
else:
self._ssl_context = None
if root_path in {"", "/"} and debug:
_debug_warning_exposed_files(root_path) _debug_warning_exposed_files(root_path)
self.stopped = True self.stopped = True
@ -172,7 +247,7 @@ class Server: # pylint: disable=too-many-instance-attributes
raise RuntimeError(f"Cannot start server on {host}:{port}") from error raise RuntimeError(f"Cannot start server on {host}:{port}") from error
def serve_forever( def serve_forever(
self, host: str, port: int = 80, *, poll_interval: float = 0.1 self, host: str = "0.0.0.0", port: int = 5000, *, poll_interval: float = 0.1
) -> None: ) -> None:
""" """
Wait for HTTP requests at the given host and port. Does not return. Wait for HTTP requests at the given host and port. Does not return.
@ -192,18 +267,32 @@ class Server: # pylint: disable=too-many-instance-attributes
except KeyboardInterrupt: # Exit on Ctrl-C e.g. during development except KeyboardInterrupt: # Exit on Ctrl-C e.g. during development
self.stop() self.stop()
return return
except Exception: # pylint: disable=broad-except except Exception:
pass # Ignore exceptions in handler function pass # Ignore exceptions in handler function
def _set_socket_level_to_reuse_address(self) -> None: @staticmethod
""" def _create_server_socket(
Only for CPython, prevents "Address already in use" error when restarting the server. socket_source: _ISocketPool,
""" ssl_context: "SSLContext | None",
self._sock.setsockopt( host: str,
self._socket_source.SOL_SOCKET, self._socket_source.SO_REUSEADDR, 1 port: int,
) ) -> _ISocket:
sock = socket_source.socket(socket_source.AF_INET, socket_source.SOCK_STREAM)
def start(self, host: str, port: int = 80) -> None: # TODO: Temporary backwards compatibility, remove after CircuitPython 9.0.0 release
if implementation.version >= (9,) or implementation.name != "circuitpython":
sock.setsockopt(socket_source.SOL_SOCKET, socket_source.SO_REUSEADDR, 1)
if ssl_context is not None:
sock = ssl_context.wrap_socket(sock, server_side=True)
sock.bind((host, port))
sock.listen(10)
sock.setblocking(False) # Non-blocking socket
return sock
def start(self, host: str = "0.0.0.0", port: int = 5000) -> None:
""" """
Start the HTTP server at the given host and port. Requires calling Start the HTTP server at the given host and port. Requires calling
``.poll()`` in a while loop to handle incoming requests. ``.poll()`` in a while loop to handle incoming requests.
@ -216,16 +305,7 @@ class Server: # pylint: disable=too-many-instance-attributes
self.host, self.port = host, port self.host, self.port = host, port
self.stopped = False self.stopped = False
self._sock = self._socket_source.socket( self._sock = self._create_server_socket(self._socket_source, self._ssl_context, host, port)
self._socket_source.AF_INET, self._socket_source.SOCK_STREAM
)
if implementation.name != "circuitpython":
self._set_socket_level_to_reuse_address()
self._sock.bind((host, port))
self._sock.listen(10)
self._sock.setblocking(False) # Non-blocking socket
if self.debug: if self.debug:
_debug_started_server(self) _debug_started_server(self)
@ -244,11 +324,9 @@ class Server: # pylint: disable=too-many-instance-attributes
if self.debug: if self.debug:
_debug_stopped_server(self) _debug_stopped_server(self)
def _receive_header_bytes( def _receive_header_bytes(self, sock: _ISocket) -> bytes:
self, sock: Union["SocketPool.Socket", "socket.socket"]
) -> bytes:
"""Receive bytes until a empty line is received.""" """Receive bytes until a empty line is received."""
received_bytes = bytes() received_bytes = b""
while b"\r\n\r\n" not in received_bytes: while b"\r\n\r\n" not in received_bytes:
try: try:
length = sock.recv_into(self._buffer, len(self._buffer)) length = sock.recv_into(self._buffer, len(self._buffer))
@ -263,7 +341,7 @@ class Server: # pylint: disable=too-many-instance-attributes
def _receive_body_bytes( def _receive_body_bytes(
self, self,
sock: Union["SocketPool.Socket", "socket.socket"], sock: _ISocket,
received_body_bytes: bytes, received_body_bytes: bytes,
content_length: int, content_length: int,
) -> bytes: ) -> bytes:
@ -282,7 +360,7 @@ class Server: # pylint: disable=too-many-instance-attributes
def _receive_request( def _receive_request(
self, self,
sock: Union["SocketPool.Socket", "socket.socket"], sock: _ISocket,
client_address: Tuple[str, int], client_address: Tuple[str, int],
) -> Request: ) -> Request:
"""Receive bytes from socket until the whole request is received.""" """Receive bytes from socket until the whole request is received."""
@ -300,15 +378,11 @@ class Server: # pylint: disable=too-many-instance-attributes
received_body_bytes = request.body received_body_bytes = request.body
# Receiving remaining body bytes # Receiving remaining body bytes
request.body = self._receive_body_bytes( request.body = self._receive_body_bytes(sock, received_body_bytes, content_length)
sock, received_body_bytes, content_length
)
return request return request
def _find_handler( # pylint: disable=cell-var-from-loop def _find_handler(self, method: str, path: str) -> Union[Callable[..., "Response"], None]:
self, method: str, path: str
) -> Union[Callable[..., "Response"], None]:
""" """
Finds a handler for a given route. Finds a handler for a given route.
@ -352,7 +426,7 @@ class Server: # pylint: disable=too-many-instance-attributes
raise ServingFilesDisabledError raise ServingFilesDisabledError
# Method is GET or HEAD, try to serve a file from the filesystem. # Method is GET or HEAD, try to serve a file from the filesystem.
if request.method in (GET, HEAD): if request.method in {GET, HEAD}:
return FileResponse( return FileResponse(
request, request,
filename=request.path, filename=request.path,
@ -384,11 +458,11 @@ class Server: # pylint: disable=too-many-instance-attributes
def _set_default_server_headers(self, response: Response) -> None: def _set_default_server_headers(self, response: Response) -> None:
for name, value in self.headers.items(): for name, value in self.headers.items():
response._headers.setdefault( # pylint: disable=protected-access response._headers.setdefault(name, value)
name, value
)
def poll(self) -> str: def poll(
self,
) -> str:
""" """
Call this method inside your main loop to get the server to check for new incoming client Call this method inside your main loop to get the server to check for new incoming client
requests. When a request comes in, it will be handled by the handler function. requests. When a request comes in, it will be handled by the handler function.
@ -401,11 +475,12 @@ class Server: # pylint: disable=too-many-instance-attributes
conn = None conn = None
try: try:
if self.debug:
_debug_start_time = monotonic()
conn, client_address = self._sock.accept() conn, client_address = self._sock.accept()
conn.settimeout(self._timeout) conn.settimeout(self._timeout)
_debug_start_time = monotonic()
# Receive the whole request # Receive the whole request
if (request := self._receive_request(conn, client_address)) is None: if (request := self._receive_request(conn, client_address)) is None:
conn.close() conn.close()
@ -424,16 +499,15 @@ class Server: # pylint: disable=too-many-instance-attributes
self._set_default_server_headers(response) self._set_default_server_headers(response)
# Send the response # Send the response
response._send() # pylint: disable=protected-access response._send()
_debug_end_time = monotonic()
if self.debug: if self.debug:
_debug_end_time = monotonic()
_debug_response_sent(response, _debug_end_time - _debug_start_time) _debug_response_sent(response, _debug_end_time - _debug_start_time)
return REQUEST_HANDLED_RESPONSE_SENT return REQUEST_HANDLED_RESPONSE_SENT
except Exception as error: # pylint: disable=broad-except except Exception as error:
if isinstance(error, OSError): if isinstance(error, OSError):
# There is no data available right now, try again later. # There is no data available right now, try again later.
if error.errno == EAGAIN: if error.errno == EAGAIN:
@ -441,6 +515,15 @@ class Server: # pylint: disable=too-many-instance-attributes
# Connection reset by peer, try again later. # Connection reset by peer, try again later.
if error.errno == ECONNRESET: if error.errno == ECONNRESET:
return NO_REQUEST return NO_REQUEST
# Handshake failed, try again later.
if error.errno == MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE:
return NO_REQUEST
# CPython specific SSL related errors
if implementation.name != "circuitpython" and isinstance(error, SSLError):
# Ignore unknown SSL certificate errors
if getattr(error, "reason", None) == "SSLV3_ALERT_CERTIFICATE_UNKNOWN":
return NO_REQUEST
if self.debug: if self.debug:
_debug_exception_in_handler(error) _debug_exception_in_handler(error)
@ -530,26 +613,33 @@ class Server: # pylint: disable=too-many-instance-attributes
else: else:
raise ValueError("Server.socket_timeout must be a positive numeric value.") raise ValueError("Server.socket_timeout must be a positive numeric value.")
def __repr__(self) -> str:
host = self.host
port = self.port
root_path = self.root_path
return f"<Server {host=}, {port=}, {root_path=}>"
def _debug_warning_exposed_files(root_path: str): def _debug_warning_exposed_files(root_path: str):
"""Warns about exposing all files on the device.""" """Warns about exposing all files on the device."""
print( print(
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device through" f"WARNING: Setting root_path to '{root_path}' will expose all files on your device "
" the webserver, including potentially sensitive files like settings.toml or secrets.py. " "through the webserver, including potentially sensitive files like settings.toml. "
"Consider making a sub-directory on your device and using that for your root_path instead." "Consider making a sub-directory on your device and using that for your root_path instead."
) )
def _debug_started_server(server: "Server"): def _debug_started_server(server: "Server"):
"""Prints a message when the server starts.""" """Prints a message when the server starts."""
scheme = "https" if server.https else "http"
host, port = server.host, server.port host, port = server.host, server.port
print(f"Started development server on http://{host}:{port}") print(f"Started development server on {scheme}://{host}:{port}")
def _debug_response_sent(response: "Response", time_elapsed: float): def _debug_response_sent(response: "Response", time_elapsed: float):
"""Prints a message after a response is sent.""" """Prints a message after a response is sent."""
# pylint: disable=protected-access
client_ip = response._request.client_address[0] client_ip = response._request.client_address[0]
method = response._request.method method = response._request.method
query_params = response._request.query_params query_params = response._request.query_params
@ -564,7 +654,7 @@ def _debug_response_sent(response: "Response", time_elapsed: float):
) )
def _debug_stopped_server(server: "Server"): # pylint: disable=unused-argument def _debug_stopped_server(server: "Server"):
"""Prints a message after the server stops.""" """Prints a message after the server stops."""
print("Stopped development server") print("Stopped development server")

View file

@ -8,7 +8,7 @@
""" """
class Status: # pylint: disable=too-few-public-methods class Status:
"""HTTP status code.""" """HTTP status code."""
def __init__(self, code: int, text: str): def __init__(self, code: int, text: str):
@ -21,14 +21,17 @@ class Status: # pylint: disable=too-few-public-methods
self.code = code self.code = code
self.text = text self.text = text
def __repr__(self): def __eq__(self, other: "Status"):
return f'Status({self.code}, "{self.text}")' return self.code == other.code and self.text == other.text
def __str__(self): def __str__(self):
return f"{self.code} {self.text}" return f"{self.code} {self.text}"
def __eq__(self, other: "Status"): def __repr__(self):
return self.code == other.code and self.text == other.text code = self.code
text = self.text
return f'<Status {code}, "{text}">'
SWITCHING_PROTOCOLS_101 = Status(101, "Switching Protocols") SWITCHING_PROTOCOLS_101 = Status(101, "Switching Protocols")

View file

@ -4,6 +4,9 @@
.. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py) .. If your library file(s) are nested in a directory (e.g. /adafruit_foo/foo.py)
.. use this format as the module name: "adafruit_foo.foo" .. use this format as the module name: "adafruit_foo.foo"
API Reference
#############
.. automodule:: adafruit_httpserver .. automodule:: adafruit_httpserver
:members: :members:

View file

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import datetime
import os import os
import sys import sys
import datetime
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath(".."))
@ -54,9 +52,7 @@ project = "Adafruit CircuitPython HTTPServer Library"
creation_year = "2022" creation_year = "2022"
current_year = str(datetime.datetime.now().year) current_year = str(datetime.datetime.now().year)
year_duration = ( year_duration = (
current_year current_year if current_year == creation_year else creation_year + " - " + current_year
if current_year == creation_year
else creation_year + " - " + current_year
) )
copyright = year_duration + " Dan Halbert" copyright = year_duration + " Dan Halbert"
author = "Dan Halbert" author = "Dan Halbert"
@ -116,7 +112,6 @@ napoleon_numpy_docstring = False
import sphinx_rtd_theme import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme" html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path(), "."]
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,

View file

@ -1,47 +1,21 @@
Simple Test .. note::
----------- All examples in this document are using ``Server`` in ``debug`` mode.
This mode is useful for development, but it is not recommended to use it in production.
More about Debug mode at the end of Examples section.
**All examples in this document are using** ``Server`` **in** ``debug`` **mode.** Different ways of starting the server
**This mode is useful for development, but it is not recommended to use it in production.** -------------------------------------
**More about Debug mode at the end of Examples section.**
This is the minimal example of using the library with CircuitPython. There are several ways to start the server on CircuitPython, mostly depending on the device you are using and
This example is serving a simple static text message. whether you have access to external network.
It also manually connects to the WiFi network. Functionally, all of them are the same, not features of the server are limited or disabled in any way.
.. literalinclude:: ../examples/httpserver_simpletest_manual.py Below you can find examples of different ways to start the server:
:caption: examples/httpserver_simpletest_manual.py
:emphasize-lines: 12-17
:linenos:
It is also possible to use Ethernet instead of WiFi. .. toctree::
The only difference in usage is related to configuring the ``socket_source`` differently.
.. literalinclude:: ../examples/httpserver_ethernet_simpletest.py starting_methods
:caption: examples/httpserver_ethernet_simpletest.py
:emphasize-lines: 13-23
:linenos:
Although there is nothing wrong with this approach, from the version 8.0.0 of CircuitPython,
`it is possible to use the environment variables <https://docs.circuitpython.org/en/latest/docs/environment.html#circuitpython-behavior>`_
defined in ``settings.toml`` file to store secrets and configure the WiFi network.
This is the same example as above, but it uses the ``settings.toml`` file to configure the WiFi network.
**From now on, all the examples will use the** ``settings.toml`` **file to configure the WiFi network.**
.. literalinclude:: ../examples/settings.toml
:caption: settings.toml
:lines: 5-
:linenos:
Note that we still need to import ``socketpool`` and ``wifi`` modules.
.. literalinclude:: ../examples/httpserver_simpletest_auto.py
:caption: examples/httpserver_simpletest_auto.py
:emphasize-lines: 11
:linenos:
CPython usage CPython usage
-------------------- --------------------
@ -65,7 +39,7 @@ In order to save memory, we are unregistering unused MIME types and registering
.. literalinclude:: ../examples/httpserver_static_files_serving.py .. literalinclude:: ../examples/httpserver_static_files_serving.py
:caption: examples/httpserver_static_files_serving.py :caption: examples/httpserver_static_files_serving.py
:emphasize-lines: 12-18,23-26 :emphasize-lines: 11-17,22-25
:linenos: :linenos:
You can also serve a specific file from the handler. You can also serve a specific file from the handler.
@ -77,7 +51,7 @@ By doing that, you can serve files from multiple directories, and decide exactly
.. literalinclude:: ../examples/httpserver_handler_serves_file.py .. literalinclude:: ../examples/httpserver_handler_serves_file.py
:caption: examples/httpserver_handler_serves_file.py :caption: examples/httpserver_handler_serves_file.py
:emphasize-lines: 13,22 :emphasize-lines: 12,21
:linenos: :linenos:
.. literalinclude:: ../examples/home.html .. literalinclude:: ../examples/home.html
@ -100,7 +74,7 @@ a running total of the last 10 samples.
.. literalinclude:: ../examples/httpserver_start_and_poll.py .. literalinclude:: ../examples/httpserver_start_and_poll.py
:caption: examples/httpserver_start_and_poll.py :caption: examples/httpserver_start_and_poll.py
:emphasize-lines: 29,38 :emphasize-lines: 28,37
:linenos: :linenos:
@ -112,7 +86,7 @@ without needing to manually manage the timing of each task.
.. literalinclude:: ../examples/httpserver_start_and_poll_asyncio.py .. literalinclude:: ../examples/httpserver_start_and_poll_asyncio.py
:caption: examples/httpserver_start_and_poll_asyncio.py :caption: examples/httpserver_start_and_poll_asyncio.py
:emphasize-lines: 5,33,42,45,50,55-62 :emphasize-lines: 5,6,34,43,46,51,56-63
:linenos: :linenos:
Server with MDNS Server with MDNS
@ -122,12 +96,12 @@ It is possible to use the MDNS protocol to make the server accessible via a host
to an IP address. It is worth noting that it takes a bit longer to get the response from the server to an IP address. It is worth noting that it takes a bit longer to get the response from the server
when accessing it via the hostname. when accessing it via the hostname.
In this example, the server is accessible via the IP and ``http://custom-mdns-hostname.local/``. In this example, the server is accessible via the IP and ``http://custom-mdns-hostname.local:5000/``.
On some routers it is also possible to use ``http://custom-mdns-hostname/``, but **this is not guaranteed to work**. On some routers it is also possible to use ``http://custom-mdns-hostname:5000/``, but **this is not guaranteed to work**.
.. literalinclude:: ../examples/httpserver_mdns.py .. literalinclude:: ../examples/httpserver_mdns.py
:caption: examples/httpserver_mdns.py :caption: examples/httpserver_mdns.py
:emphasize-lines: 12-14 :emphasize-lines: 11-13
:linenos: :linenos:
Get CPU information Get CPU information
@ -141,7 +115,7 @@ More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
.. literalinclude:: ../examples/httpserver_cpu_information.py .. literalinclude:: ../examples/httpserver_cpu_information.py
:caption: examples/httpserver_cpu_information.py :caption: examples/httpserver_cpu_information.py
:emphasize-lines: 9,15-18,33 :emphasize-lines: 9,14-17,32
:linenos: :linenos:
Handling different methods Handling different methods
@ -160,7 +134,7 @@ In example below, handler for ``/api`` and ``/api/`` route will be called when a
.. literalinclude:: ../examples/httpserver_methods.py .. literalinclude:: ../examples/httpserver_methods.py
:caption: examples/httpserver_methods.py :caption: examples/httpserver_methods.py
:emphasize-lines: 8,19,26,30,49 :emphasize-lines: 8,18,25,29,46
:linenos: :linenos:
Change NeoPixel color Change NeoPixel color
@ -182,7 +156,7 @@ Tested on ESP32-S2 Feather.
.. literalinclude:: ../examples/httpserver_neopixel.py .. literalinclude:: ../examples/httpserver_neopixel.py
:caption: examples/httpserver_neopixel.py :caption: examples/httpserver_neopixel.py
:emphasize-lines: 26-28,41,52,68,74 :emphasize-lines: 25-27,40,51,67,73
:linenos: :linenos:
Templates Templates
@ -207,7 +181,7 @@ You can find more information about the template syntax in the
.. literalinclude:: ../examples/httpserver_templates.py .. literalinclude:: ../examples/httpserver_templates.py
:caption: examples/httpserver_templates.py :caption: examples/httpserver_templates.py
:emphasize-lines: 12-15,49-55 :emphasize-lines: 12-15,51-59
:linenos: :linenos:
Form data parsing Form data parsing
@ -222,7 +196,7 @@ It is important to use correct ``enctype``, depending on the type of data you wa
- ``application/x-www-form-urlencoded`` - For sending simple text data without any special characters including spaces. - ``application/x-www-form-urlencoded`` - For sending simple text data without any special characters including spaces.
If you use it, values will be automatically parsed as strings, but special characters will be URL encoded If you use it, values will be automatically parsed as strings, but special characters will be URL encoded
e.g. ``"Hello World! ^-$%"`` will be saved as ``"Hello+World%21+%5E-%24%25"`` e.g. ``"Hello World! ^-$%"`` will be saved as ``"Hello+World%21+%5E-%24%25"``
- ``multipart/form-data`` - For sending textwith special characters and files - ``multipart/form-data`` - For sending text with special characters and files
When used, non-file values will be automatically parsed as strings and non plain text files will be saved as ``bytes``. When used, non-file values will be automatically parsed as strings and non plain text files will be saved as ``bytes``.
e.g. ``"Hello World! ^-$%"`` will be saved as ``'Hello World! ^-$%'``, and e.g. a PNG file will be saved as ``b'\x89PNG\r\n\x1a\n\x00\...``. e.g. ``"Hello World! ^-$%"`` will be saved as ``'Hello World! ^-$%'``, and e.g. a PNG file will be saved as ``b'\x89PNG\r\n\x1a\n\x00\...``.
- ``text/plain`` - For sending text data with special characters. - ``text/plain`` - For sending text data with special characters.
@ -235,7 +209,7 @@ return only the first one.
.. literalinclude:: ../examples/httpserver_form_data.py .. literalinclude:: ../examples/httpserver_form_data.py
:caption: examples/httpserver_form_data.py :caption: examples/httpserver_form_data.py
:emphasize-lines: 32,47,50 :emphasize-lines: 31,46,49
:linenos: :linenos:
Cookies Cookies
@ -249,7 +223,7 @@ In order to set cookies, pass ``cookies`` dictionary to ``Response`` constructo
.. literalinclude:: ../examples/httpserver_cookies.py .. literalinclude:: ../examples/httpserver_cookies.py
:caption: examples/httpserver_cookies.py :caption: examples/httpserver_cookies.py
:emphasize-lines: 70,74-75,82 :emphasize-lines: 69,73-74,81
:linenos: :linenos:
Chunked response Chunked response
@ -261,7 +235,7 @@ constructor.
.. literalinclude:: ../examples/httpserver_chunked.py .. literalinclude:: ../examples/httpserver_chunked.py
:caption: examples/httpserver_chunked.py :caption: examples/httpserver_chunked.py
:emphasize-lines: 8,21-26,28 :emphasize-lines: 8,20-25,27
:linenos: :linenos:
URL parameters and wildcards URL parameters and wildcards
@ -297,7 +271,7 @@ In both cases, wildcards will not match empty path segment, so ``/api/.../users`
.. literalinclude:: ../examples/httpserver_url_parameters.py .. literalinclude:: ../examples/httpserver_url_parameters.py
:caption: examples/httpserver_url_parameters.py :caption: examples/httpserver_url_parameters.py
:emphasize-lines: 30-34,53-54,65-66 :emphasize-lines: 29-31,48-49,60-61
:linenos: :linenos:
Authentication Authentication
@ -310,7 +284,7 @@ If you want to apply authentication to the whole server, you need to call ``.req
.. literalinclude:: ../examples/httpserver_authentication_server.py .. literalinclude:: ../examples/httpserver_authentication_server.py
:caption: examples/httpserver_authentication_server.py :caption: examples/httpserver_authentication_server.py
:emphasize-lines: 8,11-16,20 :emphasize-lines: 8,10-15,19
:linenos: :linenos:
On the other hand, if you want to apply authentication to a set of routes, you need to call ``require_authentication`` function. On the other hand, if you want to apply authentication to a set of routes, you need to call ``require_authentication`` function.
@ -318,7 +292,7 @@ In both cases you can check if ``request`` is authenticated by calling ``check_a
.. literalinclude:: ../examples/httpserver_authentication_handlers.py .. literalinclude:: ../examples/httpserver_authentication_handlers.py
:caption: examples/httpserver_authentication_handlers.py :caption: examples/httpserver_authentication_handlers.py
:emphasize-lines: 9-16,22-27,35,49,61 :emphasize-lines: 9-16,21-26,34,48,60
:linenos: :linenos:
Redirects Redirects
@ -335,7 +309,7 @@ Alternatively, you can pass a ``status`` object directly to ``Redirect`` constru
.. literalinclude:: ../examples/httpserver_redirects.py .. literalinclude:: ../examples/httpserver_redirects.py
:caption: examples/httpserver_redirects.py :caption: examples/httpserver_redirects.py
:emphasize-lines: 22-26,32,38,50,62 :emphasize-lines: 21-25,31,37,49,61
:linenos: :linenos:
Server-Sent Events Server-Sent Events
@ -348,12 +322,14 @@ This can be overcomed by periodically polling the server, but it is not an elega
Response is initialized on ``return``, events can be sent using ``.send_event()`` method. Due to the nature of SSE, it is necessary to store the Response is initialized on ``return``, events can be sent using ``.send_event()`` method. Due to the nature of SSE, it is necessary to store the
response object somewhere, so that it can be accessed later. response object somewhere, so that it can be accessed later.
**Because of the limited number of concurrently open sockets, it is not possible to process more than one SSE response at the same time.
This might change in the future, but for now, it is recommended to use SSE only with one client at a time.** .. warning::
Because of the limited number of concurrently open sockets, it is **not possible to process more than one SSE response at the same time**.
This might change in the future, but for now, it is recommended to use SSE **only with one client at a time**.
.. literalinclude:: ../examples/httpserver_sse.py .. literalinclude:: ../examples/httpserver_sse.py
:caption: examples/httpserver_sse.py :caption: examples/httpserver_sse.py
:emphasize-lines: 10,17,46-53,63 :emphasize-lines: 11,17,46-53,63
:linenos: :linenos:
Websockets Websockets
@ -370,14 +346,61 @@ This is anologous to calling ``.poll()`` on the ``Server`` object.
The following example uses ``asyncio``, which has to be installed separately. It is not necessary to use ``asyncio`` to use Websockets, The following example uses ``asyncio``, which has to be installed separately. It is not necessary to use ``asyncio`` to use Websockets,
but it is recommended as it makes it easier to handle multiple tasks. It can be used in any of the examples, but here it is particularly useful. but it is recommended as it makes it easier to handle multiple tasks. It can be used in any of the examples, but here it is particularly useful.
**Because of the limited number of concurrently open sockets, it is not possible to process more than one Websocket response at the same time. .. warning::
This might change in the future, but for now, it is recommended to use Websocket only with one client at a time.** Because of the limited number of concurrently open sockets, it is **not possible to process more than one Websocket response at the same time**.
This might change in the future, but for now, it is recommended to use Websocket **only with one client at a time**.
.. literalinclude:: ../examples/httpserver_websocket.py .. literalinclude:: ../examples/httpserver_websocket.py
:caption: examples/httpserver_websocket.py :caption: examples/httpserver_websocket.py
:emphasize-lines: 12,20,65-72,88,99 :emphasize-lines: 14,21,66-73,89,100
:linenos: :linenos:
Custom response types e.g. video streaming
------------------------------------------
The built-in response types may not always meet your specific requirements. In such cases, you can define custom response types and implement
the necessary logic.
The example below demonstrates a ``XMixedReplaceResponse`` class, which uses the ``multipart/x-mixed-replace`` content type to stream video frames
from a camera, similar to a CCTV system.
To ensure the server remains responsive, a global list of open connections is maintained. By running tasks asynchronously, the server can stream
video to multiple clients while simultaneously handling other requests.
.. literalinclude:: ../examples/httpserver_video_stream.py
:caption: examples/httpserver_video_stream.py
:emphasize-lines: 30-72,87
:linenos:
HTTPS
-----
.. warning::
HTTPS on CircuitPython **works only on boards with enough memory e.g. ESP32-S3**.
When you want to expose your server to the internet or an untrusted network, it is recommended to use HTTPS.
Together with authentication, it provides a relatively secure way to communicate with the server.
.. note::
Using HTTPS slows down the server, because of additional work with encryption and decryption.
Enabling HTTPS is straightforward and comes down to passing the path to the certificate and key files to the ``Server`` constructor
and setting ``https=True``.
.. literalinclude:: ../examples/httpserver_https.py
:caption: examples/httpserver_https.py
:emphasize-lines: 14-16
:linenos:
To create your own certificate, you can use the following command:
.. code-block:: bash
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem
You might have to change permissions of the files, so that the server can read them.
Multiple servers Multiple servers
---------------- ----------------
@ -387,7 +410,7 @@ Using ``.serve_forever()`` for this is not possible because of it's blocking beh
Each server **must have a different port number**. Each server **must have a different port number**.
In order to distinguish between responses from different servers a 'X-Server' header is added to each response. To distinguish between responses from different servers a 'X-Server' header is added to each response.
**This is an optional step**, both servers will work without it. **This is an optional step**, both servers will work without it.
In combination with separate authentication and diffrent ``root_path`` this allows creating moderately complex setups. In combination with separate authentication and diffrent ``root_path`` this allows creating moderately complex setups.
@ -395,7 +418,7 @@ You can share same handler functions between servers or use different ones for e
.. literalinclude:: ../examples/httpserver_multiple_servers.py .. literalinclude:: ../examples/httpserver_multiple_servers.py
:caption: examples/httpserver_multiple_servers.py :caption: examples/httpserver_multiple_servers.py
:emphasize-lines: 13-14,16-17,20,28,36-37,48-49,54-55 :emphasize-lines: 12-13,15-16,19,27,35-36,47-48,53-54
:linenos: :linenos:
Debug mode Debug mode
@ -412,7 +435,7 @@ occurs during handling of the request in ``.serve_forever()``.
This is how the logs might look like when debug mode is enabled:: This is how the logs might look like when debug mode is enabled::
Started development server on http://192.168.0.100:80 Started development server on http://192.168.0.100:5000
192.168.0.101 -- "GET /" 194 -- "200 OK" 154 -- 96ms 192.168.0.101 -- "GET /" 194 -- "200 OK" 154 -- 96ms
192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172 -- 123ms 192.168.0.101 -- "GET /example" 134 -- "404 Not Found" 172 -- 123ms
192.168.0.102 -- "POST /api" 1241 -- "401 Unauthorized" 95 -- 64ms 192.168.0.102 -- "POST /api" 1241 -- "401 Unauthorized" 95 -- 64ms
@ -430,5 +453,5 @@ This is the default format of the logs::
If you need more information about the server or request, or you want it in a different format you can modify If you need more information about the server or request, or you want it in a different format you can modify
functions at the bottom of ``adafruit_httpserver/server.py`` that start with ``_debug_...``. functions at the bottom of ``adafruit_httpserver/server.py`` that start with ``_debug_...``.
NOTE: .. note::
*This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.* This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.

80
docs/starting_methods.rst Normal file
View file

@ -0,0 +1,80 @@
Manual WiFi
-----------
This is the minimal example of using the library with CircuitPython.
This example is serving a simple static text message.
It also manually connects to the WiFi network. SSID and password are stored in the code, but they
can as well be stored in the ``settings.toml`` file, and then read from there using ``os.getenv()``.
.. literalinclude:: ../examples/httpserver_simpletest_manual_wifi.py
:caption: examples/httpserver_simpletest_manual_wifi.py
:emphasize-lines: 10-17
:linenos:
Manual AP (access point)
------------------------
If there is no external network available, it is possible to create an access point (AP) and run a server on it.
It is important to note that only devices connected to the AP will be able to access the server and depending on the device,
it may not be able to access the internet.
.. literalinclude:: ../examples/httpserver_simpletest_manual_ap.py
:caption: examples/httpserver_simpletest_manual_ap.py
:emphasize-lines: 10-15,29
:linenos:
Manual Ethernet
---------------
Ethernet can also be used to connect to the location network.
.. literalinclude:: ../examples/httpserver_simpletest_manual_ethernet.py
:caption: examples/httpserver_simpletest_manual_ethernet.py
:emphasize-lines: 11-20
:linenos:
Automatic WiFi using ``settings.toml``
--------------------------------------
From the version 8.0.0 of CircuitPython,
`it is possible to use the environment variables <https://docs.circuitpython.org/en/latest/docs/environment.html#circuitpython-behavior>`_
defined in ``settings.toml`` file to store secrets and configure the WiFi network
using the ``CIRCUITPY_WIFI_SSID`` and ``CIRCUITPY_WIFI_PASSWORD`` variables.
By default the library uses ``0.0.0.0`` and port ``5000`` for the server, as port ``80`` is reserved for the CircuitPython Web Workflow.
If you want to use port ``80`` , you need to set ``CIRCUITPY_WEB_API_PORT`` to any other port, and then set ``port`` parameter in ``Server`` constructor to ``80`` .
This is the same example as above, but it uses the ``settings.toml`` file to configure the WiFi network.
.. note::
From now on, all the examples will use the ``settings.toml`` file to configure the WiFi network.
.. literalinclude:: ../examples/settings.toml
:caption: settings.toml
:lines: 5-
:linenos:
Note that we still need to import ``socketpool`` and ``wifi`` modules.
.. literalinclude:: ../examples/httpserver_simpletest_auto_settings_toml.py
:caption: examples/httpserver_simpletest_auto_settings_toml.py
:emphasize-lines: 10
:linenos:
Helper for socket pool using ``adafruit_connection_manager``
------------------------------------------------------------
If you do not want to configure the socket pool manually, you can use the ``adafruit_connection_manager`` library,
which provides helpers for getting socket pool and SSL context for common boards.
Note that it is not installed by default.
You can read `more about it here <https://docs.circuitpython.org/projects/connectionmanager/en/latest/index.html>`_.
.. literalinclude:: ../examples/httpserver_simpletest_auto_connection_manager.py
:caption: examples/httpserver_simpletest_auto_connection_manager.py
:emphasize-lines: 6,10
:linenos:

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2024 Michał Pokusa
SPDX-License-Identifier: MIT

View file

@ -5,17 +5,16 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, UNAUTHORIZED_401 from adafruit_httpserver import UNAUTHORIZED_401, Request, Response, Server
from adafruit_httpserver.authentication import ( from adafruit_httpserver.authentication import (
AuthenticationError, AuthenticationError,
Basic, Basic,
Token,
Bearer, Bearer,
Token,
check_authentication, check_authentication,
require_authentication, require_authentication,
) )
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, Basic, Token, Bearer from adafruit_httpserver import Basic, Bearer, Request, Response, Server, Token
# Create a list of available authentication methods. # Create a list of available authentication methods.
auths = [ auths = [

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, ChunkedResponse from adafruit_httpserver import ChunkedResponse, Request, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, GET, Headers from adafruit_httpserver import GET, Headers, Request, Response, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)

View file

@ -6,8 +6,7 @@ import microcontroller
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, JSONResponse from adafruit_httpserver import JSONResponse, Request, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)

View file

@ -4,8 +4,7 @@
import socket import socket
from adafruit_httpserver import Server, Request, Response from adafruit_httpserver import Request, Response, Server
pool = socket pool = socket
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, GET, POST from adafruit_httpserver import GET, POST, Request, Response, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)

View file

@ -6,8 +6,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, FileResponse from adafruit_httpserver import FileResponse, Request, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/default-static-folder", debug=True) server = Server(pool, "/default-static-folder", debug=True)

View file

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2024 Michał Pokusa
#
# SPDX-License-Identifier: Unlicense
import socketpool
import wifi
from adafruit_httpserver import Request, Response, Server
pool = socketpool.SocketPool(wifi.radio)
server = Server(
pool,
root_path="/static",
https=True,
certfile="cert.pem",
keyfile="key.pem",
debug=True,
)
@server.route("/")
def base(request: Request):
"""
Serve a default static plain text message.
"""
return Response(request, "Hello from the CircuitPython HTTPS Server!")
server.serve_forever(str(wifi.radio.ipv4_address), 443)

View file

@ -6,12 +6,11 @@ import mdns
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, FileResponse from adafruit_httpserver import FileResponse, Request, Server
mdns_server = mdns.Server(wifi.radio) mdns_server = mdns.Server(wifi.radio)
mdns_server.hostname = "custom-mdns-hostname" mdns_server.hostname = "custom-mdns-hostname"
mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=80) mdns_server.advertise_service(service_type="_http", protocol="_tcp", port=5000)
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, JSONResponse, GET, POST, PUT, DELETE from adafruit_httpserver import DELETE, GET, POST, PUT, JSONResponse, Request, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)
@ -27,7 +26,7 @@ def api(request: Request):
return JSONResponse(request, objects) return JSONResponse(request, objects)
# Upload or update objects # Upload or update objects
if request.method in [POST, PUT]: if request.method in {POST, PUT}:
uploaded_object = request.json() uploaded_object = request.json()
# Find object with same ID # Find object with same ID
@ -41,9 +40,7 @@ def api(request: Request):
# If not found, add it # If not found, add it
objects.append(uploaded_object) objects.append(uploaded_object)
return JSONResponse( return JSONResponse(request, {"message": "Object added", "object": uploaded_object})
request, {"message": "Object added", "object": uploaded_object}
)
# Delete objects # Delete objects
if request.method == DELETE: if request.method == DELETE:
@ -59,9 +56,7 @@ def api(request: Request):
) )
# If not found, return error # If not found, return error
return JSONResponse( return JSONResponse(request, {"message": "Object not found", "object": deleted_object})
request, {"message": "Object not found", "object": deleted_object}
)
# If we get here, something went wrong # If we get here, something went wrong
return JSONResponse(request, {"message": "Something went wrong"}) return JSONResponse(request, {"message": "Something went wrong"})

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response from adafruit_httpserver import Request, Response, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
@ -42,11 +41,11 @@ def home(request: Request):
return Response(request, "Hello from home!") return Response(request, "Hello from home!")
id_address = str(wifi.radio.ipv4_address) ip_address = str(wifi.radio.ipv4_address)
# Start the servers. # Start the servers.
bedroom_server.start(id_address, 5000) bedroom_server.start(ip_address, 5000)
office_server.start(id_address, 8000) office_server.start(ip_address, 8000)
while True: while True:
try: try:

View file

@ -7,8 +7,7 @@ import neopixel
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Route, as_route, Request, Response, GET, POST from adafruit_httpserver import GET, POST, Request, Response, Route, Server, as_route
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -6,16 +6,15 @@ import socketpool
import wifi import wifi
from adafruit_httpserver import ( from adafruit_httpserver import (
Server, MOVED_PERMANENTLY_301,
NOT_FOUND_404,
POST,
Redirect,
Request, Request,
Response, Response,
Redirect, Server,
POST,
NOT_FOUND_404,
MOVED_PERMANENTLY_301,
) )
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)

View file

@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2024 DJDevon3
#
# SPDX-License-Identifier: MIT
import wifi
from adafruit_connection_manager import get_radio_socketpool
from adafruit_httpserver import Request, Response, Server
pool = get_radio_socketpool(wifi.radio)
server = Server(pool, "/static", debug=True)
@server.route("/")
def base(request: Request):
"""
Serve a default static plain text message.
"""
return Response(request, "Hello from the CircuitPython HTTP Server!")
server.serve_forever(str(wifi.radio.ipv4_address))

View file

@ -5,8 +5,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response from adafruit_httpserver import Request, Response, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: 2024 Michał Pokusa
#
# SPDX-License-Identifier: Unlicense
import socketpool
import wifi
from adafruit_httpserver import Request, Response, Server
AP_SSID = "..."
AP_PASSWORD = "..."
print("Creating access point...")
wifi.radio.start_ap(ssid=AP_SSID, password=AP_PASSWORD)
print(f"Created access point {AP_SSID}")
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)
@server.route("/")
def base(request: Request):
"""
Serve a default static plain text message.
"""
return Response(request, "Hello from the CircuitPython HTTP Server!")
server.serve_forever(str(wifi.radio.ipv4_address_ap))

View file

@ -1,29 +1,28 @@
# SPDX-FileCopyrightText: 2023 Tim C for Adafruit Industries # SPDX-FileCopyrightText: 2023 Tim C for Adafruit Industries
#
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import adafruit_connection_manager
import board import board
import digitalio import digitalio
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
from adafruit_httpserver import Server, Request, Response
print("Wiznet5k HTTPServer Test") from adafruit_httpserver import Request, Response, Server
# For Adafruit Ethernet FeatherWing # For Adafruit Ethernet FeatherWing
cs = digitalio.DigitalInOut(board.D10) cs = digitalio.DigitalInOut(board.D10)
# For Particle Ethernet FeatherWing # For Particle Ethernet FeatherWing
# cs = digitalio.DigitalInOut(board.D5) # cs = digitalio.DigitalInOut(board.D5)
spi_bus = board.SPI() spi_bus = board.SPI()
# Initialize ethernet interface with DHCP # Initialize ethernet interface with DHCP
eth = WIZNET5K(spi_bus, cs) eth = WIZNET5K(spi_bus, cs)
# Set the interface on the socket source pool = adafruit_connection_manager.get_radio_socketpool(eth)
socket.set_interface(eth)
# Initialize the server server = Server(pool, "/static", debug=True)
server = Server(socket, "/static", debug=True)
@server.route("/") @server.route("/")

View file

@ -2,21 +2,20 @@
# #
# SPDX-License-Identifier: Unlicense # SPDX-License-Identifier: Unlicense
import os
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response from adafruit_httpserver import Request, Response, Server
ssid = os.getenv("WIFI_SSID") WIFI_SSID = "..."
password = os.getenv("WIFI_PASSWORD") WIFI_PASSWORD = "..."
print("Connecting to", ssid) print(f"Connecting to {WIFI_SSID}...")
wifi.radio.connect(ssid, password) wifi.radio.connect(WIFI_SSID, WIFI_PASSWORD)
print("Connected to", ssid) print(f"Connected to {WIFI_SSID}")
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -3,12 +3,12 @@
# SPDX-License-Identifier: Unlicense # SPDX-License-Identifier: Unlicense
from time import monotonic from time import monotonic
import microcontroller import microcontroller
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, SSEResponse, GET from adafruit_httpserver import GET, Request, Response, Server, SSEResponse
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)
@ -43,7 +43,7 @@ def client(request: Request):
@server.route("/connect-client", GET) @server.route("/connect-client", GET)
def connect_client(request: Request): def connect_client(request: Request):
global sse_response # pylint: disable=global-statement global sse_response
if sse_response is not None: if sse_response is not None:
sse_response.close() # Close any existing connection sse_response.close() # Close any existing connection

View file

@ -6,13 +6,12 @@ import socketpool
import wifi import wifi
from adafruit_httpserver import ( from adafruit_httpserver import (
Server,
REQUEST_HANDLED_RESPONSE_SENT, REQUEST_HANDLED_RESPONSE_SENT,
Request,
FileResponse, FileResponse,
Request,
Server,
) )
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -2,18 +2,19 @@
# #
# SPDX-License-Identifier: Unlicense # SPDX-License-Identifier: Unlicense
from asyncio import create_task, gather, run, sleep as async_sleep from asyncio import create_task, gather, run
from asyncio import sleep as async_sleep
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import ( from adafruit_httpserver import (
Server,
REQUEST_HANDLED_RESPONSE_SENT, REQUEST_HANDLED_RESPONSE_SENT,
Request,
FileResponse, FileResponse,
Request,
Server,
) )
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True) server = Server(pool, "/static", debug=True)

View file

@ -6,8 +6,7 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, MIMETypes from adafruit_httpserver import MIMETypes, Server
MIMETypes.configure( MIMETypes.configure(
default_to="text/plain", default_to="text/plain",

View file

@ -7,7 +7,7 @@ import re
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, FileResponse from adafruit_httpserver import FileResponse, Request, Response, Server
try: try:
from adafruit_templateengine import render_template from adafruit_templateengine import render_template

View file

@ -5,22 +5,21 @@
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response from adafruit_httpserver import Request, Response, Server
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)
class Device: class Device:
def turn_on(self): # pylint: disable=no-self-use def turn_on(self):
print("Turning on device.") print("Turning on device.")
def turn_off(self): # pylint: disable=no-self-use def turn_off(self):
print("Turning off device.") print("Turning off device.")
def get_device(device_id: str) -> Device: # pylint: disable=unused-argument def get_device(device_id: str) -> Device:
""" """
This is a **made up** function that returns a `Device` object. This is a **made up** function that returns a `Device` object.
""" """
@ -29,25 +28,21 @@ def get_device(device_id: str) -> Device: # pylint: disable=unused-argument
@server.route("/device/<device_id>/action/<action>") @server.route("/device/<device_id>/action/<action>")
@server.route("/device/emergency-power-off/<device_id>") @server.route("/device/emergency-power-off/<device_id>")
def perform_action( def perform_action(request: Request, device_id: str, action: str = "emergency_power_off"):
request: Request, device_id: str, action: str = "emergency_power_off"
):
""" """
Performs an "action" on a specified device. Performs an "action" on a specified device.
""" """
device = get_device(device_id) device = get_device(device_id)
if action in ["turn_on"]: if action in {"turn_on"}:
device.turn_on() device.turn_on()
elif action in ["turn_off", "emergency_power_off"]: elif action in {"turn_off", "emergency_power_off"}:
device.turn_off() device.turn_off()
else: else:
return Response(request, f"Unknown action ({action})") return Response(request, f"Unknown action ({action})")
return Response( return Response(request, f"Action ({action}) performed on device with ID: {device_id}")
request, f"Action ({action}) performed on device with ID: {device_id}"
)
@server.route("/device/<device_id>/status/<date>") @server.route("/device/<device_id>/status/<date>")

View file

@ -0,0 +1,123 @@
# SPDX-FileCopyrightText: 2024 Michał Pokusa
#
# SPDX-License-Identifier: Unlicense
try:
from typing import Dict, List, Tuple, Union
except ImportError:
pass
from asyncio import create_task, gather, run, sleep
from random import choice
import socketpool
import wifi
from adafruit_pycamera import PyCamera
from adafruit_httpserver import OK_200, Headers, Request, Response, Server, Status
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True)
camera = PyCamera()
camera.display.brightness = 0
camera.mode = 0 # JPEG, required for `capture_into_jpeg()`
camera.resolution = "1280x720"
camera.effect = 0 # No effect
class XMixedReplaceResponse(Response):
def __init__(
self,
request: Request,
frame_content_type: str,
*,
status: Union[Status, Tuple[int, str]] = OK_200,
headers: Union[Headers, Dict[str, str]] = None,
cookies: Dict[str, str] = None,
) -> None:
super().__init__(
request=request,
headers=headers,
cookies=cookies,
status=status,
)
self._boundary = self._get_random_boundary()
self._headers.setdefault(
"Content-Type", f"multipart/x-mixed-replace; boundary={self._boundary}"
)
self._frame_content_type = frame_content_type
@staticmethod
def _get_random_boundary() -> str:
symbols = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return "--" + "".join([choice(symbols) for _ in range(16)])
def send_frame(self, frame: Union[str, bytes] = "") -> None:
encoded_frame = bytes(frame.encode("utf-8") if isinstance(frame, str) else frame)
self._send_bytes(self._request.connection, bytes(f"{self._boundary}\r\n", "utf-8"))
self._send_bytes(
self._request.connection,
bytes(f"Content-Type: {self._frame_content_type}\r\n\r\n", "utf-8"),
)
self._send_bytes(self._request.connection, encoded_frame)
self._send_bytes(self._request.connection, bytes("\r\n", "utf-8"))
def _send(self) -> None:
self._send_headers()
def close(self) -> None:
self._close_connection()
stream_connections: List[XMixedReplaceResponse] = []
@server.route("/frame")
def frame_handler(request: Request):
frame = camera.capture_into_jpeg()
return Response(request, body=frame, content_type="image/jpeg")
@server.route("/stream")
def stream_handler(request: Request):
response = XMixedReplaceResponse(request, frame_content_type="image/jpeg")
stream_connections.append(response)
return response
async def send_stream_frames():
while True:
await sleep(0.1)
frame = camera.capture_into_jpeg()
for connection in iter(stream_connections):
try:
connection.send_frame(frame)
except BrokenPipeError:
connection.close()
stream_connections.remove(connection)
async def handle_http_requests():
server.start(str(wifi.radio.ipv4_address))
while True:
await sleep(0)
server.poll()
async def main():
await gather(
create_task(send_stream_frames()),
create_task(handle_http_requests()),
)
run(main())

View file

@ -2,15 +2,16 @@
# #
# SPDX-License-Identifier: Unlicense # SPDX-License-Identifier: Unlicense
from asyncio import create_task, gather, run, sleep as async_sleep from asyncio import create_task, gather, run
from asyncio import sleep as async_sleep
import board import board
import microcontroller import microcontroller
import neopixel import neopixel
import socketpool import socketpool
import wifi import wifi
from adafruit_httpserver import Server, Request, Response, Websocket, GET from adafruit_httpserver import GET, Request, Response, Server, Websocket
pool = socketpool.SocketPool(wifi.radio) pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True) server = Server(pool, debug=True)
@ -62,7 +63,7 @@ def client(request: Request):
@server.route("/connect-websocket", GET) @server.route("/connect-websocket", GET)
def connect_client(request: Request): def connect_client(request: Request):
global websocket # pylint: disable=global-statement global websocket
if websocket is not None: if websocket is not None:
websocket.close() # Close any existing connection websocket.close() # Close any existing connection

112
ruff.toml Normal file
View file

@ -0,0 +1,112 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
target-version = "py38"
line-length = 100
[lint]
preview = true
select = ["I", "PL", "UP"]
extend-select = [
"D419", # empty-docstring
"E501", # line-too-long
"W291", # trailing-whitespace
"PLC0414", # useless-import-alias
"PLC2401", # non-ascii-name
"PLC2801", # unnecessary-dunder-call
"PLC3002", # unnecessary-direct-lambda-call
"E999", # syntax-error
"PLE0101", # return-in-init
"F706", # return-outside-function
"F704", # yield-outside-function
"PLE0116", # continue-in-finally
"PLE0117", # nonlocal-without-binding
"PLE0241", # duplicate-bases
"PLE0302", # unexpected-special-method-signature
"PLE0604", # invalid-all-object
"PLE0605", # invalid-all-format
"PLE0643", # potential-index-error
"PLE0704", # misplaced-bare-raise
"PLE1141", # dict-iter-missing-items
"PLE1142", # await-outside-async
"PLE1205", # logging-too-many-args
"PLE1206", # logging-too-few-args
"PLE1307", # bad-string-format-type
"PLE1310", # bad-str-strip-call
"PLE1507", # invalid-envvar-value
"PLE2502", # bidirectional-unicode
"PLE2510", # invalid-character-backspace
"PLE2512", # invalid-character-sub
"PLE2513", # invalid-character-esc
"PLE2514", # invalid-character-nul
"PLE2515", # invalid-character-zero-width-space
"PLR0124", # comparison-with-itself
"PLR0202", # no-classmethod-decorator
"PLR0203", # no-staticmethod-decorator
"UP004", # useless-object-inheritance
"PLR0206", # property-with-parameters
"PLR0904", # too-many-public-methods
"PLR0911", # too-many-return-statements
"PLR0912", # too-many-branches
"PLR0913", # too-many-arguments
"PLR0914", # too-many-locals
"PLR0915", # too-many-statements
"PLR0916", # too-many-boolean-expressions
"PLR1702", # too-many-nested-blocks
"PLR1704", # redefined-argument-from-local
"PLR1711", # useless-return
"C416", # unnecessary-comprehension
"PLR1733", # unnecessary-dict-index-lookup
"PLR1736", # unnecessary-list-index-lookup
# ruff reports this rule is unstable
#"PLR6301", # no-self-use
"PLW0108", # unnecessary-lambda
"PLW0120", # useless-else-on-loop
"PLW0127", # self-assigning-variable
"PLW0129", # assert-on-string-literal
"B033", # duplicate-value
"PLW0131", # named-expr-without-context
"PLW0245", # super-without-brackets
"PLW0406", # import-self
"PLW0602", # global-variable-not-assigned
"PLW0603", # global-statement
"PLW0604", # global-at-module-level
# fails on the try: import typing used by libraries
#"F401", # unused-import
"F841", # unused-variable
"E722", # bare-except
"PLW0711", # binary-op-exception
"PLW1501", # bad-open-mode
"PLW1508", # invalid-envvar-default
"PLW1509", # subprocess-popen-preexec-fn
"PLW2101", # useless-with-lock
"PLW3301", # nested-min-max
]
ignore = [
"PLR2004", # magic-value-comparison
"UP030", # format literals
"PLW1514", # unspecified-encoding
"PLR0913", # too-many-arguments
"PLR0915", # too-many-statements
"PLR0917", # too-many-positional-arguments
"PLR0904", # too-many-public-methods
"PLR0912", # too-many-branches
"PLR0916", # too-many-boolean-expressions
"PLR6301", # could-be-static no-self-use
"PLC0415", # import outside toplevel
"PLC2701", # private import
"PLR0911", # too many return
"PLW1641", # object not implement hash
"PLW0603", # global statement
"PLC1901", # string falsey simplified
]
[format]
line-ending = "lf"