Compare commits

...

15 commits

Author SHA1 Message Date
9cf832fd88
ilot/freescout: upgrade to 1.8.160
Some checks failed
/ lint (pull_request) Successful in 29s
/ deploy-x86_64 (pull_request) Has been skipped
/ build-x86_64 (pull_request) Failing after 44s
/ deploy-aarch64 (pull_request) Has been skipped
/ build-aarch64 (pull_request) Failing after 1m33s
2025-01-05 18:35:48 -05:00
bdf770d7d7
ilot/uvicorn: new aport
Some checks failed
/ lint (pull_request) Failing after 28s
/ deploy-x86_64 (pull_request) Successful in 26s
/ build-x86_64 (pull_request) Successful in 2m35s
/ deploy-aarch64 (pull_request) Successful in 54s
/ build-aarch64 (pull_request) Successful in 3m48s
2025-01-05 11:35:07 -05:00
01a8bc900f
ilot/authentik: fix impersonnate api 2024-12-07 09:34:07 -05:00
8a3689b757
ilot/wikijs: remove prebuilts 2024-12-01 15:44:39 -05:00
37e23225dd
ilot/authentik: fix depend syntax 2024-12-01 15:44:03 -05:00
ea28bc8eee
ilot/authentik: missing uvicorn 2024-12-01 15:32:17 -05:00
f20094dd9e
forgejo-ci: check v3.21 branch 2024-12-01 15:15:31 -05:00
06cfdea8b8
ilot/listmonk: upgrade to 4.1.0 2024-12-01 15:07:36 -05:00
1ecc969d96
ilot/py3-django-tenant-schemas: drop due to in community 2024-12-01 15:01:00 -05:00
f5b189f460
ilot/py3-pyrad: drop due to in community 2024-12-01 15:00:08 -05:00
d8e3e4f9f4
ilot/py3-drf-orjson-renderer: drop due to in community 2024-12-01 14:59:26 -05:00
0689e2c3c7
ilot/py3-django-tenants: drop due to in community 2024-12-01 14:58:47 -05:00
ba5cf2f94c
backports/forgejo-runner: drop due to in community 2024-12-01 14:57:23 -05:00
d059d73a8e
ilot/authentik: upgrade to 2024.8.6
Some checks failed
/ lint (pull_request) Successful in 30s
/ build-x86_64 (pull_request) Successful in 44m22s
/ deploy-x86_64 (pull_request) Successful in 31s
/ deploy-aarch64 (pull_request) Has been cancelled
/ build-aarch64 (pull_request) Has been cancelled
2024-11-23 19:30:29 -05:00
77d3ecf5d9
ilot/authentik: upgrade to 2024.8.5
All checks were successful
/ lint (pull_request) Successful in 29s
/ deploy-x86_64 (pull_request) Successful in 31s
/ build-x86_64 (pull_request) Successful in 46m27s
/ deploy-aarch64 (pull_request) Successful in 1m3s
/ build-aarch64 (pull_request) Successful in 2h30m29s
2024-11-21 09:39:40 -05:00
23 changed files with 1341 additions and 4123 deletions

View file

@ -11,7 +11,7 @@ jobs:
container: container:
image: alpine:latest image: alpine:latest
env: env:
downstream: https://forge.ilot.io/api/packages/ilot/alpine/v3.20/ilot downstream: https://forge.ilot.io/api/packages/ilot/alpine/v3.21/ilot
FORGEJO_TOKEN: ${{ secrets.forgejo_token }} FORGEJO_TOKEN: ${{ secrets.forgejo_token }}
LABEL_NUMBER: 8 LABEL_NUMBER: 8
steps: steps:

View file

@ -1,47 +0,0 @@
# Contributor: Patrycja Rosa <alpine@ptrcnull.me>
# Maintainer: Patrycja Rosa <alpine@ptrcnull.me>
pkgname=forgejo-runner
pkgver=3.5.0
pkgrel=2
pkgdesc="CI/CD job runner for Forgejo"
url="https://code.forgejo.org/forgejo/runner"
arch="all"
license="MIT"
makedepends="go"
install="$pkgname.pre-install $pkgname.pre-upgrade"
subpackages="$pkgname-openrc"
source="$pkgname-$pkgver.tar.gz::https://code.forgejo.org/forgejo/runner/archive/v$pkgver.tar.gz
forgejo-runner.logrotate
forgejo-runner.initd
forgejo-runner.confd
"
builddir="$srcdir/runner"
options="!check" # tests require running forgejo
build() {
go build \
-o forgejo-runner \
-ldflags "-X gitea.com/gitea/act_runner/internal/pkg/ver.version=$pkgver"
./forgejo-runner generate-config > config.example.yaml
}
check() {
go test ./...
}
package() {
install -Dm755 forgejo-runner -t "$pkgdir"/usr/bin/
install -Dm644 config.example.yaml -t "$pkgdir"/etc/forgejo-runner/
install -Dm755 "$srcdir"/forgejo-runner.initd "$pkgdir"/etc/init.d/forgejo-runner
install -Dm644 "$srcdir"/forgejo-runner.confd "$pkgdir"/etc/conf.d/forgejo-runner
install -Dm644 "$srcdir"/forgejo-runner.logrotate "$pkgdir"/etc/logrotate.d/forgejo-runner
}
sha512sums="
e78968a5f9b6e797fb759a5c8cbf46a5c2fef2083dabc88599c9017729faface963576c63a948b0add424cb267902e864fb1a1b619202660296976d93e670713 forgejo-runner-3.5.0.tar.gz
a3c7238b0c63053325d31e09277edd88690ef5260854517f82d9042d6173fb5d24ebfe36e1d7363673dd8801972638a6e69b6af8ad43debb6057515c73655236 forgejo-runner.logrotate
bb0c6fbe90109c77f9ef9cb0d35d20b8033be0e4b7a60839b596aa5528dfa24309ec894d8c04066bf8fb30143e63a5fd8cc6fc89aac364422b583e0f840e2da6 forgejo-runner.initd
e11eab27f88f1181112389befa7de3aa0bac7c26841861918707ede53335535425c805e6682e25704e9c8a6aecba3dc13e20900a99df1183762b012b62f26d5f forgejo-runner.confd
"

View file

@ -1,17 +0,0 @@
# Configuration for /etc/init.d/forgejo-runner
# Path to the config file (--config).
#cfgfile="/etc/forgejo-runner/config.yaml"
# Path to the working directory (--working-directory).
#datadir="/var/lib/forgejo-runner"
# Path to the log file where stdout/stderr will be redirected.
# Leave empty/commented out to use syslog instead.
#output_log="/var/log/forgejo-runner.log"
# You may change this to root, e.g. to run jobs in LXC
#command_user="forgejo-runner"
# Comment out to run without process supervisor.
supervisor=supervise-daemon

View file

@ -1,38 +0,0 @@
#!/sbin/openrc-run
description="Forgejo CI Runner"
name="Forgejo Runner"
: ${cfgfile:="/etc/forgejo-runner/config.yaml"}
: ${datadir:="/var/lib/forgejo-runner"}
: ${command_user:="forgejo-runner"}
command="/usr/bin/forgejo-runner"
command_args="daemon --config $cfgfile"
command_background="yes"
directory="$datadir"
pidfile="/run/$RC_SVCNAME.pid"
depend() {
need net
use dns logger
}
start_pre() {
checkpath -d -o "$command_user" /etc/forgejo-runner
checkpath -d -o "$command_user" "$datadir"
if ! [ -e "$cfgfile" ]; then
eerror "Config file $cfgfile doesn't exist."
eerror "You can generate it with: forgejo-runner generate-config,"
eerror "or use the auto-generated one in /etc/forgejo-runner/config.example.yaml"
return 1
fi
if [ "$error_log" ]; then
output_log="$error_log"
else
output_logger="logger -t '${RC_SVCNAME}' -p daemon.info"
error_logger="logger -t '${RC_SVCNAME}' -p daemon.error"
fi
}

View file

@ -1,5 +0,0 @@
/var/log/forgejo-runner.log {
copytruncate
missingok
notifempty
}

View file

@ -1,14 +0,0 @@
#!/bin/sh
addgroup -S forgejo-runner 2>/dev/null
adduser -S -D -H -h /var/lib/forgejo-runner -s /sbin/nologin -G forgejo-runner -g forgejo-runner forgejo-runner 2>/dev/null
cat >&2 <<EOF
* In order to setup the runner, create a config file
* in /etc/forgejo-runner/config.yaml (either from .example.yaml,
* or generating your own with 'forgejo-runner generate-config'),
* then register it with 'doas -u forgejo-runner forgejo-runner register'
* ran in the /var/lib/forgejo-runner directory.
EOF
exit 0

View file

@ -1 +0,0 @@
forgejo-runner.pre-install

View file

@ -0,0 +1,48 @@
From 7b88a0ba662a05076ee209f4b21caf9bed972b6a Mon Sep 17 00:00:00 2001
From: Jens Langhammer <jens@goauthentik.io>
Date: Mon, 25 Nov 2024 16:48:40 +0100
Subject: [PATCH] web/admin: fix impersonate API call
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
---
web/src/admin/groups/RelatedUserList.ts | 1 +
web/src/admin/users/UserListPage.ts | 1 +
web/src/admin/users/UserViewPage.ts | 1 +
3 files changed, 3 insertions(+)
diff --git a/web/src/admin/groups/RelatedUserList.ts b/web/src/admin/groups/RelatedUserList.ts
index 72a9b62a1856..80474f5938e8 100644
--- a/web/src/admin/groups/RelatedUserList.ts
+++ b/web/src/admin/groups/RelatedUserList.ts
@@ -219,6 +219,7 @@ export class RelatedUserList extends WithBrandConfig(WithCapabilitiesConfig(Tabl
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: item.pk,
+ impersonationRequest: { reason: "" },
})
.then(() => {
window.location.href = "/";
diff --git a/web/src/admin/users/UserListPage.ts b/web/src/admin/users/UserListPage.ts
index 1264c00814fc..6b0f19a42e88 100644
--- a/web/src/admin/users/UserListPage.ts
+++ b/web/src/admin/users/UserListPage.ts
@@ -272,6 +272,7 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: item.pk,
+ impersonationRequest: { reason: "" },
})
.then(() => {
window.location.href = "/";
diff --git a/web/src/admin/users/UserViewPage.ts b/web/src/admin/users/UserViewPage.ts
index 119ffdb3716a..83e2661e6294 100644
--- a/web/src/admin/users/UserViewPage.ts
+++ b/web/src/admin/users/UserViewPage.ts
@@ -215,6 +215,7 @@ export class UserViewPage extends WithCapabilitiesConfig(AKElement) {
return new CoreApi(DEFAULT_CONFIG)
.coreUsersImpersonateCreate({
id: user.pk,
+ impersonationRequest: { reason: "" },
})
.then(() => {
window.location.href = "/";

View file

@ -1,14 +1,15 @@
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net> # Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net> # Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=authentik pkgname=authentik
pkgver=2024.8.4 pkgver=2024.8.6
pkgrel=0 pkgrel=1
pkgdesc="An open-source Identity Provider focused on flexibility and versatility" pkgdesc="An open-source Identity Provider focused on flexibility and versatility"
url="https://github.com/goauthentik/authentik" url="https://github.com/goauthentik/authentik"
# s390x: missing py3-celery py3-flower and py3-kombu # s390x: missing py3-celery py3-flower and py3-kombu
# armhf/armv7/x86: out of memory error when building goauthentik # armhf/armv7/x86: out of memory error when building goauthentik
# ppc64le: not supported by Rollup build # ppc64le: not supported by Rollup build
arch="aarch64 x86_64" # missing uvicorn
# arch="aarch64 x86_64"
license="MIT" license="MIT"
depends=" depends="
bash bash
@ -64,7 +65,7 @@ depends="
py3-django-prometheus py3-django-prometheus
py3-django-pglock py3-django-pglock
py3-django-redis py3-django-redis
py3-django-rest-framework~=3.14.0 py3-django-rest-framework~3.14.0
py3-django-rest-framework-guardian py3-django-rest-framework-guardian
py3-django-storages py3-django-storages
py3-django-tenants py3-django-tenants
@ -180,6 +181,7 @@ source="
fix-ak-bash.patch fix-ak-bash.patch
root-settings-csrf_trusted_origins.patch root-settings-csrf_trusted_origins.patch
go-downgrade-1.22.patch go-downgrade-1.22.patch
12184_fix-impersonnate-api.patch
" "
builddir="$srcdir/"authentik-version-$pkgver builddir="$srcdir/"authentik-version-$pkgver
subpackages="$pkgname-openrc $pkgname-doc $pkgname-pyc" subpackages="$pkgname-openrc $pkgname-doc $pkgname-pyc"
@ -333,7 +335,7 @@ pyc() {
} }
sha512sums=" sha512sums="
63548adc1ff93f603d133f1a23357ac1fedd975e790b81e1ad1ce17c7b32a58197c2fe49e6199362d3e90f873cd010b14b2e83b254f81b0198663657a2532e91 authentik-2024.8.4.tar.gz ede869ff73e83707819d1cdc0c73bc4dc445fbf4a8ac27140245a3fe77949fc6b9cfa4ebb5de935956a5a1d9faf340720e8259287805fccdb0f141294f54e3cc authentik-2024.8.6.tar.gz
4defb4fe3a4230f4aa517fbecd5e5b8bcef2a64e1b40615660ae9eec33597310a09df5e126f4d39ce7764bd1716c0a7040637699135c103cbc1879593c6c06f1 authentik.openrc 4defb4fe3a4230f4aa517fbecd5e5b8bcef2a64e1b40615660ae9eec33597310a09df5e126f4d39ce7764bd1716c0a7040637699135c103cbc1879593c6c06f1 authentik.openrc
6cb03b9b69df39bb4539fe05c966536314d766b2e9307a92d87070ba5f5b7e7ab70f1b5ee1ab3c0c50c23454f9c5a4caec29e63fdf411bbb7a124ad687569b89 authentik-worker.openrc 6cb03b9b69df39bb4539fe05c966536314d766b2e9307a92d87070ba5f5b7e7ab70f1b5ee1ab3c0c50c23454f9c5a4caec29e63fdf411bbb7a124ad687569b89 authentik-worker.openrc
351e6920d987861f8bf0d7ab2f942db716a8dbdad1f690ac662a6ef29ac0fd46cf817cf557de08f1c024703503d36bc8b46f0d9eb1ecaeb399dce4c3bb527d17 authentik-ldap.openrc 351e6920d987861f8bf0d7ab2f942db716a8dbdad1f690ac662a6ef29ac0fd46cf817cf557de08f1c024703503d36bc8b46f0d9eb1ecaeb399dce4c3bb527d17 authentik-ldap.openrc
@ -342,4 +344,5 @@ f1a3cb215b6210fa7d857a452a9f2bc4dc0520e49b9fa7027547cff093d740a7e2548f1bf1f8831f
3e47db684a3f353dcecdb7bab8836b9d5198766735d77f676a51d952141a0cf9903fcb92e6306c48d2522d7a1f3028b37247fdc1dc74d4d6e043da7eb4f36d49 fix-ak-bash.patch 3e47db684a3f353dcecdb7bab8836b9d5198766735d77f676a51d952141a0cf9903fcb92e6306c48d2522d7a1f3028b37247fdc1dc74d4d6e043da7eb4f36d49 fix-ak-bash.patch
5c60e54b6a7829d611af66f5cb8184a002b5ae927efbd024c054a7c176fcb9efcfbe5685279ffcf0390b0f0abb3bb03e02782c6867c2b38d1ad2d508aae83fa0 root-settings-csrf_trusted_origins.patch 5c60e54b6a7829d611af66f5cb8184a002b5ae927efbd024c054a7c176fcb9efcfbe5685279ffcf0390b0f0abb3bb03e02782c6867c2b38d1ad2d508aae83fa0 root-settings-csrf_trusted_origins.patch
badff70b19aad79cf16046bd46cb62db25c2a8b85b2673ce7c44c42eb60d42f6fcb1b9a7a7236c00f24803b25d3c66a4d64423f7ce14a59763b8415db292a5b9 go-downgrade-1.22.patch badff70b19aad79cf16046bd46cb62db25c2a8b85b2673ce7c44c42eb60d42f6fcb1b9a7a7236c00f24803b25d3c66a4d64423f7ce14a59763b8415db292a5b9 go-downgrade-1.22.patch
5d409cb41d4a506df30618bdaf325188d304d879e2d23fe49277db6ae8fe9341803d7eee54c9a5dbdfa87267146ea1647995b2adb503fbf8f8c4d7f861ce9de6 12184_fix-impersonnate-api.patch
" "

View file

@ -1,15 +1,15 @@
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net> # Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net> # Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=freescout pkgname=freescout
pkgver=1.8.152 pkgver=1.8.160
pkgrel=0 pkgrel=0
pkgdesc="Free self-hosted help desk & shared mailbox" pkgdesc="Free self-hosted help desk & shared mailbox"
arch="noarch" arch="noarch"
url="freescout.net" url="freescout.net"
license="AGPL-3.0" license="AGPL-3.0"
_php=php83 _php=php84
_php_mods="-fpm -mbstring -xml -imap -zip -gd -curl -intl -tokenizer -pdo_pgsql -openssl -session -iconv -fileinfo -dom -pcntl" _php_mods="-fpm -mbstring -xml -imap -zip -gd -curl -intl -tokenizer -pdo_pgsql -openssl -session -iconv -fileinfo -dom -pcntl"
depends="$_php ${_php_mods//-/$_php-} nginx postgresql pwgen" depends="$_php ${_php_mods//-/$_php-} nginx postgresql pwgen bash"
makedepends="composer pcre" makedepends="composer pcre"
install="$pkgname.post-install $pkgname.post-upgrade $pkgname.pre-install" install="$pkgname.post-install $pkgname.post-upgrade $pkgname.pre-install"
source=" source="
@ -75,8 +75,8 @@ package() {
install -m755 -D "$srcdir"/freescout-manage.sh "$pkgdir"/usr/bin/freescout-manage install -m755 -D "$srcdir"/freescout-manage.sh "$pkgdir"/usr/bin/freescout-manage
} }
sha512sums=" sha512sums="
0e4d6d4a1aaeba2d39db8678e436f3c46c1a1fd79ea2b37c9ac95cbb319306b818991981987f6ac7dc8100a084d4189fa12f7639b24e2744705fa409ac349864 freescout-1.8.152.tar.gz 8441385a36d9ee5b542936f34e7700e86e1595d9a16b07afeac42bf48409ba0ecd1c542bc82b48afb0bb9201c7219bd146fe9455491ba40116dc66953b994488 freescout-1.8.160.tar.gz
e4af6c85dc12f694bef2a02e4664e31ed50b2c109914d7ffad5001c2bbd764ef25b17ecaa59ff55ef41bccf17169bf910d1a08888364bdedd0ecc54d310e661f freescout.nginx e4af6c85dc12f694bef2a02e4664e31ed50b2c109914d7ffad5001c2bbd764ef25b17ecaa59ff55ef41bccf17169bf910d1a08888364bdedd0ecc54d310e661f freescout.nginx
7ce9b3ee3a979db44f5e6d7daa69431e04a5281f364ae7be23e5a0a0547f96abc858d2a8010346be2fb99bd2355fb529e7030ed20d54f310249e61ed5db4d0ba freescout-manage.sh 7ce9b3ee3a979db44f5e6d7daa69431e04a5281f364ae7be23e5a0a0547f96abc858d2a8010346be2fb99bd2355fb529e7030ed20d54f310249e61ed5db4d0ba freescout-manage.sh
3416da98d71aea5a7093913ea34e783e21ff05dca90bdc5ff3d00c548db5889f6d0ec98441cd65ab9f590be5cd59fdd0d7f1c98b5deef7bb3adbc8db435ec9bf rename-client-to-membre-fr-en.patch 0cba00b7d945ce84f72a2812d40028a073a5278856f610e46dbfe0ac78deff6bf5eba7643635fa4bc64d070c4d49eb47d24ea0a05ba1e6ea76690bfd77906366 rename-client-to-membre-fr-en.patch
" "

View file

@ -38,7 +38,7 @@ index 00000000..82d26052
+} +}
\ No newline at end of file \ No newline at end of file
diff --git a/resources/lang/fr.json.orig b/resources/lang/fr.json diff --git a/resources/lang/fr.json.orig b/resources/lang/fr.json
index ff8d9d4..98d158f 100644 index 6264973..8a7037e 100644
--- a/resources/lang/fr.json.orig --- a/resources/lang/fr.json.orig
+++ b/resources/lang/fr.json +++ b/resources/lang/fr.json
@@ -26,8 +26,8 @@ @@ -26,8 +26,8 @@
@ -201,8 +201,8 @@ index ff8d9d4..98d158f 100644
- "This number is not visible to customers. It is only used to track conversations within :app_name": "Ce numéro n'est pas visible pour les clients. Il est uniquement utilisé pour suivre les conversations dans :app_name", - "This number is not visible to customers. It is only used to track conversations within :app_name": "Ce numéro n'est pas visible pour les clients. Il est uniquement utilisé pour suivre les conversations dans :app_name",
+ "This number is not visible to customers. It is only used to track conversations within :app_name": "Ce numéro n'est pas visible pour les membres. Il est uniquement utilisé pour suivre les conversations dans :app_name", + "This number is not visible to customers. It is only used to track conversations within :app_name": "Ce numéro n'est pas visible pour les membres. Il est uniquement utilisé pour suivre les conversations dans :app_name",
"This password is incorrect.": "Ce mot de passe est incorrect.", "This password is incorrect.": "Ce mot de passe est incorrect.",
- "This reply will go to the customer. :%switch_start%Switch to a note:switch_end if you are replying to :user_name.": "Cette réponse ira au client. :%switch_start%Passez à une note:switch_end si vous répondez à :user_name.", - "This reply will go to the customer. :%switch_start%Switch to a note:%switch_end% if you are replying to :user_name.": "Cette réponse ira au client. :%switch_start%Passez à une note:%switch_end% si vous répondez à :user_name.",
+ "This reply will go to the customer. :%switch_start%Switch to a note:switch_end if you are replying to :user_name.": "Cette réponse ira au membre. :%switch_start%Passez à une note:switch_end si vous répondez à :user_name.", + "This reply will go to the customer. :%switch_start%Switch to a note:%switch_end% if you are replying to :user_name.": "Cette réponse ira au membre. :%switch_start%Passez à une note:%switch_end% si vous répondez à :user_name.",
"This setting gives you control over what page loads after you perform an action (send a reply, add a note, change conversation status or assignee).": "Ce paramètre vous permet de contrôler la page qui se charge après avoir effectué une action (envoyer une réponse, ajouter une note, etc.).", "This setting gives you control over what page loads after you perform an action (send a reply, add a note, change conversation status or assignee).": "Ce paramètre vous permet de contrôler la page qui se charge après avoir effectué une action (envoyer une réponse, ajouter une note, etc.).",
- "This text will be added to the beginning of each email reply sent to a customer.": "Ce texte sera ajouté au début de chaque réponse par e-mail envoyée à un client.", - "This text will be added to the beginning of each email reply sent to a customer.": "Ce texte sera ajouté au début de chaque réponse par e-mail envoyée à un client.",
+ "This text will be added to the beginning of each email reply sent to a customer.": "Ce texte sera ajouté au début de chaque réponse par e-mail envoyée à un membre.", + "This text will be added to the beginning of each email reply sent to a customer.": "Ce texte sera ajouté au début de chaque réponse par e-mail envoyée à un membre.",

View file

@ -1,7 +1,7 @@
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net> # Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net> # Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=listmonk pkgname=listmonk
pkgver=4.0.1 pkgver=4.1.0
pkgrel=0 pkgrel=0
pkgdesc='Self-hosted newsletter and mailing list manager with a modern dashboard' pkgdesc='Self-hosted newsletter and mailing list manager with a modern dashboard'
arch="all" arch="all"
@ -67,7 +67,7 @@ package() {
ln -s /etc/listmonk/config.toml "$pkgdir"/usr/share/webapps/listmonk/config.toml ln -s /etc/listmonk/config.toml "$pkgdir"/usr/share/webapps/listmonk/config.toml
} }
sha512sums=" sha512sums="
ae5c338b756bb9d84739ab8b04b591e33ee7f6a579725083ec95e2609dbff55adbd1f2a11c0487b971aa030a3a35347cf54966820e1320b4144351935b2497d8 listmonk-4.0.1.tar.gz 936b33d6de1d69ee4e7f768810116ac997c516754aace0371089bc8106bebee944197864afc11b7bc5725afa9a4f195d6629957bfcdd37c847e3780aa34558ec listmonk-4.1.0.tar.gz
939450af4b23708e3d23a5a88fad4c24b957090bdd21351a6dd520959e52e45e5fcac117a3eafa280d9506616dae39ad3943589571f008cac5abe1ffd8062424 listmonk.sh 939450af4b23708e3d23a5a88fad4c24b957090bdd21351a6dd520959e52e45e5fcac117a3eafa280d9506616dae39ad3943589571f008cac5abe1ffd8062424 listmonk.sh
8e9c0b1f335c295fb741418246eb17c7566e5e4200a284c6483433e8ddbf5250aa692435211cf062ad1dfcdce3fae9148def28f03f2492d33fe5e66cbeebd4bd listmonk.openrc 8e9c0b1f335c295fb741418246eb17c7566e5e4200a284c6483433e8ddbf5250aa692435211cf062ad1dfcdce3fae9148def28f03f2492d33fe5e66cbeebd4bd listmonk.openrc
" "

File diff suppressed because it is too large Load diff

View file

@ -1,43 +0,0 @@
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=py3-django-tenants
#_pkgreal is used by apkbuild-pypi to find modules at PyPI
_pkgreal=django-tenants
pkgver=3.6.1
pkgrel=5
pkgdesc="Tenant support for Django using PostgreSQL schemas."
url="https://pypi.python.org/project/django-tenants"
arch="noarch"
license="MIT"
depends="py3-django py3-psycopg py3-gunicorn py3-coverage"
checkdepends="python3-dev py3-pytest"
makedepends="py3-setuptools py3-gpep517 py3-wheel"
source="
$pkgname-$pkgver.tar.gz::https://codeload.github.com/django-tenants/django-tenants/tar.gz/refs/tags/v$pkgver
997_update-from-pgclone-schema.patch
"
builddir="$srcdir/$_pkgreal-$pkgver"
options="!check" # Requires setting up test database
subpackages="$pkgname-pyc"
build() {
gpep517 build-wheel \
--wheel-dir .dist \
--output-fd 3 3>&1 >&2
}
check() {
python3 -m venv --clear --without-pip --system-site-packages .testenv
.testenv/bin/python3 -m installer .dist/*.whl
DJANGO_SETTINGS_MODULE=tests.settings .testenv/bin/python3 -m pytest -v
}
package() {
python3 -m installer -d "$pkgdir" \
.dist/*.whl
}
sha512sums="
b18afce81ccc89e49fcc4ebe85d90be602415ca898c1660a4e71e2bef6a3ed2e8c724e94b61d8c6f48f3fb19eb2a87d6a6f5bbf449b3e2f661f87e4b5638eafb py3-django-tenants-3.6.1.tar.gz
f2424bb188db2e3c7d13c15e5bdf0959c6f794e68dbc677c8b876d4faa321f78aded5565539f1bfd97583c6df0fcc19ec05abe203b08407e4446dd7194756825 997_update-from-pgclone-schema.patch
"

View file

@ -1,39 +0,0 @@
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=py3-drf-orjson-renderer
#_pkgreal is used by apkbuild-pypi to find modules at PyPI
_pkgreal=drf_orjson_renderer
pkgver=1.7.3
_gittag=9a59352f82e262bd78ccc0228361bcb321a33623
pkgrel=0
pkgdesc="Django RestFramework JSON Renderer Backed by orjson"
url="https://pypi.python.org/project/drf-orjson-renderer"
arch="noarch"
license="MIT"
depends="py3-django-rest-framework py3-orjson"
checkdepends="py3-pytest-django py3-numpy"
makedepends="py3-setuptools py3-gpep517 py3-wheel"
source="$pkgname-$pkgver.tar.gz::https://github.com/brianjbuck/drf_orjson_renderer/archive/$_gittag.tar.gz"
builddir="$srcdir/$_pkgreal-$_gittag"
subpackages="$pkgname-pyc"
build() {
gpep517 build-wheel \
--wheel-dir .dist \
--output-fd 3 3>&1 >&2
}
check() {
python3 -m venv --clear --without-pip --system-site-packages .testenv
.testenv/bin/python3 -m installer .dist/*.whl
.testenv/bin/python3 -m pytest -v
}
package() {
python3 -m installer -d "$pkgdir" \
.dist/*.whl
}
sha512sums="
7870aebf6bcc249228b1620f4b50124eef54e251dcac236e23be4287284461617d630b073d2e9122f66779a908dfd69c5e16b486b23de0114b06b3df6b468e95 py3-drf-orjson-renderer-1.7.3.tar.gz
"

View file

@ -1,39 +0,0 @@
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=py3-pyrad
#_pkgreal is used by apkbuild-pypi to find modules at PyPI
_pkgreal=pyrad
pkgver=2.4
pkgrel=0
pkgdesc="Python RADIUS Implementation"
url="https://pypi.python.org/project/pyrad"
arch="noarch"
license="BSD-3-Clause"
depends="py3-netaddr"
checkdepends="py3-pytest"
makedepends="py3-setuptools py3-gpep517 py3-wheel poetry"
source="$pkgname-$pkgver.tar.gz::https://github.com/pyradius/pyrad/archive/refs/tags/$pkgver.tar.gz"
options="!check" # TODO
builddir="$srcdir/$_pkgreal-$pkgver"
subpackages="$pkgname-pyc"
build() {
gpep517 build-wheel \
--wheel-dir .dist \
--output-fd 3 3>&1 >&2
}
check() {
python3 -m venv --clear --without-pip --system-site-packages .testenv
.testenv/bin/python3 -m installer .dist/*.whl
.testenv/bin/python3 -m pytest -v
}
package() {
python3 -m installer -d "$pkgdir" \
.dist/*.whl
}
sha512sums="
e4f4c687596bd226cf2cdb409a8d940c7b665fb7f722d09113dd9a1b05ab176ce8f920b235918ec01695f262930d13b4057b199cf6aac72afa54950c1fb59166 py3-pyrad-2.4.tar.gz
"

View file

@ -1,41 +0,0 @@
# Contributor: Antoine Martin (ayakael) <dev@ayakael.net>
# Maintainer: Antoine Martin (ayakael) <dev@ayakael.net>
pkgname=py3-tenant-schemas-celery
#_pkgreal is used by apkbuild-pypi to find modules at PyPI
_pkgreal=tenant-schemas-celery
pkgver=2.2.0
pkgrel=2
pkgdesc="Celery integration for django-tenant-schemas and django-tenants"
url="https://pypi.python.org/project/tenant-schemas-celery"
arch="noarch"
license="MIT"
depends="py3-django-tenants py3-celery"
checkdepends="python3-dev py3-pytest"
makedepends="py3-setuptools py3-gpep517 py3-wheel"
source="
$pkgname-$pkgver.tar.gz::https://codeload.github.com/maciej-gol/tenant-schemas-celery/tar.gz/refs/tags/$pkgver
"
options="!check" # Test suite wants docker
builddir="$srcdir/$_pkgreal-$pkgver"
subpackages="$pkgname-pyc"
build() {
gpep517 build-wheel \
--wheel-dir .dist \
--output-fd 3 3>&1 >&2
}
check() {
python3 -m venv --clear --without-pip --system-site-packages .testenv
.testenv/bin/python3 -m installer .dist/*.whl
DJANGO_SETTINGS_MODULE=tests.settings .testenv/bin/python3 -m pytest -v
}
package() {
python3 -m installer -d "$pkgdir" \
.dist/*.whl
}
sha512sums="
dad71011306936dc84d966797b113008780750e9e973513092bec892be0d1468e0a0e7e8e2fcca9765309a27767e1c72bdaad7c8aca16353ae1eef783c239148 py3-tenant-schemas-celery-2.2.0.tar.gz
"

View file

@ -0,0 +1,618 @@
diff --git a/docs/deployment.md b/docs/deployment.md
index d69fcf8..99dfbf3 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -60,7 +60,7 @@ Options:
--loop [auto|asyncio|uvloop] Event loop implementation. [default: auto]
--http [auto|h11|httptools] HTTP protocol implementation. [default:
auto]
- --ws [auto|none|websockets|wsproto]
+ --ws [auto|none|websockets|websockets-sansio|wsproto]
WebSocket protocol implementation.
[default: auto]
--ws-max-size INTEGER WebSocket max size message in bytes
diff --git a/docs/index.md b/docs/index.md
index bb6fc32..50e2ab9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -130,7 +130,7 @@ Options:
--loop [auto|asyncio|uvloop] Event loop implementation. [default: auto]
--http [auto|h11|httptools] HTTP protocol implementation. [default:
auto]
- --ws [auto|none|websockets|wsproto]
+ --ws [auto|none|websockets|websockets-sansio|wsproto]
WebSocket protocol implementation.
[default: auto]
--ws-max-size INTEGER WebSocket max size message in bytes
diff --git a/pyproject.toml b/pyproject.toml
index 0a89966..8771bfb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,6 +92,10 @@ filterwarnings = [
"ignore:Uvicorn's native WSGI implementation is deprecated.*:DeprecationWarning",
"ignore: 'cgi' is deprecated and slated for removal in Python 3.13:DeprecationWarning",
"ignore: remove second argument of ws_handler:DeprecationWarning:websockets",
+ "ignore: websockets.legacy is deprecated.*:DeprecationWarning",
+ "ignore: websockets.server.WebSocketServerProtocol is deprecated.*:DeprecationWarning",
+ "ignore: websockets.client.connect is deprecated.*:DeprecationWarning",
+ "ignore: websockets.exceptions.InvalidStatusCode is deprecated",
]
[tool.coverage.run]
diff --git a/tests/conftest.py b/tests/conftest.py
index 1b0c0e8..7061a14 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -233,9 +233,9 @@ def unused_tcp_port() -> int:
marks=pytest.mark.skipif(not importlib.util.find_spec("wsproto"), reason="wsproto not installed."),
id="wsproto",
),
+ pytest.param("uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol", id="websockets"),
pytest.param(
- "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
- id="websockets",
+ "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol", id="websockets-sansio"
),
]
)
diff --git a/tests/middleware/test_logging.py b/tests/middleware/test_logging.py
index f27633a..63d7daf 100644
--- a/tests/middleware/test_logging.py
+++ b/tests/middleware/test_logging.py
@@ -49,7 +49,9 @@ async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
await send({"type": "http.response.body", "body": b"", "more_body": False})
-async def test_trace_logging(caplog: pytest.LogCaptureFixture, logging_config, unused_tcp_port: int):
+async def test_trace_logging(
+ caplog: pytest.LogCaptureFixture, logging_config: dict[str, typing.Any], unused_tcp_port: int
+):
config = Config(
app=app,
log_level="trace",
@@ -91,8 +93,8 @@ async def test_trace_logging_on_http_protocol(http_protocol_cls, caplog, logging
async def test_trace_logging_on_ws_protocol(
ws_protocol_cls: WSProtocol,
- caplog,
- logging_config,
+ caplog: pytest.LogCaptureFixture,
+ logging_config: dict[str, typing.Any],
unused_tcp_port: int,
):
async def websocket_app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
@@ -104,7 +106,7 @@ async def test_trace_logging_on_ws_protocol(
elif message["type"] == "websocket.disconnect":
break
- async def open_connection(url):
+ async def open_connection(url: str):
async with websockets.client.connect(url) as websocket:
return websocket.open
diff --git a/tests/middleware/test_proxy_headers.py b/tests/middleware/test_proxy_headers.py
index 0ade974..d300c45 100644
--- a/tests/middleware/test_proxy_headers.py
+++ b/tests/middleware/test_proxy_headers.py
@@ -465,6 +465,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
host, port = scope["client"]
await send({"type": "websocket.accept"})
await send({"type": "websocket.send", "text": f"{scheme}://{host}:{port}"})
+ await send({"type": "websocket.close"})
app_with_middleware = ProxyHeadersMiddleware(websocket_app, trusted_hosts="*")
config = Config(
diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py
index 15ccfdd..e728544 100644
--- a/tests/protocols/test_websocket.py
+++ b/tests/protocols/test_websocket.py
@@ -7,6 +7,8 @@ from copy import deepcopy
import httpx
import pytest
import websockets
+import websockets.asyncio
+import websockets.asyncio.client
import websockets.client
import websockets.exceptions
from typing_extensions import TypedDict
@@ -601,12 +603,9 @@ async def test_connection_lost_before_handshake_complete(
await send_accept_task.wait()
disconnect_message = await receive() # type: ignore
- response: httpx.Response | None = None
-
async def websocket_session(uri: str):
- nonlocal response
async with httpx.AsyncClient() as client:
- response = await client.get(
+ await client.get(
f"http://127.0.0.1:{unused_tcp_port}",
headers={
"upgrade": "websocket",
@@ -623,9 +622,6 @@ async def test_connection_lost_before_handshake_complete(
send_accept_task.set()
await asyncio.sleep(0.1)
- assert response is not None
- assert response.status_code == 500, response.text
- assert response.text == "Internal Server Error"
assert disconnect_message == {"type": "websocket.disconnect", "code": 1006}
await task
@@ -920,6 +916,9 @@ async def test_server_reject_connection_with_body_nolength(
async def test_server_reject_connection_with_invalid_msg(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
+ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
+ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
+
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "websocket"
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@@ -951,6 +950,9 @@ async def test_server_reject_connection_with_invalid_msg(
async def test_server_reject_connection_with_missing_body(
ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int
):
+ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
+ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
+
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
assert scope["type"] == "websocket"
assert "extensions" in scope and "websocket.http.response" in scope["extensions"]
@@ -986,6 +988,8 @@ async def test_server_multiple_websocket_http_response_start_events(
The server should raise an exception if it sends multiple
websocket.http.response.start events.
"""
+ if ws_protocol_cls.__name__ == "WebSocketsSansIOProtocol":
+ pytest.skip("WebSocketsSansIOProtocol sends both start and body messages in one message.")
exception_message: str | None = None
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
diff --git a/uvicorn/config.py b/uvicorn/config.py
index 664d191..cbfeea6 100644
--- a/uvicorn/config.py
+++ b/uvicorn/config.py
@@ -25,7 +25,7 @@ from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from uvicorn.middleware.wsgi import WSGIMiddleware
HTTPProtocolType = Literal["auto", "h11", "httptools"]
-WSProtocolType = Literal["auto", "none", "websockets", "wsproto"]
+WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
LifespanType = Literal["auto", "on", "off"]
LoopSetupType = Literal["none", "auto", "asyncio", "uvloop"]
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
@@ -47,6 +47,7 @@ WS_PROTOCOLS: dict[WSProtocolType, str | None] = {
"auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
"none": None,
"websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
+ "websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol",
"wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
}
LIFESPAN: dict[LifespanType, str] = {
diff --git a/uvicorn/protocols/websockets/websockets_sansio_impl.py b/uvicorn/protocols/websockets/websockets_sansio_impl.py
new file mode 100644
index 0000000..994af07
--- /dev/null
+++ b/uvicorn/protocols/websockets/websockets_sansio_impl.py
@@ -0,0 +1,405 @@
+from __future__ import annotations
+
+import asyncio
+import logging
+from asyncio.transports import BaseTransport, Transport
+from http import HTTPStatus
+from typing import Any, Literal, cast
+from urllib.parse import unquote
+
+from websockets import InvalidState
+from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
+from websockets.frames import Frame, Opcode
+from websockets.http11 import Request
+from websockets.server import ServerProtocol
+
+from uvicorn._types import (
+ ASGIReceiveEvent,
+ ASGISendEvent,
+ WebSocketAcceptEvent,
+ WebSocketCloseEvent,
+ WebSocketDisconnectEvent,
+ WebSocketReceiveEvent,
+ WebSocketResponseBodyEvent,
+ WebSocketResponseStartEvent,
+ WebSocketScope,
+ WebSocketSendEvent,
+)
+from uvicorn.config import Config
+from uvicorn.logging import TRACE_LOG_LEVEL
+from uvicorn.protocols.utils import (
+ ClientDisconnected,
+ get_local_addr,
+ get_path_with_query_string,
+ get_remote_addr,
+ is_ssl,
+)
+from uvicorn.server import ServerState
+
+
+class WebSocketsSansIOProtocol(asyncio.Protocol):
+ def __init__(
+ self,
+ config: Config,
+ server_state: ServerState,
+ app_state: dict[str, Any],
+ _loop: asyncio.AbstractEventLoop | None = None,
+ ) -> None:
+ if not config.loaded:
+ config.load() # pragma: no cover
+
+ self.config = config
+ self.app = config.loaded_app
+ self.loop = _loop or asyncio.get_event_loop()
+ self.logger = logging.getLogger("uvicorn.error")
+ self.root_path = config.root_path
+ self.app_state = app_state
+
+ # Shared server state
+ self.connections = server_state.connections
+ self.tasks = server_state.tasks
+ self.default_headers = server_state.default_headers
+
+ # Connection state
+ self.transport: asyncio.Transport = None # type: ignore[assignment]
+ self.server: tuple[str, int] | None = None
+ self.client: tuple[str, int] | None = None
+ self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
+
+ # WebSocket state
+ self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue()
+ self.handshake_initiated = False
+ self.handshake_complete = False
+ self.close_sent = False
+ self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None
+
+ extensions = []
+ if self.config.ws_per_message_deflate:
+ extensions = [ServerPerMessageDeflateFactory()]
+ self.conn = ServerProtocol(
+ extensions=extensions,
+ max_size=self.config.ws_max_size,
+ logger=logging.getLogger("uvicorn.error"),
+ )
+
+ self.read_paused = False
+ self.writable = asyncio.Event()
+ self.writable.set()
+
+ # Buffers
+ self.bytes = b""
+
+ def connection_made(self, transport: BaseTransport) -> None:
+ """Called when a connection is made."""
+ transport = cast(Transport, transport)
+ self.connections.add(self)
+ self.transport = transport
+ self.server = get_local_addr(transport)
+ self.client = get_remote_addr(transport)
+ self.scheme = "wss" if is_ssl(transport) else "ws"
+
+ if self.logger.level <= TRACE_LOG_LEVEL:
+ prefix = "%s:%d - " % self.client if self.client else ""
+ self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
+
+ def connection_lost(self, exc: Exception | None) -> None:
+ code = 1005 if self.handshake_complete else 1006
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
+ self.connections.remove(self)
+
+ if self.logger.level <= TRACE_LOG_LEVEL:
+ prefix = "%s:%d - " % self.client if self.client else ""
+ self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
+
+ self.handshake_complete = True
+ if exc is None:
+ self.transport.close()
+
+ def eof_received(self) -> None:
+ pass
+
+ def shutdown(self) -> None:
+ if self.handshake_complete:
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
+ self.conn.send_close(1012)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+ else:
+ self.send_500_response()
+ self.transport.close()
+
+ def data_received(self, data: bytes) -> None:
+ self.conn.receive_data(data)
+ parser_exc = self.conn.parser_exc
+ if parser_exc is not None:
+ self.handle_parser_exception()
+ return
+ self.handle_events()
+
+ def handle_events(self) -> None:
+ for event in self.conn.events_received():
+ if isinstance(event, Request):
+ self.handle_connect(event)
+ if isinstance(event, Frame):
+ if event.opcode == Opcode.CONT:
+ self.handle_cont(event)
+ elif event.opcode == Opcode.TEXT:
+ self.handle_text(event)
+ elif event.opcode == Opcode.BINARY:
+ self.handle_bytes(event)
+ elif event.opcode == Opcode.PING:
+ self.handle_ping(event)
+ elif event.opcode == Opcode.CLOSE:
+ self.handle_close(event)
+
+ # Event handlers
+
+ def handle_connect(self, event: Request) -> None:
+ self.request = event
+ self.response = self.conn.accept(event)
+ self.handshake_initiated = True
+ if self.response.status_code != 101:
+ self.handshake_complete = True
+ self.close_sent = True
+ self.conn.send_response(self.response)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+ self.transport.close()
+ return
+
+ headers = [
+ (key.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
+ for key, value in event.headers.raw_items()
+ ]
+ raw_path, _, query_string = event.path.partition("?")
+ self.scope: WebSocketScope = {
+ "type": "websocket",
+ "asgi": {"version": self.config.asgi_version, "spec_version": "2.3"},
+ "http_version": "1.1",
+ "scheme": self.scheme,
+ "server": self.server,
+ "client": self.client,
+ "root_path": self.root_path,
+ "path": unquote(raw_path),
+ "raw_path": raw_path.encode("ascii"),
+ "query_string": query_string.encode("ascii"),
+ "headers": headers,
+ "subprotocols": event.headers.get_all("Sec-WebSocket-Protocol"),
+ "state": self.app_state.copy(),
+ "extensions": {"websocket.http.response": {}},
+ }
+ self.queue.put_nowait({"type": "websocket.connect"})
+ task = self.loop.create_task(self.run_asgi())
+ task.add_done_callback(self.on_task_complete)
+ self.tasks.add(task)
+
+ def handle_cont(self, event: Frame) -> None:
+ self.bytes += event.data
+ if event.fin:
+ self.send_receive_event_to_app()
+
+ def handle_text(self, event: Frame) -> None:
+ self.bytes = event.data
+ self.curr_msg_data_type: Literal["text", "bytes"] = "text"
+ if event.fin:
+ self.send_receive_event_to_app()
+
+ def handle_bytes(self, event: Frame) -> None:
+ self.bytes = event.data
+ self.curr_msg_data_type = "bytes"
+ if event.fin:
+ self.send_receive_event_to_app()
+
+ def send_receive_event_to_app(self) -> None:
+ data_type = self.curr_msg_data_type
+ msg: WebSocketReceiveEvent
+ if data_type == "text":
+ msg = {"type": "websocket.receive", data_type: self.bytes.decode()}
+ else:
+ msg = {"type": "websocket.receive", data_type: self.bytes}
+ self.queue.put_nowait(msg)
+ if not self.read_paused:
+ self.read_paused = True
+ self.transport.pause_reading()
+
+ def handle_ping(self, event: Frame) -> None:
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+
+ def handle_close(self, event: Frame) -> None:
+ if not self.close_sent and not self.transport.is_closing():
+ disconnect_event: WebSocketDisconnectEvent = {
+ "type": "websocket.disconnect",
+ "code": self.conn.close_rcvd.code, # type: ignore[union-attr]
+ "reason": self.conn.close_rcvd.reason, # type: ignore[union-attr]
+ }
+ self.queue.put_nowait(disconnect_event)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+ self.transport.close()
+
+ def handle_parser_exception(self) -> None:
+ disconnect_event: WebSocketDisconnectEvent = {
+ "type": "websocket.disconnect",
+ "code": self.conn.close_sent.code, # type: ignore[union-attr]
+ "reason": self.conn.close_sent.reason, # type: ignore[union-attr]
+ }
+ self.queue.put_nowait(disconnect_event)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+ self.close_sent = True
+ self.transport.close()
+
+ def on_task_complete(self, task: asyncio.Task[None]) -> None:
+ self.tasks.discard(task)
+
+ async def run_asgi(self) -> None:
+ try:
+ result = await self.app(self.scope, self.receive, self.send)
+ except ClientDisconnected:
+ self.transport.close()
+ except BaseException:
+ self.logger.exception("Exception in ASGI application\n")
+ self.send_500_response()
+ self.transport.close()
+ else:
+ if not self.handshake_complete:
+ msg = "ASGI callable returned without completing handshake."
+ self.logger.error(msg)
+ self.send_500_response()
+ self.transport.close()
+ elif result is not None:
+ msg = "ASGI callable should return None, but returned '%s'."
+ self.logger.error(msg, result)
+ self.transport.close()
+
+ def send_500_response(self) -> None:
+ if self.initial_response or self.handshake_complete:
+ return
+ response = self.conn.reject(500, "Internal Server Error")
+ self.conn.send_response(response)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+
+ async def send(self, message: ASGISendEvent) -> None:
+ await self.writable.wait()
+
+ message_type = message["type"]
+
+ if not self.handshake_complete and self.initial_response is None:
+ if message_type == "websocket.accept":
+ message = cast(WebSocketAcceptEvent, message)
+ self.logger.info(
+ '%s - "WebSocket %s" [accepted]',
+ self.scope["client"],
+ get_path_with_query_string(self.scope),
+ )
+ headers = [
+ (name.decode("latin-1").lower(), value.decode("latin-1").lower())
+ for name, value in (self.default_headers + list(message.get("headers", [])))
+ ]
+ accepted_subprotocol = message.get("subprotocol")
+ if accepted_subprotocol:
+ headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol))
+ self.response.headers.update(headers)
+
+ if not self.transport.is_closing():
+ self.handshake_complete = True
+ self.conn.send_response(self.response)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+
+ elif message_type == "websocket.close":
+ message = cast(WebSocketCloseEvent, message)
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
+ self.logger.info(
+ '%s - "WebSocket %s" 403',
+ self.scope["client"],
+ get_path_with_query_string(self.scope),
+ )
+ response = self.conn.reject(HTTPStatus.FORBIDDEN, "")
+ self.conn.send_response(response)
+ output = self.conn.data_to_send()
+ self.close_sent = True
+ self.handshake_complete = True
+ self.transport.write(b"".join(output))
+ self.transport.close()
+ elif message_type == "websocket.http.response.start" and self.initial_response is None:
+ message = cast(WebSocketResponseStartEvent, message)
+ if not (100 <= message["status"] < 600):
+ raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"])
+ self.logger.info(
+ '%s - "WebSocket %s" %d',
+ self.scope["client"],
+ get_path_with_query_string(self.scope),
+ message["status"],
+ )
+ headers = [
+ (name.decode("latin-1"), value.decode("latin-1"))
+ for name, value in list(message.get("headers", []))
+ ]
+ self.initial_response = (message["status"], headers, b"")
+ else:
+ msg = (
+ "Expected ASGI message 'websocket.accept', 'websocket.close' "
+ "or 'websocket.http.response.start' "
+ "but got '%s'."
+ )
+ raise RuntimeError(msg % message_type)
+
+ elif not self.close_sent and self.initial_response is None:
+ try:
+ if message_type == "websocket.send":
+ message = cast(WebSocketSendEvent, message)
+ bytes_data = message.get("bytes")
+ text_data = message.get("text")
+ if text_data:
+ self.conn.send_text(text_data.encode())
+ elif bytes_data:
+ self.conn.send_binary(bytes_data)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+
+ elif message_type == "websocket.close" and not self.transport.is_closing():
+ message = cast(WebSocketCloseEvent, message)
+ code = message.get("code", 1000)
+ reason = message.get("reason", "") or ""
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
+ self.conn.send_close(code, reason)
+ output = self.conn.data_to_send()
+ self.transport.write(b"".join(output))
+ self.close_sent = True
+ self.transport.close()
+ else:
+ msg = "Expected ASGI message 'websocket.send' or 'websocket.close'," " but got '%s'."
+ raise RuntimeError(msg % message_type)
+ except InvalidState:
+ raise ClientDisconnected()
+ elif self.initial_response is not None:
+ if message_type == "websocket.http.response.body":
+ message = cast(WebSocketResponseBodyEvent, message)
+ body = self.initial_response[2] + message["body"]
+ self.initial_response = self.initial_response[:2] + (body,)
+ if not message.get("more_body", False):
+ response = self.conn.reject(self.initial_response[0], body.decode())
+ response.headers.update(self.initial_response[1])
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
+ self.conn.send_response(response)
+ output = self.conn.data_to_send()
+ self.close_sent = True
+ self.transport.write(b"".join(output))
+ self.transport.close()
+ else:
+ msg = "Expected ASGI message 'websocket.http.response.body' " "but got '%s'."
+ raise RuntimeError(msg % message_type)
+
+ else:
+ msg = "Unexpected ASGI message '%s', after sending 'websocket.close'."
+ raise RuntimeError(msg % message_type)
+
+ async def receive(self) -> ASGIReceiveEvent:
+ message = await self.queue.get()
+ if self.read_paused and self.queue.empty():
+ self.read_paused = False
+ self.transport.resume_reading()
+ return message
diff --git a/uvicorn/server.py b/uvicorn/server.py
index cca2e85..50c5ed2 100644
--- a/uvicorn/server.py
+++ b/uvicorn/server.py
@@ -23,9 +23,10 @@ if TYPE_CHECKING:
from uvicorn.protocols.http.h11_impl import H11Protocol
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
+ from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
- Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol]
+ Protocols = Union[H11Protocol, HttpToolsProtocol, WSProtocol, WebSocketProtocol, WebSocketsSansIOProtocol]
HANDLED_SIGNALS = (
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.

View file

@ -0,0 +1,567 @@
diff --git a/requirements.txt b/requirements.txt
index e26e6b3..b16569f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@ h11 @ git+https://github.com/python-hyper/h11.git@master
# Explicit optionals
a2wsgi==1.10.7
wsproto==1.2.0
-websockets==13.1
+websockets==14.1
# Packaging
build==1.2.2.post1
diff --git a/tests/middleware/test_logging.py b/tests/middleware/test_logging.py
index 63d7daf..5aef174 100644
--- a/tests/middleware/test_logging.py
+++ b/tests/middleware/test_logging.py
@@ -8,8 +8,7 @@ import typing
import httpx
import pytest
-import websockets
-import websockets.client
+from websockets.asyncio.client import connect
from tests.utils import run_server
from uvicorn import Config
@@ -107,8 +106,8 @@ async def test_trace_logging_on_ws_protocol(
break
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.open
+ async with connect(url):
+ return True
config = Config(
app=websocket_app,
diff --git a/tests/middleware/test_proxy_headers.py b/tests/middleware/test_proxy_headers.py
index d300c45..4b5f195 100644
--- a/tests/middleware/test_proxy_headers.py
+++ b/tests/middleware/test_proxy_headers.py
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
import httpx
import httpx._transports.asgi
import pytest
-import websockets.client
+from websockets.asyncio.client import connect
from tests.response import Response
from tests.utils import run_server
@@ -479,7 +479,7 @@ async def test_proxy_headers_websocket_x_forwarded_proto(
async with run_server(config):
url = f"ws://127.0.0.1:{unused_tcp_port}"
headers = {X_FORWARDED_FOR: "1.2.3.4", X_FORWARDED_PROTO: forwarded_proto}
- async with websockets.client.connect(url, extra_headers=headers) as websocket:
+ async with connect(url, additional_headers=headers) as websocket:
data = await websocket.recv()
assert data == expected
diff --git a/tests/protocols/test_websocket.py b/tests/protocols/test_websocket.py
index e728544..b9035ec 100644
--- a/tests/protocols/test_websocket.py
+++ b/tests/protocols/test_websocket.py
@@ -12,6 +12,8 @@ import websockets.asyncio.client
import websockets.client
import websockets.exceptions
from typing_extensions import TypedDict
+from websockets.asyncio.client import ClientConnection, connect
+from websockets.exceptions import ConnectionClosed, ConnectionClosedError, InvalidHandshake, InvalidStatus
from websockets.extensions.permessage_deflate import ClientPerMessageDeflateFactory
from websockets.typing import Subprotocol
@@ -130,8 +132,8 @@ async def test_accept_connection(ws_protocol_cls: WSProtocol, http_protocol_cls:
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.open
+ async with connect(url):
+ return True
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -146,7 +148,7 @@ async def test_shutdown(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProt
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config) as server:
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}"):
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}"):
# Attempt shutdown while connection is still open
await server.shutdown()
@@ -160,8 +162,8 @@ async def test_supports_permessage_deflate_extension(
async def open_connection(url: str):
extension_factories = [ClientPerMessageDeflateFactory()]
- async with websockets.client.connect(url, extensions=extension_factories) as websocket:
- return [extension.name for extension in websocket.extensions]
+ async with connect(url, extensions=extension_factories) as websocket:
+ return [extension.name for extension in websocket.protocol.extensions]
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -180,8 +182,8 @@ async def test_can_disable_permessage_deflate_extension(
# enable per-message deflate on the client, so that we can check the server
# won't support it when it's disabled.
extension_factories = [ClientPerMessageDeflateFactory()]
- async with websockets.client.connect(url, extensions=extension_factories) as websocket:
- return [extension.name for extension in websocket.extensions]
+ async with connect(url, extensions=extension_factories) as websocket:
+ return [extension.name for extension in websocket.protocol.extensions]
config = Config(
app=App,
@@ -203,8 +205,8 @@ async def test_close_connection(ws_protocol_cls: WSProtocol, http_protocol_cls:
async def open_connection(url: str):
try:
- await websockets.client.connect(url)
- except websockets.exceptions.InvalidHandshake:
+ await connect(url)
+ except InvalidHandshake:
return False
return True # pragma: no cover
@@ -224,8 +226,8 @@ async def test_headers(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProto
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url, extra_headers=[("username", "abraão")]) as websocket:
- return websocket.open
+ async with connect(url, additional_headers=[("username", "abraão")]):
+ return True
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -239,8 +241,9 @@ async def test_extra_headers(ws_protocol_cls: WSProtocol, http_protocol_cls: HTT
await self.send({"type": "websocket.accept", "headers": [(b"extra", b"header")]})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.response_headers
+ async with connect(url) as websocket:
+ assert websocket.response
+ return websocket.response.headers
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -258,8 +261,8 @@ async def test_path_and_raw_path(ws_protocol_cls: WSProtocol, http_protocol_cls:
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.open
+ async with connect(url):
+ return True
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -276,7 +279,7 @@ async def test_send_text_data_to_client(
await self.send({"type": "websocket.send", "text": "123"})
async def get_data(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
return await websocket.recv()
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
@@ -294,7 +297,7 @@ async def test_send_binary_data_to_client(
await self.send({"type": "websocket.send", "bytes": b"123"})
async def get_data(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
return await websocket.recv()
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
@@ -313,7 +316,7 @@ async def test_send_and_close_connection(
await self.send({"type": "websocket.close"})
async def get_data(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
data = await websocket.recv()
is_open = True
try:
@@ -342,7 +345,7 @@ async def test_send_text_data_to_server(
await self.send({"type": "websocket.send", "text": _text})
async def send_text(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
await websocket.send("abc")
return await websocket.recv()
@@ -365,7 +368,7 @@ async def test_send_binary_data_to_server(
await self.send({"type": "websocket.send", "bytes": _bytes})
async def send_text(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
await websocket.send(b"abc")
return await websocket.recv()
@@ -387,7 +390,7 @@ async def test_send_after_protocol_close(
await self.send({"type": "websocket.send", "text": "123"})
async def get_data(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
data = await websocket.recv()
is_open = True
try:
@@ -407,14 +410,14 @@ async def test_missing_handshake(ws_protocol_cls: WSProtocol, http_protocol_cls:
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
pass
- async def connect(url: str):
- await websockets.client.connect(url)
+ async def open_connection(url: str):
+ await connect(url)
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info:
- await connect(f"ws://127.0.0.1:{unused_tcp_port}")
- assert exc_info.value.status_code == 500
+ with pytest.raises(InvalidStatus) as exc_info:
+ await open_connection(f"ws://127.0.0.1:{unused_tcp_port}")
+ assert exc_info.value.response.status_code == 500
async def test_send_before_handshake(
@@ -423,14 +426,14 @@ async def test_send_before_handshake(
async def app(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
await send({"type": "websocket.send", "text": "123"})
- async def connect(url: str):
- await websockets.client.connect(url)
+ async def open_connection(url: str):
+ await connect(url)
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info:
- await connect(f"ws://127.0.0.1:{unused_tcp_port}")
- assert exc_info.value.status_code == 500
+ with pytest.raises(InvalidStatus) as exc_info:
+ await open_connection(f"ws://127.0.0.1:{unused_tcp_port}")
+ assert exc_info.value.response.status_code == 500
async def test_duplicate_handshake(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
@@ -440,10 +443,10 @@ async def test_duplicate_handshake(ws_protocol_cls: WSProtocol, http_protocol_cl
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
- with pytest.raises(websockets.exceptions.ConnectionClosed):
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
+ with pytest.raises(ConnectionClosed):
_ = await websocket.recv()
- assert websocket.close_code == 1006
+ assert websocket.protocol.close_code == 1006
async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
@@ -458,10 +461,10 @@ async def test_asgi_return_value(ws_protocol_cls: WSProtocol, http_protocol_cls:
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
- with pytest.raises(websockets.exceptions.ConnectionClosed):
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
+ with pytest.raises(ConnectionClosed):
_ = await websocket.recv()
- assert websocket.close_code == 1006
+ assert websocket.protocol.close_code == 1006
@pytest.mark.parametrize("code", [None, 1000, 1001])
@@ -493,13 +496,13 @@ async def test_app_close(
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
await websocket.ping()
await websocket.send("abc")
- with pytest.raises(websockets.exceptions.ConnectionClosed):
+ with pytest.raises(ConnectionClosed):
await websocket.recv()
- assert websocket.close_code == (code or 1000)
- assert websocket.close_reason == (reason or "")
+ assert websocket.protocol.close_code == (code or 1000)
+ assert websocket.protocol.close_reason == (reason or "")
async def test_client_close(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTPProtocol, unused_tcp_port: int):
@@ -518,7 +521,7 @@ async def test_client_close(ws_protocol_cls: WSProtocol, http_protocol_cls: HTTP
break
async def websocket_session(url: str):
- async with websockets.client.connect(url) as websocket:
+ async with connect(url) as websocket:
await websocket.ping()
await websocket.send("abc")
await websocket.close(code=1001, reason="custom reason")
@@ -555,7 +558,7 @@ async def test_client_connection_lost(
port=unused_tcp_port,
)
async with run_server(config):
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
websocket.transport.close()
await asyncio.sleep(0.1)
got_disconnect_event_before_shutdown = got_disconnect_event
@@ -583,7 +586,7 @@ async def test_client_connection_lost_on_send(
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
url = f"ws://127.0.0.1:{unused_tcp_port}"
- async with websockets.client.connect(url):
+ async with connect(url):
await asyncio.sleep(0.1)
disconnect.set()
@@ -642,11 +645,11 @@ async def test_send_close_on_server_shutdown(
disconnect_message = message
break
- websocket: websockets.client.WebSocketClientProtocol | None = None
+ websocket: ClientConnection | None = None
async def websocket_session(uri: str):
nonlocal websocket
- async with websockets.client.connect(uri) as ws_connection:
+ async with connect(uri) as ws_connection:
websocket = ws_connection
await server_shutdown_event.wait()
@@ -676,9 +679,7 @@ async def test_subprotocols(
await self.send({"type": "websocket.accept", "subprotocol": subprotocol})
async def get_subprotocol(url: str):
- async with websockets.client.connect(
- url, subprotocols=[Subprotocol("proto1"), Subprotocol("proto2")]
- ) as websocket:
+ async with connect(url, subprotocols=[Subprotocol("proto1"), Subprotocol("proto2")]) as websocket:
return websocket.subprotocol
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
@@ -688,7 +689,7 @@ async def test_subprotocols(
MAX_WS_BYTES = 1024 * 1024 * 16
-MAX_WS_BYTES_PLUS1 = MAX_WS_BYTES + 1
+MAX_WS_BYTES_PLUS1 = MAX_WS_BYTES + 10
@pytest.mark.parametrize(
@@ -731,15 +732,15 @@ async def test_send_binary_data_to_server_bigger_than_default_on_websockets(
port=unused_tcp_port,
)
async with run_server(config):
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}", max_size=client_size_sent) as ws:
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}", max_size=client_size_sent) as ws:
await ws.send(b"\x01" * client_size_sent)
if expected_result == 0:
data = await ws.recv()
assert data == b"\x01" * client_size_sent
else:
- with pytest.raises(websockets.exceptions.ConnectionClosedError):
+ with pytest.raises(ConnectionClosedError):
await ws.recv()
- assert ws.close_code == expected_result
+ assert ws.protocol.close_code == expected_result
async def test_server_reject_connection(
@@ -764,10 +765,10 @@ async def test_server_reject_connection(
disconnected_message = await receive()
async def websocket_session(url: str):
- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info:
- async with websockets.client.connect(url):
+ with pytest.raises(InvalidStatus) as exc_info:
+ async with connect(url):
pass # pragma: no cover
- assert exc_info.value.status_code == 403
+ assert exc_info.value.response.status_code == 403
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -937,10 +938,10 @@ async def test_server_reject_connection_with_invalid_msg(
await send(message)
async def websocket_session(url: str):
- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info:
- async with websockets.client.connect(url):
+ with pytest.raises(InvalidStatus) as exc_info:
+ async with connect(url):
pass # pragma: no cover
- assert exc_info.value.status_code == 404
+ assert exc_info.value.response.status_code == 404
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -971,10 +972,10 @@ async def test_server_reject_connection_with_missing_body(
# no further message
async def websocket_session(url: str):
- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info:
- async with websockets.client.connect(url):
+ with pytest.raises(InvalidStatus) as exc_info:
+ async with connect(url):
pass # pragma: no cover
- assert exc_info.value.status_code == 404
+ assert exc_info.value.response.status_code == 404
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -1014,17 +1015,17 @@ async def test_server_multiple_websocket_http_response_start_events(
exception_message = str(exc)
async def websocket_session(url: str):
- with pytest.raises(websockets.exceptions.InvalidStatusCode) as exc_info:
- async with websockets.client.connect(url):
+ with pytest.raises(InvalidStatus) as exc_info:
+ async with connect(url):
pass # pragma: no cover
- assert exc_info.value.status_code == 404
+ assert exc_info.value.response.status_code == 404
config = Config(app=app, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
await websocket_session(f"ws://127.0.0.1:{unused_tcp_port}")
assert exception_message == (
- "Expected ASGI message 'websocket.http.response.body' but got " "'websocket.http.response.start'."
+ "Expected ASGI message 'websocket.http.response.body' but got 'websocket.http.response.start'."
)
@@ -1053,7 +1054,7 @@ async def test_server_can_read_messages_in_buffer_after_close(
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
- async with websockets.client.connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
+ async with connect(f"ws://127.0.0.1:{unused_tcp_port}") as websocket:
await websocket.send(b"abc")
await websocket.send(b"abc")
await websocket.send(b"abc")
@@ -1070,8 +1071,9 @@ async def test_default_server_headers(
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.response_headers
+ async with connect(url) as websocket:
+ assert websocket.response
+ return websocket.response.headers
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -1085,8 +1087,9 @@ async def test_no_server_headers(ws_protocol_cls: WSProtocol, http_protocol_cls:
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.response_headers
+ async with connect(url) as websocket:
+ assert websocket.response
+ return websocket.response.headers
config = Config(
app=App,
@@ -1108,8 +1111,9 @@ async def test_no_date_header_on_wsproto(http_protocol_cls: HTTPProtocol, unused
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.response_headers
+ async with connect(url) as websocket:
+ assert websocket.response
+ return websocket.response.headers
config = Config(
app=App,
@@ -1140,8 +1144,9 @@ async def test_multiple_server_header(
)
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.response_headers
+ async with connect(url) as websocket:
+ assert websocket.response
+ return websocket.response.headers
config = Config(app=App, ws=ws_protocol_cls, http=http_protocol_cls, lifespan="off", port=unused_tcp_port)
async with run_server(config):
@@ -1176,8 +1181,8 @@ async def test_lifespan_state(ws_protocol_cls: WSProtocol, http_protocol_cls: HT
await self.send({"type": "websocket.accept"})
async def open_connection(url: str):
- async with websockets.client.connect(url) as websocket:
- return websocket.open
+ async with connect(url):
+ return True
async def app_wrapper(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable):
if scope["type"] == "lifespan":
diff --git a/uvicorn/protocols/websockets/websockets_impl.py b/uvicorn/protocols/websockets/websockets_impl.py
index cd6c54f..685d6b6 100644
--- a/uvicorn/protocols/websockets/websockets_impl.py
+++ b/uvicorn/protocols/websockets/websockets_impl.py
@@ -13,8 +13,7 @@ from websockets.datastructures import Headers
from websockets.exceptions import ConnectionClosed
from websockets.extensions.base import ServerExtensionFactory
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
-from websockets.legacy.server import HTTPResponse
-from websockets.server import WebSocketServerProtocol
+from websockets.legacy.server import HTTPResponse, WebSocketServerProtocol
from websockets.typing import Subprotocol
from uvicorn._types import (
diff --git a/uvicorn/protocols/websockets/wsproto_impl.py b/uvicorn/protocols/websockets/wsproto_impl.py
index 828afe5..5d84bff 100644
--- a/uvicorn/protocols/websockets/wsproto_impl.py
+++ b/uvicorn/protocols/websockets/wsproto_impl.py
@@ -149,12 +149,13 @@ class WSProtocol(asyncio.Protocol):
self.writable.set() # pragma: full coverage
def shutdown(self) -> None:
- if self.handshake_complete:
- self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
- output = self.conn.send(wsproto.events.CloseConnection(code=1012))
- self.transport.write(output)
- else:
- self.send_500_response()
+ if not self.response_started:
+ if self.handshake_complete:
+ self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
+ output = self.conn.send(wsproto.events.CloseConnection(code=1012))
+ self.transport.write(output)
+ else:
+ self.send_500_response()
self.transport.close()
def on_task_complete(self, task: asyncio.Task[None]) -> None:
@@ -221,13 +222,15 @@ class WSProtocol(asyncio.Protocol):
def send_500_response(self) -> None:
if self.response_started or self.handshake_complete:
return # we cannot send responses anymore
+ reject_data = b"Internal Server Error"
headers: list[tuple[bytes, bytes]] = [
(b"content-type", b"text/plain; charset=utf-8"),
+ (b"content-length", str(len(reject_data)).encode()),
(b"connection", b"close"),
(b"content-length", b"21"),
]
output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True))
- output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error"))
+ output += self.conn.send(wsproto.events.RejectData(data=reject_data))
self.transport.write(output)
async def run_asgi(self) -> None:

59
ilot/uvicorn/APKBUILD Normal file
View file

@ -0,0 +1,59 @@
maintainer="Michał Polański <michal@polanski.me>"
pkgname=uvicorn
pkgver=0.34.0
pkgrel=0
pkgdesc="Lightning-fast ASGI server"
url="https://www.uvicorn.org/"
license="BSD-3-Clause"
# disable due to lack of support for websockets 14
# https://gitlab.alpinelinux.org/alpine/aports/-/issues/16646
arch="noarch"
depends="py3-click py3-h11"
makedepends="py3-gpep517 py3-hatchling"
checkdepends="
py3-a2wsgi
py3-dotenv
py3-httptools
py3-httpx
py3-pytest
py3-pytest-mock
py3-trustme
py3-typing-extensions
py3-watchfiles
py3-websockets
py3-wsproto
py3-yaml
"
subpackages="$pkgname-pyc"
source="https://github.com/encode/uvicorn/archive/$pkgver/uvicorn-$pkgver.tar.gz
test_multiprocess.patch
2540_add-websocketssansioprotocol.patch
2541_bump-wesockets-on-requirements.patch
fix-test-wsgi.patch
"
build() {
gpep517 build-wheel \
--wheel-dir .dist \
--output-fd 3 3>&1 >&2
}
check() {
python3 -m venv --clear --without-pip --system-site-packages .testenv
.testenv/bin/python3 -m installer .dist/*.whl
.testenv/bin/python3 -m pytest \
-k "not test_close_connection_with_multiple_requests" # a known issue
}
package() {
python3 -m installer -d "$pkgdir" \
.dist/uvicorn-$pkgver-py3-none-any.whl
}
sha512sums="
260782e385a2934049da8c474750958826afe1bfe23b38fe2f6420f355af7a537563f8fe6ac3830814c7469203703d10f4f9f3d6e53e79113bfd2fd34f7a7c72 uvicorn-0.34.0.tar.gz
cfad91dd84f8974362f52d754d7a29f09d07927a46acaa0eb490b6115a5729d84d6df94fead10ccd4cce7f5ea376f1348b0f59daede661dd8373a3851c313c46 test_multiprocess.patch
858e9a7baaf1c12e076aecd81aaaf622b35a59dcaabea4ee1bfc4cda704c9fe271b1cc616a5910d845393717e4989cecb3b04be249cb5d0df1001ec5224c293f 2540_add-websocketssansioprotocol.patch
f8a8c190981b9070232ea985880685bc801947cc7f673d59abf73d3e68bc2e13515ad200232a1de2af0808bc85da48a341f57d47caf87bcc190bfdc3c45718e0 2541_bump-wesockets-on-requirements.patch
379963f9ccbda013e4a0bc3441eee70a581c91f60206aedc15df6a8737950824b7cb8d867774fc415763449bb3e0bba66601e8551101bfc1741098acd035f0cc fix-test-wsgi.patch
"

View file

@ -0,0 +1,13 @@
diff --git a/tests/middleware/test_wsgi.py.orig b/tests/middleware/test_wsgi.py
index 6003f27..2750487 100644
--- a/tests/middleware/test_wsgi.py.orig
+++ b/tests/middleware/test_wsgi.py
@@ -73,7 +73,7 @@ async def test_wsgi_post(wsgi_middleware: Callable) -> None:
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
response = await client.post("/", json={"example": 123})
assert response.status_code == 200
- assert response.text == '{"example":123}'
+ assert response.text == '{"example": 123}'
@pytest.mark.anyio

View file

@ -0,0 +1,14 @@
Wait a bit longer, otherwise the workers might
not have time to finish restarting.
--- a/tests/supervisors/test_multiprocess.py
+++ b/tests/supervisors/test_multiprocess.py
@@ -132,7 +132,7 @@ def test_multiprocess_sighup() -> None:
time.sleep(1)
pids = [p.pid for p in supervisor.processes]
supervisor.signal_queue.append(signal.SIGHUP)
- time.sleep(1)
+ time.sleep(3)
assert pids != [p.pid for p in supervisor.processes]
supervisor.signal_queue.append(signal.SIGINT)
supervisor.join_all()

View file

@ -49,6 +49,9 @@ package() {
install -Dm644 "$builddir"/package.json -t "$pkgdir"/usr/lib/bundles/wikijs install -Dm644 "$builddir"/package.json -t "$pkgdir"/usr/lib/bundles/wikijs
cp -aR "$builddir"/assets "$builddir"/server "$builddir"/node_modules "$pkgdir"/usr/lib/bundles/wikijs cp -aR "$builddir"/assets "$builddir"/server "$builddir"/node_modules "$pkgdir"/usr/lib/bundles/wikijs
# remove prebuilts
rm -Rf "$pkgdir"/usr/lib/bundles/wikijs/node_modules/*/prebuilds
mkdir -p "$pkgdir"/var/lib/wikijs mkdir -p "$pkgdir"/var/lib/wikijs
chown 5494:5494 "$pkgdir"/var/lib/wikijs chown 5494:5494 "$pkgdir"/var/lib/wikijs
} }