Compare commits
89 commits
master
...
release/v1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c8f23fb42 | ||
|
|
5fa7c5c898 | ||
|
|
0658edd9b9 | ||
|
|
903e437009 | ||
|
|
ad4e7f4762 | ||
|
|
389571522f | ||
|
|
90fa8cd7b8 | ||
|
|
98586f4fd2 | ||
|
|
56f46172ce | ||
|
|
68d9240c0c | ||
|
|
47cb58c9b1 | ||
|
|
0de30771bd | ||
|
|
f33ed12fb6 | ||
|
|
afafbd6459 | ||
|
|
26aea68acb | ||
|
|
5910cbcc9a | ||
|
|
2de4870269 | ||
|
|
5f6ac7bd57 | ||
|
|
04661720de | ||
|
|
bf8e2c19cf | ||
|
|
93ed72d5dc | ||
|
|
0f684cf005 | ||
|
|
05d1bc216d | ||
|
|
c645478b98 | ||
|
|
153ee43b18 | ||
|
|
a5e61f9c5c | ||
|
|
b69158241a | ||
|
|
75ce33e898 | ||
|
|
db3701c3d2 | ||
|
|
9c5c9d743d | ||
|
|
315f07c5ae | ||
|
|
c518f281e8 | ||
|
|
6b46159fd0 | ||
|
|
096921ab07 | ||
|
|
8016ee7f29 | ||
|
|
2474c7be61 | ||
|
|
408fac3295 | ||
|
|
4c5f0798bc | ||
|
|
e79714e1f6 | ||
|
|
3d3d23f6f2 | ||
|
|
b081f4a5b6 | ||
|
|
e4de8e285e | ||
|
|
4d8e2e30eb | ||
|
|
5288c64cbb | ||
|
|
b83d0a3614 | ||
|
|
42ccaaccc6 | ||
|
|
bdd9d892e8 | ||
|
|
c2ca782bf6 | ||
|
|
ef40069427 | ||
|
|
9845c85510 | ||
|
|
49604c1e37 | ||
|
|
0fb50c5a57 | ||
|
|
843354e18d | ||
|
|
c506cec36a | ||
|
|
65f3102a7f | ||
|
|
dd187a1069 | ||
|
|
fdf45fd114 | ||
|
|
6ec39a9303 | ||
|
|
60a01ea511 | ||
|
|
38bc403058 | ||
|
|
bab6c52706 | ||
|
|
8726427dd8 | ||
|
|
d558a65609 | ||
|
|
7bd5eb04b5 | ||
|
|
f25e3f766c | ||
|
|
e8ae877a48 | ||
|
|
edf450613a | ||
|
|
a9472d321c | ||
|
|
9541cee98a | ||
|
|
3940b4cec7 | ||
|
|
0d7a5d4ccd | ||
|
|
bde6344c1e | ||
|
|
f607529be3 | ||
|
|
390791bb81 | ||
|
|
24c6602ec5 | ||
|
|
ea03dc1018 | ||
|
|
d8ac419c18 | ||
|
|
809ce6522b | ||
|
|
4a1cf1d007 | ||
|
|
151add38a4 | ||
|
|
9c31f044ce | ||
|
|
4993a5a3ea | ||
|
|
1a4e20ce3e | ||
|
|
cefbcf9318 | ||
|
|
107ed91120 | ||
|
|
5a6c6431e9 | ||
|
|
5e25606880 | ||
|
|
21d8b8c541 | ||
|
|
23d5128ea7 |
95 changed files with 2875 additions and 668 deletions
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
.gitignore
|
||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
FROM python:3.10-slim as base
|
||||||
|
|
||||||
|
LABEL org.label-schema.schema-version="1.0"
|
||||||
|
LABEL org.label-schema.vendor="Tiara Rodney (victoryk.it)"
|
||||||
|
LABEL org.label-schema.name="victorykit/httpaste"
|
||||||
|
LABEL org.label-schema.description="a versatile HTTP pastebin"
|
||||||
|
LABEL org.label-schema.vcs-url="https://bitbucket.org/victorykit/httpaste"
|
||||||
|
LABEL org.label-schema.docker.cmd="docker run {image-id} {httpaste-args}"
|
||||||
|
LABEL org.label-schema.version=$BUILD_VERSION
|
||||||
|
LABEL org.label-schema.build-date=$BUILD_DATE
|
||||||
|
|
||||||
|
WORKDIR /usr/local/src/httpaste
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y libffi-dev gcc fontconfig && \
|
||||||
|
python3 -m pip install pipenv && \
|
||||||
|
python3 -m pipenv install --deploy --system --verbose && \
|
||||||
|
python3 setup.py install && \
|
||||||
|
apt-get remove -y libffi-dev gcc && apt-get autoremove -y && apt-get clean -y
|
||||||
|
|
||||||
|
ENTRYPOINT ["httpaste"]
|
||||||
|
|
||||||
|
|
||||||
|
FROM base as uwsgi
|
||||||
|
|
||||||
|
ENTRYPOINT ["uwsgi", "--master", "--enable-threads", "--manage-script-name", "-w", "httpaste.wsgi:application"]
|
||||||
|
|
||||||
|
CMD ["-s", "/tmp/yourapplication.sock"]
|
||||||
5
Pipfile
5
Pipfile
|
|
@ -7,7 +7,10 @@ name = 'pypi'
|
||||||
python_version = '3'
|
python_version = '3'
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
httpaste = {editable = true, path = "."}
|
httpaste-victorykit = {editable = true, path = "."}
|
||||||
|
flup = '==1.0.3'
|
||||||
|
mysql-connector-python = '==8.0.28'
|
||||||
|
uWSGI = '==2.0.20'
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
tox = '==3.23.0'
|
tox = '==3.23.0'
|
||||||
170
Pipfile.lock
generated
170
Pipfile.lock
generated
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "6fc8f1480cab514207ed13c95c3533fd240e04aa466d8fe781b969aa42b6313d"
|
"sha256": "e8725ecbf33a0d4931d941bfa72dcb15bbcdbdcff1048ea65a4025146018a498"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -96,11 +96,11 @@
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5e0d195c2067da3136efb897449ec1e9e6c98282fbf30d7f9e164af9be901a6b",
|
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
|
||||||
"sha256:7ab900e38149c9872376e8f9b5986ddcaf68c0f413cf73678a0bca5547e6f976"
|
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==8.1.1"
|
"version": "==8.1.2"
|
||||||
},
|
},
|
||||||
"clickclick": {
|
"clickclick": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -110,9 +110,6 @@
|
||||||
"version": "==20.10.2"
|
"version": "==20.10.2"
|
||||||
},
|
},
|
||||||
"connexion": {
|
"connexion": {
|
||||||
"extras": [
|
|
||||||
"swagger-ui"
|
|
||||||
],
|
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0ba5c163d34cb3cb3bf597d5b95fc14bad5d3596bf10ec86e32cdb63f68d0c8a",
|
"sha256:0ba5c163d34cb3cb3bf597d5b95fc14bad5d3596bf10ec86e32cdb63f68d0c8a",
|
||||||
"sha256:26a570a0283bbe4cdaf5d90dfb3441aaf8e18cb9de10f3f96bbc128a8a3d8b47"
|
"sha256:26a570a0283bbe4cdaf5d90dfb3441aaf8e18cb9de10f3f96bbc128a8a3d8b47"
|
||||||
|
|
@ -154,9 +151,13 @@
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.1.1"
|
"version": "==2.1.1"
|
||||||
},
|
},
|
||||||
"httpaste": {
|
"flup": {
|
||||||
"editable": true,
|
"hashes": [
|
||||||
"path": "."
|
"sha256:5eb09f26eb0751f8380d8ac43d1dfb20e1d42eca0fa45ea9289fa532a79cd159",
|
||||||
|
"sha256:ca9fd78e1cc0431da1236f73fafd1c01db684675b4d369460d5f5c62e6f0b8d6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.0.3"
|
||||||
},
|
},
|
||||||
"httpaste-victorykit": {
|
"httpaste-victorykit": {
|
||||||
"editable": true,
|
"editable": true,
|
||||||
|
|
@ -256,6 +257,33 @@
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.1.1"
|
"version": "==2.1.1"
|
||||||
},
|
},
|
||||||
|
"mysql-connector-python": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:04d75ec7c181e7907df3d40c2a573063f25ecfc5a95a7a90374861c02ce738a9",
|
||||||
|
"sha256:47f059bc2a7378acd56ac7a60b0526a2ba95d96b696a875b5b233a0feae30980",
|
||||||
|
"sha256:4d126ce5e03675d926a9e49ce1638d06af43ca7bac2b502d93373dc9425d386f",
|
||||||
|
"sha256:50c87ff50762f4a0cc0816365dde0e7de763949e125488b8e872de6471e0e427",
|
||||||
|
"sha256:687071dc9e51892d0861bbcbcbd48e0f3579e3155f2a0ec310198704137c775a",
|
||||||
|
"sha256:73c5149b33401610e28589d1fc669cba11d3b16215a8f6a75f63ece1f3af5f88",
|
||||||
|
"sha256:77ec293e265d01db1896a8e63a16b3d5c848a885cf76c77148adfed8453846e8",
|
||||||
|
"sha256:78bb1abb57bbb85263d65a240a901195e3de0e0992f25e42c48af0869079bb74",
|
||||||
|
"sha256:7d518491d6d51b186b3182b3698b1560d9bd80675c055163359d0aeea0001de1",
|
||||||
|
"sha256:8d8dd02e0e6bb7262156a836c3e83582d1a1a1ebb9d72e777a46813709404601",
|
||||||
|
"sha256:91be638d1b084835edf7aa426d85228174611a1cd6f016ca0f6d4339ac3d9d7b",
|
||||||
|
"sha256:aaec9d13fc0177e421a3c4392f0eaf86347b825949d5dfc202d535cdb1e07f04",
|
||||||
|
"sha256:b3a747c5efd6de7b76686ab93834186e2276a62684600dbede615537040436ca",
|
||||||
|
"sha256:b4c5ce835078555b6640921cae036daad46884dd21027f43c742fb505221e4e6",
|
||||||
|
"sha256:bb317b179bfbb3e86c771bb2b34794188a2d2b010cdaa1b4d1b5ea0961d0812c",
|
||||||
|
"sha256:bd89598b173aa0fc525b59fff6e3598ff3cabad4260a3bb49cf420eac10d3b3b",
|
||||||
|
"sha256:bdb4f187f737316d1c403085b2fb7c91717268d052ecbfc86066cef59f6d72a4",
|
||||||
|
"sha256:c76d771fdce1314b07619efff184ec03f56abef6b4ccdc686d3a995f5b225fec",
|
||||||
|
"sha256:d559f69e8b58ac248e37d30e5676718adf69eeff56ed8a7c03f064d74af68f99",
|
||||||
|
"sha256:e008127430c8dc66bb1b6d6c7a17498ec57ffa81188fc1f8c9f764363c01d12e",
|
||||||
|
"sha256:f5da43c77d409c8135132f5b5aee9ac91c2e97c3f87352e1b3017438a9cb9b82"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==8.0.28"
|
||||||
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
||||||
|
|
@ -264,6 +292,80 @@
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==21.3"
|
"version": "==21.3"
|
||||||
},
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e",
|
||||||
|
"sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595",
|
||||||
|
"sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512",
|
||||||
|
"sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c",
|
||||||
|
"sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477",
|
||||||
|
"sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a",
|
||||||
|
"sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4",
|
||||||
|
"sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e",
|
||||||
|
"sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5",
|
||||||
|
"sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378",
|
||||||
|
"sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a",
|
||||||
|
"sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652",
|
||||||
|
"sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7",
|
||||||
|
"sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a",
|
||||||
|
"sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a",
|
||||||
|
"sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6",
|
||||||
|
"sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165",
|
||||||
|
"sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160",
|
||||||
|
"sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331",
|
||||||
|
"sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b",
|
||||||
|
"sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458",
|
||||||
|
"sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033",
|
||||||
|
"sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8",
|
||||||
|
"sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481",
|
||||||
|
"sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58",
|
||||||
|
"sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7",
|
||||||
|
"sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3",
|
||||||
|
"sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea",
|
||||||
|
"sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34",
|
||||||
|
"sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3",
|
||||||
|
"sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8",
|
||||||
|
"sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581",
|
||||||
|
"sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244",
|
||||||
|
"sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef",
|
||||||
|
"sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0",
|
||||||
|
"sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2",
|
||||||
|
"sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97",
|
||||||
|
"sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==9.1.0"
|
||||||
|
},
|
||||||
|
"protobuf": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:001c2160c03b6349c04de39cf1a58e342750da3632f6978a1634a3dcca1ec10e",
|
||||||
|
"sha256:0b250c60256c8824219352dc2a228a6b49987e5bf94d3ffcf4c46585efcbd499",
|
||||||
|
"sha256:1d24c81c2310f0063b8fc1c20c8ed01f3331be9374b4b5c2de846f69e11e21fb",
|
||||||
|
"sha256:1eb13f5a5a59ca4973bcfa2fc8fff644bd39f2109c3f7a60bd5860cb6a49b679",
|
||||||
|
"sha256:25d2fcd6eef340082718ec9ad2c58d734429f2b1f7335d989523852f2bba220b",
|
||||||
|
"sha256:32bf4a90c207a0b4e70ca6dd09d43de3cb9898f7d5b69c2e9e3b966a7f342820",
|
||||||
|
"sha256:38fd9eb74b852e4ee14b16e9670cd401d147ee3f3ec0d4f7652e0c921d6227f8",
|
||||||
|
"sha256:47257d932de14a7b6c4ae1b7dbf592388153ee35ec7cae216b87ae6490ed39a3",
|
||||||
|
"sha256:4eda68bd9e2a4879385e6b1ea528c976f59cd9728382005cc54c28bcce8db983",
|
||||||
|
"sha256:52bae32a147c375522ce09bd6af4d2949aca32a0415bc62df1456b3ad17c6001",
|
||||||
|
"sha256:542f25a4adf3691a306dcc00bf9a73176554938ec9b98f20f929a044f80acf1b",
|
||||||
|
"sha256:5b5860b790498f233cdc8d635a17fc08de62e59d4dcd8cdb6c6c0d38a31edf2b",
|
||||||
|
"sha256:6efe066a7135233f97ce51a1aa007d4fb0be28ef093b4f88dac4ad1b3a2b7b6f",
|
||||||
|
"sha256:71b2c3d1cd26ed1ec7c8196834143258b2ad7f444efff26fdc366c6f5e752702",
|
||||||
|
"sha256:7a53d4035427b9dbfbb397f46642754d294f131e93c661d056366f2a31438263",
|
||||||
|
"sha256:7dcd84dc31ebb35ade755e06d1561d1bd3b85e85dbdbf6278011fc97b22810db",
|
||||||
|
"sha256:88c8be0558bdfc35e68c42ae5bf785eb9390d25915d4863bbc7583d23da77074",
|
||||||
|
"sha256:8be43a91ab66fe995e85ccdbdd1046d9f0443d59e060c0840319290de25b7d33",
|
||||||
|
"sha256:8d84453422312f8275455d1cb52d850d6a4d7d714b784e41b573c6f5bfc2a029",
|
||||||
|
"sha256:9d0f3aca8ca51c8b5e204ab92bd8afdb2a8e3df46bd0ce0bd39065d79aabcaa4",
|
||||||
|
"sha256:a1eebb6eb0653e594cb86cd8e536b9b083373fca9aba761ade6cd412d46fb2ab",
|
||||||
|
"sha256:bc14037281db66aa60856cd4ce4541a942040686d290e3f3224dd3978f88f554",
|
||||||
|
"sha256:fbcbb068ebe67c4ff6483d2e2aa87079c325f8470b24b098d6bf7d4d21d57a69",
|
||||||
|
"sha256:fd7133b885e356fa4920ead8289bb45dc6f185a164e99e10279f33732ed5ce15"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.20.0"
|
||||||
|
},
|
||||||
"pycparser": {
|
"pycparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
|
"sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9",
|
||||||
|
|
@ -281,11 +383,11 @@
|
||||||
},
|
},
|
||||||
"pyparsing": {
|
"pyparsing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
|
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
|
||||||
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
|
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version >= '3.6.8'",
|
||||||
"version": "==3.0.7"
|
"version": "==3.0.8"
|
||||||
},
|
},
|
||||||
"pyrsistent": {
|
"pyrsistent": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -361,13 +463,6 @@
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||||
"version": "==2.27.1"
|
"version": "==2.27.1"
|
||||||
},
|
},
|
||||||
"swagger-ui-bundle": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:b462aa1460261796ab78fd4663961a7f6f347ce01760f1303bbbdf630f11f516",
|
|
||||||
"sha256:cea116ed81147c345001027325c1ddc9ca78c1ee7319935c3c75d3669279d575"
|
|
||||||
],
|
|
||||||
"version": "==0.0.9"
|
|
||||||
},
|
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
|
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
|
||||||
|
|
@ -376,21 +471,28 @@
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
"version": "==1.26.9"
|
"version": "==1.26.9"
|
||||||
},
|
},
|
||||||
|
"uwsgi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0.20"
|
||||||
|
},
|
||||||
"werkzeug": {
|
"werkzeug": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:094ecfc981948f228b30ee09dbfe250e474823b69b9b1292658301b5894bbf08",
|
"sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6",
|
||||||
"sha256:9b55466a3e99e13b1f0686a66117d39bda85a992166e0a79aedfcf3586328f7a"
|
"sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.1.0"
|
"version": "==2.1.1"
|
||||||
},
|
},
|
||||||
"zipp": {
|
"zipp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
|
"sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
|
||||||
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
|
"sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==3.7.0"
|
"version": "==3.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
|
|
@ -443,11 +545,11 @@
|
||||||
},
|
},
|
||||||
"pyparsing": {
|
"pyparsing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
|
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
|
||||||
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
|
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_full_version >= '3.6.8'",
|
||||||
"version": "==3.0.7"
|
"version": "==3.0.8"
|
||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -475,11 +577,11 @@
|
||||||
},
|
},
|
||||||
"virtualenv": {
|
"virtualenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66",
|
"sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a",
|
||||||
"sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"
|
"sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==20.14.0"
|
"version": "==20.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**NOTE**: httpaste is publicly hosted at [httpaste.it](http://httpaste.it) and as a hidden Tor service ([https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion](https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion)).
|
**NOTE**: httpaste is publicly hosted at [httpaste.it](http://httpaste.it) and as a [Tor Onion Service](https://community.torproject.org/onion-services/overview/) ([http://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion](http://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion)).
|
||||||
Both services are to be considered evaluatory, as long as the source code
|
Both services are to be considered evaluatory, as long as the source code
|
||||||
is in pre-release. Regarding voidance of pre-release status, see [Open Issues](https://victorykit.atlassian.net/issues/?jql=project%20%3D%20HTTPASTE%20AND%20fixVersion%20in%20(1.1.0-beta%2C%201.2.0-beta%2C%201.3.0)), for more information.
|
is in pre-release. Regarding voidance of pre-release status, see [Open Issues](https://victorykit.atlassian.net/issues/?jql=project%20%3D%20HTTPASTE%20AND%20fixVersion%20in%20(1.1.0-beta%2C%201.2.0-beta%2C%201.3.0)), for more information.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ httpaste - versatile HTTP pastebin
|
||||||
.. image:: _assets/images/favpng_parrot-royalty-free-cartoon.png
|
.. image:: _assets/images/favpng_parrot-royalty-free-cartoon.png
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
httpaste is publicly hosted at `httpaste.it`_ and as a hidden Tor service (`<https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion>`_).
|
httpaste is publicly hosted at `httpaste.it`_ and as a `Tor Onion Service`_ (`<http://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion>`_).
|
||||||
Both services are to be considered evaluatory, as long as the source code
|
Both services are to be considered evaluatory, as long as the source code
|
||||||
is in pre-release. Regarding voidance of pre-release status, see `Open Issues`_, for more information.
|
is in pre-release. Regarding voidance of pre-release status, see `Open Issues`_, for more information.
|
||||||
|
|
||||||
|
|
@ -79,6 +79,8 @@ This program uses licensed third-party software.
|
||||||
ARCHITECTURE
|
ARCHITECTURE
|
||||||
CONTRIBUTING
|
CONTRIBUTING
|
||||||
|
|
||||||
|
|
||||||
|
.. _Tor Onion Service: https://community.torproject.org/onion-services/overview/
|
||||||
.. _ix.io: http://ix.io/
|
.. _ix.io: http://ix.io/
|
||||||
.. _sprunge.us: http://sprunge.us
|
.. _sprunge.us: http://sprunge.us
|
||||||
.. _pygments: https://pygments.org/
|
.. _pygments: https://pygments.org/
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,17 @@ The backend can be configured within the `[backend]` section of the configuratio
|
||||||
SQLite
|
SQLite
|
||||||
------
|
------
|
||||||
|
|
||||||
.. autoclass:: httpaste.backend.sqlite.Parameters
|
.. autoclass:: httpaste.backend.sqlite.Config
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Filesystem
|
Filesystem
|
||||||
----------
|
----------
|
||||||
|
|
||||||
.. autoclass:: httpaste.backend.file.Parameters
|
.. autoclass:: httpaste.backend.file.Config
|
||||||
|
:members:
|
||||||
|
|
||||||
|
MySQL
|
||||||
|
-----
|
||||||
|
|
||||||
|
.. autoclass:: httpaste.backend.mysql.Config
|
||||||
:members:
|
:members:
|
||||||
43
samples/httpaste.it/docker-compose.yml
Normal file
43
samples/httpaste.it/docker-compose.yml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
httpaste:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: uwsgi
|
||||||
|
environment:
|
||||||
|
HTTPASTE_CONFIGPATH: /usr/local/httpaste/config.ini
|
||||||
|
volumes:
|
||||||
|
-
|
||||||
|
type: volume
|
||||||
|
source: system-shared
|
||||||
|
target: /shared
|
||||||
|
volume:
|
||||||
|
nocopy: true
|
||||||
|
- ./httpaste/usr/local/httpaste/config.ini:/usr/local/httpaste/config.ini
|
||||||
|
command: -s /shared/uwsgi.sock --chmod-socket=666
|
||||||
|
httpd:
|
||||||
|
build:
|
||||||
|
context: ./httpd
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
-
|
||||||
|
type: volume
|
||||||
|
source: system-shared
|
||||||
|
target: /shared
|
||||||
|
volume:
|
||||||
|
nocopy: true
|
||||||
|
- ./httpd/usr/local/apache2/conf/httpd.conf:/usr/local/apache2/conf/httpd.conf
|
||||||
|
- ./httpd/usr/local/apache2/ssl:/usr/local/apache2/ssl
|
||||||
|
tor:
|
||||||
|
build:
|
||||||
|
context: ./tor
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- ./tor/etc/tor/torrc:/etc/tor/torrc
|
||||||
|
- ./tor/var/lib/tor/hidden_service:/tor/var/lib/tor/hidden_service
|
||||||
|
volumes:
|
||||||
|
system-shared:
|
||||||
17
samples/httpaste.it/httpaste.service
Normal file
17
samples/httpaste.it/httpaste.service
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=httpaste (via Docker Compose)
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/usr/local/src/httpaste/samples/httpaste.it
|
||||||
|
ExecStart=docker-compose up
|
||||||
|
ExecStop=docker-compose down
|
||||||
|
TimeoutStartSec=0
|
||||||
|
Restart=on-failure
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
StartLimitBurst=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
38
samples/httpaste.it/httpaste/usr/local/httpaste/config.ini
Normal file
38
samples/httpaste.it/httpaste/usr/local/httpaste/config.ini
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
[context]
|
||||||
|
salt = '&)UxB-_$Lk$m=CB}dw[d85{-ZWR?uUNx'
|
||||||
|
hmac_iter = 20000
|
||||||
|
|
||||||
|
[model.paste]
|
||||||
|
default_encoding = 'utf-8'
|
||||||
|
id_size = 8
|
||||||
|
key_size = 32
|
||||||
|
default_lifetime = 5
|
||||||
|
default_max_lifetime = 1440
|
||||||
|
|
||||||
|
|
||||||
|
[controller.paste]
|
||||||
|
default_mime_type = 'text/plain'
|
||||||
|
default_linenos = False
|
||||||
|
default_syntax = False
|
||||||
|
default_formatter = 'terminal256'
|
||||||
|
|
||||||
|
|
||||||
|
[backend]
|
||||||
|
type = file
|
||||||
|
|
||||||
|
[backend.file]
|
||||||
|
base_dirname = 'sample_data'
|
||||||
|
|
||||||
|
[backend.sqlite]
|
||||||
|
path = 'devel/sample.db'
|
||||||
|
|
||||||
|
[backend.mysql]
|
||||||
|
user = 'example-user'
|
||||||
|
password = 'my_cool_secret'
|
||||||
|
database = 'httpaste'
|
||||||
|
host = '127.0.0.1'
|
||||||
|
|
||||||
|
|
||||||
|
[server]
|
||||||
|
swagger_ui = False
|
||||||
|
bind_address = 'sample.sock'
|
||||||
14
samples/httpaste.it/httpd/Dockerfile
Normal file
14
samples/httpaste.it/httpd/Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
FROM httpd:2.4
|
||||||
|
|
||||||
|
RUN apt-get update -y && apt-get install -y \
|
||||||
|
libapache2-mod-proxy-uwsgi \
|
||||||
|
libapache2-mod-evasive \
|
||||||
|
libapache2-mod-security2
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/local/apache2/crs-tecmint
|
||||||
|
|
||||||
|
ADD https://github.com/SpiderLabs/owasp-modsecurity-crs/archive/refs/tags/v3.2.0.tar.gz /usr/local/apache2/crs/master
|
||||||
|
|
||||||
|
RUN cd /usr/local/apache2/crs && \
|
||||||
|
tar -xzf master && \
|
||||||
|
cp owasp-modsecurity-crs-3.2.0/crs-setup.conf.example owasp-modsecurity-crs-3.2.0/crs-setup.conf
|
||||||
107
samples/httpaste.it/httpd/usr/local/apache2/conf/httpd.conf
Normal file
107
samples/httpaste.it/httpd/usr/local/apache2/conf/httpd.conf
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
|
||||||
|
ServerRoot "/usr/local/apache2"
|
||||||
|
|
||||||
|
Listen 0.0.0.0:80
|
||||||
|
|
||||||
|
LoadModule mpm_event_module modules/mod_mpm_event.so
|
||||||
|
LoadModule authn_core_module modules/mod_authn_core.so
|
||||||
|
LoadModule authz_core_module modules/mod_authz_core.so
|
||||||
|
#LoadModule brotli_module modules/mod_brotli.so
|
||||||
|
LoadModule mime_module modules/mod_mime.so
|
||||||
|
LoadModule log_config_module modules/mod_log_config.so
|
||||||
|
#LoadModule log_debug_module modules/mod_log_debug.so
|
||||||
|
#LoadModule log_forensic_module modules/mod_log_forensic.so
|
||||||
|
LoadModule env_module modules/mod_env.so
|
||||||
|
LoadModule proxy_module modules/mod_proxy.so
|
||||||
|
LoadModule proxy_uwsgi_module modules/mod_proxy_uwsgi.so
|
||||||
|
LoadModule unixd_module modules/mod_unixd.so
|
||||||
|
LoadModule access_compat_module modules/mod_access_compat.so
|
||||||
|
LoadModule security2_module /usr/lib/apache2/modules/mod_security2.so
|
||||||
|
LoadModule evasive20_module /usr/lib/apache2/modules/mod_evasive20.so
|
||||||
|
LoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so
|
||||||
|
|
||||||
|
<IfModule unixd_module>
|
||||||
|
User www-data
|
||||||
|
Group www-data
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
ServerAdmin you@example.com
|
||||||
|
ServerSignature Off
|
||||||
|
ServerTokens Prod
|
||||||
|
|
||||||
|
<IfModule security2_module>
|
||||||
|
Include crs/owasp-modsecurity-crs-3.2.0/crs-setup.conf
|
||||||
|
Include crs/owasp-modsecurity-crs-3.2.0/rules/*.conf
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule mod_evasive24.c>
|
||||||
|
DOSHashTableSize 3097
|
||||||
|
DOSPageCount 3
|
||||||
|
DOSSiteCount 10
|
||||||
|
DOSPageInterval 1
|
||||||
|
DOSSiteInterval 1
|
||||||
|
DOSBlockingPeriod 10
|
||||||
|
DOSCloseSocket On
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
ErrorLog /proc/self/fd/2
|
||||||
|
|
||||||
|
LogLevel warn
|
||||||
|
|
||||||
|
<IfModule log_config_module>
|
||||||
|
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||||
|
LogFormat "%h %l %u %t \"%r\" %>s %b" common
|
||||||
|
|
||||||
|
<IfModule logio_module>
|
||||||
|
# You need to enable mod_logio.c to use %I and %O
|
||||||
|
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
CustomLog /proc/self/fd/1 common
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
<IfModule ssl_module>
|
||||||
|
SSLRandomSeed startup builtin
|
||||||
|
SSLRandomSeed connect builtin
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
ServerName 127.0.0.1
|
||||||
|
|
||||||
|
<VirtualHost *:80>
|
||||||
|
<Location />
|
||||||
|
Deny from all
|
||||||
|
Allow from none
|
||||||
|
</Location>
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost 0.0.0.0:80>
|
||||||
|
#ProxyPreserveHost On
|
||||||
|
ServerName httpaste.it
|
||||||
|
ServerAlias localhost
|
||||||
|
SetEnv proxy-sendchunks
|
||||||
|
ProxyPass "/" "unix:/shared/uwsgi.sock|uwsgi://localhost/"
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost 0.0.0.0:80>
|
||||||
|
#ProxyPreserveHost On
|
||||||
|
ServerAlias *.onion
|
||||||
|
SetEnv proxy-sendchunks
|
||||||
|
ProxyPass "/" "unix:/shared/uwsgi.sock|uwsgi://localhost/"
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<IfFile 'ssl/private.key'>
|
||||||
|
Listen 0.0.0.0:443
|
||||||
|
|
||||||
|
|
||||||
|
<VirtualHost 0.0.0.0:443>
|
||||||
|
#ProxyPreserveHost On
|
||||||
|
ServerName httpaste.it
|
||||||
|
ServerAlias localhost
|
||||||
|
SSLEngine on
|
||||||
|
SSLCertificateFile "ssl/certificate.crt"
|
||||||
|
SSLCertificateChainFile "ssl/ca_bundle.crt"
|
||||||
|
SSLCertificateKeyFile "ssl/private.key"
|
||||||
|
SetEnv proxy-sendchunks
|
||||||
|
ProxyPass "/" "unix:/shared/uwsgi.sock|uwsgi://localhost/"
|
||||||
|
</VirtualHost>
|
||||||
|
</IfFile>
|
||||||
2
samples/httpaste.it/httpd/usr/local/apache2/ssl/.gitignore
vendored
Normal file
2
samples/httpaste.it/httpd/usr/local/apache2/ssl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.key
|
||||||
|
*.crt
|
||||||
10
samples/httpaste.it/tor/Dockerfile
Normal file
10
samples/httpaste.it/tor/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
FROM debian:bullseye-slim
|
||||||
|
|
||||||
|
RUN apt-get update -y && apt-get install -y tor
|
||||||
|
|
||||||
|
COPY ./usr/local/sbin/hostname.sh /usr/local/sbin/hostname
|
||||||
|
RUN chmod +x /usr/local/sbin/hostname
|
||||||
|
|
||||||
|
USER debian-tor
|
||||||
|
|
||||||
|
ENTRYPOINT ["tor"]
|
||||||
3
samples/httpaste.it/tor/etc/tor/torrc
Normal file
3
samples/httpaste.it/tor/etc/tor/torrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
DataDirectory /var/lib/tor
|
||||||
|
HiddenServiceDir /var/lib/tor/hidden_service/
|
||||||
|
HiddenServicePort 80 httpd:80
|
||||||
3
samples/httpaste.it/tor/usr/local/sbin/hostname.sh
Executable file
3
samples/httpaste.it/tor/usr/local/sbin/hostname.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
prop=HiddenServiceDir
|
||||||
|
cat $(grep $prop /etc/tor/torrc | sed "s/$prop //g")/hostname
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
*
|
||||||
|
|
@ -23,6 +23,7 @@ install_requires =
|
||||||
connexion>=2.13.0,<3
|
connexion>=2.13.0,<3
|
||||||
cryptography>=36.0.2,<37
|
cryptography>=36.0.2,<37
|
||||||
pygments>=2.11.2,<3
|
pygments>=2.11.2,<3
|
||||||
|
Pillow>=9.1.0,<10
|
||||||
zip_safe = true
|
zip_safe = true
|
||||||
package_dir =
|
package_dir =
|
||||||
=src
|
=src
|
||||||
|
|
@ -41,3 +42,4 @@ where = src
|
||||||
* =
|
* =
|
||||||
*.json
|
*.json
|
||||||
*.sql
|
*.sql
|
||||||
|
*.html
|
||||||
|
|
@ -9,8 +9,6 @@ SYNOPSIS
|
||||||
|
|
||||||
HTTP [POST|PUT|DELETE|GET] {url}paste/[public|private]
|
HTTP [POST|PUT|DELETE|GET] {url}paste/[public|private]
|
||||||
|
|
||||||
{url}ui
|
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
|
|
||||||
This program offers an HTTP interface for storing public and private data
|
This program offers an HTTP interface for storing public and private data
|
||||||
|
|
@ -21,7 +19,7 @@ DESCRIPTION
|
||||||
listed on any index, since it isn't technically possible (by design).
|
listed on any index, since it isn't technically possible (by design).
|
||||||
|
|
||||||
All pastes are symetrically encrypted with an HMAC derived key using
|
All pastes are symetrically encrypted with an HMAC derived key using
|
||||||
{hmac_iterations} iterations and SHA-512 hashing, a server-side salt and a
|
{hmac_iterations} iterations and SHA-256 hashing, a server-side salt and a
|
||||||
randomly generated password. Public paste's passwords are derived from
|
randomly generated password. Public paste's passwords are derived from
|
||||||
their ids. Private paste's passwords are randomly generated and stored
|
their ids. Private paste's passwords are randomly generated and stored
|
||||||
inside a symetrically encrypted personal database, with the encryption key
|
inside a symetrically encrypted personal database, with the encryption key
|
||||||
|
|
@ -115,12 +113,12 @@ EXAMPLES
|
||||||
|
|
||||||
SEE ALSO
|
SEE ALSO
|
||||||
|
|
||||||
Documentation <https://victorykit.bitbucket.org/httpaste>
|
Documentation <https://victorykit.bitbucket.io/httpaste>
|
||||||
|
|
||||||
Sources <https://bitbucket.org/victorykit/httpaste>
|
Sources <https://bitbucket.org/victorykit/httpaste>
|
||||||
|
|
||||||
Host (HTTPS) <https://httpaste.it>
|
Host (HTTP) <http://httpaste.it>
|
||||||
(HTTP) <http://httpaste.it>
|
(Onion) <http://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion>
|
||||||
|
|
||||||
NOTES
|
NOTES
|
||||||
|
|
||||||
|
|
@ -137,21 +135,24 @@ NOTES
|
||||||
SUCH DAMAGES.
|
SUCH DAMAGES.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import NamedTuple, Tuple, Any
|
from typing import NamedTuple
|
||||||
from string import ascii_uppercase, digits, ascii_letters, punctuation
|
|
||||||
from inspect import isclass
|
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from ast import literal_eval
|
|
||||||
from io import StringIO
|
|
||||||
from os import environ
|
|
||||||
from importlib.resources import path as resource_path
|
from importlib.resources import path as resource_path
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from connexion import FlaskApp
|
from connexion import FlaskApp
|
||||||
from connexion.resolver import RestyResolver
|
from connexion.resolver import RestyResolver
|
||||||
|
|
||||||
from httpaste.model import Backend
|
from httpaste.server import get_server_config
|
||||||
from httpaste.backend import get_backend_map
|
from httpaste.server import Config as ServerConfig
|
||||||
from httpaste.helper.common import generate_random_string
|
from httpaste.context import get_context_config
|
||||||
|
from httpaste.context import Config as ContextConfig
|
||||||
|
from httpaste.model import get_model_config
|
||||||
|
from httpaste.model import Config as ModelConfig
|
||||||
|
from httpaste.backend import get_backend_config
|
||||||
|
from httpaste.backend import Config as BackendConfig
|
||||||
|
from httpaste.helper.config import get_configparser, CONFIGPATH_ENVIRON
|
||||||
|
from httpaste.helper.url import url_upgrade_to_https, url_has_tld
|
||||||
from httpaste.helper.http import (
|
from httpaste.helper.http import (
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
|
|
@ -160,147 +161,48 @@ from httpaste.helper.http import (
|
||||||
UnauthorizedError)
|
UnauthorizedError)
|
||||||
|
|
||||||
|
|
||||||
CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIGPATH'
|
class Config(NamedTuple):
|
||||||
|
|
||||||
|
|
||||||
def get_sanitized_config_charset(charset: str):
|
|
||||||
|
|
||||||
for x in ["$", "%"]:
|
|
||||||
|
|
||||||
charset = charset.replace(x, f'{x}{x}')
|
|
||||||
|
|
||||||
return charset
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigError(Exception):
|
|
||||||
"""Config Exception
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
"""httpaste global config
|
|
||||||
"""
|
"""
|
||||||
salt: bytes = get_sanitized_config_charset(generate_random_string(
|
context: ContextConfig
|
||||||
32, ascii_letters + digits + punctuation)).encode('utf-8')
|
server: ServerConfig
|
||||||
paste_id_size: int = 8
|
model: ModelConfig
|
||||||
paste_id_charset: str = ascii_letters + digits
|
backend: BackendConfig
|
||||||
paste_key_size: int = 32
|
|
||||||
paste_key_charset: str = get_sanitized_config_charset(
|
|
||||||
ascii_letters + digits + punctuation)
|
|
||||||
paste_lifetime: int = 5
|
|
||||||
backend: Backend = None
|
|
||||||
hmac_iterations: int = 20000
|
|
||||||
paste_default_encoding: str = 'utf-8'
|
|
||||||
|
|
||||||
|
|
||||||
class ServerConfig:
|
def get_config(configIni: ConfigParser, path: Path):
|
||||||
"""connexion config
|
|
||||||
"""
|
|
||||||
swagger_ui: bool = True
|
|
||||||
bind_address = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_config_path(var_name: str = CONFIGPATH_ENVIRON):
|
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
from httpaste.model import Config as ModelConfig
|
||||||
|
|
||||||
return environ[var_name]
|
context_config = get_context_config(configIni)
|
||||||
except KeyError as e:
|
server_config = get_server_config(configIni)
|
||||||
|
model_config = get_model_config(configIni, path)
|
||||||
|
backend_config = get_backend_config(configIni, path)
|
||||||
|
|
||||||
raise ConfigError(
|
return Config(
|
||||||
f'environment variable \'{var_name}\' not set.') from e
|
context=context_config,
|
||||||
|
server=server_config,
|
||||||
|
model=model_config,
|
||||||
|
backend=backend_config
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str) -> Tuple[Config, ServerConfig]:
|
def load_config(path: str = None, var_name: str = CONFIGPATH_ENVIRON):
|
||||||
"""get config objects from file
|
|
||||||
"""
|
|
||||||
|
|
||||||
_config = ConfigParser()
|
|
||||||
_config.read(path)
|
|
||||||
|
|
||||||
backends = get_backend_map()
|
|
||||||
bconf = dict(_config.items('backend'))
|
|
||||||
btype = bconf.pop('type')
|
|
||||||
|
|
||||||
try:
|
|
||||||
bcl, bparamcl = backends[btype]
|
|
||||||
except KeyError as e:
|
|
||||||
bids = ', '.join(backends.keys())
|
|
||||||
raise ConfigError(' '.join((
|
|
||||||
f'invalid backend \'{btype}\' in \'{path}\'. ',
|
|
||||||
f'must be any of [{bids}]'
|
|
||||||
))) from e
|
|
||||||
|
|
||||||
config = dict(_config.items('general'))
|
|
||||||
server_config = dict(_config.items('server'))
|
|
||||||
|
|
||||||
c = Config()
|
|
||||||
sc = ServerConfig()
|
|
||||||
|
|
||||||
# typecast model_backend section items
|
|
||||||
bconf = {k: literal_eval(v) for k, v in bconf.items()}
|
|
||||||
# initialize model backend
|
|
||||||
c.backend = bcl(bparamcl(**bconf))
|
|
||||||
|
|
||||||
# typecast general section items
|
|
||||||
for k, v in config.items():
|
|
||||||
setattr(c, k, literal_eval(v))
|
|
||||||
# typecast server section items
|
|
||||||
for k, v in server_config.items():
|
|
||||||
setattr(sc, k, literal_eval(v))
|
|
||||||
|
|
||||||
c.salt = c.salt.encode('utf-8')
|
|
||||||
|
|
||||||
return c, sc
|
|
||||||
|
|
||||||
|
|
||||||
def default_config() -> str:
|
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = ConfigParser()
|
configIni, path = get_configparser(path, var_name)
|
||||||
|
|
||||||
config['general'] = {
|
return get_config(configIni, Path(path).resolve().parent)
|
||||||
'salt': Config.salt.decode('utf-8'),
|
|
||||||
'paste_key_charset': Config.paste_key_charset,
|
|
||||||
'paste_id_charset': Config.paste_id_charset
|
|
||||||
}
|
|
||||||
|
|
||||||
for literal in [
|
|
||||||
'paste_id_size',
|
|
||||||
'paste_key_size',
|
|
||||||
'paste_lifetime'
|
|
||||||
]:
|
|
||||||
config['general'][literal] = str(getattr(Config, literal))
|
|
||||||
|
|
||||||
config['backend'] = {
|
|
||||||
'type': 'sqlite',
|
|
||||||
'path': 'file::memory:?cache=shared'
|
|
||||||
}
|
|
||||||
|
|
||||||
config['server'] = {}
|
|
||||||
for literal in [
|
|
||||||
'swagger_ui',
|
|
||||||
'bind_address'
|
|
||||||
]:
|
|
||||||
config['server'][literal] = str(getattr(ServerConfig, literal))
|
|
||||||
|
|
||||||
stream = StringIO()
|
|
||||||
config.write(stream)
|
|
||||||
stream.seek(0)
|
|
||||||
|
|
||||||
return stream.read()
|
|
||||||
|
|
||||||
|
|
||||||
def get_flask_app(
|
def get_flask_app(config: Config) -> FlaskApp:
|
||||||
config: Config,
|
|
||||||
server_config: ServerConfig = ServerConfig) -> FlaskApp:
|
|
||||||
"""get a flask app object
|
"""get a flask app object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options = {"swagger_ui": server_config.swagger_ui}
|
options = {"swagger_ui": config.server.swagger_ui}
|
||||||
|
|
||||||
#context manager returns a pathlib.Path object
|
#context manager returns a pathlib.Path object
|
||||||
with resource_path('httpaste.schema', 'httpaste.openapi.json') as path:
|
with resource_path('httpaste.schema', 'httpaste.openapi.json') as path:
|
||||||
|
|
@ -335,13 +237,25 @@ def get_flask_app(
|
||||||
response.headers['WWW-Authenticate'] = 'Basic realm="private"'
|
response.headers['WWW-Authenticate'] = 'Basic realm="private"'
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@application.app.before_request
|
||||||
|
def before_request_func():
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
request._view = {}
|
||||||
|
|
||||||
|
if config.server.request_ssl:
|
||||||
|
|
||||||
|
https_url = url_upgrade_to_https(request.url, config.server.ssl_port)
|
||||||
|
|
||||||
|
if https_url != request.url and not url_has_tld(request.url, 'onion'):
|
||||||
|
|
||||||
|
request._view['before_request__ssl_url'] = https_url
|
||||||
|
|
||||||
return application
|
return application
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
Config,
|
Config,
|
||||||
ServerConfig,
|
|
||||||
load_config,
|
load_config,
|
||||||
default_config,
|
|
||||||
get_flask_app
|
get_flask_app
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ def command_standalone(**kwargs):
|
||||||
'Please install it by running \'python3 -m pip install gevent\'.'
|
'Please install it by running \'python3 -m pip install gevent\'.'
|
||||||
))) from e
|
))) from e
|
||||||
|
|
||||||
config, server_config = load_config(kwargs.get('config_path'))
|
config = load_config(kwargs.get('config_path'))
|
||||||
|
|
||||||
application = get_flask_app(config, server_config)
|
application = get_flask_app(config)
|
||||||
|
|
||||||
http_server = WSGIServer(('', kwargs.get('port')), application)
|
http_server = WSGIServer(('', kwargs.get('port')), application)
|
||||||
http_server.serve_forever()
|
http_server.serve_forever()
|
||||||
|
|
@ -122,7 +122,7 @@ def parser():
|
||||||
|
|
||||||
p_standalone = sp.add_parser('standalone', help=command_standalone.__doc__)
|
p_standalone = sp.add_parser('standalone', help=command_standalone.__doc__)
|
||||||
p_standalone.add_argument('--config-path', '-c', required=True)
|
p_standalone.add_argument('--config-path', '-c', required=True)
|
||||||
p_standalone.add_argument('--port', '-p', default=8080)
|
p_standalone.add_argument('--port', '-p', default=8082)
|
||||||
|
|
||||||
p_wsgi = sp.add_parser('wsgi', help=command_wsgi.__doc__)
|
p_wsgi = sp.add_parser('wsgi', help=command_wsgi.__doc__)
|
||||||
p_wsgi.add_argument('--echo', '-e', action='store_true')
|
p_wsgi.add_argument('--echo', '-e', action='store_true')
|
||||||
|
|
|
||||||
|
|
@ -2,63 +2,114 @@
|
||||||
|
|
||||||
implements backend of model
|
implements backend of model
|
||||||
"""
|
"""
|
||||||
import sys
|
from abc import ABC, abstractmethod
|
||||||
from inspect import isclass
|
from importlib import import_module
|
||||||
from typing import Dict, Tuple
|
from configparser import ConfigParser
|
||||||
|
from typing import NamedTuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from httpaste.model import Backend, UserDataSchema, PasteDataSchema, User, Paste
|
from httpaste.schema import User, Paste, UserDataSchema, PasteDataSchema
|
||||||
from .sqlite import Parameters as SqliteParameters
|
from httpaste.helper.config import get_config, ConfigError
|
||||||
from .sqlite import User as SqliteUser
|
|
||||||
from .sqlite import Paste as SqlitePaste
|
|
||||||
from .sqlite import get_connection as get_sqlite_connection
|
|
||||||
from .file import Parameters as FileParameters
|
|
||||||
from .file import User as FileUser
|
|
||||||
from .file import Paste as FilePaste
|
|
||||||
|
|
||||||
|
|
||||||
class SQLite(Backend):
|
class BackendError(Exception):
|
||||||
"""SQLite backend interface
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameter_class = SqliteParameters
|
|
||||||
user: SqliteUser
|
|
||||||
paste: SqlitePaste
|
|
||||||
|
|
||||||
def __init__(self, parameters: SqliteParameters):
|
class ObjectBackend(ABC):
|
||||||
|
"""
|
||||||
parameters = SqliteParameters(parameters.path, get_sqlite_connection(parameters))
|
|
||||||
|
|
||||||
self.user = SqliteUser(parameters, User)
|
|
||||||
self.paste = SqlitePaste(parameters, Paste)
|
|
||||||
|
|
||||||
|
|
||||||
class File(Backend):
|
|
||||||
"""File backend interface
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameter_class = FileParameters
|
@abstractmethod
|
||||||
user: FileUser
|
def load(self, proto: object) -> object:
|
||||||
paste: FilePaste
|
pass
|
||||||
|
|
||||||
def __init__(self, parameters: FileParameters):
|
@abstractmethod
|
||||||
|
def dump(self, model: object) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
self.user = FileUser(parameters, User, UserDataSchema)
|
@abstractmethod
|
||||||
self.paste = FilePaste(parameters, Paste, PasteDataSchema)
|
def delete(self, proto: object) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def init(self) -> object:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def sanitize(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_backend_map() -> Dict[str, Tuple[type, type]]:
|
class BackendInterface(ABC):
|
||||||
"""get a map of backend ids and their classes
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
mod = sys.modules[__name__]
|
@abstractmethod
|
||||||
out = {}
|
def __init__(self, params: object,
|
||||||
|
user_model_class: type,
|
||||||
|
paste_model_class: type,
|
||||||
|
user_schema: type,
|
||||||
|
paste_schema: type) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
for i in dir(mod):
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def user(self) -> ObjectBackend:
|
||||||
|
pass
|
||||||
|
|
||||||
obj = getattr(mod, i)
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def paste(self) -> ObjectBackend:
|
||||||
|
pass
|
||||||
|
|
||||||
if isclass(obj) and obj.__module__ == __name__:
|
|
||||||
|
|
||||||
out[i.lower()] = (obj, obj.parameter_class)
|
class Config(NamedTuple):
|
||||||
|
"""Backend Configuration
|
||||||
|
"""
|
||||||
|
interface: type
|
||||||
|
config: dict
|
||||||
|
|
||||||
return out
|
|
||||||
|
def load_backend(config: Config) -> BackendInterface:
|
||||||
|
"""load a backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
backend = config.interface(config.config, Paste, User, PasteDataSchema,
|
||||||
|
UserDataSchema)
|
||||||
|
|
||||||
|
return backend
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend_config(configIni: ConfigParser, path:Path) -> Config:
|
||||||
|
"""retrieve a cascaded backend configuration from an INI config object
|
||||||
|
"""
|
||||||
|
|
||||||
|
if 'backend' not in configIni:
|
||||||
|
|
||||||
|
raise ConfigError('missing [backend] section.')
|
||||||
|
|
||||||
|
if 'type' not in configIni['backend']:
|
||||||
|
|
||||||
|
raise ConfigError('missing [backend] \'type\'.')
|
||||||
|
|
||||||
|
mod_name = configIni['backend']['type']
|
||||||
|
|
||||||
|
section = f'backend.{mod_name}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod = import_module(f'.{mod_name}', 'httpaste.backend')
|
||||||
|
except ImportError as e:
|
||||||
|
raise BackendError(f'backend \'{mod_name}\' does not exist: {e}') from e
|
||||||
|
else:
|
||||||
|
interface = mod.Backend
|
||||||
|
config = get_config(configIni, section, mod.Config, path)
|
||||||
|
|
||||||
|
return Config(interface=interface,config=config)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
load_backend,
|
||||||
|
get_backend_config
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -3,110 +3,117 @@
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
from httpaste.backend import BackendInterface as BackendAbc
|
||||||
from . import user
|
from httpaste.backend import ObjectBackend as ObjectBackendAbc
|
||||||
from . import paste
|
|
||||||
|
|
||||||
|
|
||||||
class Parameters(NamedTuple):
|
class Config(NamedTuple):
|
||||||
"""Filesystem backend parameters
|
"""Filesystem backend config
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: path of base directory
|
#: path of base directory
|
||||||
base_dirname: str
|
base_dirname: Path
|
||||||
#: basename of users table directory
|
#: basename of users table directory
|
||||||
user_dirname: Optional[str] = 'users'
|
user_dirname: str = 'users'
|
||||||
#: basename of pastes table directory
|
#: basename of pastes table directory
|
||||||
paste_dirname: Optional[str] = 'pastes'
|
paste_dirname: str = 'pastes'
|
||||||
|
|
||||||
|
|
||||||
class User(object):
|
class ObjectBackendBc(ObjectBackendAbc):
|
||||||
"""Filesystem user model backend
|
|
||||||
"""
|
|
||||||
|
|
||||||
dirname: Path
|
dirname: Path
|
||||||
path: Path
|
path: Path
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parameters: Parameters,
|
interface: object,
|
||||||
|
basename_attr: str,
|
||||||
|
config: Config,
|
||||||
model_class: type,
|
model_class: type,
|
||||||
model_schema: type):
|
model_schema: type):
|
||||||
|
|
||||||
|
self.interface = interface
|
||||||
self.model_class = model_class
|
self.model_class = model_class
|
||||||
|
|
||||||
self.model_schema = model_schema
|
self.model_schema = model_schema
|
||||||
|
self.dirname = path.join(config.base_dirname,
|
||||||
self.dirname = path.join(parameters.base_dirname,
|
getattr(config, basename_attr))
|
||||||
parameters.user_dirname)
|
|
||||||
|
|
||||||
self.path = Path(self.dirname)
|
self.path = Path(self.dirname)
|
||||||
|
|
||||||
def load(self, proto: object):
|
def load(self, proto: object):
|
||||||
|
|
||||||
return user.load(proto, self.path, self.model_class, self.model_schema)
|
return self.interface.load(proto, self.path, self.model_class, self.model_schema)
|
||||||
|
|
||||||
def dump(self, model: object):
|
def dump(self, model: object):
|
||||||
|
|
||||||
return user.dump(model, self.path, self.model_schema)
|
return self.interface.dump(model, self.path, self.model_schema)
|
||||||
|
|
||||||
def delete(self, proto: object):
|
def delete(self, proto: object):
|
||||||
|
|
||||||
return user.delete(proto, self.path)
|
return self.interface.delete(proto, self.path)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
|
|
||||||
return user.init(self.path)
|
return self.interface.init(self.path)
|
||||||
|
|
||||||
def sanitize(self):
|
def sanitize(self):
|
||||||
|
|
||||||
if self.path.exists():
|
if self.path.exists():
|
||||||
return user.sanitize(self.path, self.model_class, self.model_schema)
|
return self.interface.sanitize(self.path, self.model_class, self.model_schema)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Paste(object):
|
class UserBackend(ObjectBackendBc):
|
||||||
|
"""Filesystem user model backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
|
||||||
|
from . import user
|
||||||
|
|
||||||
|
super().__init__(user, 'user_dirname', *args)
|
||||||
|
|
||||||
|
|
||||||
|
class PasteBackend(ObjectBackendBc):
|
||||||
"""Filesystem paste model backend
|
"""Filesystem paste model backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dirname: str
|
def __init__(self, *args):
|
||||||
path: Path
|
|
||||||
|
|
||||||
def __init__(
|
from . import paste
|
||||||
self,
|
|
||||||
parameters: Parameters,
|
|
||||||
model_class: type,
|
|
||||||
model_schema: type):
|
|
||||||
|
|
||||||
self.model_class = model_class
|
super().__init__(paste, 'paste_dirname', *args)
|
||||||
|
|
||||||
self.model_schema = model_schema
|
|
||||||
|
|
||||||
self.dirname = path.join(parameters.base_dirname,
|
class Backend(BackendAbc):
|
||||||
parameters.paste_dirname)
|
"""File backend interface
|
||||||
|
"""
|
||||||
|
|
||||||
self.path = Path(self.dirname)
|
_user: UserBackend
|
||||||
|
_paste: PasteBackend
|
||||||
|
|
||||||
def load(self, proto: object):
|
def __init__(self,
|
||||||
|
config: Config,
|
||||||
|
paste_model_class: type,
|
||||||
|
user_model_class: type,
|
||||||
|
paste_schema: type,
|
||||||
|
user_schema: type):
|
||||||
|
|
||||||
return paste.load(proto, self.path, self.model_class, self.model_schema)
|
self._user = UserBackend(config, user_model_class, user_schema)
|
||||||
|
self._paste = PasteBackend(config, paste_model_class, paste_schema)
|
||||||
|
|
||||||
def dump(self, model: object):
|
@property
|
||||||
|
def user(self) -> UserBackend:
|
||||||
|
|
||||||
return paste.dump(model, self.path, self.model_schema)
|
return self._user
|
||||||
|
|
||||||
def delete(self, proto: object):
|
@property
|
||||||
|
def paste(self) -> PasteBackend:
|
||||||
|
|
||||||
return paste.delete(proto, self.path)
|
return self._paste
|
||||||
|
|
||||||
def init(self):
|
|
||||||
|
|
||||||
return paste.init(self.path)
|
__all__ = [
|
||||||
|
Config,
|
||||||
def sanitize(self):
|
Backend
|
||||||
|
]
|
||||||
if self.path.exists():
|
|
||||||
return paste.sanitize(self.path, self.model_class, self.model_schema)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
124
src/httpaste/backend/mysql/__init__.py
Normal file
124
src/httpaste/backend/mysql/__init__.py
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
"""MySQL backend
|
||||||
|
"""
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
from httpaste.backend import BackendInterface as BackendAbc
|
||||||
|
from httpaste.backend import ObjectBackend as ObjectBackendAbc
|
||||||
|
|
||||||
|
|
||||||
|
class Config(NamedTuple):
|
||||||
|
"""MySQL config
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: user name
|
||||||
|
user: str
|
||||||
|
#: user password
|
||||||
|
password: str
|
||||||
|
#: hostname or IP address
|
||||||
|
host: str
|
||||||
|
#: database identifier
|
||||||
|
database: str
|
||||||
|
#: a mysql.connection.MySQLConnection object (does not apply to config)
|
||||||
|
connection: object = None
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectBackendBc(ObjectBackendAbc):
|
||||||
|
|
||||||
|
connection: object
|
||||||
|
|
||||||
|
def __init__(self, interface: object, config: Config, model_class: type) -> None:
|
||||||
|
|
||||||
|
self.interface = interface
|
||||||
|
self.model_class = model_class
|
||||||
|
self.connection = get_connection(config)
|
||||||
|
|
||||||
|
def load(self, proto: object) -> object:
|
||||||
|
|
||||||
|
return self.interface.load(proto, self.connection, self.model_class)
|
||||||
|
|
||||||
|
def dump(self, model: object) -> None:
|
||||||
|
|
||||||
|
return self.interface.dump(model, self.connection)
|
||||||
|
|
||||||
|
def delete(self, proto: object) -> None:
|
||||||
|
|
||||||
|
return self.interface.delete(proto, self.connection)
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
|
||||||
|
return self.interface.init(self.connection)
|
||||||
|
|
||||||
|
def sanitize(self) -> None:
|
||||||
|
|
||||||
|
return self.interface.sanitize(self.connection, self.model_class)
|
||||||
|
|
||||||
|
|
||||||
|
class UserBackend(ObjectBackendBc):
|
||||||
|
"""MySQL user model backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args) -> None:
|
||||||
|
|
||||||
|
from . import user
|
||||||
|
|
||||||
|
super().__init__(paste, *args)
|
||||||
|
|
||||||
|
|
||||||
|
class PasteBackend(ObjectBackendBc):
|
||||||
|
"""MySQL paste model backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
connection: object
|
||||||
|
|
||||||
|
def __init__(self, *args) -> None:
|
||||||
|
|
||||||
|
from . import paste
|
||||||
|
|
||||||
|
super().__init__(paste, *args)
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(BackendAbc):
|
||||||
|
"""MySQL backend interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
user: UserBackend
|
||||||
|
paste: PasteBackend
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
config: Config,
|
||||||
|
paste_model_class: type,
|
||||||
|
user_model_class: type,
|
||||||
|
paste_schema: type,
|
||||||
|
user_schema: type):
|
||||||
|
|
||||||
|
self.user = UserBackend(config, user_model_class, user_schema)
|
||||||
|
self.paste = PasteBackend(config, paste_model_class, paste_schema)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(config: Config) -> object:
|
||||||
|
"""get a mysql.connection.MySQLConnection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
from mysql.connector import connect
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(' '.join((
|
||||||
|
'\'mysql-connector-python\' is not installed.',
|
||||||
|
'Install it by running',
|
||||||
|
'\'python3 -m pip install mysql-connector-python\'.'
|
||||||
|
))) from e
|
||||||
|
|
||||||
|
if config.connection is not None:
|
||||||
|
|
||||||
|
return config.connection
|
||||||
|
|
||||||
|
connection = connect(user=config.user, password=config.password,
|
||||||
|
host=config.host,
|
||||||
|
database=config.database)
|
||||||
|
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
Config,
|
||||||
|
Backend
|
||||||
|
]
|
||||||
122
src/httpaste/backend/mysql/paste.py
Normal file
122
src/httpaste/backend/mysql/paste.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
from os import path
|
||||||
|
from time import time
|
||||||
|
from mysql.connector.connection import MySQLConnection
|
||||||
|
|
||||||
|
|
||||||
|
def load(proto:object, connection: MySQLConnection, model_class: type):
|
||||||
|
"""load a paste model
|
||||||
|
|
||||||
|
:param model: model prototype
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
:param model_class: model class
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
|
statement = '''SELECT pid, data, data_hash, sub, expiration, encoding
|
||||||
|
FROM httpaste_pastes
|
||||||
|
WHERE pid=%s'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (proto.pid,))
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row is not None:
|
||||||
|
|
||||||
|
return model_class(
|
||||||
|
row['pid'],
|
||||||
|
row['sub'],
|
||||||
|
row['data'],
|
||||||
|
row['data_hash'],
|
||||||
|
row['expiration'],
|
||||||
|
row['encoding'])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def dump(model:object, connection: MySQLConnection):
|
||||||
|
"""dump a paste model
|
||||||
|
|
||||||
|
:param model: model object
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
:param model_class: model class
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = '''REPLACE INTO httpaste_pastes
|
||||||
|
(pid, data, data_hash, sub, expiration, encoding)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s)'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (model.pid, model.data, model.data_hash,
|
||||||
|
model.sub, model.expiration, model.encoding))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete(proto: object, connection: MySQLConnection):
|
||||||
|
"""delete a paste model
|
||||||
|
|
||||||
|
:param model: model prototype
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
:param model_class: model class
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = '''DELETE FROM httpaste_pastes
|
||||||
|
WHERE pid=%s'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (proto.pid,))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def init(connection: MySQLConnection):
|
||||||
|
"""initialize paste model table
|
||||||
|
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = '''CREATE TABLE `httpaste_pastes` (
|
||||||
|
`pid` blob NOT NULL,
|
||||||
|
`data` longblob NOT NULL,
|
||||||
|
`data_hash` blob NOT NULL,
|
||||||
|
`sub` blob DEFAULT NULL,
|
||||||
|
`expiration` int(16) NOT NULL,
|
||||||
|
`encoding` tinytext DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`pid`(128))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'''
|
||||||
|
|
||||||
|
cursor.execute(statement)
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(connection: MySQLConnection, model_class:type):
|
||||||
|
"""sanitize paste model table
|
||||||
|
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
|
statement = '''SELECT pid
|
||||||
|
FROM httpaste_pastes
|
||||||
|
WHERE expiration < %s AND expiration > 0'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (time(),))
|
||||||
|
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
|
||||||
|
delete(model_class(row['pid']))
|
||||||
|
|
||||||
|
return None
|
||||||
99
src/httpaste/backend/mysql/user.py
Normal file
99
src/httpaste/backend/mysql/user.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
from os import path
|
||||||
|
from mysql.connector.connection import MySQLConnection
|
||||||
|
|
||||||
|
|
||||||
|
def load(proto:object, connection: MySQLConnection, model_class: type):
|
||||||
|
"""load a user model
|
||||||
|
|
||||||
|
:param model: model prototype
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
:param model_class: model class
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor(dictionary=True)
|
||||||
|
|
||||||
|
statement = '''SELECT sub, key_hash, paste_index
|
||||||
|
FROM httpaste_users
|
||||||
|
WHERE sub=%s'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (proto.sub,))
|
||||||
|
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row is not None:
|
||||||
|
|
||||||
|
return model_class(row['sub'], row['key_hash'], row['paste_index'])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def dump(model:object, connection: MySQLConnection):
|
||||||
|
"""dump a user model
|
||||||
|
|
||||||
|
:param model: model object
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
:param model_class: model class
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = '''REPLACE INTO httpaste_users
|
||||||
|
(sub, key_hash, paste_index)
|
||||||
|
VALUES (%s, %s, %s)'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (model.sub, model.key_hash, model.index))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete(proto: object, connection: MySQLConnection):
|
||||||
|
"""delete a user model
|
||||||
|
|
||||||
|
:param model: model prototype
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
:param model_class: model class
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = '''DELETE FROM httpaste_users
|
||||||
|
WHERE sub=%s'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (proto.sub,))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def init(connection: MySQLConnection):
|
||||||
|
"""initialize user model table
|
||||||
|
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = '''CREATE TABLE `httpaste_users` (
|
||||||
|
`sub` blob NOT NULL,
|
||||||
|
`key_hash` blob NOT NULL,
|
||||||
|
`paste_index` blob NOT NULL,
|
||||||
|
PRIMARY KEY (`sub`(128))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'''
|
||||||
|
|
||||||
|
cursor.execute(statement)
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(connection: MySQLConnection, model_class: type):
|
||||||
|
"""sanitize user model table
|
||||||
|
|
||||||
|
:param connection: mysql connector connection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
@ -2,96 +2,113 @@
|
||||||
"""
|
"""
|
||||||
from sqlite3 import Connection, Row, connect
|
from sqlite3 import Connection, Row, connect
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from . import user
|
from httpaste.backend import BackendInterface as BackendAbc
|
||||||
from . import paste
|
from httpaste.backend import ObjectBackend as ObjectBackendAbc
|
||||||
|
|
||||||
|
|
||||||
class Parameters(NamedTuple):
|
class Config(NamedTuple):
|
||||||
"""SQLite backend parameters
|
"""SQLite backend config
|
||||||
"""
|
"""
|
||||||
|
|
||||||
#: local path or URI
|
#: local path or URI
|
||||||
path: str
|
uri: Path
|
||||||
|
user_table_name: str = 'httpaste_users'
|
||||||
|
paste_table_name: str = 'httpaste_pastes'
|
||||||
#: a sqlite3.Connection object (does not apply to config)
|
#: a sqlite3.Connection object (does not apply to config)
|
||||||
connection: Optional[object] = None
|
connection: Connection = None
|
||||||
|
|
||||||
|
|
||||||
class User(object):
|
class ObjectBackendBc(ObjectBackendAbc):
|
||||||
"""SQLite user model backend
|
|
||||||
|
connection: object
|
||||||
|
|
||||||
|
def __init__(self, interface: object, table_name_attr: str, config: Config, model_class: type, schema: type) -> None:
|
||||||
|
|
||||||
|
self.interface = interface
|
||||||
|
self.model_class = model_class
|
||||||
|
self.connection = get_connection(config)
|
||||||
|
self.table = getattr(config, table_name_attr)
|
||||||
|
|
||||||
|
def load(self, proto: object) -> object:
|
||||||
|
|
||||||
|
return self.interface.load(proto, self.connection, self.table, self.model_class)
|
||||||
|
|
||||||
|
def dump(self, model: object) -> None:
|
||||||
|
|
||||||
|
return self.interface.dump(model, self.connection, self.table)
|
||||||
|
|
||||||
|
def delete(self, proto: object) -> None:
|
||||||
|
|
||||||
|
return self.interface.delete(proto, self.connection, self.table)
|
||||||
|
|
||||||
|
def init(self) -> None:
|
||||||
|
|
||||||
|
return self.interface.init(self.connection, self.table)
|
||||||
|
|
||||||
|
def sanitize(self) -> None:
|
||||||
|
|
||||||
|
return self.interface.sanitize(self.connection, self.table, self.model_class)
|
||||||
|
|
||||||
|
|
||||||
|
class UserBackend(ObjectBackendBc):
|
||||||
|
"""sqlite user model backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
connection: Connection
|
def __init__(self, *args) -> None:
|
||||||
|
|
||||||
def __init__(self, parameters: Parameters, model_class: type):
|
from . import user
|
||||||
|
|
||||||
self.model_class = model_class
|
super().__init__(paste, 'user_table_name', *args)
|
||||||
|
|
||||||
self.connection = get_connection(parameters)
|
|
||||||
|
|
||||||
def load(self, proto: object):
|
|
||||||
|
|
||||||
return user.load(proto, self.connection, self.model_class)
|
|
||||||
|
|
||||||
def dump(self, model: object):
|
|
||||||
|
|
||||||
return user.dump(model, self.connection)
|
|
||||||
|
|
||||||
def delete(self, proto: object):
|
|
||||||
|
|
||||||
return user.delete(proto, self.connection)
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
|
|
||||||
return user.init(self.connection)
|
|
||||||
|
|
||||||
def sanitize(self):
|
|
||||||
|
|
||||||
return user.sanitize(self.connection, self.model_class)
|
|
||||||
|
|
||||||
|
|
||||||
class Paste(object):
|
class PasteBackend(ObjectBackendBc):
|
||||||
"""SQLite paste model backend
|
"""sqlite paste model backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
connection: Connection
|
connection: object
|
||||||
|
|
||||||
def __init__(self, parameters: Parameters, model_class: type):
|
def __init__(self, *args) -> None:
|
||||||
|
|
||||||
self.model_class = model_class
|
from . import paste
|
||||||
|
|
||||||
self.connection = get_connection(parameters)
|
super().__init__(paste, 'paste_table_name', *args)
|
||||||
|
|
||||||
def load(self, proto: object):
|
|
||||||
|
|
||||||
return paste.load(proto, self.connection, self.model_class)
|
|
||||||
|
|
||||||
def dump(self, model: object):
|
|
||||||
|
|
||||||
return paste.dump(model, self.connection)
|
|
||||||
|
|
||||||
def delete(self, proto: object):
|
|
||||||
|
|
||||||
return paste.delete(proto, self.connection)
|
|
||||||
|
|
||||||
def init(self):
|
|
||||||
|
|
||||||
return paste.init(self.connection)
|
|
||||||
|
|
||||||
def sanitize(self):
|
|
||||||
|
|
||||||
return paste.sanitize(self.connection, self.model_class)
|
|
||||||
|
|
||||||
|
|
||||||
def get_connection(parameters: Parameters):
|
class Backend(BackendAbc):
|
||||||
|
"""sqlite backend interface
|
||||||
|
"""
|
||||||
|
|
||||||
|
user: UserBackend
|
||||||
|
paste: PasteBackend
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
config: Config,
|
||||||
|
paste_model_class: type,
|
||||||
|
user_model_class: type,
|
||||||
|
paste_schema: type,
|
||||||
|
user_schema: type):
|
||||||
|
|
||||||
|
self.user = UserBackend(config, user_model_class, user_schema)
|
||||||
|
self.paste = PasteBackend(config, paste_model_class, paste_schema)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection(config: Config):
|
||||||
"""get an sqlite connection object
|
"""get an sqlite connection object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if parameters.connection:
|
if config.connection:
|
||||||
|
|
||||||
return parameters.connection
|
return config.connection
|
||||||
|
|
||||||
connection = connect(parameters.path, check_same_thread=False)
|
connection = connect(config.uri, check_same_thread=False)
|
||||||
connection.row_factory = Row
|
connection.row_factory = Row
|
||||||
|
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
Config,
|
||||||
|
Backend
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,77 +6,102 @@ from time import time
|
||||||
from importlib.resources import open_text
|
from importlib.resources import open_text
|
||||||
|
|
||||||
|
|
||||||
def load(proto: object, connection: Connection, model_class: type):
|
def load(proto: object, connection: Connection, table: str, model_class: type):
|
||||||
"""load a paste
|
"""load a paste
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cur = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
cur.execute(
|
statement = f'''SELECT pid, data, data_hash, sub, expiration, encoding
|
||||||
'SELECT pid, data, data_hash, sub, expiration, encoding FROM pastes WHERE pid=?',
|
FROM {table}
|
||||||
(proto.pid,
|
WHERE pid=?'''
|
||||||
))
|
|
||||||
|
|
||||||
result = cur.fetchone()
|
cursor.execute(statement, (proto.pid,))
|
||||||
|
|
||||||
if result:
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
if row is not None:
|
||||||
|
|
||||||
return model_class(
|
return model_class(
|
||||||
result['pid'],
|
row['pid'],
|
||||||
result['sub'],
|
row['sub'],
|
||||||
result['data'],
|
row['data'],
|
||||||
result['data_hash'],
|
row['data_hash'],
|
||||||
result['expiration'],
|
row['expiration'],
|
||||||
result['encoding'])
|
row['encoding'])
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def dump(model: object, connection: Connection):
|
def dump(model: object, connection: Connection, table: str) -> None:
|
||||||
"""dump a paste
|
"""dump a paste
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cur = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
cur.execute(
|
statement = f'''INSERT INTO "{table}"
|
||||||
'''INSERT INTO pastes (pid, data, data_hash, sub, expiration, encoding)
|
(pid, data, data_hash, sub, expiration, encoding)
|
||||||
VALUES (?,?,?,?,?,?)''',
|
VALUES (?,?,?,?,?,?)'''
|
||||||
(model.pid,
|
|
||||||
|
values = (model.pid,
|
||||||
model.data,
|
model.data,
|
||||||
model.data_hash,
|
model.data_hash,
|
||||||
model.sub,
|
model.sub,
|
||||||
model.expiration,
|
model.expiration,
|
||||||
model.encoding))
|
model.encoding)
|
||||||
|
|
||||||
|
cursor.execute(statement, values)
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete(proto: object, connection: Connection, table: str) -> None:
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
cursor.execute(f'''DELETE FROM {table} WHERE pid=?''', (proto.pid,))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def init(connection: Connection, table: str):
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = f'''CREATE TABLE IF NOT EXISTS "{table}" (
|
||||||
|
"pid" BLOB NOT NULL UNIQUE,
|
||||||
|
"data" BLOB NOT NULL,
|
||||||
|
"data_hash" BLOB NOT NULL,
|
||||||
|
"sub" BLOB UNIQUE,
|
||||||
|
"expiration" INTEGER NOT NULL,
|
||||||
|
"encoding" TEXT,
|
||||||
|
PRIMARY KEY("pid")
|
||||||
|
);'''
|
||||||
|
|
||||||
|
cursor.execute(statement)
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
def delete(proto: object, connection: Connection) -> bool:
|
def sanitize(connection: Connection, table: str, model_class: type) -> int:
|
||||||
|
|
||||||
cur = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
cur.execute('''DELETE FROM pastes WHERE pid=?''', (proto.pid,))
|
statement = f'''SELECT pid FROM {table}
|
||||||
|
WHERE expiration < ? AND expiration > 0'''
|
||||||
|
|
||||||
connection.commit()
|
cursor.execute(statement, (int(time()),))
|
||||||
|
|
||||||
|
srow_count = 0
|
||||||
def init(connection: Connection):
|
|
||||||
|
|
||||||
cur = connection.cursor()
|
|
||||||
|
|
||||||
with open_text('httpaste.backend.sqlite', 'paste.sql') as fh:
|
|
||||||
|
|
||||||
cur.execute(fh.read())
|
|
||||||
|
|
||||||
connection.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize(connection: Connection, model_class: type) -> bool:
|
|
||||||
|
|
||||||
cur = connection.cursor()
|
|
||||||
|
|
||||||
cur.execute('''SELECT pid FROM pastes WHERE expiration < ? AND expiration > 0''', (int(time()),))
|
|
||||||
|
|
||||||
for row in cur.fetchall():
|
for row in cur.fetchall():
|
||||||
|
|
||||||
delete(model_class(row['pid']))
|
delete(model_class(row['pid']))
|
||||||
|
|
||||||
|
srow_count += 1
|
||||||
|
|
||||||
|
return srow_count
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS "pastes" (
|
|
||||||
"pid" BLOB NOT NULL UNIQUE,
|
|
||||||
"data" BLOB NOT NULL,
|
|
||||||
"data_hash" BLOB NOT NULL,
|
|
||||||
"sub" BLOB UNIQUE,
|
|
||||||
"expiration" INTEGER NOT NULL,
|
|
||||||
"encoding" TEXT,
|
|
||||||
PRIMARY KEY("pid")
|
|
||||||
);
|
|
||||||
|
|
@ -6,56 +6,73 @@ from httpaste.model import User
|
||||||
from importlib.resources import open_text
|
from importlib.resources import open_text
|
||||||
|
|
||||||
|
|
||||||
def load(proto: User, connection: Connection):
|
def load(proto: object, connection: Connection, table: str, model_class: type):
|
||||||
"""load a user
|
"""load a user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cur = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
cur.execute(
|
statement = f'''SELECT sub, key_hash, paste_index
|
||||||
'SELECT sub, key_hash, paste_index FROM users WHERE sub=?', (proto.sub,))
|
FROM {table}
|
||||||
|
WHERE sub=?'''
|
||||||
|
|
||||||
result = cur.fetchone()
|
cursor.execute(statement, (proto.sub,))
|
||||||
|
|
||||||
if result:
|
row = cursor.fetchone()
|
||||||
|
|
||||||
return User(result['sub'], result['key_hash'], result['paste_index'])
|
if row is not None:
|
||||||
|
|
||||||
|
return model_class(row['sub'], row['key_hash'], row['paste_index'])
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def dump(model: User, connection: Connection):
|
def dump(model: object, connection: Connection, table: str) -> None:
|
||||||
"""dump a user
|
"""dump a user
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cur = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
|
||||||
cur.execute('''INSERT OR REPLACE INTO users (sub, key_hash, paste_index)
|
statement = f'''INSERT OR REPLACE INTO {table}
|
||||||
VALUES (?,?,?)''', (model.sub, model.key_hash, model.index))
|
(sub, key_hash, paste_index)
|
||||||
|
VALUES (?,?,?)'''
|
||||||
|
|
||||||
|
cursor.execute(statement, (model.sub, model.key_hash, model.index))
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
def delete(proto: object, connection: Connection) -> bool:
|
|
||||||
|
|
||||||
cur = connection.cursor()
|
|
||||||
|
|
||||||
cur.execute('''DELETE FROM users WHERE sub=?''', (proto.sub,))
|
|
||||||
|
|
||||||
connection.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def init(connection: Connection):
|
|
||||||
|
|
||||||
cur = connection.cursor()
|
|
||||||
|
|
||||||
with open_text('httpaste.backend.sqlite', 'user.sql') as fh:
|
|
||||||
|
|
||||||
cur.execute(fh.read())
|
|
||||||
|
|
||||||
connection.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize(connection: Connection, model_class) -> bool:
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete(proto: object, connection: Connection, table: str) -> None:
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
cursor.execute(f'''DELETE FROM {table} WHERE sub=?''', (proto.sub,))
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def init(connection: Connection, table: str) -> None:
|
||||||
|
|
||||||
|
cursor = connection.cursor()
|
||||||
|
|
||||||
|
statement = f'''CREATE TABLE IF NOT EXISTS "{table}" (
|
||||||
|
"sub" BLOB NOT NULL UNIQUE,
|
||||||
|
"key_hash" BLOB NOT NULL,
|
||||||
|
"paste_index" BLOB,
|
||||||
|
PRIMARY KEY("sub")
|
||||||
|
);'''
|
||||||
|
|
||||||
|
cursor.execute(statement)
|
||||||
|
|
||||||
|
connection.commit()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(connection: Connection, table: str, model_class) -> int:
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
CREATE TABLE IF NOT EXISTS "users" (
|
|
||||||
"sub" BLOB NOT NULL UNIQUE,
|
|
||||||
"key_hash" BLOB NOT NULL,
|
|
||||||
"paste_index" BLOB,
|
|
||||||
PRIMARY KEY("sub")
|
|
||||||
);
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
"""httpaste CGI entrypoint
|
"""httpaste CGI entrypoint
|
||||||
"""
|
"""
|
||||||
from wsgiref.handlers import CGIHandler
|
from wsgiref.handlers import CGIHandler
|
||||||
from httpaste import load_config, get_flask_app, get_config_path
|
from httpaste import load_config, get_flask_app
|
||||||
|
|
||||||
config, server_config = load_config(get_config_path())
|
config = load_config()
|
||||||
|
|
||||||
application = get_flask_app(config, server_config)
|
application = get_flask_app(config)
|
||||||
|
|
||||||
CGIHandler().run(application)
|
CGIHandler().run(application)
|
||||||
|
|
|
||||||
18
src/httpaste/context.py
Executable file
18
src/httpaste/context.py
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from typing import NamedTuple
|
||||||
|
from string import ascii_uppercase, digits, ascii_letters, punctuation
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
from httpaste.helper.common import generate_random_string
|
||||||
|
from httpaste.helper.config import get_sanitized_config_charset, get_config
|
||||||
|
|
||||||
|
class Config(NamedTuple):
|
||||||
|
"""httpaste global config
|
||||||
|
"""
|
||||||
|
salt: bytes = get_sanitized_config_charset(generate_random_string(
|
||||||
|
32, ascii_letters + digits + punctuation)).encode('utf-8')
|
||||||
|
hmac_iter: int = 20000
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_config(configIni: ConfigParser) -> Config:
|
||||||
|
|
||||||
|
return get_config(configIni, 'context', Config)
|
||||||
|
|
@ -5,12 +5,14 @@ import httpaste
|
||||||
def get(**kwargs):
|
def get(**kwargs):
|
||||||
|
|
||||||
config = current_app.httpaste
|
config = current_app.httpaste
|
||||||
|
context = config.context
|
||||||
|
model = config.model
|
||||||
|
|
||||||
|
|
||||||
return httpaste.__doc__.format(
|
return httpaste.__doc__.format(
|
||||||
url=connexion.request.url,
|
url=connexion.request.url,
|
||||||
hmac_iterations=config.hmac_iterations,
|
hmac_iterations=context.hmac_iter,
|
||||||
paste_lifetime=config.paste_lifetime,
|
paste_lifetime=model.paste.default_lifetime,
|
||||||
paste_max_lifetime=str(round(config.paste_max_lifetime / 60)),
|
paste_max_lifetime=str(round(model.paste.default_max_lifetime / 60)),
|
||||||
paste_default_encoding=config.paste_default_encoding
|
paste_default_encoding=model.paste.default_encoding
|
||||||
), 200
|
), 302, {'Location': '/ui'}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ from flask import current_app
|
||||||
from httpaste.helper.common import decode, DecodeError, join_url
|
from httpaste.helper.common import decode, DecodeError, join_url
|
||||||
import httpaste.model.paste as paste_model
|
import httpaste.model.paste as paste_model
|
||||||
import httpaste.model.user as user_model
|
import httpaste.model.user as user_model
|
||||||
from httpaste.helper.http import BadRequestError, GoneError, NotFoundError
|
from httpaste.backend import load_backend
|
||||||
from httpaste.model import (
|
from httpaste.helper.http import BadRequestError, GoneError, NotFoundError, ForbiddenError
|
||||||
|
from httpaste.helper.syntax import highlight
|
||||||
|
from httpaste.schema import (
|
||||||
PasteKey,
|
PasteKey,
|
||||||
PasteData,
|
PasteData,
|
||||||
PasteLifetime,
|
PasteLifetime,
|
||||||
|
|
@ -15,6 +17,13 @@ from httpaste.model import (
|
||||||
Sub)
|
Sub)
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
default_mime_type: str = 'text/plain'
|
||||||
|
default_linenos: bool = False
|
||||||
|
default_syntax: bool = False
|
||||||
|
default_formatter: str = 'terminal256'
|
||||||
|
|
||||||
|
|
||||||
def delete(**kwargs):
|
def delete(**kwargs):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
@ -45,12 +54,15 @@ def get(**kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = current_app.httpaste
|
config = current_app.httpaste
|
||||||
|
backend = load_backend(config.backend)
|
||||||
|
context = config.context
|
||||||
|
|
||||||
|
syntax = kwargs.get('syntax')
|
||||||
|
formatter = kwargs.get('format', Config.default_formatter)
|
||||||
|
linenos = kwargs.get('linenos', Config.default_linenos)
|
||||||
|
mime = kwargs.get('mime', Config.default_mime_type)
|
||||||
|
|
||||||
pid = PasteKey(kwargs['id'].encode('utf-8'))
|
pid = PasteKey(kwargs['id'].encode('utf-8'))
|
||||||
syntax = kwargs.get('syntax')
|
|
||||||
formatter = kwargs.get('format', 'terminal256')
|
|
||||||
linenos = kwargs.get('linenos', False)
|
|
||||||
mime = kwargs.get('mime', 'text/plain')
|
|
||||||
|
|
||||||
if kwargs.get('user') is not None:
|
if kwargs.get('user') is not None:
|
||||||
# authenticated
|
# authenticated
|
||||||
|
|
@ -58,26 +70,23 @@ def get(**kwargs):
|
||||||
key = MasterKey(kwargs['token_info'].get('master_key'))
|
key = MasterKey(kwargs['token_info'].get('master_key'))
|
||||||
sub = Sub(kwargs['token_info'].get('sub'))
|
sub = Sub(kwargs['token_info'].get('sub'))
|
||||||
|
|
||||||
pkey = user_model.load_paste_key(pid, sub, key, config.backend.user,
|
pkey = user_model.load_paste_key(pid, sub, key, backend.user, context)
|
||||||
config.salt, config.hmac_iterations)
|
|
||||||
|
|
||||||
def call(): return paste_model.get_safe(pid, pkey, sub,
|
def call(): return paste_model.get_safe(pid, pkey, sub,
|
||||||
config.backend.paste,
|
config.model.paste,
|
||||||
config.salt, config.hmac_iterations)
|
backend.paste, context)
|
||||||
else:
|
else:
|
||||||
# unauthenticated
|
# unauthenticated
|
||||||
|
|
||||||
def call(): return paste_model.get(pid, config.backend.paste,
|
def call(): return paste_model.get(pid, backend.paste, context)
|
||||||
config.salt, config.hmac_iterations)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data, expiration, encoding = call()
|
data, expiration, encoding = call()
|
||||||
except paste_model.LifetimeError as e:
|
except paste_model.LifetimeError as e:
|
||||||
if kwargs.get('user') is not None:
|
if kwargs.get('user') is not None:
|
||||||
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
|
paste_model.remove_safe(pid, sub, pkey, backend.paste, context)
|
||||||
config.salt, config.hmac_iterations)
|
|
||||||
else:
|
else:
|
||||||
paste_model.remove(pid, config.backend.paste)
|
paste_model.remove(pid, backend.paste)
|
||||||
raise GoneError(str(e)) from e
|
raise GoneError(str(e)) from e
|
||||||
except paste_model.NotFoundError as e:
|
except paste_model.NotFoundError as e:
|
||||||
raise NotFoundError(str(e))
|
raise NotFoundError(str(e))
|
||||||
|
|
@ -87,16 +96,17 @@ def get(**kwargs):
|
||||||
# burn after read
|
# burn after read
|
||||||
if expiration < 0:
|
if expiration < 0:
|
||||||
if kwargs.get('user') is not None:
|
if kwargs.get('user') is not None:
|
||||||
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
|
paste_model.remove_safe(pid, sub, pkey, backend.paste, context)
|
||||||
config.salt, config.hmac_iterations)
|
|
||||||
else:
|
else:
|
||||||
paste_model.remove(pid, config.backend.paste)
|
paste_model.remove(pid, backend.paste)
|
||||||
|
|
||||||
|
if encoding is not None:
|
||||||
|
data = data.decode(encoding)
|
||||||
|
|
||||||
if syntax is not None:
|
if syntax is not None:
|
||||||
data = highlight(data, str(syntax), formatter, linenos)
|
data = highlight(data, str(syntax), formatter, linenos)
|
||||||
|
|
||||||
if encoding is not None:
|
|
||||||
data = data.decode(encoding)
|
|
||||||
|
|
||||||
return ConnexionResponse(
|
return ConnexionResponse(
|
||||||
status_code=200,
|
status_code=200,
|
||||||
|
|
@ -110,12 +120,14 @@ def post(**kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = current_app.httpaste
|
config = current_app.httpaste
|
||||||
|
backend = load_backend(config.backend)
|
||||||
|
context = config.context
|
||||||
|
|
||||||
if kwargs['body'].get('data') is None:
|
if kwargs['body'].get('data') is None:
|
||||||
raise BadRequestError('form field \'data\' missing.')
|
raise BadRequestError('form field \'data\' missing.')
|
||||||
|
|
||||||
encoding = PasteEncoding(kwargs.get('encoding', 'utf-8'))
|
encoding = PasteEncoding(kwargs.get('encoding', 'utf-8'))
|
||||||
lifetime = PasteLifetime(kwargs.get('lifetime', config.paste_lifetime))
|
lifetime = PasteLifetime(kwargs.get('lifetime', config.model.paste.default_lifetime))
|
||||||
|
|
||||||
if encoding not in ['utf-8', 'utf-16', 'ascii']:
|
if encoding not in ['utf-8', 'utf-16', 'ascii']:
|
||||||
try:
|
try:
|
||||||
|
|
@ -135,15 +147,15 @@ def post(**kwargs):
|
||||||
sub = Sub(kwargs['token_info'].get('sub'))
|
sub = Sub(kwargs['token_info'].get('sub'))
|
||||||
|
|
||||||
pid, pkey = paste_model.create_safe(pdata, lifetime, sub, encoding,
|
pid, pkey = paste_model.create_safe(pdata, lifetime, sub, encoding,
|
||||||
config.backend.paste, config.salt, config.hmac_iterations)
|
config.model.paste, backend.paste,
|
||||||
|
context)
|
||||||
|
|
||||||
user_model.dump_paste_key(pid, pkey, sub, key, config.backend.user,
|
user_model.dump_paste_key(pid, pkey, sub, key, backend.user, context)
|
||||||
config.salt, config.hmac_iterations)
|
|
||||||
else:
|
else:
|
||||||
# unauthenticated
|
# unauthenticated
|
||||||
|
|
||||||
pid = paste_model.create(pdata, lifetime, encoding, config.backend.paste,
|
pid = paste_model.create(pdata, lifetime, encoding, config.model.paste,
|
||||||
config.salt, config.hmac_iterations)
|
backend.paste, context)
|
||||||
|
|
||||||
|
|
||||||
base_url = join_url(request.root_url, request.path)
|
base_url = join_url(request.root_url, request.path)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ def search(**kwargs):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(args)
|
|
||||||
|
|
||||||
return 'Hallo', 200
|
return 'Hallo', 200
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
21
src/httpaste/controller/ui/__init__.py
Normal file
21
src/httpaste/controller/ui/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
from httpaste.helper.template import views, render_template_with_context
|
||||||
|
from httpaste import __doc__ as man_page
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/search.html")
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'paste_index_url': '/ui/paste',
|
||||||
|
'user_index_url': '/ui/user',
|
||||||
|
'man_page': man_page,
|
||||||
|
'user': kwargs.get('user'),
|
||||||
|
'delete_session_url': '/ui/user/session/delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
103
src/httpaste/controller/ui/paste/__init__.py
Normal file
103
src/httpaste/controller/ui/paste/__init__.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
from io import BytesIO
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
from connexion import request
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from httpaste.helper.template import views, render_template_with_context
|
||||||
|
from httpaste.helper.url import url_query_string, url_append_query_param
|
||||||
|
from httpaste.helper.syntax import syntax_shortnames, format_shortnames
|
||||||
|
from httpaste.helper.http import mime_types
|
||||||
|
|
||||||
|
from httpaste.controller.paste import post as post_raw
|
||||||
|
from httpaste.controller.paste import get as get_raw
|
||||||
|
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/paste/search.html")
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'create_public_paste_url': '/ui/paste/public',
|
||||||
|
'create_private_paste_url': '/ui/paste/private',
|
||||||
|
'user': kwargs.get('user'),
|
||||||
|
'delete_session_url': '/ui/user/session/delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
|
|
||||||
|
|
||||||
|
def post(**kwargs):
|
||||||
|
|
||||||
|
#rewriting strict form to mixed (as expected by cascaded controller)
|
||||||
|
data = kwargs['body'].pop('data')
|
||||||
|
kwargs = {**kwargs, **kwargs['body']}
|
||||||
|
kwargs.pop('body')
|
||||||
|
kwargs['body'] = {'data': data}
|
||||||
|
|
||||||
|
#prepare octet stream data for cascaded controller
|
||||||
|
if kwargs.get('data').filename:
|
||||||
|
bfr = BytesIO()
|
||||||
|
kwargs.get('data').save(bfr)
|
||||||
|
bfr.seek(0)
|
||||||
|
kwargs['body']['data'] = b64encode(bfr.read()).decode('utf-8')
|
||||||
|
kwargs['encoding'] = 'base64'
|
||||||
|
|
||||||
|
output, status_code = post_raw(**kwargs)
|
||||||
|
|
||||||
|
#TODO: lifetime=-1 no preview handler
|
||||||
|
|
||||||
|
url = output.strip('\n')
|
||||||
|
if kwargs.get('lifetime') and int(kwargs['lifetime']) < 0:
|
||||||
|
url = url_append_query_param(url, 'preview', 'False')
|
||||||
|
|
||||||
|
return output, 302, {'Location': url}
|
||||||
|
|
||||||
|
|
||||||
|
def get(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/paste/get.html")
|
||||||
|
|
||||||
|
base_path = f'paste/public/{kwargs["id"]}'
|
||||||
|
|
||||||
|
if kwargs.get('user'):
|
||||||
|
base_path = f'paste/private/{kwargs["id"]}'
|
||||||
|
|
||||||
|
raw_paste_url = f'{request.host_url}{base_path}'
|
||||||
|
paste_url = raw_paste_url
|
||||||
|
|
||||||
|
paste_url_query = {}
|
||||||
|
for field in ['format', 'mime', 'syntax']:
|
||||||
|
if kwargs.get(field):
|
||||||
|
paste_url_query[field] = kwargs[field]
|
||||||
|
|
||||||
|
if paste_url_query:
|
||||||
|
paste_url = '?'.join((paste_url, url_query_string(paste_url_query)))
|
||||||
|
|
||||||
|
preview_url = f'/ui/{base_path}'
|
||||||
|
if kwargs.get('preview'):
|
||||||
|
paste_url_query['preview'] = kwargs['preview']
|
||||||
|
preview_url = '?'.join((preview_url, url_query_string(paste_url_query)))
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'raw_paste_url': raw_paste_url,
|
||||||
|
'paste_url': paste_url,
|
||||||
|
'preview_url': preview_url,
|
||||||
|
'query': {
|
||||||
|
'format': kwargs.get('format', ''),
|
||||||
|
'syntax': kwargs.get('syntax', ''),
|
||||||
|
'mime': kwargs.get('mime', ''),
|
||||||
|
'preview': kwargs.get('preview', True)
|
||||||
|
},
|
||||||
|
'syntax_shortnames': syntax_shortnames(),
|
||||||
|
'format_shortnames': format_shortnames(),
|
||||||
|
'mime_types': mime_types()
|
||||||
|
}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
28
src/httpaste/controller/ui/paste/private.py
Normal file
28
src/httpaste/controller/ui/paste/private.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from httpaste.helper.template import views, render_template_with_context
|
||||||
|
from httpaste.controller.ui.paste import post as post_proxy
|
||||||
|
from httpaste.controller.ui.paste import get as get_proxy
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/paste/private/search.html")
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'paste_form_url': '/ui/paste/private',
|
||||||
|
}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
|
|
||||||
|
|
||||||
|
def post(**kwargs):
|
||||||
|
|
||||||
|
return post_proxy(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get(**kwargs):
|
||||||
|
|
||||||
|
return get_proxy(**kwargs)
|
||||||
28
src/httpaste/controller/ui/paste/public.py
Normal file
28
src/httpaste/controller/ui/paste/public.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from httpaste.helper.template import views, render_template_with_context
|
||||||
|
from httpaste.controller.ui.paste import post as post_proxy
|
||||||
|
from httpaste.controller.ui.paste import get as get_proxy
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/paste/public/search.html")
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'paste_form_url': '/ui/paste/public'
|
||||||
|
}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
|
|
||||||
|
|
||||||
|
def post(**kwargs):
|
||||||
|
|
||||||
|
return post_proxy(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get(**kwargs):
|
||||||
|
|
||||||
|
return get_proxy(**kwargs)
|
||||||
16
src/httpaste/controller/ui/user/__init__.py
Normal file
16
src/httpaste/controller/ui/user/__init__.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
from httpaste.helper.template import views, render_template_with_context
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/user/search.html")
|
||||||
|
|
||||||
|
variables = {
|
||||||
|
'delete_session_url': '/ui/user/session/delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
20
src/httpaste/controller/ui/user/session/__init__.py
Normal file
20
src/httpaste/controller/ui/user/session/__init__.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
from httpaste.helper.template import views, render_template_with_context
|
||||||
|
from httpaste.controller.user.session import delete as raw_delete
|
||||||
|
|
||||||
|
from connexion import request
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
template = views.get_template("viewport/ui/user/session/search.html")
|
||||||
|
|
||||||
|
variables = {'session_delete_url': request.path + '/delete'}
|
||||||
|
|
||||||
|
with current_app.app_context():
|
||||||
|
view_render = render_template_with_context(template, **variables)
|
||||||
|
|
||||||
|
return view_render, 200
|
||||||
|
|
||||||
|
|
||||||
|
def delete(**kwargs):
|
||||||
|
|
||||||
|
return raw_delete(**kwargs)
|
||||||
6
src/httpaste/controller/ui/user/session/delete.py
Normal file
6
src/httpaste/controller/ui/user/session/delete.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from httpaste.helper.template import views
|
||||||
|
from httpaste.controller.ui.user.session import delete as proxy_delete
|
||||||
|
|
||||||
|
def search(**kwargs):
|
||||||
|
|
||||||
|
return proxy_delete(**kwargs)
|
||||||
|
|
@ -2,22 +2,31 @@
|
||||||
"""
|
"""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from httpaste.helper.http import ForbiddenError
|
from httpaste.helper.http import UnauthorizedError
|
||||||
from httpaste.model.user import authenticate, AuthenticationError
|
from httpaste.model.user import authenticate, AuthenticationError
|
||||||
|
from httpaste.backend import load_backend
|
||||||
|
|
||||||
def post(*args, **kwargs):
|
def post(*args, **kwargs):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = current_app.httpaste
|
config = current_app.httpaste
|
||||||
|
backend = load_backend(config.backend)
|
||||||
|
context = config.context
|
||||||
|
|
||||||
user_id = args[0].encode('utf-8')
|
user_id = args[0].encode('utf-8')
|
||||||
password = args[1].encode('utf-8')
|
password = args[1].encode('utf-8')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
return authenticate(user_id, password, config.backend.user, config.salt, config.hmac_iterations)
|
return authenticate(user_id, password, backend.user, context)
|
||||||
except AuthenticationError as e:
|
except AuthenticationError as e:
|
||||||
|
|
||||||
raise ForbiddenError('You shall not pass!') from e
|
raise UnauthorizedError('You shall not pass!') from e
|
||||||
|
|
||||||
|
|
||||||
|
def delete(**kwargs):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise UnauthorizedError('Authentication Rejection requested by client')
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
"""httpaste FastCGI entrypoint
|
"""httpaste FastCGI entrypoint
|
||||||
"""
|
"""
|
||||||
from flup.server.fcgi import WSGIServer
|
from flup.server.fcgi import WSGIServer
|
||||||
from httpaste import load_config, get_flask_app, get_config_path
|
from httpaste import load_config, get_flask_app
|
||||||
|
|
||||||
config, server_config = load_config(get_config_path())
|
config = load_config()
|
||||||
|
|
||||||
application = get_flask_app(config, server_config)
|
application = get_flask_app(config)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
|
|
||||||
113
src/httpaste/helper/config.py
Executable file
113
src/httpaste/helper/config.py
Executable file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from configparser import ConfigParser, NoSectionError
|
||||||
|
from typing import Optional, NamedTuple
|
||||||
|
from os import environ
|
||||||
|
from ast import literal_eval
|
||||||
|
|
||||||
|
|
||||||
|
CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIGPATH'
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_sanitized_config_charset(charset: str):
|
||||||
|
|
||||||
|
for x in ["$", "%"]:
|
||||||
|
|
||||||
|
charset = charset.replace(x, f'{x}{x}')
|
||||||
|
|
||||||
|
return charset
|
||||||
|
|
||||||
|
|
||||||
|
def typecast(obj: dict, aclass: type, dirname:Optional[Path] = None) -> dict:
|
||||||
|
"""typecast a dictionary according to class annotations
|
||||||
|
|
||||||
|
:param obj: dictionary to typecast
|
||||||
|
:param aclass: class containing typehint annotations
|
||||||
|
:param basepath: basepath for filesystem path typecasting
|
||||||
|
|
||||||
|
:returns: typecasted dictionary
|
||||||
|
"""
|
||||||
|
|
||||||
|
casted = {}
|
||||||
|
|
||||||
|
for k,v in obj.items():
|
||||||
|
|
||||||
|
v = v.strip('\'"')
|
||||||
|
|
||||||
|
try:
|
||||||
|
bclass = aclass.__annotations__[k]
|
||||||
|
except KeyError as e:
|
||||||
|
raise KeyError(f'{k}: not allowed') from e
|
||||||
|
|
||||||
|
if issubclass(bclass, Path) and not str(v[0]).startswith(os.path.sep):
|
||||||
|
if not isinstance(dirname, Path):
|
||||||
|
raise TypeError('no dirname for Path type specified.')
|
||||||
|
|
||||||
|
casted[k] = casted_val = dirname / v
|
||||||
|
elif issubclass(bclass, bytes):
|
||||||
|
casted[k] = bclass(v.encode('utf-8'))
|
||||||
|
elif issubclass(bclass, bool):
|
||||||
|
casted[k] = literal_eval(v)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
casted_val = bclass(v)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError(f'{k}: {e}') from e
|
||||||
|
else:
|
||||||
|
casted[k] = casted_val
|
||||||
|
|
||||||
|
return casted
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(configIni: ConfigParser, section: str, bclass: type, dirname: Path = None) -> object:
|
||||||
|
"""get an object-oriented configuration from an INI file
|
||||||
|
|
||||||
|
:param configIni: configparser.Configparser object with initialized stream
|
||||||
|
:param section: name of section to get configuration for
|
||||||
|
:param bclass: configuration base class
|
||||||
|
:param dirname: directory name of INI file stream
|
||||||
|
|
||||||
|
:returns: initialized configuration instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_config = dict(configIni.items(section))
|
||||||
|
except NoSectionError as e:
|
||||||
|
raw_config = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
casted_config = typecast(raw_config, bclass, dirname)
|
||||||
|
except KeyError as e:
|
||||||
|
raise ConfigError(f'[{section}] {e}') from e
|
||||||
|
except ValueError as e:
|
||||||
|
raise ConfigError(f'[{section}] {e}') from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = bclass(**casted_config)
|
||||||
|
except TypeError as e:
|
||||||
|
raise ConfigError(f'[{section}] {e}') from e
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_configparser(path: Path = None, var_name: str = CONFIGPATH_ENVIRON):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
try:
|
||||||
|
path = environ[var_name]
|
||||||
|
except KeyError as e:
|
||||||
|
raise ConfigError(
|
||||||
|
f'environment variable \'{var_name}\' not set.') from e
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
configIni.read(path)
|
||||||
|
|
||||||
|
return configIni, path
|
||||||
|
|
@ -8,7 +8,7 @@ from cryptography.hazmat.primitives import hashes
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
|
||||||
from httpaste import Config
|
from httpaste.context import Config
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_HMAC_ITERATIONS = 20000
|
DEFAULT_HMAC_ITERATIONS = 20000
|
||||||
|
|
@ -38,7 +38,7 @@ def dhash(data: bytes):
|
||||||
return hashlib.sha512(data).digest()
|
return hashlib.sha512(data).digest()
|
||||||
|
|
||||||
|
|
||||||
def derive_key(main_key: str, salt: bytes = Config.salt, iterations:int=DEFAULT_HMAC_ITERATIONS) -> bytes:
|
def derive_key(main_key: str, salt: bytes, iterations:int=DEFAULT_HMAC_ITERATIONS) -> bytes:
|
||||||
"""derive a key from a main key
|
"""derive a key from a main key
|
||||||
|
|
||||||
:param main_key: main key to derive from
|
:param main_key: main key to derive from
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from mimetypes import types_map as mime_types_map
|
||||||
|
|
||||||
class BadRequestError(RuntimeError):
|
class BadRequestError(RuntimeError):
|
||||||
def __init__(self, msg=None):
|
def __init__(self, msg=None):
|
||||||
|
|
@ -21,7 +22,7 @@ class UnauthorizedError(RuntimeError):
|
||||||
return {
|
return {
|
||||||
"detail": str(error),
|
"detail": str(error),
|
||||||
"status": 401,
|
"status": 401,
|
||||||
"title": "Unauthorized s",
|
"title": "Unauthorized",
|
||||||
}, 401
|
}, 401
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,3 +63,11 @@ class NotFoundError(RuntimeError):
|
||||||
"status": 404,
|
"status": 404,
|
||||||
"title": "Not Found",
|
"title": "Not Found",
|
||||||
}, 404
|
}, 404
|
||||||
|
|
||||||
|
|
||||||
|
def mime_types():
|
||||||
|
|
||||||
|
types = list(set(mime_types_map.values()))
|
||||||
|
types.sort()
|
||||||
|
|
||||||
|
return types
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from pygments.lexers import get_lexer_by_name, find_lexer_class_by_name
|
from pygments.lexers import (get_lexer_by_name, find_lexer_class_by_name, get_all_lexers)
|
||||||
from pygments.formatters import find_formatter_class, HtmlFormatter
|
from pygments.formatters import (find_formatter_class, HtmlFormatter, get_all_formatters)
|
||||||
|
|
||||||
|
|
||||||
def highlight(
|
def highlight(
|
||||||
|
|
@ -18,3 +18,13 @@ def highlight(
|
||||||
formatter = find_formatter_class(format_alias)(linenos=linenos)
|
formatter = find_formatter_class(format_alias)(linenos=linenos)
|
||||||
|
|
||||||
return highlight(data, get_lexer_by_name(lexer_alias), formatter)
|
return highlight(data, get_lexer_by_name(lexer_alias), formatter)
|
||||||
|
|
||||||
|
|
||||||
|
def syntax_shortnames():
|
||||||
|
|
||||||
|
return {l[0]:l[1][0] for l in get_all_lexers() if len(l[1]) > 0}
|
||||||
|
|
||||||
|
|
||||||
|
def format_shortnames():
|
||||||
|
|
||||||
|
return [f.aliases[0] for f in get_all_formatters()]
|
||||||
25
src/httpaste/helper/template.py
Normal file
25
src/httpaste/helper/template.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
views = Environment(
|
||||||
|
loader=PackageLoader("httpaste", "view"),
|
||||||
|
autoescape=select_autoescape()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_template_with_context(template: object, **kwargs):
|
||||||
|
"""render a template with global context variables
|
||||||
|
|
||||||
|
the definition of a global context is abstract, it does neither apply
|
||||||
|
to Flask, nor to Jinja2 and only acts as a bridge for passing
|
||||||
|
variables from flask to jinja2, without having to define them within
|
||||||
|
each controller.
|
||||||
|
|
||||||
|
:param template: jinja2 template object
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
return template.render(**{**kwargs, **{
|
||||||
|
'flask': namedtuple('Flask', request._view.keys())(**request._view)
|
||||||
|
}})
|
||||||
54
src/httpaste/helper/url.py
Normal file
54
src/httpaste/helper/url.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
|
|
||||||
|
def url_query_string(fields:dict):
|
||||||
|
|
||||||
|
return '&'.join([f'{k}={v}' for k,v in fields.items()])
|
||||||
|
|
||||||
|
|
||||||
|
def url_append_query_param(url:str, name: str, value:str):
|
||||||
|
|
||||||
|
urlcomps = urlparse(url)
|
||||||
|
|
||||||
|
q = parse_qs(urlcomps.query)
|
||||||
|
|
||||||
|
q[name] = value
|
||||||
|
|
||||||
|
qs = url_query_string(q)
|
||||||
|
|
||||||
|
return urlcomps._replace(query=qs).geturl()
|
||||||
|
|
||||||
|
|
||||||
|
def url_upgrade_to_https(url: str, port: Optional[int] = 443):
|
||||||
|
|
||||||
|
urlcomps = urlparse(url)
|
||||||
|
|
||||||
|
urlcomps = urlcomps._replace(scheme='https')
|
||||||
|
|
||||||
|
if url != urlcomps.geturl():
|
||||||
|
|
||||||
|
hostname = urlcomps.netloc.rsplit(':', 1)[0]
|
||||||
|
|
||||||
|
if port != 443:
|
||||||
|
netloc = ':'.join((hostname, str(port)))
|
||||||
|
else:
|
||||||
|
netloc = hostname
|
||||||
|
|
||||||
|
urlcomps = urlcomps._replace(netloc=netloc)
|
||||||
|
|
||||||
|
return urlcomps.geturl()
|
||||||
|
|
||||||
|
|
||||||
|
def url_has_tld(url:str, tld:str):
|
||||||
|
|
||||||
|
urlcomps = urlparse(url)
|
||||||
|
|
||||||
|
hostname = urlcomps.netloc.rsplit(':', 1)[0]
|
||||||
|
|
||||||
|
hostname_levels = hostname.rsplit('.', 1)
|
||||||
|
|
||||||
|
if len(hostname_levels) > 1 and hostname_levels[-1:][0] == tld:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
@ -1,144 +1,19 @@
|
||||||
"""Model
|
"""Model
|
||||||
"""
|
"""
|
||||||
from typing import NamedTuple, Optional, Dict, Union, Any, TypedDict
|
from typing import NamedTuple
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from httpaste.model.paste import Config as PasteConfig
|
||||||
|
from httpaste.model.paste import get_paste_model_config
|
||||||
|
|
||||||
|
class Config(NamedTuple):
|
||||||
|
"""Model Configuration"""
|
||||||
|
paste: PasteConfig
|
||||||
|
|
||||||
|
|
||||||
class PasteDataSchema:
|
def get_model_config(configIni: ConfigParser, path:Path) -> Config:
|
||||||
"""Paste Interface schema between Model and Backend
|
|
||||||
"""
|
|
||||||
pid = bytes
|
|
||||||
data = bytes
|
|
||||||
data_hash = bytes
|
|
||||||
sub = bytes
|
|
||||||
timestamp = int
|
|
||||||
lifetime = int
|
|
||||||
expiration = int
|
|
||||||
encoding = str
|
|
||||||
|
|
||||||
|
paste_config = get_paste_model_config(configIni)
|
||||||
|
|
||||||
class UserDataSchema:
|
return Config(paste=paste_config)
|
||||||
"""User Interface Schema between Model and Backend
|
|
||||||
"""
|
|
||||||
sub = bytes
|
|
||||||
key_hash = bytes
|
|
||||||
index = bytes
|
|
||||||
|
|
||||||
|
|
||||||
class Backend(object):
|
|
||||||
"""Backend
|
|
||||||
"""
|
|
||||||
parameter_class: str
|
|
||||||
|
|
||||||
|
|
||||||
class Salt(bytes):
|
|
||||||
"""Salt
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteData(PasteDataSchema.data):
|
|
||||||
"""Paste Data
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteHash(PasteDataSchema.data_hash):
|
|
||||||
"""Paste Data Hash
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteTimestamp(PasteDataSchema.timestamp):
|
|
||||||
"""Paste Timestamp
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteEncoding(PasteDataSchema.encoding):
|
|
||||||
"""
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteExpiration(PasteDataSchema.expiration):
|
|
||||||
"""Paste Expiration
|
|
||||||
|
|
||||||
< 0: after first acccess
|
|
||||||
0: never
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteLifetime(PasteDataSchema.lifetime):
|
|
||||||
"""Paste Lifetime
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteSub(PasteDataSchema.sub):
|
|
||||||
"""Hashed user id
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class KeyHash(UserDataSchema.key_hash):
|
|
||||||
"""User Master Key Hash
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteKey(bytes):
|
|
||||||
"""Paste encryption key
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PasteId(PasteDataSchema.pid):
|
|
||||||
"""Paste unique identifier
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class MasterKey(bytes):
|
|
||||||
"""User's master encryption key
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Sub(UserDataSchema.sub):
|
|
||||||
"""User id
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Index(TypedDict):
|
|
||||||
"""User Paste Index
|
|
||||||
"""
|
|
||||||
auth_expires: int
|
|
||||||
pastes: Dict[str, Dict[str, Any]]
|
|
||||||
|
|
||||||
|
|
||||||
class SerializedIndex(UserDataSchema.index):
|
|
||||||
"""User Paste Index (serialized)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class User(NamedTuple):
|
|
||||||
"""Global User Model (and Prototype)
|
|
||||||
|
|
||||||
non-optional values are prototype values
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: user id
|
|
||||||
sub: Sub
|
|
||||||
#: user's master key hash
|
|
||||||
key_hash: Optional[KeyHash] = None
|
|
||||||
#: user's paste index
|
|
||||||
index: Optional[Union[Index, SerializedIndex]] = None
|
|
||||||
|
|
||||||
|
|
||||||
class Paste(NamedTuple):
|
|
||||||
"""Global Paste Model (and Prototype)
|
|
||||||
|
|
||||||
non-optional values are prototype values
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: paste id
|
|
||||||
pid: PasteId
|
|
||||||
#: paste owner
|
|
||||||
sub: Optional[PasteSub] = None
|
|
||||||
#: paste data
|
|
||||||
data: Optional[PasteData] = None
|
|
||||||
#: paste data hash
|
|
||||||
data_hash: Optional[PasteHash] = None
|
|
||||||
#: paste timestamp
|
|
||||||
expiration: Optional[PasteExpiration] = None
|
|
||||||
#: paste encoding
|
|
||||||
encoding: Optional[PasteEncoding] = None
|
|
||||||
|
|
@ -2,17 +2,32 @@
|
||||||
"""paste model interface
|
"""paste model interface
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, NamedTuple
|
||||||
import time
|
import time
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from string import ascii_uppercase, digits, ascii_letters, punctuation
|
||||||
|
|
||||||
from httpaste import Config
|
|
||||||
|
from httpaste.context import Config as ContextConfig
|
||||||
from httpaste.helper.crypto import dhash, shash, encrypt, decrypt
|
from httpaste.helper.crypto import dhash, shash, encrypt, decrypt
|
||||||
|
from httpaste.helper.config import get_sanitized_config_charset, get_config
|
||||||
from httpaste.helper.common import generate_random_string
|
from httpaste.helper.common import generate_random_string
|
||||||
from httpaste.model import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt,
|
from httpaste.schema import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt,
|
||||||
PasteData, PasteHash, PasteTimestamp, PasteSub,
|
PasteData, PasteHash, PasteTimestamp, PasteSub,
|
||||||
PasteLifetime, PasteEncoding, PasteExpiration)
|
PasteLifetime, PasteEncoding, PasteExpiration)
|
||||||
|
|
||||||
|
|
||||||
|
class Config(NamedTuple):
|
||||||
|
id_size: int = 8
|
||||||
|
id_charset: str = ascii_letters + digits
|
||||||
|
key_size: int = 32
|
||||||
|
key_charset: str = get_sanitized_config_charset(ascii_letters + digits + punctuation)
|
||||||
|
default_lifetime: int = 5
|
||||||
|
default_max_lifetime: int = 1440
|
||||||
|
default_min_lifetime: int = 1
|
||||||
|
default_encoding: str = 'utf-8'
|
||||||
|
|
||||||
|
|
||||||
class NotFoundError(Exception):
|
class NotFoundError(Exception):
|
||||||
"""Paste Exception
|
"""Paste Exception
|
||||||
"""
|
"""
|
||||||
|
|
@ -38,9 +53,7 @@ class BackendError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def generate_paste_id(
|
def generate_paste_id(length: int, charset: str) -> bytes:
|
||||||
length: int = Config.paste_id_size,
|
|
||||||
charset: str = Config.paste_id_charset) -> bytes:
|
|
||||||
"""generate a paste id
|
"""generate a paste id
|
||||||
|
|
||||||
:param length: length of id
|
:param length: length of id
|
||||||
|
|
@ -50,9 +63,7 @@ def generate_paste_id(
|
||||||
return generate_random_string(length, charset).encode('utf-8')
|
return generate_random_string(length, charset).encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
def generate_paste_key(
|
def generate_paste_key(length: int, charset: str) -> bytes:
|
||||||
length: int = Config.paste_key_size,
|
|
||||||
charset: str = Config.paste_key_charset) -> bytes:
|
|
||||||
"""generate a paste encryption key
|
"""generate a paste encryption key
|
||||||
|
|
||||||
:param length: length of key
|
:param length: length of key
|
||||||
|
|
@ -83,7 +94,7 @@ def load(proto: Paste, backend: object) -> Optional[Paste]:
|
||||||
if proto.sub and model.sub != shash(
|
if proto.sub and model.sub != shash(
|
||||||
proto.sub,
|
proto.sub,
|
||||||
model.data_hash,
|
model.data_hash,
|
||||||
proto.pid) or not proto.sub and model.sub:
|
proto.pid) or (not proto.sub and model.sub):
|
||||||
|
|
||||||
raise SubError('Paste not owned by user')
|
raise SubError('Paste not owned by user')
|
||||||
|
|
||||||
|
|
@ -98,8 +109,7 @@ def load_safe(
|
||||||
proto: Paste,
|
proto: Paste,
|
||||||
key: PasteKey,
|
key: PasteKey,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig):
|
||||||
hmac_iter: int = Config.hmac_iterations):
|
|
||||||
"""load an encrypted paste model
|
"""load an encrypted paste model
|
||||||
|
|
||||||
:param proto: paste model prototype
|
:param proto: paste model prototype
|
||||||
|
|
@ -109,7 +119,7 @@ def load_safe(
|
||||||
|
|
||||||
model = load(proto, backend)
|
model = load(proto, backend)
|
||||||
|
|
||||||
data = decrypt(model.data, key, salt, hmac_iter)
|
data = decrypt(model.data, key, context.salt, context.hmac_iter)
|
||||||
|
|
||||||
if model.data_hash and dhash(data) != model.data_hash:
|
if model.data_hash and dhash(data) != model.data_hash:
|
||||||
|
|
||||||
|
|
@ -131,10 +141,7 @@ def dump(model: Paste, backend: object) -> None:
|
||||||
:param backend: model backend object
|
:param backend: model backend object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
|
||||||
backend.dump(model)
|
backend.dump(model)
|
||||||
except Exception as e:
|
|
||||||
raise BackendError(str(e)) from e
|
|
||||||
|
|
||||||
|
|
||||||
def delete(proto: Paste, backend: object) -> None:
|
def delete(proto: Paste, backend: object) -> None:
|
||||||
|
|
@ -158,13 +165,12 @@ def delete_safe(
|
||||||
proto: Paste,
|
proto: Paste,
|
||||||
key: PasteKey,
|
key: PasteKey,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig) -> None:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> None:
|
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model = load_safe(proto, key, backend, salt, hmac_iter)
|
model = load_safe(proto, key, backend, context)
|
||||||
except LifetimeError:
|
except LifetimeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -177,9 +183,9 @@ def create(
|
||||||
data: PasteData,
|
data: PasteData,
|
||||||
lifetime: PasteLifetime,
|
lifetime: PasteLifetime,
|
||||||
encoding: PasteEncoding,
|
encoding: PasteEncoding,
|
||||||
|
config: Config,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig) -> PasteId:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> PasteId:
|
|
||||||
"""create an unencrypted paste
|
"""create an unencrypted paste
|
||||||
|
|
||||||
:param data: paste data
|
:param data: paste data
|
||||||
|
|
@ -187,18 +193,20 @@ def create(
|
||||||
:param backend: model backend object
|
:param backend: model backend object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pid = PasteId(generate_paste_id())
|
pid = PasteId(generate_paste_id(config.id_size, config.id_charset))
|
||||||
safe_pid = PasteId(dhash(pid))
|
safe_pid = PasteId(dhash(pid))
|
||||||
data_hash = PasteHash(dhash(data))
|
data_hash = PasteHash(dhash(data))
|
||||||
sub = None
|
sub = None
|
||||||
timestamp = PasteTimestamp(int(time.time()))
|
timestamp = PasteTimestamp(int(time.time()))
|
||||||
|
|
||||||
if lifetime < 0:
|
if lifetime is None:
|
||||||
|
lifetime = config.default_lifetime
|
||||||
|
elif lifetime < 0:
|
||||||
expiration = -1
|
expiration = -1
|
||||||
else:
|
else:
|
||||||
expiration = PasteExpiration(timestamp + (lifetime * 60))
|
expiration = PasteExpiration(timestamp + (lifetime * 60))
|
||||||
|
|
||||||
safe_data = PasteData(encrypt(data, pid, salt, hmac_iter))
|
safe_data = PasteData(encrypt(data, pid, context.salt, context.hmac_iter))
|
||||||
|
|
||||||
model = Paste(
|
model = Paste(
|
||||||
safe_pid,
|
safe_pid,
|
||||||
|
|
@ -217,9 +225,9 @@ def create_safe(data: PasteData,
|
||||||
lifetime: PasteLifetime,
|
lifetime: PasteLifetime,
|
||||||
sub: Sub,
|
sub: Sub,
|
||||||
encoding: PasteEncoding,
|
encoding: PasteEncoding,
|
||||||
|
config: Config,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig) -> Tuple[PasteId,PasteKey]:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> Tuple[PasteId,PasteKey]:
|
|
||||||
"""create an encrypted paste
|
"""create an encrypted paste
|
||||||
|
|
||||||
:param data: paste data
|
:param data: paste data
|
||||||
|
|
@ -229,19 +237,21 @@ def create_safe(data: PasteData,
|
||||||
:param salt: randomization salt
|
:param salt: randomization salt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pid = PasteId(generate_paste_id())
|
pid = PasteId(generate_paste_id(config.id_size, config.id_charset))
|
||||||
safe_pid = PasteId(dhash(pid))
|
safe_pid = PasteId(dhash(pid))
|
||||||
pkey = PasteKey(generate_paste_key())
|
pkey = PasteKey(generate_paste_key(config.key_size, config.key_charset))
|
||||||
data_hash = PasteHash(dhash(data))
|
data_hash = PasteHash(dhash(data))
|
||||||
safe_sub = PasteSub(shash(sub, data_hash, pid))
|
safe_sub = PasteSub(shash(sub, data_hash, pid))
|
||||||
timestamp = PasteTimestamp(int(time.time()))
|
timestamp = PasteTimestamp(int(time.time()))
|
||||||
|
|
||||||
if lifetime < 0:
|
if lifetime is None:
|
||||||
|
lifetime = config.default_lifetime
|
||||||
|
elif lifetime < 0:
|
||||||
expiration = -1
|
expiration = -1
|
||||||
else:
|
else:
|
||||||
expiration = PasteExpiration(timestamp + (lifetime * 60))
|
expiration = PasteExpiration(timestamp + (lifetime * 60))
|
||||||
|
|
||||||
safe_data = PasteData(encrypt(data, pkey, salt, hmac_iter))
|
safe_data = PasteData(encrypt(data, pkey, context.salt, context.hmac_iter))
|
||||||
|
|
||||||
dump(Paste(
|
dump(Paste(
|
||||||
safe_pid,
|
safe_pid,
|
||||||
|
|
@ -269,21 +279,20 @@ def remove_safe(
|
||||||
sub: Sub,
|
sub: Sub,
|
||||||
key: PasteKey,
|
key: PasteKey,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig):
|
||||||
hmac_iter: int = Config.hmac_iterations):
|
|
||||||
|
|
||||||
proto = Paste(pid, sub)
|
proto = Paste(pid, sub)
|
||||||
|
|
||||||
delete_safe(proto, key, backend, salt, hmac_iter)
|
delete_safe(proto, key, backend, context)
|
||||||
|
|
||||||
|
|
||||||
def get(pid: PasteId, backend: object, salt: Salt = Config.salt, hmac_iter: int = Config.hmac_iterations) -> PasteData:
|
def get(pid: PasteId, backend: object, context: ContextConfig) -> PasteData:
|
||||||
"""conveniently load an unencrypted paste
|
"""conveniently load an unencrypted paste
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = load(Paste(pid), backend)
|
model = load(Paste(pid), backend)
|
||||||
|
|
||||||
data = decrypt(model.data, pid, salt, hmac_iter)
|
data = decrypt(model.data, pid, context.salt, context.hmac_iter)
|
||||||
|
|
||||||
return PasteData(data), model.expiration, model.encoding
|
return PasteData(data), model.expiration, model.encoding
|
||||||
|
|
||||||
|
|
@ -292,12 +301,22 @@ def get_safe(
|
||||||
pid: PasteId,
|
pid: PasteId,
|
||||||
pkey: PasteKey,
|
pkey: PasteKey,
|
||||||
sub: Sub,
|
sub: Sub,
|
||||||
|
config: Config,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig) -> PasteData:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> PasteData:
|
|
||||||
"""conveniently load an encrypted paste
|
"""conveniently load an encrypted paste
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter)
|
model = load_safe(Paste(pid, sub), pkey, backend, context)
|
||||||
|
|
||||||
return PasteData(model.data), model.expiration, model.encoding
|
return PasteData(model.data), model.expiration, model.encoding
|
||||||
|
|
||||||
|
|
||||||
|
def get_paste_model_config(configIni: ConfigParser) -> Config:
|
||||||
|
|
||||||
|
return get_config(configIni, 'model.paste', Config)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
get_paste_model_config
|
||||||
|
]
|
||||||
|
|
@ -5,7 +5,7 @@ import json
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from httpaste import Config
|
from httpaste.context import Config as ContextConfig
|
||||||
from httpaste.helper.crypto import (
|
from httpaste.helper.crypto import (
|
||||||
dhash,
|
dhash,
|
||||||
shash,
|
shash,
|
||||||
|
|
@ -13,7 +13,7 @@ from httpaste.helper.crypto import (
|
||||||
decrypt,
|
decrypt,
|
||||||
derive_key,
|
derive_key,
|
||||||
DecryptionError)
|
DecryptionError)
|
||||||
from httpaste.model import (
|
from httpaste.schema import (
|
||||||
User,
|
User,
|
||||||
KeyHash,
|
KeyHash,
|
||||||
Index,
|
Index,
|
||||||
|
|
@ -39,8 +39,7 @@ def _load(
|
||||||
proto: User,
|
proto: User,
|
||||||
master_key: str,
|
master_key: str,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig) -> Optional[User]:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> Optional[User]:
|
|
||||||
"""load user model
|
"""load user model
|
||||||
|
|
||||||
:param model: user model prototype
|
:param model: user model prototype
|
||||||
|
|
@ -55,7 +54,7 @@ def _load(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
serialized_data = decrypt(model.index, master_key, salt, hmac_iter)
|
serialized_data = decrypt(model.index, master_key, context.salt, context.hmac_iter)
|
||||||
except DecryptionError as e:
|
except DecryptionError as e:
|
||||||
raise IndexError('unable to decrypt user index') from e
|
raise IndexError('unable to decrypt user index') from e
|
||||||
else:
|
else:
|
||||||
|
|
@ -71,8 +70,7 @@ def _dump(
|
||||||
model: User,
|
model: User,
|
||||||
key: MasterKey,
|
key: MasterKey,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig) -> None:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> None:
|
|
||||||
"""dump a user model
|
"""dump a user model
|
||||||
|
|
||||||
:param model: user model
|
:param model: user model
|
||||||
|
|
@ -87,7 +85,7 @@ def _dump(
|
||||||
|
|
||||||
serialized_index = json.dumps(model.index).encode('utf-8')
|
serialized_index = json.dumps(model.index).encode('utf-8')
|
||||||
|
|
||||||
safe_index = SerializedIndex(encrypt(serialized_index, key, salt, hmac_iter))
|
safe_index = SerializedIndex(encrypt(serialized_index, key, context.salt, context.hmac_iter))
|
||||||
|
|
||||||
backend.dump(User(*model[:-1], safe_index))
|
backend.dump(User(*model[:-1], safe_index))
|
||||||
|
|
||||||
|
|
@ -96,7 +94,8 @@ def load_paste_key(
|
||||||
pid: PasteId,
|
pid: PasteId,
|
||||||
sub: Sub,
|
sub: Sub,
|
||||||
key: MasterKey,
|
key: MasterKey,
|
||||||
backend: object, salt: Salt = Config.salt, hmac_iter: int = Config.hmac_iterations) -> Optional[PasteKey]:
|
backend: object,
|
||||||
|
context: ContextConfig) -> Optional[PasteKey]:
|
||||||
"""load a user paste key
|
"""load a user paste key
|
||||||
|
|
||||||
:param pid: paste id
|
:param pid: paste id
|
||||||
|
|
@ -106,7 +105,7 @@ def load_paste_key(
|
||||||
:param salt: randomization salt
|
:param salt: randomization salt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = _load(User(sub), key, backend, salt, hmac_iter)
|
model = _load(User(sub), key, backend, context)
|
||||||
|
|
||||||
for k, v in model.index.get('pastes').items():
|
for k, v in model.index.get('pastes').items():
|
||||||
|
|
||||||
|
|
@ -123,8 +122,7 @@ def dump_paste_key(
|
||||||
sub: Sub,
|
sub: Sub,
|
||||||
key: MasterKey,
|
key: MasterKey,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: str = Config.salt,
|
context: ContextConfig) -> None:
|
||||||
hmac_iter: int = Config.hmac_iterations) -> None:
|
|
||||||
"""dump a user paste key
|
"""dump a user paste key
|
||||||
|
|
||||||
:param pid: paste id
|
:param pid: paste id
|
||||||
|
|
@ -134,21 +132,20 @@ def dump_paste_key(
|
||||||
:param backend: user model backend
|
:param backend: user model backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = _load(User(sub), key, backend, salt, hmac_iter)
|
model = _load(User(sub), key, backend, context)
|
||||||
|
|
||||||
model.index.setdefault('pastes', {})[pid.hex()] = {
|
model.index.setdefault('pastes', {})[pid.hex()] = {
|
||||||
'key': pkey.hex()
|
'key': pkey.hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
_dump(model, key, backend, salt, hmac_iter)
|
_dump(model, key, backend, context)
|
||||||
|
|
||||||
|
|
||||||
def authenticate(
|
def authenticate(
|
||||||
user_id: bytes,
|
user_id: bytes,
|
||||||
password: bytes,
|
password: bytes,
|
||||||
backend: object,
|
backend: object,
|
||||||
salt: Salt = Config.salt,
|
context: ContextConfig):
|
||||||
hmac_iter: int = Config.hmac_iterations):
|
|
||||||
"""authenticate a user
|
"""authenticate a user
|
||||||
|
|
||||||
:param user_id: human-readable user id
|
:param user_id: human-readable user id
|
||||||
|
|
@ -156,7 +153,7 @@ def authenticate(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sub = Sub(dhash(user_id))
|
sub = Sub(dhash(user_id))
|
||||||
key = MasterKey(derive_key(password, salt, hmac_iter))
|
key = MasterKey(derive_key(password, context.salt, context.hmac_iter))
|
||||||
key_hash = KeyHash(dhash(key))
|
key_hash = KeyHash(dhash(key))
|
||||||
|
|
||||||
proto = User(sub)
|
proto = User(sub)
|
||||||
|
|
@ -164,7 +161,7 @@ def authenticate(
|
||||||
bogus_decline_msg = 'unable to authenticate'
|
bogus_decline_msg = 'unable to authenticate'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model = _load(proto, key, backend, salt, hmac_iter)
|
model = _load(proto, key, backend, context)
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
raise AuthenticationError(bogus_decline_msg) from e
|
raise AuthenticationError(bogus_decline_msg) from e
|
||||||
|
|
||||||
|
|
@ -175,7 +172,7 @@ def authenticate(
|
||||||
}
|
}
|
||||||
|
|
||||||
model = User(sub, key_hash, Index(data))
|
model = User(sub, key_hash, Index(data))
|
||||||
_dump(model, key, backend, salt, hmac_iter)
|
_dump(model, key, backend, context)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
if model.key_hash != key_hash:
|
if model.key_hash != key_hash:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
from typing import NamedTuple, Optional, Dict, Union, Any, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class PasteDataSchema:
|
||||||
|
"""Paste Interface schema between Model and Backend
|
||||||
|
"""
|
||||||
|
pid = bytes
|
||||||
|
data = bytes
|
||||||
|
data_hash = bytes
|
||||||
|
sub = bytes
|
||||||
|
timestamp = int
|
||||||
|
lifetime = int
|
||||||
|
expiration = int
|
||||||
|
encoding = str
|
||||||
|
|
||||||
|
|
||||||
|
class UserDataSchema:
|
||||||
|
"""User Interface Schema between Model and Backend
|
||||||
|
"""
|
||||||
|
sub = bytes
|
||||||
|
key_hash = bytes
|
||||||
|
index = bytes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Salt(bytes):
|
||||||
|
"""Salt
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteData(PasteDataSchema.data):
|
||||||
|
"""Paste Data
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteHash(PasteDataSchema.data_hash):
|
||||||
|
"""Paste Data Hash
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteTimestamp(PasteDataSchema.timestamp):
|
||||||
|
"""Paste Timestamp
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteEncoding(PasteDataSchema.encoding):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteExpiration(PasteDataSchema.expiration):
|
||||||
|
"""Paste Expiration
|
||||||
|
|
||||||
|
< 0: after first acccess
|
||||||
|
0: never
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteLifetime(PasteDataSchema.lifetime):
|
||||||
|
"""Paste Lifetime
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteSub(PasteDataSchema.sub):
|
||||||
|
"""Hashed user id
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class KeyHash(UserDataSchema.key_hash):
|
||||||
|
"""User Master Key Hash
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteKey(bytes):
|
||||||
|
"""Paste encryption key
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteId(PasteDataSchema.pid):
|
||||||
|
"""Paste unique identifier
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class MasterKey(bytes):
|
||||||
|
"""User's master encryption key
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Sub(UserDataSchema.sub):
|
||||||
|
"""User id
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Index(TypedDict):
|
||||||
|
"""User Paste Index
|
||||||
|
"""
|
||||||
|
auth_expires: int
|
||||||
|
pastes: Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
class SerializedIndex(UserDataSchema.index):
|
||||||
|
"""User Paste Index (serialized)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class User(NamedTuple):
|
||||||
|
"""Global User Model (and Prototype)
|
||||||
|
|
||||||
|
non-optional values are prototype values
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: user id
|
||||||
|
sub: Sub
|
||||||
|
#: user's master key hash
|
||||||
|
key_hash: Optional[KeyHash] = None
|
||||||
|
#: user's paste index
|
||||||
|
index: Optional[Union[Index, SerializedIndex]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Paste(NamedTuple):
|
||||||
|
"""Global Paste Model (and Prototype)
|
||||||
|
|
||||||
|
non-optional values are prototype values
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: paste id
|
||||||
|
pid: PasteId
|
||||||
|
#: paste owner
|
||||||
|
sub: Optional[PasteSub] = None
|
||||||
|
#: paste data
|
||||||
|
data: Optional[PasteData] = None
|
||||||
|
#: paste data hash
|
||||||
|
data_hash: Optional[PasteHash] = None
|
||||||
|
#: paste timestamp
|
||||||
|
expiration: Optional[PasteExpiration] = None
|
||||||
|
#: paste encoding
|
||||||
|
encoding: Optional[PasteEncoding] = None
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get description",
|
"description": "get description",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"303": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"content": {
|
"content": {
|
||||||
"text/plain": {
|
"text/plain": {
|
||||||
|
|
@ -185,6 +185,277 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/ui": {
|
||||||
|
"get": {
|
||||||
|
"description": "create a new public paste",
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste location",
|
||||||
|
"content": {
|
||||||
|
"text/html": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/paste": {
|
||||||
|
"get": {
|
||||||
|
"description": "create a new public paste",
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste location",
|
||||||
|
"content": {
|
||||||
|
"text/html": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/paste/public": {
|
||||||
|
"get": {
|
||||||
|
"description": "create a new public paste UI-driven",
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste location",
|
||||||
|
"content": {
|
||||||
|
"text/html": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "create a new public paste",
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/pastePost"
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/lifetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste location",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasteURL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/paste/private": {
|
||||||
|
"get": {
|
||||||
|
"description": "create a new public paste UI-driven",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste location",
|
||||||
|
"content": {
|
||||||
|
"text/html": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "create a new public paste",
|
||||||
|
"requestBody": {
|
||||||
|
"$ref": "#/components/requestBodies/pastePost"
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/lifetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/encoding"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste location",
|
||||||
|
"content": {
|
||||||
|
"text/plain": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasteURL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/paste/public/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "get a public paste",
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/syntax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/linenos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/mime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/ui_preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste data. content type may vary.",
|
||||||
|
"content": {
|
||||||
|
"text/html": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasteData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/paste/private/{id}": {
|
||||||
|
"get": {
|
||||||
|
"description": "get a public paste",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/syntax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/linenos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/mime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/ui_preview"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste data. content type may vary.",
|
||||||
|
"content": {
|
||||||
|
"text/html": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/PasteData"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/user/session": {
|
||||||
|
"get": {
|
||||||
|
"description": "get a public paste",
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"basicAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste data. content type may vary.",
|
||||||
|
"content": {
|
||||||
|
"text/html": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/user": {
|
||||||
|
"get": {
|
||||||
|
"description": "get a public paste",
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste data. content type may vary.",
|
||||||
|
"content": {
|
||||||
|
"text/html": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ui/user/session/delete": {
|
||||||
|
"get": {
|
||||||
|
"description": "get a public paste",
|
||||||
|
"security": [
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste data. content type may vary.",
|
||||||
|
"content": {
|
||||||
|
"text/html": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|
@ -215,9 +486,9 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "binary"
|
"format": "binary"
|
||||||
},
|
},
|
||||||
"rsa_public_key": {
|
"fileName": {
|
||||||
"description": "RSA public key",
|
"type": "string",
|
||||||
"type": "string"
|
"format": "binary"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
@ -294,6 +565,15 @@
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ui_preview": {
|
||||||
|
"description": "enable preview in UI",
|
||||||
|
"name": "preview",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
|
|
|
||||||
19
src/httpaste/server.py
Executable file
19
src/httpaste/server.py
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from typing import NamedTuple
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
from httpaste.helper.config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
class Config(NamedTuple):
|
||||||
|
"""connexion config
|
||||||
|
"""
|
||||||
|
swagger_ui: bool = True
|
||||||
|
bind_address: str = None
|
||||||
|
request_ssl: bool = True
|
||||||
|
ssl_port: int = 443
|
||||||
|
|
||||||
|
|
||||||
|
def get_server_config(configIni: ConfigParser) -> Config:
|
||||||
|
|
||||||
|
return get_config(configIni, 'server', Config)
|
||||||
0
src/httpaste/view/__init__.py
Normal file
0
src/httpaste/view/__init__.py
Normal file
0
src/httpaste/view/container/__init__.py
Normal file
0
src/httpaste/view/container/__init__.py
Normal file
39
src/httpaste/view/container/get_paste_form.html
Normal file
39
src/httpaste/view/container/get_paste_form.html
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<form class="color-theme font-theme" action="{{preview_url}}" method="get" enctype="multipart/form-data">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<label for="syntax">Syntax</label>
|
||||||
|
<!--<input type="text" name="syntax" value="{{ query['syntax'] }}"/>-->
|
||||||
|
|
||||||
|
<select name="syntax">
|
||||||
|
<option {{ 'selected=""'if not query['syntax'] }} value>Disabled</option>
|
||||||
|
{% for name, shortname in syntax_shortnames.items() %}
|
||||||
|
<option value="{{shortname}}" {{ 'selected=""'if query['syntax'] == shortname }}>{{ name }}</option>
|
||||||
|
{% endfor %}</select>
|
||||||
|
</summary>
|
||||||
|
<small>language to highlight syntax for</small>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<label for="format">Format</label>
|
||||||
|
<select name="format">
|
||||||
|
<option {{ 'selected=""'if not query['format'] }} value>Disabled</option>
|
||||||
|
{% for shortname in format_shortnames %}
|
||||||
|
<option value="{{shortname}}" {{ 'selected=""'if query['format'] == shortname }}>{{ shortname }}</option>
|
||||||
|
{% endfor %}</select>
|
||||||
|
</summary>
|
||||||
|
<small>output format of highlighted syntax</small>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<label for="mime">Content-Type (MIME)</label>
|
||||||
|
<select name="mime">
|
||||||
|
<option {{ 'selected=""'if not query['mime'] }} value>Disabled</option>
|
||||||
|
{% for mime in mime_types %}
|
||||||
|
<option value="{{mime}}" {{ 'selected=""'if query['mime'] == mime }}>{{ mime }}</option>
|
||||||
|
{% endfor %}</select>
|
||||||
|
</summary>
|
||||||
|
<small>content-type Header the server should return</small>
|
||||||
|
</details>
|
||||||
|
<input type="text" name="preview" value="{{ query['preview'] }}" style="display: none"/>
|
||||||
|
<input type="submit" value="☝ Refresh"/>
|
||||||
|
</form>
|
||||||
26
src/httpaste/view/container/post_paste_form.html
Normal file
26
src/httpaste/view/container/post_paste_form.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<form class="color-theme font-theme" action="{{paste_form_url}}" method="post" enctype="multipart/form-data" target="_top">
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<label for="data">Data
|
||||||
|
<textarea name="data" cols="60" rows="10" style="width:100%"></textarea><br/>
|
||||||
|
<input type="file" id="myfile" name="data"/>
|
||||||
|
</label>
|
||||||
|
</summary>
|
||||||
|
<small>
|
||||||
|
Either supply a past text, or upload a file.
|
||||||
|
</small>
|
||||||
|
</details>
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
<label for="lifetime">Lifetime
|
||||||
|
<input type="number" id="lname" name="lifetime" value="5">
|
||||||
|
</label>
|
||||||
|
</summary>
|
||||||
|
<small>
|
||||||
|
Set a paste’s lifetime to make it expire after a specified amount of time.
|
||||||
|
The lifetime must be provided in minutes and cannot be less than 1 (, unless lesser than 0).
|
||||||
|
A lifetime of 0 will evaluate to a lifetime 1. A lifetime of less than 0 will make the paste expire after first read.
|
||||||
|
</small>
|
||||||
|
</details>
|
||||||
|
<input type="submit" value="☝ Paste"/>
|
||||||
|
</form>
|
||||||
0
src/httpaste/view/frame/__init__.py
Normal file
0
src/httpaste/view/frame/__init__.py
Normal file
16
src/httpaste/view/frame/base.html
Normal file
16
src/httpaste/view/frame/base.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
48
src/httpaste/view/frame/decorated.html
Normal file
48
src/httpaste/view/frame/decorated.html
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title></title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: #DDD;
|
||||||
|
padding: 0 1em 0 1em;
|
||||||
|
font-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
form > *, menu > li {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blinking{
|
||||||
|
animation:blinkingText 1.0s infinite;
|
||||||
|
animation-timing-function: step-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinkingText {
|
||||||
|
0%{ color: red; }
|
||||||
|
50%{ color: transparent; }
|
||||||
|
100%{ color: red; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
{% if flask.before_request__ssl_url is defined %}
|
||||||
|
<p>
|
||||||
|
<strong class="blinking warning">WARNING:</strong> Communication not encrypted - SSL/TLS will not be enforced. Visit <a href="{{ flask.before_request__ssl_url }}">{{ flask.before_request__ssl_url }}</a>, for SSL/TLS encryption.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
</header>
|
||||||
|
<hr/>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
0
src/httpaste/view/viewport/__init__.py
Normal file
0
src/httpaste/view/viewport/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/paste/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/paste/__init__.py
Normal file
36
src/httpaste/view/viewport/ui/paste/get.html
Normal file
36
src/httpaste/view/viewport/ui/paste/get.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends 'frame/decorated.html' %}
|
||||||
|
{% block header %}
|
||||||
|
<h1><a href="/">httpaste</a> - <a href="/ui/paste">Paste</a> - Conditioner</h1>
|
||||||
|
<p>
|
||||||
|
Preview and conditon an existing paste
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% if query['preview'] %}
|
||||||
|
Preview
|
||||||
|
<iframe src="{{paste_url}}" title="as" style="resize:both; width: 100%"></iframe>
|
||||||
|
{% else %}
|
||||||
|
<p><b>Preview is disabled.</b>
|
||||||
|
<br/>
|
||||||
|
This probably happened because the paste is set to expire after read.
|
||||||
|
<br/>
|
||||||
|
<i>You can still proceed to condition the paste URL.</i>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% include 'container/get_paste_form.html' %}
|
||||||
|
<hr/>
|
||||||
|
<figure>
|
||||||
|
<figcaption><strong>URLs</strong></figcaption>
|
||||||
|
<dl>
|
||||||
|
<dt>Formatted</dt>
|
||||||
|
<dd>
|
||||||
|
<a href="{{paste_url}}" target="_blank">{{paste_url}}</a>
|
||||||
|
</dd>
|
||||||
|
<dt>Raw</dt>
|
||||||
|
<dd>
|
||||||
|
<a href="{{raw_paste_url}}" target="_blank">{{raw_paste_url}}</a>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</figure>
|
||||||
|
{% endblock %}
|
||||||
0
src/httpaste/view/viewport/ui/paste/private/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/paste/private/__init__.py
Normal file
14
src/httpaste/view/viewport/ui/paste/private/search.html
Normal file
14
src/httpaste/view/viewport/ui/paste/private/search.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'frame/decorated.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1><a href="/">httpaste</a> - <a href="/ui/paste">Paste</a> - Private</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Private pastes are authenticated
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% include 'container/post_paste_form.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
0
src/httpaste/view/viewport/ui/paste/public/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/paste/public/__init__.py
Normal file
14
src/httpaste/view/viewport/ui/paste/public/search.html
Normal file
14
src/httpaste/view/viewport/ui/paste/public/search.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'frame/decorated.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1><a href="/">httpaste</a> - <a href="/ui/paste">Paste</a> - Public</h1>
|
||||||
|
<p>
|
||||||
|
Public pastes are not indexed and can only be accessed by knowing their respective
|
||||||
|
paste id.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% include 'container/post_paste_form.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
36
src/httpaste/view/viewport/ui/paste/search.html
Normal file
36
src/httpaste/view/viewport/ui/paste/search.html
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{% extends 'frame/decorated.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1><a href="/">httpaste</a> - Paste</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
All pastes are symetrically encrypted server-side with an HMAC derived key
|
||||||
|
and SHA-256 hashing, a server-side salt and a randomly generated password.
|
||||||
|
Public paste’s passwords are derived from their ids. Private paste’s passwords
|
||||||
|
are randomly generated and stored inside a symetrically encrypted personal database,
|
||||||
|
with the encryption key also being derived through the same HMAC mechanism,
|
||||||
|
where a HTTP basic authentication act as the master password.
|
||||||
|
Paste ids, usernames, and any other identifiable attributes are only stored
|
||||||
|
inside the storage backend as keyed and salted BLAKE2 hashes.
|
||||||
|
</p>
|
||||||
|
<p><strong>Note:</strong>
|
||||||
|
The initial creation of a private paste will prompt for login credentials.
|
||||||
|
If the login credentials are not known, they will be created automatically.
|
||||||
|
If it is required to authenticate with other credentials, clear your local
|
||||||
|
HTTP authentication cache.
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<figure>
|
||||||
|
<figcaption><strong>Navigation</strong></figcaption>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<a href="{{create_private_paste_url}}">Create a Private Paste</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{create_public_paste_url}}">Create a Public Paste</a>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
41
src/httpaste/view/viewport/ui/search.html
Normal file
41
src/httpaste/view/viewport/ui/search.html
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{% extends 'frame/decorated.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1><a href="/">httpaste</a> - versatile HTTP pastebin</h1>
|
||||||
|
<p>
|
||||||
|
This is the user interface of the hosted version of <a href="https://victorykit.bitbucket.io/httpaste" target="_blank">httpaste</a>,
|
||||||
|
a program which offers an HTTP interface for storing public and private data (a.k.a. pastes),
|
||||||
|
commonly referred to as a pastebin application. It is inspired by
|
||||||
|
<a href='http://sprunge.us' target='_blank'>sprunge.us</a> and <a href='http://ix.io' target="_blank">ix.io</a>.
|
||||||
|
It aims for a higher degree of privacy control than available commercial pastebin products.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
httpaste features include:
|
||||||
|
<ul>
|
||||||
|
<li>Authenticated private and unlisted public pasting</li>
|
||||||
|
<li>Lifetime control (including Burn-After-Read expiration)</li>
|
||||||
|
<li>Encoded and Binary Data Upload</li>
|
||||||
|
<li>Syntax Higlighting</li>
|
||||||
|
<li>Output Formatting</li>
|
||||||
|
<li>Output Content-Type Control</li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
A pseudo man page for CLI usage is available via HTTP GET of this host's root document.
|
||||||
|
(e.g. `<code>$ curl httpaste.it</code>`)
|
||||||
|
</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<figure>
|
||||||
|
<figcaption><strong>Navigation</strong></figcaption>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<a href="{{paste_index_url}}">Paste</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{user_index_url}}">User</a>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</figure>
|
||||||
|
{% endblock %}
|
||||||
0
src/httpaste/view/viewport/ui/user/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/user/__init__.py
Normal file
16
src/httpaste/view/viewport/ui/user/search.html
Normal file
16
src/httpaste/view/viewport/ui/user/search.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'frame/decorated.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1><a href="/">httpaste</a> - User</h1>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<figure>
|
||||||
|
<figcaption><strong>Navigation</strong></figcaption>
|
||||||
|
<menu>
|
||||||
|
<li>
|
||||||
|
<a href="{{delete_session_url}}" target="_blank">Clear Local HTTP Authentication Cache</a>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
0
src/httpaste/view/viewport/ui/user/session/__init__.py
Normal file
0
src/httpaste/view/viewport/ui/user/session/__init__.py
Normal file
6
src/httpaste/view/viewport/ui/user/session/search.html
Normal file
6
src/httpaste/view/viewport/ui/user/session/search.html
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends 'frame/base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<a href="{{ session_delete_url }}">Clear Local HTTP Authentication Cache</a>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
0
src/httpaste/views/__init__.py
Normal file
0
src/httpaste/views/__init__.py
Normal file
0
src/httpaste/views/container/__init__.py
Normal file
0
src/httpaste/views/container/__init__.py
Normal file
0
src/httpaste/views/frame/__init__.py
Normal file
0
src/httpaste/views/frame/__init__.py
Normal file
0
src/httpaste/views/viewport/__init__.py
Normal file
0
src/httpaste/views/viewport/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/paste/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/paste/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/paste/private/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/paste/private/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/paste/public/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/paste/public/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/user/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/user/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/user/session/__init__.py
Normal file
0
src/httpaste/views/viewport/ui/user/session/__init__.py
Normal file
|
|
@ -1,8 +1,8 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""httpaste WSGI entrypoint
|
"""httpaste WSGI entrypoint
|
||||||
"""
|
"""
|
||||||
from httpaste import load_config, get_flask_app, get_config_path
|
from httpaste import load_config, get_flask_app
|
||||||
|
|
||||||
config, server_config = load_config(get_config_path())
|
config = load_config()
|
||||||
|
|
||||||
application = get_flask_app(config, server_config)
|
application = get_flask_app(config)
|
||||||
|
|
|
||||||
0
tests/httpaste/backend/__init__.py
Normal file
0
tests/httpaste/backend/__init__.py
Normal file
134
tests/httpaste/backend/test__init__.py
Executable file
134
tests/httpaste/backend/test__init__.py
Executable file
|
|
@ -0,0 +1,134 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import pytest
|
||||||
|
from textwrap import dedent
|
||||||
|
from unittest.mock import mock_open, patch
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def module():
|
||||||
|
|
||||||
|
from httpaste import backend
|
||||||
|
|
||||||
|
return backend
|
||||||
|
|
||||||
|
|
||||||
|
class Test_get_backend_config():
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(self, module):
|
||||||
|
|
||||||
|
self.func = module.get_backend_config
|
||||||
|
|
||||||
|
def test_default_file(self, module):
|
||||||
|
|
||||||
|
data = dedent("""
|
||||||
|
[backend]
|
||||||
|
type = file
|
||||||
|
|
||||||
|
[backend.file]
|
||||||
|
base_dirname = 'sample_data'
|
||||||
|
""")
|
||||||
|
|
||||||
|
path = Path('/foo')
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read(str(path))
|
||||||
|
|
||||||
|
config = self.func(configIni, path)
|
||||||
|
|
||||||
|
assert isinstance(config, module.Config)
|
||||||
|
assert issubclass(config.interface, module.BackendInterface)
|
||||||
|
assert str(config.config.base_dirname) == '/foo/sample_data'
|
||||||
|
|
||||||
|
def test_sqlite(self, module):
|
||||||
|
|
||||||
|
data = dedent("""
|
||||||
|
[backend]
|
||||||
|
type = sqlite
|
||||||
|
|
||||||
|
[backend.sqlite]
|
||||||
|
uri = 'foobar.db'
|
||||||
|
""")
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read('void')
|
||||||
|
|
||||||
|
config = self.func(configIni, Path('/foo'))
|
||||||
|
|
||||||
|
assert str(config.config.uri) == '/foo/foobar.db'
|
||||||
|
|
||||||
|
def test_mysql(self, module):
|
||||||
|
|
||||||
|
data = dedent("""
|
||||||
|
[backend]
|
||||||
|
type = mysql
|
||||||
|
|
||||||
|
[backend.mysql]
|
||||||
|
user = 'foo'
|
||||||
|
password = bar
|
||||||
|
host = manana
|
||||||
|
database = test
|
||||||
|
""")
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read('void')
|
||||||
|
|
||||||
|
config = self.func(configIni, Path('/foo'))
|
||||||
|
|
||||||
|
assert config.config.user == 'foo'
|
||||||
|
assert config.config.password == 'bar'
|
||||||
|
assert config.config.host == 'manana'
|
||||||
|
assert config.config.database == 'test'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#class Test_load():
|
||||||
|
#
|
||||||
|
# @pytest.fixture(autouse=True)
|
||||||
|
# def setup(self, module):
|
||||||
|
#
|
||||||
|
# self.func = module.load
|
||||||
|
#
|
||||||
|
# def test_missing_parameter(self, module):
|
||||||
|
#
|
||||||
|
# config = module.Config()
|
||||||
|
# config.name = 'file'
|
||||||
|
# config.parameters = {}
|
||||||
|
#
|
||||||
|
# with pytest.raises(module.BackendError):
|
||||||
|
# self.func(config)
|
||||||
|
#
|
||||||
|
# def test_unknown_parameter(self, module):
|
||||||
|
#
|
||||||
|
# config = module.Config()
|
||||||
|
# config.name = 'file'
|
||||||
|
# config.parameters = {
|
||||||
|
# 'base_dirname': 'foofoo',
|
||||||
|
# 'foo': 'bar'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# with pytest.raises(module.BackendError):
|
||||||
|
# self.func(config)
|
||||||
|
#
|
||||||
|
# def test_file(self, module):
|
||||||
|
#
|
||||||
|
# config = module.Config()
|
||||||
|
# config.name = 'file'
|
||||||
|
# config.parameters = {
|
||||||
|
# 'base_dirname': 'foofoo',
|
||||||
|
# 'user_dirnamea': 'test'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# backend = self.func(config)
|
||||||
|
#
|
||||||
|
# assert isinstance(backend, module.BackendInterface)
|
||||||
0
tests/httpaste/helper/__init__.py
Normal file
0
tests/httpaste/helper/__init__.py
Normal file
149
tests/httpaste/helper/test_config.py
Normal file
149
tests/httpaste/helper/test_config.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import pytest
|
||||||
|
from typing import NamedTuple
|
||||||
|
from unittest.mock import mock_open, patch
|
||||||
|
from textwrap import dedent
|
||||||
|
from pathlib import Path
|
||||||
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def module():
|
||||||
|
|
||||||
|
from httpaste.helper import config
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_aclass():
|
||||||
|
|
||||||
|
class Foobar(NamedTuple):
|
||||||
|
foo: int
|
||||||
|
bar: str = 'test'
|
||||||
|
|
||||||
|
return Foobar
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_aclass_special():
|
||||||
|
|
||||||
|
class Foobar(NamedTuple):
|
||||||
|
foobar: Path
|
||||||
|
|
||||||
|
return Foobar
|
||||||
|
|
||||||
|
|
||||||
|
class Test_typecast():
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(self, module, mock_aclass):
|
||||||
|
|
||||||
|
self.func = module.typecast
|
||||||
|
|
||||||
|
self.mock_aclass = mock_aclass
|
||||||
|
|
||||||
|
def test_default(self, module):
|
||||||
|
|
||||||
|
foobar = {
|
||||||
|
'foo': '45'
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.func(foobar, self.mock_aclass)
|
||||||
|
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
|
||||||
|
assert result['foo'] == 45
|
||||||
|
assert result.get('bar') is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_type_mismatch(self, module):
|
||||||
|
|
||||||
|
foobar = {
|
||||||
|
'foo': 'foobar'
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
|
||||||
|
self.func(foobar, self.mock_aclass)
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_key(self, module):
|
||||||
|
|
||||||
|
foobar = {
|
||||||
|
'foo': '45',
|
||||||
|
'foobar': 'foobar'
|
||||||
|
}
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
|
||||||
|
self.func(foobar, self.mock_aclass)
|
||||||
|
|
||||||
|
|
||||||
|
class Test_get_config():
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(self, module, mock_aclass):
|
||||||
|
|
||||||
|
self.func = module.get_config
|
||||||
|
|
||||||
|
self.mock_aclass = mock_aclass
|
||||||
|
|
||||||
|
def test_default(self):
|
||||||
|
|
||||||
|
data = dedent("""
|
||||||
|
[foobar]
|
||||||
|
foo = 45
|
||||||
|
""")
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read(str('void'))
|
||||||
|
|
||||||
|
result = self.func(configIni, 'foobar', self.mock_aclass)
|
||||||
|
|
||||||
|
assert isinstance(result, self.mock_aclass)
|
||||||
|
|
||||||
|
assert result.foo == 45
|
||||||
|
|
||||||
|
def test_relative_path(self, mock_aclass_special):
|
||||||
|
|
||||||
|
data = dedent("""
|
||||||
|
[foobar]
|
||||||
|
foobar = 'bar/foo'
|
||||||
|
""")
|
||||||
|
|
||||||
|
dirname = Path('/foo/bar')
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read(str('void'))
|
||||||
|
|
||||||
|
result = self.func(configIni, 'foobar', mock_aclass_special, dirname)
|
||||||
|
|
||||||
|
assert isinstance(result, mock_aclass_special)
|
||||||
|
assert isinstance(result.foobar, Path)
|
||||||
|
assert str(result.foobar) == '/foo/bar/bar/foo'
|
||||||
|
|
||||||
|
def test_absolute_path(self, mock_aclass_special):
|
||||||
|
|
||||||
|
data = dedent("""
|
||||||
|
[foobar]
|
||||||
|
foobar = '/bar/foo'
|
||||||
|
""")
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read(str('void'))
|
||||||
|
|
||||||
|
result = self.func(configIni, 'foobar', mock_aclass_special)
|
||||||
|
|
||||||
|
assert isinstance(result, mock_aclass_special)
|
||||||
|
assert isinstance(result.foobar, Path)
|
||||||
|
assert str(result.foobar) == '/bar/foo'
|
||||||
79
tests/httpaste/model/test_paste.py
Executable file
79
tests/httpaste/model/test_paste.py
Executable file
|
|
@ -0,0 +1,79 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import pytest
|
||||||
|
from textwrap import dedent
|
||||||
|
from unittest.mock import mock_open, patch
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def module():
|
||||||
|
|
||||||
|
from httpaste.model import paste
|
||||||
|
|
||||||
|
return paste
|
||||||
|
|
||||||
|
|
||||||
|
class Test_get_paste_model_config():
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def setup(self, module):
|
||||||
|
|
||||||
|
self.func = module.get_paste_model_config
|
||||||
|
|
||||||
|
def test_default(self, module):
|
||||||
|
|
||||||
|
data = ''
|
||||||
|
|
||||||
|
configIni = ConfigParser()
|
||||||
|
|
||||||
|
with patch('builtins.open', mock_open(read_data=data)):
|
||||||
|
|
||||||
|
configIni.read('void')
|
||||||
|
|
||||||
|
result = self.func(configIni)
|
||||||
|
|
||||||
|
assert isinstance(result, module._Config)
|
||||||
|
assert isinstance(result.id_size, int), result.id_size
|
||||||
|
assert isinstance(result.key_size, int), result.key_size
|
||||||
|
|
||||||
|
|
||||||
|
#class Test_load():
|
||||||
|
#
|
||||||
|
# @pytest.fixture(autouse=True)
|
||||||
|
# def setup(self, module):
|
||||||
|
#
|
||||||
|
# self.func = module.load
|
||||||
|
#
|
||||||
|
# def test_missing_parameter(self, module):
|
||||||
|
#
|
||||||
|
# config = module.Config()
|
||||||
|
# config.name = 'file'
|
||||||
|
# config.parameters = {}
|
||||||
|
#
|
||||||
|
# with pytest.raises(module.BackendError):
|
||||||
|
# self.func(config)
|
||||||
|
#
|
||||||
|
# def test_unknown_parameter(self, module):
|
||||||
|
#
|
||||||
|
# config = module.Config()
|
||||||
|
# config.name = 'file'
|
||||||
|
# config.parameters = {
|
||||||
|
# 'base_dirname': 'foofoo',
|
||||||
|
# 'foo': 'bar'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# with pytest.raises(module.BackendError):
|
||||||
|
# self.func(config)
|
||||||
|
#
|
||||||
|
# def test_file(self, module):
|
||||||
|
#
|
||||||
|
# config = module.Config()
|
||||||
|
# config.name = 'file'
|
||||||
|
# config.parameters = {
|
||||||
|
# 'base_dirname': 'foofoo',
|
||||||
|
# 'user_dirnamea': 'test'
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# backend = self.func(config)
|
||||||
|
#
|
||||||
|
# assert isinstance(backend, module.BackendInterface)
|
||||||
9
tox.ini
9
tox.ini
|
|
@ -35,6 +35,15 @@ deps =
|
||||||
commands =
|
commands =
|
||||||
python3 -m build {posargs}
|
python3 -m build {posargs}
|
||||||
|
|
||||||
|
[testenv:build-docker]
|
||||||
|
description = build docker image
|
||||||
|
passenv =
|
||||||
|
DOCKER_*
|
||||||
|
allowlist_externals =
|
||||||
|
docker
|
||||||
|
sh
|
||||||
|
commands =
|
||||||
|
docker image build -t victorykit/httpaste .
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
description = build documentation
|
description = build documentation
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue