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 818 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
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
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pycqa/pylint
rev: v2.17.4
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.4
hooks:
- id: pylint
name: pylint (library code)
types: [python]
args:
- --disable=consider-using-f-string
exclude: "^(docs/|examples/|tests/|setup.py$)"
- id: pylint
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
- id: ruff-format
- id: ruff
args: ["--fix"]
- repo: https://github.com/fsfe/reuse-tool
rev: v3.0.1
hooks:
- id: reuse

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
version: 2
sphinx:
configuration: docs/conf.py
build:
os: ubuntu-20.04
os: ubuntu-lts-latest
tools:
python: "3"

View file

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

View file

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

View file

@ -8,7 +8,7 @@
"""
try:
from typing import Union, List
from typing import List, Union
except ImportError:
pass
@ -40,15 +40,13 @@ class Token:
return f"{self.prefix} {self._value}"
class Bearer(Token): # pylint: disable=too-few-public-methods
class Bearer(Token):
"""Represents HTTP Bearer Token Authentication."""
prefix = "Bearer"
def check_authentication(
request: Request, auths: List[Union[Basic, Token, Bearer]]
) -> bool:
def check_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> bool:
"""
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)
def require_authentication(
request: Request, auths: List[Union[Basic, Token, Bearer]]
) -> None:
def require_authentication(request: Request, auths: List[Union[Basic, Token, Bearer]]) -> None:
"""
Checks if the request is authorized and raises ``AuthenticationError`` if not.

View file

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

View file

@ -8,11 +8,59 @@
"""
try:
from typing import List, Dict, Union, Any
from typing import Any, Dict, List, Tuple, Union
except ImportError:
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:
"""Interface with shared methods for QueryParams, FormData and Headers."""
@ -62,7 +110,7 @@ class _IFieldStorage:
return key in self._storage
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]:
@ -81,9 +129,7 @@ def _encode_html_entities(value: Union[str, None]) -> Union[str, None]:
class _IXSSSafeFieldStorage(_IFieldStorage):
def get(
self, field_name: str, default: Any = None, *, safe=True
) -> Union[Any, None]:
def get(self, field_name: str, default: Any = None, *, safe=True) -> Union[Any, None]:
if safe:
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]:
if safe:
return [
_encode_html_entities(value) for value in super().get_list(field_name)
]
return [_encode_html_entities(value) for value in super().get_list(field_name)]
_debug_warning_nonencoded_output()
return super().get_list(field_name)

View file

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

View file

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

View file

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

View file

@ -8,16 +8,14 @@
"""
try:
from typing import Optional, Dict, Union, Tuple, Generator, Any
from socket import socket
from socketpool import SocketPool
from typing import Any, Dict, Generator, Optional, Tuple, Union
except ImportError:
pass
import os
import json
import os
from binascii import b2a_base64
from errno import EAGAIN, ECONNRESET, ETIMEDOUT, ENOTCONN
from errno import EAGAIN, ECONNRESET, ENOTCONN, ETIMEDOUT
try:
try:
@ -35,21 +33,22 @@ from .exceptions import (
FileNotExistsError,
ParentDirectoryReferenceError,
)
from .headers import Headers
from .interfaces import _ISocket
from .mime_types import MIMETypes
from .request import Request
from .status import (
Status,
SWITCHING_PROTOCOLS_101,
OK_200,
MOVED_PERMANENTLY_301,
FOUND_302,
TEMPORARY_REDIRECT_307,
MOVED_PERMANENTLY_301,
OK_200,
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.
@ -63,7 +62,7 @@ class Response: # pylint: disable=too-few-public-methods
return Response(request, body='Some content', content_type="text/plain")
"""
def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
request: Request,
body: Union[str, bytes] = "",
@ -85,9 +84,7 @@ class Response: # pylint: disable=too-few-public-methods
self._request = request
self._body = body
self._status = status if isinstance(status, Status) else Status(*status)
self._headers = (
headers.copy() if isinstance(headers, Headers) else Headers(headers)
)
self._headers = headers.copy() if isinstance(headers, Headers) else Headers(headers)
self._cookies = cookies.copy() if cookies else {}
self._content_type = content_type
self._size = 0
@ -99,13 +96,9 @@ class Response: # pylint: disable=too-few-public-methods
) -> None:
headers = self._headers.copy()
response_message_header = (
f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
)
response_message_header = f"HTTP/1.1 {self._status.code} {self._status.text}\r\n"
headers.setdefault(
"Content-Type", content_type or self._content_type or MIMETypes.DEFAULT
)
headers.setdefault("Content-Type", content_type or self._content_type or MIMETypes.DEFAULT)
headers.setdefault("Content-Length", content_length)
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 += "\r\n"
self._send_bytes(
self._request.connection, response_message_header.encode("utf-8")
)
self._send_bytes(self._request.connection, response_message_header.encode("utf-8"))
def _send(self) -> None:
encoded_body = (
self._body.encode("utf-8") if isinstance(self._body, str) else self._body
)
encoded_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_bytes(self._request.connection, encoded_body)
@ -132,7 +121,7 @@ class Response: # pylint: disable=too-few-public-methods
def _send_bytes(
self,
conn: Union["SocketPool.Socket", "socket.socket"],
conn: _ISocket,
buffer: Union[bytes, bytearray, memoryview],
):
bytes_sent: int = 0
@ -156,7 +145,7 @@ class Response: # pylint: disable=too-few-public-methods
pass
class FileResponse(Response): # pylint: disable=too-few-public-methods
class FileResponse(Response):
"""
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')
"""
def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
request: Request,
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._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._content_type = content_type or MIMETypes.get_for_filename(self._filename)
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
if "\\" in file_path: # pylint: disable=anomalous-backslash-in-string
if "\\" in file_path:
raise BackslashInPathError(file_path)
# 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
return st_size
except (OSError, AssertionError):
raise FileNotExistsError(file_path) # pylint: disable=raise-missing-from
raise FileNotExistsError(file_path)
def _send(self) -> None:
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()
class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
class ChunkedResponse(Response):
"""
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")
"""
def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
request: Request,
body: Generator[Union[str, bytes], Any, Any],
@ -353,7 +346,7 @@ class ChunkedResponse(Response): # pylint: disable=too-few-public-methods
self._close_connection()
class JSONResponse(Response): # pylint: disable=too-few-public-methods
class JSONResponse(Response):
"""
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()
class Redirect(Response): # pylint: disable=too-few-public-methods
class Redirect(Response):
"""
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):
raise ValueError(
"Cannot specify both status and permanent/preserve_method argument"
)
raise ValueError("Cannot specify both status and permanent/preserve_method argument")
if status is None:
if preserve_method:
@ -463,7 +454,7 @@ class Redirect(Response): # pylint: disable=too-few-public-methods
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.
@ -497,7 +488,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
sse.close()
"""
def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
request: Request,
headers: Union[Headers, Dict[str, str]] = None,
@ -520,11 +511,11 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
def _send(self) -> None:
self._send_headers()
def send_event( # pylint: disable=too-many-arguments
def send_event(
self,
data: str,
event: str = None,
id: int = None, # pylint: disable=redefined-builtin,invalid-name
id: int = None,
retry: int = None,
custom_fields: Dict[str, str] = None,
) -> None:
@ -561,7 +552,7 @@ class SSEResponse(Response): # pylint: disable=too-few-public-methods
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.
@ -632,7 +623,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
return b2a_base64(response_key.digest()).strip().decode()
def __init__( # pylint: disable=too-many-arguments
def __init__(
self,
request: Request,
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:
return Websocket.CONT, None
payload = bytes()
payload = b""
if fin == Websocket.FIN and opcode == Websocket.CLOSE:
return Websocket.CLOSE, payload
@ -708,7 +699,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
length -= min(payload_length, length)
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
@ -743,9 +734,7 @@ class Websocket(Response): # pylint: disable=too-few-public-methods
if self.closed:
if fail_silently:
return None
raise RuntimeError(
"Websocket connection is closed, cannot receive messages"
)
raise RuntimeError("Websocket connection is closed, cannot receive messages")
try:
opcode, payload = self._read_frame()

View file

@ -8,7 +8,7 @@
"""
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:
from .response import Response
@ -52,9 +52,7 @@ class Route:
self._validate_path(path, append_slash)
self.path = path
self.methods = (
set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
)
self.methods = set(methods) if isinstance(methods, (set, list, tuple)) else set([methods])
self.handler = handler
self.parameters_names = [
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))
def __repr__(self) -> str:
path = repr(self.path)
methods = repr(self.methods)
handler = repr(self.handler)
path = self.path
methods = self.methods
handler = self.handler
return f"Route({path=}, {methods=}, {handler=})"
return f"<Route {path=}, {methods=}, {handler=}>"
def as_route(

View file

@ -8,9 +8,7 @@
"""
try:
from typing import Callable, Protocol, Union, List, Tuple, Dict, Iterable
from socket import socket
from socketpool import SocketPool
from typing import Callable, Dict, Iterable, List, Tuple, Union
except ImportError:
pass
@ -19,20 +17,36 @@ from sys import implementation
from time import monotonic, sleep
from traceback import print_exception
from .authentication import Basic, Token, Bearer, require_authentication
from .authentication import Basic, Bearer, Token, require_authentication
from .exceptions import (
ServerStoppedError,
AuthenticationError,
FileNotExistsError,
InvalidPathError,
ServerStoppedError,
ServingFilesDisabledError,
)
from .headers import Headers
from .interfaces import _ISocket, _ISocketPool
from .methods import GET, HEAD
from .request import Request
from .response import Response, FileResponse
from .response import FileResponse, Response
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"
@ -40,8 +54,11 @@ CONNECTION_TIMED_OUT = "connection_timed_out"
REQUEST_HANDLED_NO_RESPONSE = "request_handled_no_response"
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."""
host: str
@ -53,8 +70,50 @@ class Server: # pylint: disable=too-many-instance-attributes
root_path: str
"""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__(
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:
"""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.
:param str root_path: Root directory to serve files from
: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._timeout = 1
self._auths = []
self._routes: "List[Route]" = []
self.headers = Headers()
self._socket_source = socket_source
self._sock = None
self.headers = Headers()
self.host, self.port = None, None
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)
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
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:
"""
Wait for HTTP requests at the given host and port. Does not return.
@ -192,22 +267,32 @@ class Server: # pylint: disable=too-many-instance-attributes
except KeyboardInterrupt: # Exit on Ctrl-C e.g. during development
self.stop()
return
except Exception: # pylint: disable=broad-except
except Exception:
pass # Ignore exceptions in handler function
def _set_socket_level_to_reuse_address(self) -> None:
"""
On systems that have SO_REUSEADDR, prevents "Address already in use"
error when restarting the server.
"""
try:
self._sock.setsockopt(
self._socket_source.SOL_SOCKET, self._socket_source.SO_REUSEADDR, 1
)
except AttributeError:
pass
@staticmethod
def _create_server_socket(
socket_source: _ISocketPool,
ssl_context: "SSLContext | None",
host: str,
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
``.poll()`` in a while loop to handle incoming requests.
@ -220,15 +305,7 @@ class Server: # pylint: disable=too-many-instance-attributes
self.host, self.port = host, port
self.stopped = False
self._sock = self._socket_source.socket(
self._socket_source.AF_INET, self._socket_source.SOCK_STREAM
)
self._set_socket_level_to_reuse_address()
self._sock.bind((host, port))
self._sock.listen(10)
self._sock.setblocking(False) # Non-blocking socket
self._sock = self._create_server_socket(self._socket_source, self._ssl_context, host, port)
if self.debug:
_debug_started_server(self)
@ -247,11 +324,9 @@ class Server: # pylint: disable=too-many-instance-attributes
if self.debug:
_debug_stopped_server(self)
def _receive_header_bytes(
self, sock: Union["SocketPool.Socket", "socket.socket"]
) -> bytes:
def _receive_header_bytes(self, sock: _ISocket) -> bytes:
"""Receive bytes until a empty line is received."""
received_bytes = bytes()
received_bytes = b""
while b"\r\n\r\n" not in received_bytes:
try:
length = sock.recv_into(self._buffer, len(self._buffer))
@ -266,7 +341,7 @@ class Server: # pylint: disable=too-many-instance-attributes
def _receive_body_bytes(
self,
sock: Union["SocketPool.Socket", "socket.socket"],
sock: _ISocket,
received_body_bytes: bytes,
content_length: int,
) -> bytes:
@ -285,7 +360,7 @@ class Server: # pylint: disable=too-many-instance-attributes
def _receive_request(
self,
sock: Union["SocketPool.Socket", "socket.socket"],
sock: _ISocket,
client_address: Tuple[str, int],
) -> Request:
"""Receive bytes from socket until the whole request is received."""
@ -303,15 +378,11 @@ class Server: # pylint: disable=too-many-instance-attributes
received_body_bytes = request.body
# Receiving remaining body bytes
request.body = self._receive_body_bytes(
sock, received_body_bytes, content_length
)
request.body = self._receive_body_bytes(sock, received_body_bytes, content_length)
return request
def _find_handler( # pylint: disable=cell-var-from-loop
self, method: str, path: str
) -> Union[Callable[..., "Response"], None]:
def _find_handler(self, method: str, path: str) -> Union[Callable[..., "Response"], None]:
"""
Finds a handler for a given route.
@ -355,7 +426,7 @@ class Server: # pylint: disable=too-many-instance-attributes
raise ServingFilesDisabledError
# 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(
request,
filename=request.path,
@ -387,11 +458,11 @@ class Server: # pylint: disable=too-many-instance-attributes
def _set_default_server_headers(self, response: Response) -> None:
for name, value in self.headers.items():
response._headers.setdefault( # pylint: disable=protected-access
name, value
)
response._headers.setdefault(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
requests. When a request comes in, it will be handled by the handler function.
@ -404,11 +475,12 @@ class Server: # pylint: disable=too-many-instance-attributes
conn = None
try:
if self.debug:
_debug_start_time = monotonic()
conn, client_address = self._sock.accept()
conn.settimeout(self._timeout)
_debug_start_time = monotonic()
# Receive the whole request
if (request := self._receive_request(conn, client_address)) is None:
conn.close()
@ -427,16 +499,15 @@ class Server: # pylint: disable=too-many-instance-attributes
self._set_default_server_headers(response)
# Send the response
response._send() # pylint: disable=protected-access
_debug_end_time = monotonic()
response._send()
if self.debug:
_debug_end_time = monotonic()
_debug_response_sent(response, _debug_end_time - _debug_start_time)
return REQUEST_HANDLED_RESPONSE_SENT
except Exception as error: # pylint: disable=broad-except
except Exception as error:
if isinstance(error, OSError):
# There is no data available right now, try again later.
if error.errno == EAGAIN:
@ -444,6 +515,15 @@ class Server: # pylint: disable=too-many-instance-attributes
# Connection reset by peer, try again later.
if error.errno == ECONNRESET:
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:
_debug_exception_in_handler(error)
@ -533,26 +613,33 @@ class Server: # pylint: disable=too-many-instance-attributes
else:
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):
"""Warns about exposing all files on the device."""
print(
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device through"
" the webserver, including potentially sensitive files like settings.toml or secrets.py. "
f"WARNING: Setting root_path to '{root_path}' will expose all files on your device "
"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."
)
def _debug_started_server(server: "Server"):
"""Prints a message when the server starts."""
scheme = "https" if server.https else "http"
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):
"""Prints a message after a response is sent."""
# pylint: disable=protected-access
client_ip = response._request.client_address[0]
method = response._request.method
query_params = response._request.query_params
@ -567,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."""
print("Stopped development server")

View file

@ -8,7 +8,7 @@
"""
class Status: # pylint: disable=too-few-public-methods
class Status:
"""HTTP status code."""
def __init__(self, code: int, text: str):
@ -21,14 +21,17 @@ class Status: # pylint: disable=too-few-public-methods
self.code = code
self.text = text
def __repr__(self):
return f'Status({self.code}, "{self.text}")'
def __eq__(self, other: "Status"):
return self.code == other.code and self.text == other.text
def __str__(self):
return f"{self.code} {self.text}"
def __eq__(self, other: "Status"):
return self.code == other.code and self.text == other.text
def __repr__(self):
code = self.code
text = self.text
return f'<Status {code}, "{text}">'
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)
.. use this format as the module name: "adafruit_foo.foo"
API Reference
#############
.. automodule:: adafruit_httpserver
:members:

View file

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import datetime
import os
import sys
import datetime
sys.path.insert(0, os.path.abspath(".."))
@ -54,9 +52,7 @@ project = "Adafruit CircuitPython HTTPServer Library"
creation_year = "2022"
current_year = str(datetime.datetime.now().year)
year_duration = (
current_year
if current_year == creation_year
else creation_year + " - " + current_year
current_year if current_year == creation_year else creation_year + " - " + current_year
)
copyright = year_duration + " Dan Halbert"
author = "Dan Halbert"
@ -116,7 +112,6 @@ napoleon_numpy_docstring = False
import 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,
# 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.**
**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.**
Different ways of starting the server
-------------------------------------
This is the minimal example of using the library with CircuitPython.
This example is serving a simple static text message.
There are several ways to start the server on CircuitPython, mostly depending on the device you are using and
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
:caption: examples/httpserver_simpletest_manual.py
:emphasize-lines: 12-17
:linenos:
Below you can find examples of different ways to start the server:
It is also possible to use Ethernet instead of WiFi.
The only difference in usage is related to configuring the ``socket_source`` differently.
.. toctree::
.. literalinclude:: ../examples/httpserver_ethernet_simpletest.py
: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:
starting_methods
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
:caption: examples/httpserver_static_files_serving.py
:emphasize-lines: 12-18,23-26
:emphasize-lines: 11-17,22-25
:linenos:
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
:caption: examples/httpserver_handler_serves_file.py
:emphasize-lines: 13,22
:emphasize-lines: 12,21
:linenos:
.. literalinclude:: ../examples/home.html
@ -100,7 +74,7 @@ a running total of the last 10 samples.
.. literalinclude:: ../examples/httpserver_start_and_poll.py
:caption: examples/httpserver_start_and_poll.py
:emphasize-lines: 29,38
:emphasize-lines: 28,37
:linenos:
@ -112,7 +86,7 @@ without needing to manually manage the timing of each task.
.. literalinclude:: ../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:
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
when accessing it via the hostname.
In this example, the server is accessible via the IP and ``http://custom-mdns-hostname.local/``.
On some routers it is also possible to use ``http://custom-mdns-hostname/``, but **this is not guaranteed to work**.
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:5000/``, but **this is not guaranteed to work**.
.. literalinclude:: ../examples/httpserver_mdns.py
:caption: examples/httpserver_mdns.py
:emphasize-lines: 12-14
:emphasize-lines: 11-13
:linenos:
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
:caption: examples/httpserver_cpu_information.py
:emphasize-lines: 9,15-18,33
:emphasize-lines: 9,14-17,32
:linenos:
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
:caption: examples/httpserver_methods.py
:emphasize-lines: 8,19,26,30,49
:emphasize-lines: 8,18,25,29,46
:linenos:
Change NeoPixel color
@ -182,7 +156,7 @@ Tested on ESP32-S2 Feather.
.. literalinclude:: ../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:
Templates
@ -207,7 +181,7 @@ You can find more information about the template syntax in the
.. literalinclude:: ../examples/httpserver_templates.py
:caption: examples/httpserver_templates.py
:emphasize-lines: 12-15,49-55
:emphasize-lines: 12-15,51-59
:linenos:
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.
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"``
- ``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``.
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.
@ -235,7 +209,7 @@ return only the first one.
.. literalinclude:: ../examples/httpserver_form_data.py
:caption: examples/httpserver_form_data.py
:emphasize-lines: 32,47,50
:emphasize-lines: 31,46,49
:linenos:
Cookies
@ -249,7 +223,7 @@ In order to set cookies, pass ``cookies`` dictionary to ``Response`` constructo
.. literalinclude:: ../examples/httpserver_cookies.py
:caption: examples/httpserver_cookies.py
:emphasize-lines: 70,74-75,82
:emphasize-lines: 69,73-74,81
:linenos:
Chunked response
@ -261,7 +235,7 @@ constructor.
.. literalinclude:: ../examples/httpserver_chunked.py
:caption: examples/httpserver_chunked.py
:emphasize-lines: 8,21-26,28
:emphasize-lines: 8,20-25,27
:linenos:
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
:caption: examples/httpserver_url_parameters.py
:emphasize-lines: 30-34,53-54,65-66
:emphasize-lines: 29-31,48-49,60-61
:linenos:
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
:caption: examples/httpserver_authentication_server.py
:emphasize-lines: 8,11-16,20
:emphasize-lines: 8,10-15,19
:linenos:
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
: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:
Redirects
@ -335,7 +309,7 @@ Alternatively, you can pass a ``status`` object directly to ``Redirect`` constru
.. literalinclude:: ../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:
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 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
:caption: examples/httpserver_sse.py
:emphasize-lines: 10,17,46-53,63
:emphasize-lines: 11,17,46-53,63
:linenos:
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,
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.
This might change in the future, but for now, it is recommended to use Websocket 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 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
:caption: examples/httpserver_websocket.py
:emphasize-lines: 12,20,65-72,88,99
:emphasize-lines: 14,21,66-73,89,100
: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
----------------
@ -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**.
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.
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
: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:
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::
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 /example" 134 -- "404 Not Found" 172 -- 123ms
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
functions at the bottom of ``adafruit_httpserver/server.py`` that start with ``_debug_...``.
NOTE:
*This is an advanced usage that might change in the future. It is not recommended to modify other parts of the code.*
.. note::
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 wifi
from adafruit_httpserver import Server, Request, Response, UNAUTHORIZED_401
from adafruit_httpserver import UNAUTHORIZED_401, Request, Response, Server
from adafruit_httpserver.authentication import (
AuthenticationError,
Basic,
Token,
Bearer,
Token,
check_authentication,
require_authentication,
)
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True)

View file

@ -5,8 +5,7 @@
import socketpool
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.
auths = [

View file

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

View file

@ -5,8 +5,7 @@
import socketpool
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)
server = Server(pool, debug=True)

View file

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

View file

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

View file

@ -5,8 +5,7 @@
import socketpool
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)
server = Server(pool, debug=True)

View file

@ -6,8 +6,7 @@
import socketpool
import wifi
from adafruit_httpserver import Server, Request, FileResponse
from adafruit_httpserver import FileResponse, Request, Server
pool = socketpool.SocketPool(wifi.radio)
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 wifi
from adafruit_httpserver import Server, Request, FileResponse
from adafruit_httpserver import FileResponse, Request, Server
mdns_server = mdns.Server(wifi.radio)
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)
server = Server(pool, "/static", debug=True)

View file

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

View file

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

View file

@ -7,8 +7,7 @@ import neopixel
import socketpool
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)
server = Server(pool, "/static", debug=True)

View file

@ -6,16 +6,15 @@ import socketpool
import wifi
from adafruit_httpserver import (
Server,
MOVED_PERMANENTLY_301,
NOT_FOUND_404,
POST,
Redirect,
Request,
Response,
Redirect,
POST,
NOT_FOUND_404,
MOVED_PERMANENTLY_301,
Server,
)
pool = socketpool.SocketPool(wifi.radio)
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 wifi
from adafruit_httpserver import Server, Request, Response
from adafruit_httpserver import Request, Response, Server
pool = socketpool.SocketPool(wifi.radio)
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-License-Identifier: MIT
import adafruit_connection_manager
import board
import digitalio
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
cs = digitalio.DigitalInOut(board.D10)
# For Particle Ethernet FeatherWing
# cs = digitalio.DigitalInOut(board.D5)
spi_bus = board.SPI()
# Initialize ethernet interface with DHCP
eth = WIZNET5K(spi_bus, cs)
# Set the interface on the socket source
socket.set_interface(eth)
pool = adafruit_connection_manager.get_radio_socketpool(eth)
# Initialize the server
server = Server(socket, "/static", debug=True)
server = Server(pool, "/static", debug=True)
@server.route("/")

View file

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

View file

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

View file

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

View file

@ -2,18 +2,19 @@
#
# 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 wifi
from adafruit_httpserver import (
Server,
REQUEST_HANDLED_RESPONSE_SENT,
Request,
FileResponse,
Request,
Server,
)
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

View file

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

View file

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

View file

@ -5,22 +5,21 @@
import socketpool
import wifi
from adafruit_httpserver import Server, Request, Response
from adafruit_httpserver import Request, Response, Server
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, debug=True)
class Device:
def turn_on(self): # pylint: disable=no-self-use
def turn_on(self):
print("Turning on device.")
def turn_off(self): # pylint: disable=no-self-use
def turn_off(self):
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.
"""
@ -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/emergency-power-off/<device_id>")
def perform_action(
request: Request, device_id: str, action: str = "emergency_power_off"
):
def perform_action(request: Request, device_id: str, action: str = "emergency_power_off"):
"""
Performs an "action" on a specified device.
"""
device = get_device(device_id)
if action in ["turn_on"]:
if action in {"turn_on"}:
device.turn_on()
elif action in ["turn_off", "emergency_power_off"]:
elif action in {"turn_off", "emergency_power_off"}:
device.turn_off()
else:
return Response(request, f"Unknown action ({action})")
return Response(
request, f"Action ({action}) performed on device with ID: {device_id}"
)
return Response(request, f"Action ({action}) performed on device with ID: {device_id}")
@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
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 microcontroller
import neopixel
import socketpool
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)
server = Server(pool, debug=True)
@ -62,7 +63,7 @@ def client(request: Request):
@server.route("/connect-websocket", GET)
def connect_client(request: Request):
global websocket # pylint: disable=global-statement
global websocket
if websocket is not None:
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"