httpbinx 1.10.0__tar.gz → 1.10.2__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.
- {httpbinx-1.10.0 → httpbinx-1.10.2}/PKG-INFO +4 -9
- {httpbinx-1.10.0 → httpbinx-1.10.2}/README.md +2 -2
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/__init__.py +1 -1
- httpbinx-1.10.2/httpbinx/cli.py +58 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/constants.py +1 -7
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/helpers.py +10 -17
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/main.py +4 -10
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/meta.py +5 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/__init__.py +13 -14
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/anything.py +10 -9
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/auth.py +6 -8
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/cookies.py +12 -19
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/dynamicdata.py +48 -76
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/httpmethods.py +6 -6
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/images.py +10 -12
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/inspection/__init__.py +2 -1
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/inspection/request.py +3 -6
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/inspection/response.py +12 -26
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/redirects.py +19 -35
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/responseformats.py +15 -32
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/routers/statuscodes.py +11 -20
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/schemas.py +4 -4
- {httpbinx-1.10.0 → httpbinx-1.10.2}/pyproject.toml +33 -10
- httpbinx-1.10.2/tests/test_anything.py +44 -0
- httpbinx-1.10.2/tests/test_auth.py +27 -0
- httpbinx-1.10.2/tests/test_cookies.py +36 -0
- httpbinx-1.10.2/tests/test_dynamic_data.py +109 -0
- httpbinx-1.10.2/tests/test_http_methods.py +83 -0
- httpbinx-1.10.2/tests/test_images.py +68 -0
- httpbinx-1.10.2/tests/test_redirects.py +60 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/tests/test_request_inspection.py +10 -16
- httpbinx-1.10.2/tests/test_response_formats.py +101 -0
- httpbinx-1.10.2/tests/test_status_codes.py +37 -0
- httpbinx-1.10.0/httpbinx/cli.py +0 -65
- httpbinx-1.10.0/httpbinx/templates/trackingscripts.html +0 -18
- httpbinx-1.10.0/tests/test_anything.py +0 -28
- httpbinx-1.10.0/tests/test_dynamic_data.py +0 -70
- httpbinx-1.10.0/tests/test_http_methods.py +0 -39
- httpbinx-1.10.0/tests/test_images.py +0 -38
- httpbinx-1.10.0/tests/test_redirects.py +0 -21
- httpbinx-1.10.0/tests/test_response_formats.py +0 -46
- httpbinx-1.10.0/tests/test_status_codes.py +0 -41
- {httpbinx-1.10.0 → httpbinx-1.10.2}/LICENSE +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/examples/__init__.py +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/UTF-8-demo.txt +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/bombs/bomb-1GB.br +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/bombs/bomb-1GB.gz +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/favicon.png +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/images/httbinx_logo.png +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/images/httpbinx_cover.png +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/images/jackal.jpg +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/images/pig_icon.png +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/images/svg_logo.svg +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/static/images/wolf_1.webp +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/templates/moby.html +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/httpbinx/templates/sample.xml +0 -0
- {httpbinx-1.10.0 → httpbinx-1.10.2}/tests/__init__.py +0 -0
- /httpbinx-1.10.0/tests/common.py → /httpbinx-1.10.2/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: httpbinx
|
|
3
|
-
Version: 1.10.
|
|
3
|
+
Version: 1.10.2
|
|
4
4
|
Summary: HTTP Request & Response Service, written in Python + FastAPI.
|
|
5
5
|
Author-Email: Leo <imleowoo@outlook.com>
|
|
6
6
|
Maintainer-Email: Leo <imleowoo@outlook.com>
|
|
@@ -38,23 +38,18 @@ Project-URL: Homepage, https://github.com/imleowoo/httpbinx
|
|
|
38
38
|
Project-URL: Repository, https://github.com/imleowoo/httpbinx
|
|
39
39
|
Requires-Python: >=3.10
|
|
40
40
|
Requires-Dist: fastapi
|
|
41
|
+
Requires-Dist: pydantic
|
|
41
42
|
Requires-Dist: uvicorn
|
|
42
43
|
Requires-Dist: starlette
|
|
43
44
|
Requires-Dist: jinja2
|
|
44
45
|
Requires-Dist: brotli
|
|
45
46
|
Requires-Dist: python-multipart
|
|
46
|
-
Provides-Extra: test
|
|
47
|
-
Requires-Dist: httpx; extra == "test"
|
|
48
|
-
Requires-Dist: pytest-cov; extra == "test"
|
|
49
|
-
Requires-Dist: pytest-mock; extra == "test"
|
|
50
|
-
Requires-Dist: pytest-xdist; extra == "test"
|
|
51
|
-
Requires-Dist: pytest; extra == "test"
|
|
52
47
|
Description-Content-Type: text/markdown
|
|
53
48
|
|
|
54
49
|
](https://raw.githubusercontent.com/imleowoo/httpbinx/main/httpbinx/static/images/httpbinx_cover.png)
|
|
55
50
|
|
|
56
51
|
[](https://github.com/postmanlabs/httpbin)
|
|
57
|
-

|
|
58
53
|
|
|
59
54
|
# httpbinx
|
|
60
55
|
|
|
@@ -66,7 +61,7 @@ HTTP Request & Response Service, written in Python + FastAPI.
|
|
|
66
61
|
|
|
67
62
|
## Reference Project
|
|
68
63
|
|
|
69
|
-
A
|
|
64
|
+
A [Kenneth Reitz](http://kennethreitz.org/bitcoin) Project. See https://github.com/postmanlabs/httpbin
|
|
70
65
|
|
|
71
66
|
## Quick Start
|
|
72
67
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
](https://raw.githubusercontent.com/imleowoo/httpbinx/main/httpbinx/static/images/httpbinx_cover.png)
|
|
2
2
|
|
|
3
3
|
[](https://github.com/postmanlabs/httpbin)
|
|
4
|
-

|
|
5
5
|
|
|
6
6
|
# httpbinx
|
|
7
7
|
|
|
@@ -13,7 +13,7 @@ HTTP Request & Response Service, written in Python + FastAPI.
|
|
|
13
13
|
|
|
14
14
|
## Reference Project
|
|
15
15
|
|
|
16
|
-
A
|
|
16
|
+
A [Kenneth Reitz](http://kennethreitz.org/bitcoin) Project. See https://github.com/postmanlabs/httpbin
|
|
17
17
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""httpbinx CLI."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import fastapi
|
|
8
|
+
|
|
9
|
+
import httpbinx
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def info(_args):
|
|
13
|
+
"""Show httpbinx and environment information."""
|
|
14
|
+
print(f'httpbinx: {httpbinx.__version__}')
|
|
15
|
+
print(f'fastapi: {fastapi.__version__}')
|
|
16
|
+
print(f'python: {sys.version}')
|
|
17
|
+
print(f'platform: {platform.platform()}')
|
|
18
|
+
print(f'python executable: {sys.executable}')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def version(_args):
|
|
22
|
+
"""Show httpbinx version."""
|
|
23
|
+
print(httpbinx.__version__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def server(_args):
|
|
27
|
+
"""Start the httpbinx server."""
|
|
28
|
+
import uvicorn
|
|
29
|
+
|
|
30
|
+
uvicorn.run('httpbinx.main:app', host=_args.host, port=_args.port)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def execute():
|
|
34
|
+
"""Execute the CLI."""
|
|
35
|
+
parser = argparse.ArgumentParser(prog='httpbinx', description='httpbinx is a fastapi server for testing.')
|
|
36
|
+
parser.add_argument('--version', action='version', version='%(prog)s 1.0')
|
|
37
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
38
|
+
# version
|
|
39
|
+
parser_version = subparsers.add_parser('version', help='show version')
|
|
40
|
+
parser_version.set_defaults(func=version)
|
|
41
|
+
# info
|
|
42
|
+
parser_info = subparsers.add_parser('info', help='show info')
|
|
43
|
+
parser_info.set_defaults(func=info)
|
|
44
|
+
# server
|
|
45
|
+
parser_server = subparsers.add_parser('server', help='start server')
|
|
46
|
+
parser_server.set_defaults(func=server)
|
|
47
|
+
parser_server.add_argument('--host', type=str, help='host', default='0.0.0.0')
|
|
48
|
+
parser_server.add_argument('--port', type=int, help='port', default=80)
|
|
49
|
+
|
|
50
|
+
parse_args = parser.parse_args()
|
|
51
|
+
if hasattr(parse_args, 'func'):
|
|
52
|
+
parse_args.func(parse_args)
|
|
53
|
+
else:
|
|
54
|
+
parser.print_help()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
execute()
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
REDIRECT_LOCATION = '/redirect/1'
|
|
2
2
|
|
|
3
|
-
ACCEPTED_MEDIA_TYPES = [
|
|
4
|
-
'image/webp',
|
|
5
|
-
'image/svg+xml',
|
|
6
|
-
'image/jpeg',
|
|
7
|
-
'image/png',
|
|
8
|
-
'image/*'
|
|
9
|
-
]
|
|
3
|
+
ACCEPTED_MEDIA_TYPES = ['image/webp', 'image/svg+xml', 'image/jpeg', 'image/png', 'image/*']
|
|
10
4
|
|
|
11
5
|
ENV_COOKIES = (
|
|
12
6
|
'_gauges_unique',
|
|
@@ -8,12 +8,11 @@ from starlette.requests import Request
|
|
|
8
8
|
from starlette.responses import Response
|
|
9
9
|
from starlette.templating import Jinja2Templates
|
|
10
10
|
|
|
11
|
-
from httpbinx.constants import
|
|
12
|
-
REDIRECT_LOCATION)
|
|
11
|
+
from httpbinx.constants import ACCEPTED_MEDIA_TYPES, ASCII_ART, REDIRECT_LOCATION
|
|
13
12
|
from httpbinx.schemas import RequestAttrs, RequestInfo
|
|
14
13
|
|
|
15
14
|
# init Jinja2
|
|
16
|
-
_templates = Jinja2Templates(directory='templates')
|
|
15
|
+
_templates = Jinja2Templates(directory=str(Path(__file__).parent / 'templates'))
|
|
17
16
|
# image path
|
|
18
17
|
_images_path = Path(__file__).parent / 'static' / 'images'
|
|
19
18
|
# bomb files path
|
|
@@ -62,25 +61,19 @@ def status_code_response(code: int) -> Response:
|
|
|
62
61
|
status.HTTP_304_NOT_MODIFIED: dict(data=''),
|
|
63
62
|
status.HTTP_305_USE_PROXY: redirect,
|
|
64
63
|
status.HTTP_307_TEMPORARY_REDIRECT: redirect,
|
|
65
|
-
status.HTTP_401_UNAUTHORIZED: dict(
|
|
66
|
-
headers={'WWW-Authenticate': 'Basic realm="Fake Realm"'}
|
|
67
|
-
),
|
|
64
|
+
status.HTTP_401_UNAUTHORIZED: dict(headers={'WWW-Authenticate': 'Basic realm="Fake Realm"'}),
|
|
68
65
|
status.HTTP_402_PAYMENT_REQUIRED: dict(
|
|
69
|
-
data='Fuck you, pay me!',
|
|
70
|
-
headers={'x-more-info': 'https://vimeo.com/22053820'}
|
|
66
|
+
data='Fuck you, pay me!', headers={'x-more-info': 'https://vimeo.com/22053820'}
|
|
71
67
|
),
|
|
72
68
|
status.HTTP_406_NOT_ACCEPTABLE: dict(
|
|
73
|
-
data=json.dumps(
|
|
74
|
-
'message': 'Client did not request a supported media type.',
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
headers={'Content-Type': 'application/json'}),
|
|
78
|
-
status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED: dict(
|
|
79
|
-
headers={'Proxy-Authenticate': 'Basic realm="Fake Realm"'}
|
|
69
|
+
data=json.dumps(
|
|
70
|
+
{'message': 'Client did not request a supported media type.', 'accept': ACCEPTED_MEDIA_TYPES}
|
|
71
|
+
),
|
|
72
|
+
headers={'Content-Type': 'application/json'},
|
|
80
73
|
),
|
|
74
|
+
status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED: dict(headers={'Proxy-Authenticate': 'Basic realm="Fake Realm"'}),
|
|
81
75
|
status.HTTP_418_IM_A_TEAPOT: dict( # I'm a teapot!
|
|
82
|
-
data=ASCII_ART,
|
|
83
|
-
headers={'x-more-info': 'https://datatracker.ietf.org/doc/html/rfc2324'}
|
|
76
|
+
data=ASCII_ART, headers={'x-more-info': 'https://datatracker.ietf.org/doc/html/rfc2324'}
|
|
84
77
|
),
|
|
85
78
|
}
|
|
86
79
|
|
|
@@ -9,28 +9,22 @@ from httpbinx.routers import router
|
|
|
9
9
|
|
|
10
10
|
app = FastAPI(
|
|
11
11
|
title='httpbinx',
|
|
12
|
-
description='HTTP Request & Response Service, '
|
|
13
|
-
'written in Python + FastAPI.',
|
|
12
|
+
description='HTTP Request & Response Service, written in Python + FastAPI.',
|
|
14
13
|
docs_url='/', # swagger docs page url
|
|
15
14
|
swagger_ui_parameters={'docExpansion': 'none'},
|
|
16
|
-
openapi_tags=get_tags_metadata()
|
|
15
|
+
openapi_tags=get_tags_metadata(),
|
|
17
16
|
)
|
|
18
17
|
|
|
19
18
|
app.add_middleware(
|
|
20
|
-
CORSMiddleware,
|
|
19
|
+
CORSMiddleware, # type: ignore
|
|
21
20
|
allow_origins=['*'],
|
|
22
21
|
allow_credentials=True,
|
|
23
22
|
allow_methods=['*'],
|
|
24
23
|
allow_headers=['*'],
|
|
25
|
-
|
|
26
24
|
)
|
|
27
25
|
|
|
28
26
|
# mount static files
|
|
29
27
|
static_dir = Path(__file__).parent / 'static'
|
|
30
|
-
app.mount(
|
|
31
|
-
'/static',
|
|
32
|
-
StaticFiles(directory=static_dir),
|
|
33
|
-
name='static'
|
|
34
|
-
)
|
|
28
|
+
app.mount('/static', StaticFiles(directory=static_dir), name='static')
|
|
35
29
|
|
|
36
30
|
app.include_router(router=router)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
This module defines the metadata for API tags used in the FastAPI application.
|
|
3
3
|
These tags are used to organize and categorize different endpoints in the API documentation.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
from functools import lru_cache
|
|
6
7
|
from typing import List
|
|
7
8
|
|
|
@@ -9,11 +10,15 @@ from pydantic import BaseModel, Field
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class Tag(BaseModel):
|
|
13
|
+
"""API tag with name and description."""
|
|
14
|
+
|
|
12
15
|
name: str = Field(..., description='The name of the tag')
|
|
13
16
|
description: str = Field(..., description='A brief description of the tag')
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class TagsMetadata(BaseModel):
|
|
20
|
+
"""Container for a list of API tags."""
|
|
21
|
+
|
|
17
22
|
tags: List[Tag] = Field(..., description='List of API tags')
|
|
18
23
|
|
|
19
24
|
|
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
1
|
__all__ = ['router']
|
|
4
2
|
|
|
5
3
|
from fastapi import APIRouter
|
|
6
4
|
from starlette import status
|
|
7
5
|
from starlette.responses import RedirectResponse
|
|
8
6
|
|
|
9
|
-
from httpbinx.routers import (
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
from httpbinx.routers import (
|
|
8
|
+
anything,
|
|
9
|
+
auth,
|
|
10
|
+
cookies,
|
|
11
|
+
dynamicdata,
|
|
12
|
+
httpmethods,
|
|
13
|
+
images,
|
|
14
|
+
redirects,
|
|
15
|
+
responseformats,
|
|
16
|
+
statuscodes,
|
|
17
|
+
)
|
|
12
18
|
from httpbinx.routers.inspection import request_inspection, response_inspection
|
|
13
19
|
|
|
14
20
|
router = APIRouter()
|
|
15
21
|
|
|
16
22
|
|
|
17
|
-
@router.get(
|
|
18
|
-
'/favicon.ico',
|
|
19
|
-
summary='Returns a favicon.ico.',
|
|
20
|
-
include_in_schema=False
|
|
21
|
-
)
|
|
23
|
+
@router.get('/favicon.ico', summary='Returns a favicon.ico.', include_in_schema=False)
|
|
22
24
|
async def favicon():
|
|
23
|
-
return RedirectResponse(
|
|
24
|
-
url='/static/favicon.png',
|
|
25
|
-
status_code=status.HTTP_301_MOVED_PERMANENTLY
|
|
26
|
-
)
|
|
25
|
+
return RedirectResponse(url='/static/favicon.png', status_code=status.HTTP_301_MOVED_PERMANENTLY)
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
router.include_router(httpmethods.router)
|
|
@@ -13,6 +13,7 @@ router = APIRouter(tags=['Anything'])
|
|
|
13
13
|
|
|
14
14
|
class BombTypes(str, Enum):
|
|
15
15
|
"""The allowed bomb file compression types are: brotli and gzip."""
|
|
16
|
+
|
|
16
17
|
brotli = 'brotli'
|
|
17
18
|
gzip = 'gzip'
|
|
18
19
|
|
|
@@ -25,7 +26,7 @@ bombs_path: Path = get_bomb_file_path()
|
|
|
25
26
|
response_model=RequestInfo,
|
|
26
27
|
summary='Returns anything passed in request data.',
|
|
27
28
|
response_description='Anything passed in request',
|
|
28
|
-
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'TRACE']
|
|
29
|
+
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'TRACE'],
|
|
29
30
|
)
|
|
30
31
|
async def anything(request: Request):
|
|
31
32
|
return await to_request_info(request)
|
|
@@ -36,16 +37,16 @@ async def anything(request: Request):
|
|
|
36
37
|
response_class=FileResponse,
|
|
37
38
|
summary='Returns a bomb file.',
|
|
38
39
|
description='**It may cause your client to crash! '
|
|
39
|
-
|
|
40
|
-
response_description='Return a bomb for compressed files.'
|
|
40
|
+
'References [I use Zip Bombs to Protect my Server](https://idiallo.com/blog/zipbomb-protection)**',
|
|
41
|
+
response_description='Return a bomb for compressed files.',
|
|
41
42
|
)
|
|
42
43
|
async def bomb_file(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
*,
|
|
45
|
+
file: BombTypes = Path(
|
|
46
|
+
...,
|
|
47
|
+
title='Compression types',
|
|
48
|
+
description='The allowed bomb file compression types',
|
|
49
|
+
),
|
|
49
50
|
):
|
|
50
51
|
if file == BombTypes.gzip:
|
|
51
52
|
# dd if=/dev/zero bs=1M count=1000 | gzip -c > bomb-1GB.gz
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
"""Auth"""
|
|
2
|
+
|
|
3
3
|
from fastapi import APIRouter, HTTPException
|
|
4
4
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
5
5
|
|
|
6
|
-
router = APIRouter(
|
|
6
|
+
router = APIRouter(
|
|
7
|
+
tags=['Auth'],
|
|
8
|
+
)
|
|
7
9
|
security = HTTPBasic()
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@router.get(
|
|
11
13
|
'/basic-auth/{user}/{password}',
|
|
12
14
|
summary='Prompts the user for authorization using HTTP Basic Auth.',
|
|
13
|
-
response_description='TODO'
|
|
15
|
+
response_description='TODO',
|
|
14
16
|
)
|
|
15
|
-
async def basic_auth(
|
|
16
|
-
user: str,
|
|
17
|
-
password: str,
|
|
18
|
-
credentials: HTTPBasicCredentials = security
|
|
19
|
-
):
|
|
17
|
+
async def basic_auth(user: str, password: str, credentials: HTTPBasicCredentials = security):
|
|
20
18
|
if not (credentials and credentials.username == user and credentials.password == password):
|
|
21
19
|
raise HTTPException(status_code=401)
|
|
22
20
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
"""Cookies"""
|
|
2
|
+
|
|
3
3
|
from fastapi import APIRouter, Path, Query
|
|
4
4
|
from starlette.requests import Request
|
|
5
5
|
from starlette.responses import JSONResponse, RedirectResponse
|
|
@@ -14,16 +14,11 @@ router = APIRouter(tags=['Cookies'])
|
|
|
14
14
|
summary='Returns cookie data.',
|
|
15
15
|
response_description='Set cookies.',
|
|
16
16
|
response_class=JSONResponse,
|
|
17
|
-
|
|
18
17
|
)
|
|
19
18
|
async def cookies(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
title='Show Environment variable?',
|
|
24
|
-
include_in_schema=False
|
|
25
|
-
),
|
|
26
|
-
request: Request
|
|
19
|
+
*,
|
|
20
|
+
show_env: bool = Query(default=False, title='Show Environment variable?', include_in_schema=False),
|
|
21
|
+
request: Request,
|
|
27
22
|
):
|
|
28
23
|
resp = JSONResponse(content={'cookies': request.cookies})
|
|
29
24
|
if not show_env:
|
|
@@ -34,10 +29,9 @@ async def cookies(
|
|
|
34
29
|
|
|
35
30
|
@router.get(
|
|
36
31
|
'/cookies/set',
|
|
37
|
-
summary='Sets cookie(s) as provided by the query string '
|
|
38
|
-
'and redirects to cookie list.',
|
|
32
|
+
summary='Sets cookie(s) as provided by the query string and redirects to cookie list.',
|
|
39
33
|
response_description='Redirect to cookie list',
|
|
40
|
-
response_class=RedirectResponse
|
|
34
|
+
response_class=RedirectResponse,
|
|
41
35
|
)
|
|
42
36
|
async def set_cookies(request: Request):
|
|
43
37
|
params = request.query_params.items()
|
|
@@ -55,10 +49,10 @@ async def set_cookies(request: Request):
|
|
|
55
49
|
response_class=RedirectResponse,
|
|
56
50
|
)
|
|
57
51
|
async def set_cookie(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
*,
|
|
53
|
+
name: str = Path(..., description='Cookie Name'),
|
|
54
|
+
value: str = Path(..., description='Cookie Value'),
|
|
55
|
+
request: Request,
|
|
62
56
|
):
|
|
63
57
|
resp = RedirectResponse(request.url_for(cookies.__name__))
|
|
64
58
|
# TODO secure=True
|
|
@@ -68,10 +62,9 @@ async def set_cookie(
|
|
|
68
62
|
|
|
69
63
|
@router.get(
|
|
70
64
|
'/cookies/delete',
|
|
71
|
-
summary='Deletes cookie(s) as provided by the query string '
|
|
72
|
-
'and redirects to cookie list.',
|
|
65
|
+
summary='Deletes cookie(s) as provided by the query string and redirects to cookie list.',
|
|
73
66
|
response_description='Redirect to cookie list',
|
|
74
|
-
response_class=RedirectResponse
|
|
67
|
+
response_class=RedirectResponse,
|
|
75
68
|
)
|
|
76
69
|
async def delete_cookies(request: Request):
|
|
77
70
|
keys = request.query_params.keys()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
1
|
"""Dynamic Data"""
|
|
2
|
+
|
|
3
3
|
import asyncio
|
|
4
4
|
import base64
|
|
5
5
|
import binascii
|
|
@@ -10,8 +10,7 @@ from fastapi import APIRouter, Path, Query
|
|
|
10
10
|
from fastapi.responses import PlainTextResponse
|
|
11
11
|
from starlette import status
|
|
12
12
|
from starlette.requests import Request
|
|
13
|
-
from starlette.responses import
|
|
14
|
-
StreamingResponse)
|
|
13
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response, StreamingResponse
|
|
15
14
|
|
|
16
15
|
from httpbinx.constants import AWESOME_BASE64ENCODED
|
|
17
16
|
from httpbinx.helpers import to_request_info
|
|
@@ -22,6 +21,7 @@ router = APIRouter(tags=['Dynamic data'])
|
|
|
22
21
|
|
|
23
22
|
class OctetStreamResponse(Response):
|
|
24
23
|
"""set response headers Content-Type: application/octet-stream"""
|
|
24
|
+
|
|
25
25
|
media_type = 'application/octet-stream'
|
|
26
26
|
|
|
27
27
|
|
|
@@ -32,16 +32,12 @@ class OctetStreamResponse(Response):
|
|
|
32
32
|
response_description='Decoded base64 content.',
|
|
33
33
|
)
|
|
34
34
|
async def decode_base64(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'summary': 'A awesome base64 string',
|
|
42
|
-
'value': AWESOME_BASE64ENCODED
|
|
43
|
-
}}
|
|
44
|
-
)
|
|
35
|
+
value: str = Path(
|
|
36
|
+
...,
|
|
37
|
+
title='base64-encoded string',
|
|
38
|
+
# examples=[AWESOME_BASE64ENCODED],
|
|
39
|
+
openapi_examples={'awesome': {'summary': 'A awesome base64 string', 'value': AWESOME_BASE64ENCODED}},
|
|
40
|
+
),
|
|
45
41
|
):
|
|
46
42
|
encoded: bytes = value.encode('utf-8')
|
|
47
43
|
try:
|
|
@@ -49,8 +45,7 @@ async def decode_base64(
|
|
|
49
45
|
return PlainTextResponse(content=decoded)
|
|
50
46
|
except binascii.Error as err:
|
|
51
47
|
return PlainTextResponse(
|
|
52
|
-
content=f'Incorrect Base64 data: {value}, err_msg: {err}',
|
|
53
|
-
status_code=status.HTTP_400_BAD_REQUEST
|
|
48
|
+
content=f'Incorrect Base64 data: {value}, err_msg: {err}', status_code=status.HTTP_400_BAD_REQUEST
|
|
54
49
|
)
|
|
55
50
|
|
|
56
51
|
|
|
@@ -58,15 +53,11 @@ async def decode_base64(
|
|
|
58
53
|
'/bytes/{n}',
|
|
59
54
|
response_class=OctetStreamResponse,
|
|
60
55
|
summary='Returns n random bytes generated with given seed',
|
|
61
|
-
response_description='A delayed response.'
|
|
56
|
+
response_description='A delayed response.',
|
|
62
57
|
)
|
|
63
58
|
async def random_bytes(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
None,
|
|
67
|
-
title='random seed',
|
|
68
|
-
description='Initialize the random number generator'
|
|
69
|
-
)
|
|
59
|
+
n: int = Path(..., title='binary file size', gt=0, lt=100 * 1024), # set 100KB limit
|
|
60
|
+
seed: int = Query(None, title='random seed', description='Initialize the random number generator'),
|
|
70
61
|
):
|
|
71
62
|
if seed is not None:
|
|
72
63
|
random.seed(seed)
|
|
@@ -81,13 +72,9 @@ async def random_bytes(
|
|
|
81
72
|
methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'TRACE'],
|
|
82
73
|
response_model=RequestInfo,
|
|
83
74
|
summary='Returns a delayed response (max of 10 seconds).',
|
|
84
|
-
response_description='A delayed response.'
|
|
75
|
+
response_description='A delayed response.',
|
|
85
76
|
)
|
|
86
|
-
async def delay_response(
|
|
87
|
-
*,
|
|
88
|
-
delay: float = Path(..., ge=0, le=10, description='delay seconds'),
|
|
89
|
-
request: Request
|
|
90
|
-
):
|
|
77
|
+
async def delay_response(*, delay: float = Path(..., ge=0, le=10, description='delay seconds'), request: Request):
|
|
91
78
|
await asyncio.sleep(delay)
|
|
92
79
|
return await to_request_info(request)
|
|
93
80
|
|
|
@@ -96,25 +83,18 @@ async def delay_response(
|
|
|
96
83
|
'/drip',
|
|
97
84
|
response_class=StreamingResponse,
|
|
98
85
|
summary='Drips data over a duration after an optional initial delay.',
|
|
99
|
-
response_description='A dripped response.'
|
|
86
|
+
response_description='A dripped response.',
|
|
100
87
|
)
|
|
101
88
|
async def drip(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
default=200, title='response code',
|
|
112
|
-
description='The response code that will be returned'
|
|
113
|
-
),
|
|
114
|
-
delay: float = Query(
|
|
115
|
-
default=2, ge=0,
|
|
116
|
-
description='The amount of time (in seconds) to delay before responding'
|
|
117
|
-
),
|
|
89
|
+
duration: float = Query(default=2, description='The amount of time (in seconds) over which to drip each byte'),
|
|
90
|
+
numbytes: int = Query(
|
|
91
|
+
default=10,
|
|
92
|
+
gt=0,
|
|
93
|
+
lt=10 * 1024 * 1024, # set 10MB limit
|
|
94
|
+
description='The number of bytes to respond with',
|
|
95
|
+
),
|
|
96
|
+
code: int = Query(default=200, title='response code', description='The response code that will be returned'),
|
|
97
|
+
delay: float = Query(default=2, ge=0, description='The amount of time (in seconds) to delay before responding'),
|
|
118
98
|
):
|
|
119
99
|
await asyncio.sleep(delay)
|
|
120
100
|
# Number of seconds to pause during each data generation
|
|
@@ -125,26 +105,24 @@ async def drip(
|
|
|
125
105
|
yield b'*'
|
|
126
106
|
await asyncio.sleep(pause)
|
|
127
107
|
|
|
128
|
-
return StreamingResponse(
|
|
129
|
-
content=generate_content(),
|
|
130
|
-
media_type='application/octet-stream',
|
|
131
|
-
status_code=code
|
|
132
|
-
)
|
|
108
|
+
return StreamingResponse(content=generate_content(), media_type='application/octet-stream', status_code=code)
|
|
133
109
|
|
|
134
110
|
|
|
135
111
|
@router.get(
|
|
136
112
|
'/links/{n}/{offset}',
|
|
137
113
|
summary='Generate a page containing n links to other pages which do the same.',
|
|
138
114
|
response_class=HTMLResponse,
|
|
139
|
-
response_description='HTML links.'
|
|
115
|
+
response_description='HTML links.',
|
|
140
116
|
)
|
|
141
117
|
async def link_page(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
118
|
+
*,
|
|
119
|
+
n: int = Path(
|
|
120
|
+
...,
|
|
121
|
+
ge=1,
|
|
122
|
+
le=200, # limit to between 1 and 200 links
|
|
123
|
+
description='Number of links',
|
|
124
|
+
),
|
|
125
|
+
offset: int = Path(..., ge=0),
|
|
148
126
|
):
|
|
149
127
|
html = """
|
|
150
128
|
<!DOCTYPE html>
|
|
@@ -161,7 +139,7 @@ async def link_page(
|
|
|
161
139
|
else:
|
|
162
140
|
body += link.format(
|
|
163
141
|
href=f'/links/{n}/{i}', # TODO how to use router.url_path_for?
|
|
164
|
-
text=i
|
|
142
|
+
text=i,
|
|
165
143
|
)
|
|
166
144
|
return HTMLResponse(content=html.format(body=body))
|
|
167
145
|
|
|
@@ -170,13 +148,10 @@ async def link_page(
|
|
|
170
148
|
'/range/{numbytes}',
|
|
171
149
|
summary='Streams n random bytes generated with given seed, at given chunk size per packet.',
|
|
172
150
|
response_class=StreamingResponse,
|
|
173
|
-
response_description='Streaming Bytes'
|
|
151
|
+
response_description='Streaming Bytes',
|
|
174
152
|
)
|
|
175
153
|
async def range_request(
|
|
176
|
-
|
|
177
|
-
..., ge=1, le=100 * 1024,
|
|
178
|
-
description='number of bytes must be in the range (0, 102400]'
|
|
179
|
-
)
|
|
154
|
+
numbytes: int = Path(..., ge=1, le=100 * 1024, description='number of bytes must be in the range (0, 102400]'),
|
|
180
155
|
):
|
|
181
156
|
raise NotImplementedError
|
|
182
157
|
|
|
@@ -185,15 +160,17 @@ async def range_request(
|
|
|
185
160
|
'/stream-bytes/{n}',
|
|
186
161
|
summary='Streams n random bytes generated with given seed, at given chunk size per packet.',
|
|
187
162
|
response_class=StreamingResponse,
|
|
188
|
-
response_description='Streaming Bytes'
|
|
163
|
+
response_description='Streaming Bytes',
|
|
189
164
|
)
|
|
190
165
|
async def stream_random_bytes(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
166
|
+
n: int = Path(
|
|
167
|
+
...,
|
|
168
|
+
ge=1,
|
|
169
|
+
le=100 * 1024, # set 100KB limit
|
|
170
|
+
description='Streams n random bytes generated',
|
|
171
|
+
),
|
|
172
|
+
seed: int = Query(default=None, ge=0),
|
|
173
|
+
chunk_size: int = Query(default=10 * 1024, ge=1, le=10 * 1024),
|
|
197
174
|
):
|
|
198
175
|
if seed is not None:
|
|
199
176
|
random.seed(seed)
|
|
@@ -211,12 +188,7 @@ async def stream_random_bytes(
|
|
|
211
188
|
return StreamingResponse(content=generate_bytes(), media_type='application/octet-stream')
|
|
212
189
|
|
|
213
190
|
|
|
214
|
-
@router.get(
|
|
215
|
-
'/uuid',
|
|
216
|
-
response_class=JSONResponse,
|
|
217
|
-
summary='Return a UUID4.',
|
|
218
|
-
response_description='A UUID4.'
|
|
219
|
-
)
|
|
191
|
+
@router.get('/uuid', response_class=JSONResponse, summary='Return a UUID4.', response_description='A UUID4.')
|
|
220
192
|
async def get_uuid4():
|
|
221
193
|
out = {'uuid': str(uuid.uuid4())}
|
|
222
194
|
return JSONResponse(content=out)
|