PoorWSGI 2.6.1__tar.gz → 2.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/PKG-INFO +1 -1
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/PoorWSGI.egg-info/PKG-INFO +1 -1
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/PoorWSGI.egg-info/SOURCES.txt +1 -4
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/ChangeLog +23 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_footer.html +1 -1
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_poorwsgi_api.html +3 -1
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/documentation.rst +99 -55
- poorwsgi-2.7.0/examples/http_digest.py +200 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/large_file.py +22 -15
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/metrics.py +1 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/openapi3.py +2 -2
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/put_file.py +6 -4
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/simple.py +196 -134
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/simple_json.py +15 -3
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/websocket.py +10 -2
- poorwsgi-2.7.0/poorwsgi/fieldstorage.py +790 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/headers.py +39 -1
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/openapi_wrapper.py +1 -1
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/request.py +129 -294
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/response.py +108 -14
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/results.py +44 -36
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/session.py +1 -3
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/state.py +2 -2
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/wsgi.py +7 -8
- poorwsgi-2.7.0/setup.py +206 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/tests/test_digest.py +17 -8
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/tests/test_header.py +4 -2
- poorwsgi-2.7.0/tests/test_request.py +353 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/tests/test_responses.py +61 -9
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/tests/test_session.py +4 -0
- PoorWSGI-2.6.1/examples/async.py +0 -36
- PoorWSGI-2.6.1/examples/encoding.py +0 -54
- PoorWSGI-2.6.1/examples/http_digest.py +0 -237
- PoorWSGI-2.6.1/setup.py +0 -229
- PoorWSGI-2.6.1/test_hidden.py +0 -58
- PoorWSGI-2.6.1/tests/test_request.py +0 -210
- PoorWSGI-2.6.1/websockets_chat_async.py +0 -95
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/CONTRIBUTION.rst +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/MANIFEST.in +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/PoorWSGI.egg-info/dependency_links.txt +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/PoorWSGI.egg-info/requires.txt +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/PoorWSGI.egg-info/top_level.txt +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/README.rst +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_documentation.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_header.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_install.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_licence.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_poorwsgi.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_reference.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/_text.html +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/about.rst +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/install.rst +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/licence.txt +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/small-logo.png +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/style.css +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/doc/web.css +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/openapi.json +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/examples/test.digest +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/__init__.py +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/digest.py +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/poorwsgi/py.typed +0 -0
- {PoorWSGI-2.6.1 → poorwsgi-2.7.0}/setup.cfg +0 -0
|
@@ -2,8 +2,6 @@ CONTRIBUTION.rst
|
|
|
2
2
|
MANIFEST.in
|
|
3
3
|
README.rst
|
|
4
4
|
setup.py
|
|
5
|
-
test_hidden.py
|
|
6
|
-
websockets_chat_async.py
|
|
7
5
|
PoorWSGI.egg-info/PKG-INFO
|
|
8
6
|
PoorWSGI.egg-info/SOURCES.txt
|
|
9
7
|
PoorWSGI.egg-info/dependency_links.txt
|
|
@@ -26,8 +24,6 @@ doc/licence.txt
|
|
|
26
24
|
doc/small-logo.png
|
|
27
25
|
doc/style.css
|
|
28
26
|
doc/web.css
|
|
29
|
-
examples/async.py
|
|
30
|
-
examples/encoding.py
|
|
31
27
|
examples/http_digest.py
|
|
32
28
|
examples/large_file.py
|
|
33
29
|
examples/metrics.py
|
|
@@ -40,6 +36,7 @@ examples/test.digest
|
|
|
40
36
|
examples/websocket.py
|
|
41
37
|
poorwsgi/__init__.py
|
|
42
38
|
poorwsgi/digest.py
|
|
39
|
+
poorwsgi/fieldstorage.py
|
|
43
40
|
poorwsgi/headers.py
|
|
44
41
|
poorwsgi/openapi_wrapper.py
|
|
45
42
|
poorwsgi/py.typed
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
==== 2.7.0 ====
|
|
2
|
+
* Reserved Request.db attribute for usage
|
|
3
|
+
* Right HTTPException type annotation
|
|
4
|
+
* Fix ETag header
|
|
5
|
+
* FieldStorage and FieldStorageParser implementation based on legacy
|
|
6
|
+
cgi.FieldStorage (#22)
|
|
7
|
+
- that means Python 3.13 support (#36)
|
|
8
|
+
* Stop Python 3.8 support - some mypy annotation didn't work
|
|
9
|
+
* Field refactoring - ! possible break change ! - sorry for that
|
|
10
|
+
- FieldStorageInterface - getvalue, getfist, getlist methods
|
|
11
|
+
- new func converter name, and no default converter
|
|
12
|
+
- all default values are not convert now
|
|
13
|
+
* Some old classes, functions, attributes and parameters are notified
|
|
14
|
+
as deprecated, and will be deleted in next major version.
|
|
15
|
+
* Automatic JSONResponse from dict or list, just like from text.
|
|
16
|
+
* Debug info, documentation and Error improvements.
|
|
17
|
+
* Automatic NoContentResponse from None (#28)
|
|
18
|
+
|
|
19
|
+
==== 2.6.3 ====
|
|
20
|
+
* HTTPException has status_code property
|
|
21
|
+
* Implicit value for not set cookie is None or empty dict for OpenAPI
|
|
22
|
+
* Fix Content-Length header when partial Response
|
|
23
|
+
|
|
1
24
|
==== 2.6.1 ====
|
|
2
25
|
* Fix OpenAPI Core wrappers
|
|
3
26
|
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
{% set api = api + load_module('wsgi') %}
|
|
5
5
|
{% set api = api + load_module('request') %}
|
|
6
6
|
{% set api = api + load_module('response') %}
|
|
7
|
-
{% set api = api + load_module('
|
|
7
|
+
{% set api = api + load_module('headers') %}
|
|
8
|
+
{% set api = api + load_module('fieldstorage') %}
|
|
8
9
|
{% set api = api + load_module('session') %}
|
|
9
10
|
{% set api = api + load_module('digest') %}
|
|
11
|
+
{% set api = api + load_module('results') %}
|
|
10
12
|
{% set api = api + load_module('state') %}
|
|
11
13
|
{% set api = api + load_module('openapi_wrapper') %}
|
|
12
14
|
|
|
@@ -106,8 +106,15 @@ make_response function.
|
|
|
106
106
|
headers=None, status_code=HTTP_OK)
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
data : str, bytes, generator
|
|
110
|
-
Returned value as response body.
|
|
109
|
+
data : str, bytes, dict, list, None or generator
|
|
110
|
+
Returned value as response body. Each type of data returns different
|
|
111
|
+
response type:
|
|
112
|
+
|
|
113
|
+
- str, bytes - Response
|
|
114
|
+
- dict, list - JSONResponse
|
|
115
|
+
- None - NoContentReponse
|
|
116
|
+
- generator - GeneratorResponse
|
|
117
|
+
|
|
111
118
|
content_type : str
|
|
112
119
|
The ``Content-Type`` header which is set, if this header is not set
|
|
113
120
|
in headers.
|
|
@@ -122,11 +129,28 @@ You can use headers instead of `content_type` argument.
|
|
|
122
129
|
.. code:: python
|
|
123
130
|
|
|
124
131
|
@app.http_state(NOT_FOUND)
|
|
125
|
-
def not_found(req):
|
|
132
|
+
def not_found(req, *_):
|
|
126
133
|
return make_response(b'Page not Found',
|
|
127
134
|
headers={"Content-Type": "text/plain"},
|
|
128
135
|
status_code=NOT_FOUND)
|
|
129
136
|
|
|
137
|
+
If you return just simple type, or tuple of arguments, PoorWSGI automatically
|
|
138
|
+
call make_response function to create response for you.
|
|
139
|
+
|
|
140
|
+
.. code:: python
|
|
141
|
+
|
|
142
|
+
@app.route("/json")
|
|
143
|
+
def not_found(req):
|
|
144
|
+
"""Return JSONResponse"""
|
|
145
|
+
return {"msg": "Message", "type": "object"}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.route('/gone')
|
|
149
|
+
def gone(req):
|
|
150
|
+
"""Return NoContentResponse"""
|
|
151
|
+
return None, "", None, HTTP_GONE
|
|
152
|
+
|
|
153
|
+
|
|
130
154
|
Response
|
|
131
155
|
````````
|
|
132
156
|
Response object is one of base element of WSGI application. Response is object
|
|
@@ -167,6 +191,27 @@ This response returned these data with status code 418:
|
|
|
167
191
|
"numbers": [0, 1, 2, 3, 4]
|
|
168
192
|
}
|
|
169
193
|
|
|
194
|
+
Or you can simple return dictionary or list. It will be automatically convert
|
|
195
|
+
to JSONResponse by make_response function. So it is similar to return text or
|
|
196
|
+
bytes.
|
|
197
|
+
|
|
198
|
+
Be careful that your dict or list **have to be convertible** to JSON by
|
|
199
|
+
json.dumps function.
|
|
200
|
+
|
|
201
|
+
.. code:: python
|
|
202
|
+
|
|
203
|
+
@app.route('/dict')
|
|
204
|
+
def get_dict(_):
|
|
205
|
+
"""Return dictionary"""
|
|
206
|
+
return {"route": "/dict", "type": "dict"}
|
|
207
|
+
|
|
208
|
+
@app.route('/list')
|
|
209
|
+
def get_list(_):
|
|
210
|
+
"""Return list"""
|
|
211
|
+
return [["key", "value"], ["route", "/list"], ["type", "list"]]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
170
215
|
JSONGeneratorResponse
|
|
171
216
|
`````````````````````
|
|
172
217
|
There is JSONGeneratorResponse class too, which could return JSON, but
|
|
@@ -249,7 +294,7 @@ parameters or as constructor argument.
|
|
|
249
294
|
return NotModifiedResponse(date=time_to_http())
|
|
250
295
|
|
|
251
296
|
return FileResponse(req.document_root+"/filename",
|
|
252
|
-
headers={'
|
|
297
|
+
headers={'ETag': etag})
|
|
253
298
|
|
|
254
299
|
Partial Content
|
|
255
300
|
```````````````
|
|
@@ -501,18 +546,20 @@ or METHOD_HEAD.
|
|
|
501
546
|
|
|
502
547
|
HTTP state handlers
|
|
503
548
|
~~~~~~~~~~~~~~~~~~~
|
|
504
|
-
There are some predefined HTTP state handlers, which
|
|
549
|
+
There are some predefined HTTP state handlers, which are use when other
|
|
505
550
|
HTTP state are raised via HTTPException or any other exception which ends with
|
|
506
551
|
HTTP_INTERNAL_SERVER_ERROR status code.
|
|
507
552
|
|
|
508
553
|
You can redefined your own handlers for any combination of status code and
|
|
509
|
-
method type like routes handlers.
|
|
554
|
+
method type like routes handlers. Response from these handlers are same as in
|
|
510
555
|
route handlers.
|
|
511
556
|
|
|
557
|
+
Be sure, that some http_state handlers can add other keyword arguments.
|
|
558
|
+
|
|
512
559
|
.. code:: python
|
|
513
560
|
|
|
514
561
|
@app.http_state(state.HTTP_NOT_FOUND)
|
|
515
|
-
def page_not_found(req):
|
|
562
|
+
def page_not_found(req, *_):
|
|
516
563
|
return "Your request %s not found." % req.path, "text/plain"
|
|
517
564
|
|
|
518
565
|
If your http state (error) handler was crashed with error, internal server
|
|
@@ -617,7 +664,7 @@ poor_AutoArgs is set to On, which is default.
|
|
|
617
664
|
@app.route('/test/get')
|
|
618
665
|
def test_get(req)
|
|
619
666
|
name = req.args.getfirst('name')
|
|
620
|
-
colors = req.args.getlist('color',
|
|
667
|
+
colors = req.args.getlist('color', func=int)
|
|
621
668
|
return "Get arguments are %s" % str(req.args)
|
|
622
669
|
|
|
623
670
|
If no arguments are parsed, or if poor_AutoArgs is set to Off, req.args is
|
|
@@ -625,16 +672,16 @@ EmptyForm instance, which is dict base class too with both of methods.
|
|
|
625
672
|
|
|
626
673
|
Form arguments
|
|
627
674
|
~~~~~~~~~~~~~~
|
|
628
|
-
Request form
|
|
629
|
-
poorwsgi.
|
|
630
|
-
cgi module.
|
|
631
|
-
On, which is default, request method is POST, PUT or PATCH and request
|
|
675
|
+
Request form arguments are stored in FieldStorage class, define in
|
|
676
|
+
poorwsgi.fieldstorage module. This class is inspired by FieldStorage from
|
|
677
|
+
legacy cgi module. Variables are parsed every time, when poor_AutoForm is set
|
|
678
|
+
to On, which is default, request method is POST, PUT or PATCH and request
|
|
632
679
|
mime type is one of `Application.form_mime_types`. You can call it
|
|
633
680
|
on any other methods of course, but it must exist wsgi.input in request
|
|
634
681
|
environment from wsgi server.
|
|
635
682
|
|
|
636
683
|
req.form instance is create with poor_KeepBlankValues and poor_StrictParsing
|
|
637
|
-
variables as Args class is create, but
|
|
684
|
+
variables as Args class is create, but FieldStorageParser have file_callback
|
|
638
685
|
variable, which is configurable by Application.file_callback property.
|
|
639
686
|
|
|
640
687
|
.. code:: python
|
|
@@ -645,7 +692,7 @@ variable, which is configurable by Application.file_callback property.
|
|
|
645
692
|
# is convert to number with zero
|
|
646
693
|
# as default
|
|
647
694
|
name = req.form.getfirst('name')
|
|
648
|
-
colors = req.form.getlist('color',
|
|
695
|
+
colors = req.form.getlist('color', func=int)
|
|
649
696
|
return "Post arguments for id are %s" % (id, str(req.args))
|
|
650
697
|
|
|
651
698
|
As like Args class, if poor_AutoForm is set to Off, or if method is no POST,
|
|
@@ -705,14 +752,13 @@ There are a few variants which req.json could be:
|
|
|
705
752
|
|
|
706
753
|
File uploading
|
|
707
754
|
~~~~~~~~~~~~~~
|
|
708
|
-
By default,
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
but.
|
|
755
|
+
By default, FieldStorage store files somewhere to ``/tmp`` directory. This is
|
|
756
|
+
happened in FieldStorageParser, which calls ``TemporaryFile``. Uploaded files
|
|
757
|
+
are accessible like another form variables, but.
|
|
712
758
|
|
|
713
759
|
Any variables from FieldStorage is accessible with ``__getitem__`` method.
|
|
714
760
|
So you can get variable by ``req.form[key]``, which gets FieldStorage
|
|
715
|
-
instance. This instance has some
|
|
761
|
+
instance. This instance has some attributes, which you can test,
|
|
716
762
|
what type of variable is it.
|
|
717
763
|
|
|
718
764
|
.. code:: python
|
|
@@ -742,16 +788,16 @@ Application.file_callback property.
|
|
|
742
788
|
As you can see, this example works, but it is so bad solution of your problem.
|
|
743
789
|
Little bit better solution will be, if you store files only if exist and only
|
|
744
790
|
to special separate dictionary, which could be configurable. That you need use
|
|
745
|
-
factory to create file_callback. In next example is
|
|
746
|
-
which is not important, when `file_callback` could be set via
|
|
747
|
-
property.
|
|
791
|
+
to factory to create file_callback. In next example is written own form
|
|
792
|
+
processing, which is not important, when `file_callback` could be set via
|
|
793
|
+
Application property.
|
|
748
794
|
|
|
749
795
|
.. code:: python
|
|
750
796
|
|
|
751
797
|
from io import FileIO
|
|
752
798
|
from os.path import exists
|
|
753
799
|
|
|
754
|
-
from poorwsgi import Application, state,
|
|
800
|
+
from poorwsgi import Application, state, fieldstorage
|
|
755
801
|
|
|
756
802
|
app = Application('test')
|
|
757
803
|
|
|
@@ -783,19 +829,21 @@ property.
|
|
|
783
829
|
if req.method_number == state.METHOD_POST:
|
|
784
830
|
factory = StorageFactory('./upload')
|
|
785
831
|
try:
|
|
786
|
-
|
|
787
|
-
req,
|
|
832
|
+
parser = FieldStorageParser(
|
|
833
|
+
req.input, req.heades,
|
|
788
834
|
keep_blank_values=app.keep_blank_values,
|
|
789
835
|
strict_parsing=app.strict_parsing,
|
|
790
836
|
file_callback=factory.create)
|
|
837
|
+
req.form = parser.parser()
|
|
791
838
|
except Exception as e:
|
|
792
839
|
req.log_error(e)
|
|
793
840
|
|
|
794
841
|
CachedInput
|
|
795
842
|
~~~~~~~~~~~
|
|
796
843
|
|
|
797
|
-
When HTTP Forms are base64 encoded,
|
|
798
|
-
file. This is not so optimal. So there is CachedInput class, which
|
|
844
|
+
When HTTP Forms are base64 encoded, FieldStorageParser use readline on request
|
|
845
|
+
input file. This is not so optimal. So there is CachedInput class, which
|
|
846
|
+
is returned as wrapper around ``wsgi.input`` file.
|
|
799
847
|
|
|
800
848
|
Proccess variables
|
|
801
849
|
~~~~~~~~~~~~~~~~~~
|
|
@@ -818,14 +866,14 @@ If auto_form is set to ``True``, which is default, Request object parse input
|
|
|
818
866
|
arguments from request body at initialisation when request type is POST, PUT
|
|
819
867
|
or PATCH. There will be ``Request.form`` property which is instance of
|
|
820
868
|
``FieldStorage`` class. If you want to off this functionality, set this property
|
|
821
|
-
to ``False``. If form parsing is disabled, or
|
|
869
|
+
to ``False``. If form parsing is disabled, or JSON is detected, ``Request.form``
|
|
822
870
|
will be instance of ``EmptyForm`` with same interface and no data.
|
|
823
871
|
|
|
824
872
|
Application.form_mime_types
|
|
825
873
|
``````````````````````````````
|
|
826
|
-
List of mime types, which is
|
|
827
|
-
If input request does not have set one of these mime types, that form
|
|
828
|
-
parsed.
|
|
874
|
+
List of mime types, which is parsed as input form by ``FieldStorageParser``
|
|
875
|
+
class. If input request does not have set one of these mime types, that form
|
|
876
|
+
will not be parsed.
|
|
829
877
|
|
|
830
878
|
Application.file_callback
|
|
831
879
|
`````````````````````````
|
|
@@ -848,15 +896,15 @@ If input request does not have set one of these mime types, that
|
|
|
848
896
|
Application.keep_blank_values
|
|
849
897
|
`````````````````````````````
|
|
850
898
|
This property is set for input parameters to automatically calling Args and
|
|
851
|
-
|
|
852
|
-
property is set to ``0``. If it set to ``1``, blank values should be
|
|
853
|
-
as empty strings.
|
|
899
|
+
FieldStorageParser classes, when auto_args resp. auto_form is set. By default
|
|
900
|
+
this property is set to ``0``. If it set to ``1``, blank values should be
|
|
901
|
+
interpret as empty strings.
|
|
854
902
|
|
|
855
903
|
Application.strict_parsing
|
|
856
904
|
``````````````````````````
|
|
857
905
|
This property is set for input parameter to automatically calling Args and
|
|
858
|
-
|
|
859
|
-
variable is set to ``0``. If is set to ``1``, ValueError exception
|
|
906
|
+
FieldStorageParser classes. When auto_args resp. auto_form is set. By default
|
|
907
|
+
this variable is set to ``0``. If is set to ``1``, ValueError exception
|
|
860
908
|
could raise on parsing error. I'm sure, that you never want to set this
|
|
861
909
|
variable to ``1``. If so, use it in your own parsing.
|
|
862
910
|
|
|
@@ -878,10 +926,11 @@ variable to ``1``. If so, use it in your own parsing.
|
|
|
878
926
|
|
|
879
927
|
if req.method_number == state.METHOD_POST:
|
|
880
928
|
try:
|
|
881
|
-
|
|
882
|
-
req,
|
|
929
|
+
parser = fieldstorage.FieldStorageParser(
|
|
930
|
+
req.input, req.headers,
|
|
883
931
|
keep_blank_values=app.keep_blank_values,
|
|
884
932
|
strict_parsing=app.strict_parsing)
|
|
933
|
+
req.form = parser.parse()
|
|
885
934
|
except Exception as e:
|
|
886
935
|
logging.error("Bad request body: %s", e)
|
|
887
936
|
|
|
@@ -894,7 +943,7 @@ empty tupple will be set.
|
|
|
894
943
|
|
|
895
944
|
Application / User options
|
|
896
945
|
--------------------------
|
|
897
|
-
Like in mod_python Request, Poor WSGI
|
|
946
|
+
Like in mod_python Request, Poor WSGI Application have get_options method too.
|
|
898
947
|
This method return dictionary of application options or variables, which start
|
|
899
948
|
with ``app_`` prefix. This prefix is cut from options names.
|
|
900
949
|
|
|
@@ -910,18 +959,11 @@ And you can get these variables with get_options method:
|
|
|
910
959
|
|
|
911
960
|
.. code:: python
|
|
912
961
|
|
|
913
|
-
config =
|
|
914
|
-
|
|
915
|
-
@app.before_response()
|
|
916
|
-
def load_options(req):
|
|
917
|
-
global config
|
|
918
|
-
if config is None:
|
|
919
|
-
config = req.get_options()
|
|
920
|
-
req.config = config
|
|
962
|
+
config = app.get_options()
|
|
921
963
|
|
|
922
964
|
@app.route('/options')
|
|
923
965
|
def list_options(req):
|
|
924
|
-
return ("%s = %s" % (key, val) in
|
|
966
|
+
return ("%s = %s" % (key, val) in config.items())
|
|
925
967
|
|
|
926
968
|
Output of application url /options looks like:
|
|
927
969
|
|
|
@@ -931,13 +973,15 @@ Output of application url /options looks like:
|
|
|
931
973
|
tmp_path = tmp
|
|
932
974
|
templ = templ
|
|
933
975
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
default:
|
|
976
|
+
You can store your variables to request object too. There are few reserved
|
|
977
|
+
variables for you, which poorwsgi never use, and which are None by default:
|
|
937
978
|
|
|
938
|
-
:req.
|
|
939
|
-
|
|
940
|
-
:req.
|
|
979
|
+
:req.user: For user object, who is login, check_digest decorator set this
|
|
980
|
+
variable.
|
|
981
|
+
:req.api: For API checking. OpenAPIRequest use this variable.
|
|
982
|
+
:req.db: For single database conection per request. You can store structure
|
|
983
|
+
with more databases if you need to this vairable.
|
|
984
|
+
:req.app\_: As prefix for any your application variable.
|
|
941
985
|
|
|
942
986
|
So if you want to add any other variable, be careful to named it.
|
|
943
987
|
|
|
@@ -1030,7 +1074,7 @@ Application.secret_key property.
|
|
|
1030
1074
|
@app.route('/login', method=state.METHOD_GET_POST)
|
|
1031
1075
|
def login(req):
|
|
1032
1076
|
if req.method == 'POST':
|
|
1033
|
-
passwd = req.form.getfirst('passwd',
|
|
1077
|
+
passwd = req.form.getfirst('passwd', func=str)
|
|
1034
1078
|
if passwd != 'SecretPasswds':
|
|
1035
1079
|
log.info('Bad password')
|
|
1036
1080
|
redirect('/login', text='Bad password')
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""HTTP WWW-Authenticate Digest Example."""
|
|
2
|
+
import logging
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from os import path
|
|
5
|
+
from sys import path as python_path
|
|
6
|
+
from time import time
|
|
7
|
+
from wsgiref.simple_server import make_server
|
|
8
|
+
|
|
9
|
+
python_path.insert(
|
|
10
|
+
0, path.abspath(path.join(path.dirname(__file__), path.pardir)))
|
|
11
|
+
|
|
12
|
+
# pylint: disable=wrong-import-position
|
|
13
|
+
from poorwsgi import Application, state # noqa
|
|
14
|
+
from poorwsgi.digest import PasswordMap, check_digest, hexdigest # noqa
|
|
15
|
+
from poorwsgi.response import EmptyResponse, redirect # noqa
|
|
16
|
+
|
|
17
|
+
FILE = path.join(path.dirname(__file__), 'test.digest')
|
|
18
|
+
|
|
19
|
+
logging.getLogger().setLevel("DEBUG")
|
|
20
|
+
app = application = Application(__name__) # pylint: disable=invalid-name
|
|
21
|
+
# application = app
|
|
22
|
+
app.debug = True
|
|
23
|
+
app.secret_key = sha256(str(time()).encode()).hexdigest()
|
|
24
|
+
# app.auth_algorithm = 'SHA-256-sess'
|
|
25
|
+
app.auth_type = 'Digest'
|
|
26
|
+
app.auth_timeout = 60
|
|
27
|
+
|
|
28
|
+
ADMIN = 'Admin Zone'
|
|
29
|
+
USER = 'User Zone'
|
|
30
|
+
# user/looser, foo/bar, admin/admin, Ondřej/heslíčko
|
|
31
|
+
app.auth_map = PasswordMap(FILE)
|
|
32
|
+
app.auth_map.load()
|
|
33
|
+
|
|
34
|
+
# pylint: disable=unused-argument
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_header(title):
|
|
38
|
+
"""Return HTML header list of lines."""
|
|
39
|
+
return (
|
|
40
|
+
"<html>", "<head>",
|
|
41
|
+
'<meta http-equiv="content-type" content="text/html; charset=utf-8"/>',
|
|
42
|
+
f"<title>{__file__} - {title}</title>", "</head>", "<body>",
|
|
43
|
+
f"<h1>{__file__} - {title}</h1>")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_footer():
|
|
47
|
+
"""Return HTML footer list of lines."""
|
|
48
|
+
return ("<hr>", "<small>Copyright (c) 2020 Ondřej Tůma. See ",
|
|
49
|
+
'<a href="http://poorhttp.zeropage.cz">poorhttp.zeropage.cz</a>'
|
|
50
|
+
'</small>.', "</body>", "</html>")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_link(href, text=None, title=None):
|
|
54
|
+
"""Return HTML anchor."""
|
|
55
|
+
text = text or title or href
|
|
56
|
+
title = title or text
|
|
57
|
+
return f'<a href="{href}" title="{title}">{text}</a>'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.route('/')
|
|
61
|
+
def root(req):
|
|
62
|
+
"""Return Root (Index) page."""
|
|
63
|
+
body = ('<ul>', "<li>" + get_link('/admin_zone') +
|
|
64
|
+
" - admin zone (admin/admin)</li>", "<li>" +
|
|
65
|
+
get_link('/user_zone') + " - user zone (user/looser;sha/sha)</li>",
|
|
66
|
+
"<li>" + get_link('/user') + " - user (user/looser)</li>", "<li>" +
|
|
67
|
+
get_link('/user/utf-8') + " - utf-8 (Ondřej/heslíčko)</li>",
|
|
68
|
+
"<li>" + get_link('/foo') + " - foo (foo/bar)</li>",
|
|
69
|
+
"<li>" + get_link('/unknown') + " - unknown</li>",
|
|
70
|
+
"<li>" + get_link('/spaces in url') + " - spaces in url</li>",
|
|
71
|
+
"<li>" + get_link('/čeština v url') + " - diacitics in url</li>",
|
|
72
|
+
"<li>" + get_link('/crazy in url 🤪') + " - unicode in url</li>",
|
|
73
|
+
'</ul>')
|
|
74
|
+
|
|
75
|
+
for line in get_header("Root") + body + get_footer():
|
|
76
|
+
yield line.encode() + b'\n'
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.route('/admin_zone')
|
|
80
|
+
@check_digest(ADMIN)
|
|
81
|
+
def admin_zone(req):
|
|
82
|
+
"""Page only for ADMIN realm."""
|
|
83
|
+
body = (f'<h2>{ADMIN} test for {app.auth_algorithm} algorithm.</h2>',
|
|
84
|
+
'<ul>', '<li>' + get_link('/', 'Root') + '</li>',
|
|
85
|
+
'<li>' + get_link('/admin_zone?arg=42', 'one more time') + '</li>',
|
|
86
|
+
'</ul>')
|
|
87
|
+
|
|
88
|
+
for line in get_header("Root") + body + get_footer():
|
|
89
|
+
yield line.encode() + b'\n'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@app.route('/user_zone')
|
|
93
|
+
@check_digest(USER)
|
|
94
|
+
def user_zone(req):
|
|
95
|
+
"""Page for USER realm."""
|
|
96
|
+
body = (f'<h2>{USER} test for {app.auth_algorithm} algorithm.</h2>',
|
|
97
|
+
f'User: {req.user}', '<ul>',
|
|
98
|
+
'<li>' + get_link('/', 'Root') + '</li>', '<li>' +
|
|
99
|
+
get_link('/user_zone?param=text', 'one more time') + '</li>',
|
|
100
|
+
'</ul>')
|
|
101
|
+
|
|
102
|
+
for line in get_header("Root") + body + get_footer():
|
|
103
|
+
yield line.encode() + b'\n'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.route('/user')
|
|
107
|
+
@check_digest(USER, 'user')
|
|
108
|
+
def user_only(req):
|
|
109
|
+
"""Page for user only."""
|
|
110
|
+
body = (f'<h2>User test for {app.auth_algorithm} algorithm.</h2>',
|
|
111
|
+
f'User: {req.user}', '<ul>',
|
|
112
|
+
'<li>' + get_link('/', 'Root') + '</li>', '<li>' +
|
|
113
|
+
get_link('/user?param=1234', 'one more time') + '</li>', '</ul>')
|
|
114
|
+
|
|
115
|
+
for line in get_header("Root") + body + get_footer():
|
|
116
|
+
yield line.encode() + b'\n'
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.route('/foo')
|
|
120
|
+
@check_digest(USER, 'foo')
|
|
121
|
+
def foo_only(req):
|
|
122
|
+
"""Page for foo user only."""
|
|
123
|
+
body = (f'<h2>Foo test for {app.auth_algorithm} algorithm.</h2>',
|
|
124
|
+
f'User: {req.user}', '<ul>', '<li>' + get_link('/', 'Root') +
|
|
125
|
+
'</li>', '</ul>', '<form method="post" action="/foo/passwd">',
|
|
126
|
+
'<label>Enter new password:',
|
|
127
|
+
'<input type="password" name="password"/>', '</label>',
|
|
128
|
+
'<button type="submit">Change Password</button>', '</form>')
|
|
129
|
+
|
|
130
|
+
for line in get_header("Root") + body + get_footer():
|
|
131
|
+
yield line.encode() + b'\n'
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.route('/user/utf-8')
|
|
135
|
+
@check_digest(USER, 'Ondřej')
|
|
136
|
+
def utf8_chars(req):
|
|
137
|
+
"""Page for user only."""
|
|
138
|
+
body = (f'<h2>User test for {app.auth_algorithm} algorithm.</h2>',
|
|
139
|
+
f'User: {req.user}', '<ul>',
|
|
140
|
+
'<li>' + get_link('/', 'Root') + '</li>', '<li>' +
|
|
141
|
+
get_link('/user/utf-8?param=1234', 'one more time') + '</li>',
|
|
142
|
+
'</ul>')
|
|
143
|
+
|
|
144
|
+
for line in get_header("Root") + body + get_footer():
|
|
145
|
+
yield line.encode() + b'\n'
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.route('/foo/passwd', method=state.METHOD_POST)
|
|
149
|
+
@check_digest(USER, 'foo')
|
|
150
|
+
def foo_password(req):
|
|
151
|
+
"""Change foo's password."""
|
|
152
|
+
digest = hexdigest(req.user, USER, req.form.get('password'), app.auth_hash)
|
|
153
|
+
app.auth_map.set(USER, req.user, digest)
|
|
154
|
+
redirect('/foo')
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.route('/unknown')
|
|
158
|
+
@check_digest(USER, 'unknown')
|
|
159
|
+
def unknown_endpoint(req):
|
|
160
|
+
"""Page for digest test."""
|
|
161
|
+
return EmptyResponse()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def generic_response(url, user):
|
|
165
|
+
"""Return generic response"""
|
|
166
|
+
body = (f'<h2>{USER} test for {app.auth_algorithm} algorithm.</h2>',
|
|
167
|
+
f'User: {user}', '<ul>', '<li>' + get_link('/', 'Root') + '</li>',
|
|
168
|
+
'<li>' + get_link(url + '?param=text', 'one more time') + '</li>',
|
|
169
|
+
'</ul>')
|
|
170
|
+
|
|
171
|
+
for line in get_header("Root") + body + get_footer():
|
|
172
|
+
yield line.encode() + b'\n'
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.route('/spaces in url')
|
|
176
|
+
@check_digest(USER)
|
|
177
|
+
def spaces_in_url(req):
|
|
178
|
+
"""URL with spaces in path."""
|
|
179
|
+
return generic_response(req.path, req.user)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.route('/čeština v url')
|
|
183
|
+
@check_digest(USER)
|
|
184
|
+
def diacritics_in_url(req):
|
|
185
|
+
"""URL with diacritics in path."""
|
|
186
|
+
return generic_response(req.path, req.user)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.route('/crazy in url 🤪')
|
|
190
|
+
@check_digest(USER)
|
|
191
|
+
def crazy_in_url(req):
|
|
192
|
+
"""URL with unicode in path."""
|
|
193
|
+
return generic_response(req.path, req.user)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == '__main__':
|
|
197
|
+
# pylint: disable=invalid-name
|
|
198
|
+
httpd = make_server('127.0.0.1', 8080, app)
|
|
199
|
+
logging.info("Starting to serve on http://127.0.0.1:8080")
|
|
200
|
+
httpd.serve_forever()
|