From c0670a3580ca9935d6ba830a962b9be36e48443d Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Tue, 14 Nov 2023 17:41:34 +0530 Subject: [PATCH 01/41] feat: Add normalised schema for Container and CTF --- poetry.lock | 322 +++++++++++++++++++++++++++++++- pyproject.toml | 1 + src/pwncore/models/__init__.py | 4 + src/pwncore/models/container.py | 26 +++ src/pwncore/models/ctf.py | 14 ++ 5 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 src/pwncore/models/__init__.py create mode 100644 src/pwncore/models/container.py create mode 100644 src/pwncore/models/ctf.py diff --git a/poetry.lock b/poetry.lock index ba3d950..93508d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,20 @@ # This file is automatically @generated by Poetry and should not be changed by hand. +[[package]] +name = "aiosqlite" +version = "0.17.0" +description = "asyncio bridge to the standard sqlite3 module" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] + +[package.dependencies] +typing_extensions = ">=3.7.2" + [[package]] name = "annotated-types" version = "0.6.0" @@ -33,6 +48,76 @@ doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd- test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (<0.22)"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncpg" +version = "0.29.0" +description = "An asyncio PostgreSQL driver" +category = "main" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + +[package.extras] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "23.1.0" @@ -105,6 +190,70 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] +[[package]] +name = "ciso8601" +version = "2.3.1" +description = "Fast ISO8601 date time parser for Python written in C" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "ciso8601-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57db9a28e87f9e4fccba643fb70a9ba1515adc5e1325508eb2c10dd96620314c"}, + {file = "ciso8601-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c59646197ddbf84909b6c31d55f744cfeef51811e3910b61d0f58f2885823fd"}, + {file = "ciso8601-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a25da209193134842cd573464a5323f46fcc3ed781b633f15a34793ba7e1064"}, + {file = "ciso8601-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ae83f4e60fc7e260a4188e4ec4ac1bdd40bdb382eeda92fc266c5aa2f0a1ee"}, + {file = "ciso8601-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2c1ef17d1ea52a39b2dce6535583631ae4bfb65c76f0ee8c99413a6861a46c9e"}, + {file = "ciso8601-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3771049ba29bd1077588c0a24be1d53f7493e7cc686b2caa92f7cae129636a0e"}, + {file = "ciso8601-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:55381365366dacb57207cec610d26c9a6c0d237cb65a0cf67a2baaa5299f2366"}, + {file = "ciso8601-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f25647803c9a5aaaed130c53bbec7ea06a4f95ba5c7016f59e444b4ef7ac39e"}, + {file = "ciso8601-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:473288cd63efe6a2cf3f4b5f90394e53095358ccb13d6128f87a2da85d0f389b"}, + {file = "ciso8601-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:121d27c55f4455eaa27ba3bd602beca915df9a352f235e935636a4660321070e"}, + {file = "ciso8601-2.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef44cb4dc83f37019a356c7a72692cbe17072456f4879ca6bc0339f67eee5d00"}, + {file = "ciso8601-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:364702e338212b6c1a8643d9399ada21560cf132f363853473560625cb4207f1"}, + {file = "ciso8601-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8acb45545e6a654310c6ef788aacb2d73686646c414ceacdd9f5f78a83165af5"}, + {file = "ciso8601-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:99addd8b113f85fac549167073f317a318cd2b5841552598ceb97b97c5708a38"}, + {file = "ciso8601-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f39bb5936debf21c52e5d52b89f26857c303da80c43a72883946096a6ef5e561"}, + {file = "ciso8601-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:21cf83ca945bb26ecd95364ae2c9ed0276378e5fe35ce1b64d4c6d5b33038ea3"}, + {file = "ciso8601-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:013410263cba46748d2de29e9894341ae41223356cde7970478c32bd0984d10c"}, + {file = "ciso8601-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b26935687ef1837b56997d8c61f1d789e698be58b261410e629eda9c89812141"}, + {file = "ciso8601-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0d980a2a88030d4d8b2434623c250866a75b4979d289eba69bec445c51ace99f"}, + {file = "ciso8601-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:87721de54e008fb1c4c3978553b05a9c417aa25b76ddf5702d6f7e8d9b109288"}, + {file = "ciso8601-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:9f107a4c051e7c0416824279264d94f4ed3da0fbd82bd96ec3c3293426826de4"}, + {file = "ciso8601-2.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:02ecbd7c8336c4e1c6bb725b898e29414ee92bdc0be6c72fb07036836b1ac867"}, + {file = "ciso8601-2.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36525b1f380f4601533f4631c69911e44efb9cb50beab1da3248b0daa32bced4"}, + {file = "ciso8601-2.3.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:874d20c6339e9096baaadfd1b9610bb8d5b373a0f2858cc06de8142b98d2129c"}, + {file = "ciso8601-2.3.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:46a3663c2cf838f0149e1cdb8e4bdc95716e03cf2d5f803a6eb755d825896ebe"}, + {file = "ciso8601-2.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e8e76825f80ce313d75bbbef1d3b8bd9e0ce31dbc157d1981e9593922c9983e7"}, + {file = "ciso8601-2.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850889813f3135e0aa18f0aaec64249dd81d36a1b9bce60bb45182930c86663"}, + {file = "ciso8601-2.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c690ac24ec3407f68cdfd5e032c6cb18126ef33d6c4b3db0669b9cbb8c96bd4"}, + {file = "ciso8601-2.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:024c52d5d0670f15ca3dc53eff7345b6eaee22fba929675f6a408f9d1e159d98"}, + {file = "ciso8601-2.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7ae2c3442d042de5330672d0d28486ed92f9d7c6dc010943aa618fd361d4638"}, + {file = "ciso8601-2.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:22128f0def36fa3c4cf0c482a216e8b8ad722def08bc11c07438eff82bdcd02a"}, + {file = "ciso8601-2.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:025859ec286a994aa3f2120c0f27d053b719cabc975398338374f2cc1f961125"}, + {file = "ciso8601-2.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2a64ff58904d4418d60fa9619014ae820ae21f7aef58da46df78a4c647f951ec"}, + {file = "ciso8601-2.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d1f85c0b7fa742bbfd18177137ccbaa3f867dd06157f91595075bb959a733048"}, + {file = "ciso8601-2.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ac59453664781dfddebee51f9a36e41819993823fdb09ddc0ce0e4bd3ff0c3"}, + {file = "ciso8601-2.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:eaecca7e0c3ef9e8f5e963e212b083684e849f9a9bb25834d3042363223a73cd"}, + {file = "ciso8601-2.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ad8f417c45eea973a694599b96f40d841215bfee352cb9963383e8d66b309981"}, + {file = "ciso8601-2.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:b869396e9756a7c0696d8eb69ce1d8980bea5e25c86e5996b10d78c900a4362c"}, + {file = "ciso8601-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7eb7b5ef8714d3d1fe9f3256b7a679ad783da899a0b7503a5ace78186735f840"}, + {file = "ciso8601-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:02828107880848ff497971ebc98e6dc851ad7af8ec14a58089e0e11f3111cad6"}, + {file = "ciso8601-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:566b4a8b2f9717e54ffcdd732a7c8051a91da30a60a4f1dafb62e303a1dbac69"}, + {file = "ciso8601-2.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58a749d63f28c2eda71416c9d6014113b0748abf5fd14c502b01bd515502fedf"}, + {file = "ciso8601-2.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:cb135de0e3b8feb7e74a4f7a234e8c8545957fe8d26316a1a549553f425c629d"}, + {file = "ciso8601-2.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:695583810836a42945084b33621b22b0309701c6916689f6a3588fa44c5bc413"}, + {file = "ciso8601-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:21204d98496cf5c0511dc21533be55c2a2d34b8c65603946a116812ffbae3b2d"}, + {file = "ciso8601-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c29ea2b03dee2dc0a5d3e4a0b7d7768c597781e9fa451fe1025600f7cb55a89"}, + {file = "ciso8601-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7533256af90724b8b7a707dcd1be4b67989447595c8e1e1c28399d4fd51dac50"}, + {file = "ciso8601-2.3.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4bc9d577c0d1e57532513fc2899f5231727e28981a426767f7fa13dacb18c06"}, + {file = "ciso8601-2.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4e30501eed43eea7ef64f032c81cd1d8b2020035cbdcefad40db72e2f3bc97ff"}, + {file = "ciso8601-2.3.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070f568de3bc269268296cb9265704dc5fcb9d4c12b1f1c67536624174df5d09"}, + {file = "ciso8601-2.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:9065053c034c80c0afd74c71a4906675d07078a05cfd1cb5ff70661378cdbe60"}, + {file = "ciso8601-2.3.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac00d293cdb3d1a5c78e09b3d75c7b0292ab45d5b26853b436ff5087eba2165"}, + {file = "ciso8601-2.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:06941e2ee46701f083aeb21d13eb762d74d5ed6c46ff22119f27a42ed6edc8f9"}, + {file = "ciso8601-2.3.1.tar.gz", hash = "sha256:3212c7ffe5d8080270548b5f2692ffd2039683b6628a8d2ad456122cc5793c4c"}, +] + [[package]] name = "click" version = "8.1.7" @@ -272,6 +421,18 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "iso8601" +version = "1.1.0" +description = "Simple module to parse ISO 8601 dates" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0" +files = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -342,6 +503,66 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "orjson" +version = "3.9.10" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.9.10-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c18a4da2f50050a03d1da5317388ef84a16013302a5281d6f64e4a3f406aabc4"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5148bab4d71f58948c7c39d12b14a9005b6ab35a0bdf317a8ade9a9e4d9d0bd5"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cf7837c3b11a2dfb589f8530b3cff2bd0307ace4c301e8997e95c7468c1378e"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c62b6fa2961a1dcc51ebe88771be5319a93fd89bd247c9ddf732bc250507bc2b"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb3922a7a804755bbe6b5be9b312e746137a03600f488290318936c1a2d4dc"}, + {file = "orjson-3.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1234dc92d011d3554d929b6cf058ac4a24d188d97be5e04355f1b9223e98bbe9"}, + {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:06ad5543217e0e46fd7ab7ea45d506c76f878b87b1b4e369006bdb01acc05a83"}, + {file = "orjson-3.9.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4fd72fab7bddce46c6826994ce1e7de145ae1e9e106ebb8eb9ce1393ca01444d"}, + {file = "orjson-3.9.10-cp310-none-win32.whl", hash = "sha256:b5b7d4a44cc0e6ff98da5d56cde794385bdd212a86563ac321ca64d7f80c80d1"}, + {file = "orjson-3.9.10-cp310-none-win_amd64.whl", hash = "sha256:61804231099214e2f84998316f3238c4c2c4aaec302df12b21a64d72e2a135c7"}, + {file = "orjson-3.9.10-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cff7570d492bcf4b64cc862a6e2fb77edd5e5748ad715f487628f102815165e9"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8bc367f725dfc5cabeed1ae079d00369900231fbb5a5280cf0736c30e2adf7"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c812312847867b6335cfb264772f2a7e85b3b502d3a6b0586aa35e1858528ab1"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9edd2856611e5050004f4722922b7b1cd6268da34102667bd49d2a2b18bafb81"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:674eb520f02422546c40401f4efaf8207b5e29e420c17051cddf6c02783ff5ca"}, + {file = "orjson-3.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0dc4310da8b5f6415949bd5ef937e60aeb0eb6b16f95041b5e43e6200821fb"}, + {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e99c625b8c95d7741fe057585176b1b8783d46ed4b8932cf98ee145c4facf499"}, + {file = "orjson-3.9.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec6f18f96b47299c11203edfbdc34e1b69085070d9a3d1f302810cc23ad36bf3"}, + {file = "orjson-3.9.10-cp311-none-win32.whl", hash = "sha256:ce0a29c28dfb8eccd0f16219360530bc3cfdf6bf70ca384dacd36e6c650ef8e8"}, + {file = "orjson-3.9.10-cp311-none-win_amd64.whl", hash = "sha256:cf80b550092cc480a0cbd0750e8189247ff45457e5a023305f7ef1bcec811616"}, + {file = "orjson-3.9.10-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:602a8001bdf60e1a7d544be29c82560a7b49319a0b31d62586548835bbe2c862"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f295efcd47b6124b01255d1491f9e46f17ef40d3d7eabf7364099e463fb45f0f"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92af0d00091e744587221e79f68d617b432425a7e59328ca4c496f774a356071"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5a02360e73e7208a872bf65a7554c9f15df5fe063dc047f79738998b0506a14"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858379cbb08d84fe7583231077d9a36a1a20eb72f8c9076a45df8b083724ad1d"}, + {file = "orjson-3.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666c6fdcaac1f13eb982b649e1c311c08d7097cbda24f32612dae43648d8db8d"}, + {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3fb205ab52a2e30354640780ce4587157a9563a68c9beaf52153e1cea9aa0921"}, + {file = "orjson-3.9.10-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7ec960b1b942ee3c69323b8721df2a3ce28ff40e7ca47873ae35bfafeb4555ca"}, + {file = "orjson-3.9.10-cp312-none-win_amd64.whl", hash = "sha256:3e892621434392199efb54e69edfff9f699f6cc36dd9553c5bf796058b14b20d"}, + {file = "orjson-3.9.10-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8b9ba0ccd5a7f4219e67fbbe25e6b4a46ceef783c42af7dbc1da548eb28b6531"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e2ecd1d349e62e3960695214f40939bbfdcaeaaa62ccc638f8e651cf0970e5f"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f433be3b3f4c66016d5a20e5b4444ef833a1f802ced13a2d852c637f69729c1"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4689270c35d4bb3102e103ac43c3f0b76b169760aff8bcf2d401a3e0e58cdb7f"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bd176f528a8151a6efc5359b853ba3cc0e82d4cd1fab9c1300c5d957dc8f48c"}, + {file = "orjson-3.9.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a2ce5ea4f71681623f04e2b7dadede3c7435dfb5e5e2d1d0ec25b35530e277b"}, + {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:49f8ad582da6e8d2cf663c4ba5bf9f83cc052570a3a767487fec6af839b0e777"}, + {file = "orjson-3.9.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2a11b4b1a8415f105d989876a19b173f6cdc89ca13855ccc67c18efbd7cbd1f8"}, + {file = "orjson-3.9.10-cp38-none-win32.whl", hash = "sha256:a353bf1f565ed27ba71a419b2cd3db9d6151da426b61b289b6ba1422a702e643"}, + {file = "orjson-3.9.10-cp38-none-win_amd64.whl", hash = "sha256:e28a50b5be854e18d54f75ef1bb13e1abf4bc650ab9d635e4258c58e71eb6ad5"}, + {file = "orjson-3.9.10-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ee5926746232f627a3be1cc175b2cfad24d0170d520361f4ce3fa2fd83f09e1d"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a73160e823151f33cdc05fe2cea557c5ef12fdf276ce29bb4f1c571c8368a60"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c338ed69ad0b8f8f8920c13f529889fe0771abbb46550013e3c3d01e5174deef"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5869e8e130e99687d9e4be835116c4ebd83ca92e52e55810962446d841aba8de"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2c1e559d96a7f94a4f581e2a32d6d610df5840881a8cba8f25e446f4d792df3"}, + {file = "orjson-3.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a3a3a72c9811b56adf8bcc829b010163bb2fc308877e50e9910c9357e78521"}, + {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7f8fb7f5ecf4f6355683ac6881fd64b5bb2b8a60e3ccde6ff799e48791d8f864"}, + {file = "orjson-3.9.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c943b35ecdf7123b2d81d225397efddf0bce2e81db2f3ae633ead38e85cd5ade"}, + {file = "orjson-3.9.10-cp39-none-win32.whl", hash = "sha256:fb0b361d73f6b8eeceba47cd37070b5e6c9de5beaeaa63a1cb35c7e1a73ef088"}, + {file = "orjson-3.9.10-cp39-none-win_amd64.whl", hash = "sha256:b90f340cb6397ec7a854157fac03f0c82b744abdd1c0941a024c3c29d1340aff"}, + {file = "orjson-3.9.10.tar.gz", hash = "sha256:9ebbdbd6a046c304b1845e96fbcc5559cd296b4dfd3ad2509e33c4d9ce07d6a1"}, +] + [[package]] name = "packaging" version = "23.2" @@ -561,6 +782,18 @@ files = [ {file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"}, ] +[[package]] +name = "pypika-tortoise" +version = "0.1.6" +description = "Forked from pypika and streamline just for tortoise-orm" +category = "main" +optional = false +python-versions = ">=3.7,<4.0" +files = [ + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, +] + [[package]] name = "pytest" version = "7.4.3" @@ -582,6 +815,18 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -612,6 +857,36 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "tortoise-orm" +version = "0.20.0" +description = "Easy async ORM for python, built with relations in mind" +category = "main" +optional = false +python-versions = ">=3.8,<4.0" +files = [ + {file = "tortoise_orm-0.20.0-py3-none-any.whl", hash = "sha256:1891ad935de689ddf002c5c65c864176d28659ab6069e45f0e2cde32359bb8d9"}, + {file = "tortoise_orm-0.20.0.tar.gz", hash = "sha256:283af584d685dcc58d6cc1da35b9115bb1e41c89075eae2a19c493b39b9b41f7"}, +] + +[package.dependencies] +aiosqlite = ">=0.16.0,<0.18.0" +asyncpg = {version = "*", optional = true, markers = "extra == \"asyncpg\""} +ciso8601 = {version = "*", optional = true, markers = "sys_platform != \"win32\" and implementation_name == \"cpython\" and extra == \"accel\""} +iso8601 = ">=1.0.2,<2.0.0" +orjson = {version = "*", optional = true, markers = "extra == \"accel\""} +pypika-tortoise = ">=0.1.6,<0.2.0" +pytz = "*" +uvloop = {version = "*", optional = true, markers = "sys_platform != \"win32\" and implementation_name == \"cpython\" and extra == \"accel\""} + +[package.extras] +accel = ["ciso8601", "orjson", "uvloop"] +aiomysql = ["aiomysql"] +asyncmy = ["asyncmy (>=0.2.8,<0.3.0)"] +asyncodbc = ["asyncodbc (>=0.1.1,<0.2.0)"] +asyncpg = ["asyncpg"] +psycopg = ["psycopg[binary,pool] (>=3.0.12,<4.0.0)"] + [[package]] name = "typing-extensions" version = "4.8.0" @@ -643,7 +918,52 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "uvloop" +version = "0.19.0" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "main" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "1f16fb89526c490b78f200333dee3bd033da52fd952bb1dea4b400a71acfbc1d" +content-hash = "b73e9d0c8419296dbb811bcce5e1e9db0ddbe7084239b9df115a0ed50c1a12a1" diff --git a/pyproject.toml b/pyproject.toml index ca520ce..f7997c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ from = "src" python = "^3.11" fastapi = "^0.104.1" uvicorn = "^0.24.0" +tortoise-orm = {version = "^0.20.0", extras = ["asyncpg", "accel"]} [tool.poetry.group.dev.dependencies] mypy = "^1.6.1" diff --git a/src/pwncore/models/__init__.py b/src/pwncore/models/__init__.py new file mode 100644 index 0000000..30f6756 --- /dev/null +++ b/src/pwncore/models/__init__.py @@ -0,0 +1,4 @@ +"""pwncore.models + +Contains all Pydantic and Tortoise ORM models +""" diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py new file mode 100644 index 0000000..ec7ca2e --- /dev/null +++ b/src/pwncore/models/container.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import typing as t + +from tortoise.models import Model +from tortoise import fields +from tortoise.fields import ForeignKeyRelation + +if t.TYPE_CHECKING: + from pwncore.models.ctf import CTF + + +# Note: These are all type annotated, dont worry +class Container(Model): + id = fields.TextField(pk=True) + ctf_id: ForeignKeyRelation[CTF] = fields.ForeignKeyField("models.CTF", "id") + team_id = fields.IntField() # TODO: Foreign + flag = fields.TextField() + + +class Ports(Model): + # FUTURE PROOFING: ADD domain + container_id: ForeignKeyRelation[Container] = fields.ForeignKeyField( + "models.Container", "id" + ) + port = fields.IntField(pk=True) diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py new file mode 100644 index 0000000..2e3c7ad --- /dev/null +++ b/src/pwncore/models/ctf.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from tortoise.models import Model +from tortoise import fields +from tortoise.fields import Field + + +class CTF(Model): + name = fields.TextField() + image_name = fields.TextField() + image_config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] + + +# reveal_type(CTF().image_config) From f28619f3a226b0dc05ad09e0d95f602d535d6288 Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Tue, 14 Nov 2023 23:35:10 +0530 Subject: [PATCH 02/41] add: Problem, Hint, Image, User and related schemas fix: Foreign Key relations due to incorrect arguments --- src/pwncore/models/container.py | 11 +++++---- src/pwncore/models/ctf.py | 43 +++++++++++++++++++++++++++++---- src/pwncore/models/user.py | 18 ++++++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 src/pwncore/models/user.py diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py index ec7ca2e..a9b3415 100644 --- a/src/pwncore/models/container.py +++ b/src/pwncore/models/container.py @@ -4,23 +4,24 @@ from tortoise.models import Model from tortoise import fields -from tortoise.fields import ForeignKeyRelation if t.TYPE_CHECKING: - from pwncore.models.ctf import CTF + from pwncore.models.ctf import Problem # Note: These are all type annotated, dont worry class Container(Model): id = fields.TextField(pk=True) - ctf_id: ForeignKeyRelation[CTF] = fields.ForeignKeyField("models.CTF", "id") + ctf: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField("models.Problem", on_delete=fields.OnDelete.NO_ACTION) team_id = fields.IntField() # TODO: Foreign flag = fields.TextField() + ports: fields.ReverseRelation[Ports] + class Ports(Model): # FUTURE PROOFING: ADD domain - container_id: ForeignKeyRelation[Container] = fields.ForeignKeyField( - "models.Container", "id" + container: fields.ForeignKeyRelation[Container] = fields.ForeignKeyField( + "models.Container", related_name="ports", on_delete=fields.OnDelete.CASCADE ) port = fields.IntField(pk=True) diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py index 2e3c7ad..b5a2c4e 100644 --- a/src/pwncore/models/ctf.py +++ b/src/pwncore/models/ctf.py @@ -1,14 +1,47 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from tortoise.models import Model from tortoise import fields -from tortoise.fields import Field +if TYPE_CHECKING: + from tortoise.fields import Field + from pwncore.models.user import Team + +class Problem(Model): + name = fields.TextField() + description = fields.TextField() + points = fields.IntField() + author = fields.TextField() + + hints: fields.ReverseRelation[Hint] + image: fields.OneToOneRelation[Image] -class CTF(Model): +class Hint(Model): + hint_order = fields.SmallIntField() # 0, 1, 2 + problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField("models.Problem", related_name="hints") + text = fields.TextField() + + class Meta: + ordering = ("hint_order",) + +class Image(Model): name = fields.TextField() - image_name = fields.TextField() - image_config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] + config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] + problem: fields.OneToOneRelation[Problem] = fields.OneToOneField("models.Problem", related_name="image") + +class SolvedProblems(Model): + team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") + problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField("models.Problem") + solved_at = fields.DatetimeField(auto_now_add=True) + + class Meta: + unique_together = (("team", "problem"),) +class ViewedHints(Model): + team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") + hint: fields.ForeignKeyRelation[Hint] = fields.ForeignKeyField("models.Hint") -# reveal_type(CTF().image_config) + class Meta: + unique_together = (("team", "hint"),) \ No newline at end of file diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py new file mode 100644 index 0000000..07f0d62 --- /dev/null +++ b/src/pwncore/models/user.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from tortoise import fields +from tortoise.models import Model + +# Not too sure here + +class User(Model): + user_name = fields.TextField(unique=True) + + team: fields.ForeignKeyNullableRelation[Team] = fields.ForeignKeyField("models.Team", "id", null=True) + +class Team(Model): + team_name = fields.TextField(unique=True) + secret_hash = fields.TextField() + + # TODO: Add count constraint + members: fields.ReverseRelation[User] From 665dbfd4afdcd5226587751483c7df67de49d973 Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Tue, 14 Nov 2023 23:37:50 +0530 Subject: [PATCH 03/41] chore: format with black --- src/pwncore/models/container.py | 4 +++- src/pwncore/models/ctf.py | 21 ++++++++++++++++----- src/pwncore/models/user.py | 6 +++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py index a9b3415..bd5b3ee 100644 --- a/src/pwncore/models/container.py +++ b/src/pwncore/models/container.py @@ -12,7 +12,9 @@ # Note: These are all type annotated, dont worry class Container(Model): id = fields.TextField(pk=True) - ctf: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField("models.Problem", on_delete=fields.OnDelete.NO_ACTION) + ctf: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( + "models.Problem", on_delete=fields.OnDelete.NO_ACTION + ) team_id = fields.IntField() # TODO: Foreign flag = fields.TextField() diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py index b5a2c4e..c79367b 100644 --- a/src/pwncore/models/ctf.py +++ b/src/pwncore/models/ctf.py @@ -9,6 +9,7 @@ from tortoise.fields import Field from pwncore.models.user import Team + class Problem(Model): name = fields.TextField() description = fields.TextField() @@ -18,30 +19,40 @@ class Problem(Model): hints: fields.ReverseRelation[Hint] image: fields.OneToOneRelation[Image] + class Hint(Model): - hint_order = fields.SmallIntField() # 0, 1, 2 - problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField("models.Problem", related_name="hints") + hint_order = fields.SmallIntField() # 0, 1, 2 + problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( + "models.Problem", related_name="hints" + ) text = fields.TextField() class Meta: ordering = ("hint_order",) + class Image(Model): name = fields.TextField() config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] - problem: fields.OneToOneRelation[Problem] = fields.OneToOneField("models.Problem", related_name="image") + problem: fields.OneToOneRelation[Problem] = fields.OneToOneField( + "models.Problem", related_name="image" + ) + class SolvedProblems(Model): team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") - problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField("models.Problem") + problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( + "models.Problem" + ) solved_at = fields.DatetimeField(auto_now_add=True) class Meta: unique_together = (("team", "problem"),) + class ViewedHints(Model): team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") hint: fields.ForeignKeyRelation[Hint] = fields.ForeignKeyField("models.Hint") class Meta: - unique_together = (("team", "hint"),) \ No newline at end of file + unique_together = (("team", "hint"),) diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py index 07f0d62..f85f7f7 100644 --- a/src/pwncore/models/user.py +++ b/src/pwncore/models/user.py @@ -5,10 +5,14 @@ # Not too sure here + class User(Model): user_name = fields.TextField(unique=True) - team: fields.ForeignKeyNullableRelation[Team] = fields.ForeignKeyField("models.Team", "id", null=True) + team: fields.ForeignKeyNullableRelation[Team] = fields.ForeignKeyField( + "models.Team", "id", null=True + ) + class Team(Model): team_name = fields.TextField(unique=True) From ce3b72f0bdf34b0b9d93035096e5050321355430 Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Tue, 14 Nov 2023 23:55:05 +0530 Subject: [PATCH 04/41] add: FK to Container for Team --- src/pwncore/models/container.py | 5 ++++- src/pwncore/models/user.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py index bd5b3ee..0f514ce 100644 --- a/src/pwncore/models/container.py +++ b/src/pwncore/models/container.py @@ -7,6 +7,7 @@ if t.TYPE_CHECKING: from pwncore.models.ctf import Problem + from pwncore.models.user import Team # Note: These are all type annotated, dont worry @@ -15,7 +16,9 @@ class Container(Model): ctf: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( "models.Problem", on_delete=fields.OnDelete.NO_ACTION ) - team_id = fields.IntField() # TODO: Foreign + team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField( + "models.Team", related_name="containers" + ) flag = fields.TextField() ports: fields.ReverseRelation[Ports] diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py index f85f7f7..31fa3b3 100644 --- a/src/pwncore/models/user.py +++ b/src/pwncore/models/user.py @@ -1,8 +1,13 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from tortoise import fields from tortoise.models import Model +if TYPE_CHECKING: + from pwncore.models.container import Container + # Not too sure here @@ -20,3 +25,4 @@ class Team(Model): # TODO: Add count constraint members: fields.ReverseRelation[User] + containers: fields.ReverseRelation[Container] From 6d36cb145ab3ee61e77f5f38d6e922a1382aac65 Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Wed, 15 Nov 2023 00:07:16 +0530 Subject: [PATCH 05/41] fix: FKR on User to Team --- src/pwncore/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py index 31fa3b3..fef5a8c 100644 --- a/src/pwncore/models/user.py +++ b/src/pwncore/models/user.py @@ -15,7 +15,7 @@ class User(Model): user_name = fields.TextField(unique=True) team: fields.ForeignKeyNullableRelation[Team] = fields.ForeignKeyField( - "models.Team", "id", null=True + "models.Team", "members", null=True ) From 472f43b9a9906aa5930397052b61c0f1afa1e288 Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Wed, 15 Nov 2023 18:36:07 +0530 Subject: [PATCH 06/41] add: Trigger for User and Team fix: change column names to not contain model names --- src/pwncore/models/__init__.py | 17 ++++++++++ src/pwncore/models/container.py | 2 ++ src/pwncore/models/ctf.py | 8 +++-- src/pwncore/models/user.py | 58 +++++++++++++++++++++++++++++++-- 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/pwncore/models/__init__.py b/src/pwncore/models/__init__.py index 30f6756..fd1f23e 100644 --- a/src/pwncore/models/__init__.py +++ b/src/pwncore/models/__init__.py @@ -2,3 +2,20 @@ Contains all Pydantic and Tortoise ORM models """ + +from pwncore.models.container import Container, Ports +from pwncore.models.ctf import Problem, SolvedProblem, Hint, ViewedHint, Image +from pwncore.models.user import User, Team, generate_trigger_script + +__all__ = ( + "Container", + "Ports", + "Problem", + "SolvedProblem", + "Hint", + "ViewedHint", + "Image", + "User", + "Team", + "generate_trigger_script", +) diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py index 0f514ce..e9f9c1e 100644 --- a/src/pwncore/models/container.py +++ b/src/pwncore/models/container.py @@ -9,6 +9,8 @@ from pwncore.models.ctf import Problem from pwncore.models.user import Team +__all__ = ("Container", "Ports") + # Note: These are all type annotated, dont worry class Container(Model): diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py index c79367b..0c799f2 100644 --- a/src/pwncore/models/ctf.py +++ b/src/pwncore/models/ctf.py @@ -9,6 +9,8 @@ from tortoise.fields import Field from pwncore.models.user import Team +__all__ = ("Problem", "Hint", "Image", "SolvedProblem", "ViewedHint") + class Problem(Model): name = fields.TextField() @@ -21,7 +23,7 @@ class Problem(Model): class Hint(Model): - hint_order = fields.SmallIntField() # 0, 1, 2 + order = fields.SmallIntField() # 0, 1, 2 problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( "models.Problem", related_name="hints" ) @@ -39,7 +41,7 @@ class Image(Model): ) -class SolvedProblems(Model): +class SolvedProblem(Model): team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( "models.Problem" @@ -50,7 +52,7 @@ class Meta: unique_together = (("team", "problem"),) -class ViewedHints(Model): +class ViewedHint(Model): team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") hint: fields.ForeignKeyRelation[Hint] = fields.ForeignKeyField("models.Hint") diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py index fef5a8c..f062f42 100644 --- a/src/pwncore/models/user.py +++ b/src/pwncore/models/user.py @@ -8,11 +8,12 @@ if TYPE_CHECKING: from pwncore.models.container import Container +__all__ = ("User", "Team", "generate_trigger_script") # Not too sure here class User(Model): - user_name = fields.TextField(unique=True) + name = fields.CharField(255, unique=True) team: fields.ForeignKeyNullableRelation[Team] = fields.ForeignKeyField( "models.Team", "members", null=True @@ -20,9 +21,60 @@ class User(Model): class Team(Model): - team_name = fields.TextField(unique=True) + name = fields.CharField(255, unique=True) secret_hash = fields.TextField() - # TODO: Add count constraint + # TODO: Add count constraint somehow, using trigger right now members: fields.ReverseRelation[User] containers: fields.ReverseRelation[Container] + + +_TRIGGER_SCRIPT_TEMPLATE = """ +CREATE OR REPLACE FUNCTION check_max_users_per_team() +RETURNS TRIGGER AS $$ +BEGIN + IF ( + SELECT COUNT(*) + FROM "user" + WHERE team_id = NEW.team_id + ) >= {0} THEN + RAISE EXCEPTION 'Cannot have more than {0} users in a team'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS max_users_per_team_trigger ON "user"; + +CREATE TRIGGER max_users_per_team_trigger +BEFORE INSERT OR UPDATE ON "user" +FOR EACH ROW EXECUTE FUNCTION check_max_users_per_team(); +""" + + +def generate_trigger_script(max_users_per_team: int) -> str: + """Return the trigger script to keep relation between + :class:`pwncore.models.user.User` and :class:`pwncore.models.user.Team` + consistent + + Parameters + ---------- + max_users_per_team : :class:`int` + Maximum number of users that can be a team + + Returns + ------- + :class:`str` + The PostresSQL trigger SQL script + + Raises + ------ + :class:`TypeError` + If `max_users_per_team` is not an integer + """ + # Call at startup and run as script in a transaction + if not isinstance(max_users_per_team, int): + raise TypeError( + f"Expected int but got {type(max_users_per_team)} instance instead" + ) + return _TRIGGER_SCRIPT_TEMPLATE.format(str(max_users_per_team)) From 4b0073cefb26588b8e567daab001aeadc0bae09f Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Wed, 15 Nov 2023 19:07:44 +0530 Subject: [PATCH 07/41] change: consistent naming --- src/pwncore/models/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py index e9f9c1e..9c20e5f 100644 --- a/src/pwncore/models/container.py +++ b/src/pwncore/models/container.py @@ -15,7 +15,7 @@ # Note: These are all type annotated, dont worry class Container(Model): id = fields.TextField(pk=True) - ctf: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( + problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( "models.Problem", on_delete=fields.OnDelete.NO_ACTION ) team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField( From acd93b87afd0978c1bb54c5d174b0aaec6b53cb5 Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Wed, 15 Nov 2023 19:29:25 +0530 Subject: [PATCH 08/41] simplify: remove Image model misc: get rid of Text primary key --- src/pwncore/models/__init__.py | 3 +-- src/pwncore/models/container.py | 2 +- src/pwncore/models/ctf.py | 17 +++++------------ 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pwncore/models/__init__.py b/src/pwncore/models/__init__.py index fd1f23e..d03071f 100644 --- a/src/pwncore/models/__init__.py +++ b/src/pwncore/models/__init__.py @@ -4,7 +4,7 @@ """ from pwncore.models.container import Container, Ports -from pwncore.models.ctf import Problem, SolvedProblem, Hint, ViewedHint, Image +from pwncore.models.ctf import Problem, SolvedProblem, Hint, ViewedHint from pwncore.models.user import User, Team, generate_trigger_script __all__ = ( @@ -14,7 +14,6 @@ "SolvedProblem", "Hint", "ViewedHint", - "Image", "User", "Team", "generate_trigger_script", diff --git a/src/pwncore/models/container.py b/src/pwncore/models/container.py index 9c20e5f..47db53b 100644 --- a/src/pwncore/models/container.py +++ b/src/pwncore/models/container.py @@ -14,7 +14,7 @@ # Note: These are all type annotated, dont worry class Container(Model): - id = fields.TextField(pk=True) + docker_id = fields.CharField(128, unique=True) problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( "models.Problem", on_delete=fields.OnDelete.NO_ACTION ) diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py index 0c799f2..b4dc979 100644 --- a/src/pwncore/models/ctf.py +++ b/src/pwncore/models/ctf.py @@ -9,7 +9,7 @@ from tortoise.fields import Field from pwncore.models.user import Team -__all__ = ("Problem", "Hint", "Image", "SolvedProblem", "ViewedHint") +__all__ = ("Problem", "Hint", "SolvedProblem", "ViewedHint") class Problem(Model): @@ -18,8 +18,10 @@ class Problem(Model): points = fields.IntField() author = fields.TextField() + image_name = fields.TextField() + image_config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] + hints: fields.ReverseRelation[Hint] - image: fields.OneToOneRelation[Image] class Hint(Model): @@ -30,16 +32,7 @@ class Hint(Model): text = fields.TextField() class Meta: - ordering = ("hint_order",) - - -class Image(Model): - name = fields.TextField() - config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] - problem: fields.OneToOneRelation[Problem] = fields.OneToOneField( - "models.Problem", related_name="image" - ) - + ordering = ("order",) class SolvedProblem(Model): team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") From 5fcfc71d3bfcc86b8126e41ce065abf66454618e Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Wed, 15 Nov 2023 19:29:57 +0530 Subject: [PATCH 09/41] chore: format with black --- src/pwncore/models/ctf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pwncore/models/ctf.py b/src/pwncore/models/ctf.py index b4dc979..d37aee7 100644 --- a/src/pwncore/models/ctf.py +++ b/src/pwncore/models/ctf.py @@ -19,7 +19,7 @@ class Problem(Model): author = fields.TextField() image_name = fields.TextField() - image_config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] + image_config: Field[dict[str, list]] = fields.JSONField() # type: ignore[assignment] hints: fields.ReverseRelation[Hint] @@ -34,6 +34,7 @@ class Hint(Model): class Meta: ordering = ("order",) + class SolvedProblem(Model): team: fields.ForeignKeyRelation[Team] = fields.ForeignKeyField("models.Team") problem: fields.ForeignKeyRelation[Problem] = fields.ForeignKeyField( From bc2a4b81cfbc90e43e4a8dc213e61dc52546cffb Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Thu, 16 Nov 2023 01:03:06 +0530 Subject: [PATCH 10/41] add: Required user fields and appl side validation Missing: Config value for constraint Waiting-On: lugvitc/pwncore#5 369802ecb6ef0f87218b8eefd9a21979f3494f93 --- src/pwncore/models/user.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py index f062f42..d82fc55 100644 --- a/src/pwncore/models/user.py +++ b/src/pwncore/models/user.py @@ -3,7 +3,9 @@ from typing import TYPE_CHECKING from tortoise import fields +from tortoise.exceptions import IntegrityError from tortoise.models import Model +from tortoise.expressions import Q if TYPE_CHECKING: from pwncore.models.container import Container @@ -13,12 +15,27 @@ class User(Model): - name = fields.CharField(255, unique=True) + # Registration numbers and other identity tags + # abstractly just represents one person, expand this + # field for Identity providers + tag = fields.CharField(128, unique=True) + name = fields.CharField(255) + email = fields.TextField() + phone_num = fields.CharField(15) team: fields.ForeignKeyNullableRelation[Team] = fields.ForeignKeyField( - "models.Team", "members", null=True + "models.Team", "members", null=True, on_delete=fields.OnDelete.SET_NULL ) + async def save(self, *args, **kwargs): + # TODO: Insert/Update in one query + # Reason why we dont use pre_save: overhead, ugly + if self.team is not None: + cnt = await self.team.members.filter(~Q(id=self.pk)).count() + if cnt >= 3: + raise IntegrityError("3 or more users already exist for the team") + return await super().save(*args, **kwargs) + class Team(Model): name = fields.CharField(255, unique=True) @@ -29,6 +46,9 @@ class Team(Model): containers: fields.ReverseRelation[Container] +# Use the trigger if encountering inconsistencies in +# database (shouldn't happen ever tho) + _TRIGGER_SCRIPT_TEMPLATE = """ CREATE OR REPLACE FUNCTION check_max_users_per_team() RETURNS TRIGGER AS $$ From 819a36138a69693c4f2c1f48e28a612380e7355b Mon Sep 17 00:00:00 2001 From: WizzyGeek Date: Thu, 16 Nov 2023 11:05:50 +0530 Subject: [PATCH 11/41] misc: remove user trigger --- src/pwncore/models/__init__.py | 3 +- src/pwncore/models/user.py | 62 ++-------------------------------- 2 files changed, 4 insertions(+), 61 deletions(-) diff --git a/src/pwncore/models/__init__.py b/src/pwncore/models/__init__.py index d03071f..8e8f78c 100644 --- a/src/pwncore/models/__init__.py +++ b/src/pwncore/models/__init__.py @@ -5,7 +5,7 @@ from pwncore.models.container import Container, Ports from pwncore.models.ctf import Problem, SolvedProblem, Hint, ViewedHint -from pwncore.models.user import User, Team, generate_trigger_script +from pwncore.models.user import User, Team __all__ = ( "Container", @@ -16,5 +16,4 @@ "ViewedHint", "User", "Team", - "generate_trigger_script", ) diff --git a/src/pwncore/models/user.py b/src/pwncore/models/user.py index d82fc55..ebe5497 100644 --- a/src/pwncore/models/user.py +++ b/src/pwncore/models/user.py @@ -10,8 +10,7 @@ if TYPE_CHECKING: from pwncore.models.container import Container -__all__ = ("User", "Team", "generate_trigger_script") -# Not too sure here +__all__ = ("User", "Team") class User(Model): @@ -31,8 +30,8 @@ async def save(self, *args, **kwargs): # TODO: Insert/Update in one query # Reason why we dont use pre_save: overhead, ugly if self.team is not None: - cnt = await self.team.members.filter(~Q(id=self.pk)).count() - if cnt >= 3: + count = await self.team.members.filter(~Q(id=self.pk)).count() + if count >= 3: raise IntegrityError("3 or more users already exist for the team") return await super().save(*args, **kwargs) @@ -41,60 +40,5 @@ class Team(Model): name = fields.CharField(255, unique=True) secret_hash = fields.TextField() - # TODO: Add count constraint somehow, using trigger right now members: fields.ReverseRelation[User] containers: fields.ReverseRelation[Container] - - -# Use the trigger if encountering inconsistencies in -# database (shouldn't happen ever tho) - -_TRIGGER_SCRIPT_TEMPLATE = """ -CREATE OR REPLACE FUNCTION check_max_users_per_team() -RETURNS TRIGGER AS $$ -BEGIN - IF ( - SELECT COUNT(*) - FROM "user" - WHERE team_id = NEW.team_id - ) >= {0} THEN - RAISE EXCEPTION 'Cannot have more than {0} users in a team'; - END IF; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - -DROP TRIGGER IF EXISTS max_users_per_team_trigger ON "user"; - -CREATE TRIGGER max_users_per_team_trigger -BEFORE INSERT OR UPDATE ON "user" -FOR EACH ROW EXECUTE FUNCTION check_max_users_per_team(); -""" - - -def generate_trigger_script(max_users_per_team: int) -> str: - """Return the trigger script to keep relation between - :class:`pwncore.models.user.User` and :class:`pwncore.models.user.Team` - consistent - - Parameters - ---------- - max_users_per_team : :class:`int` - Maximum number of users that can be a team - - Returns - ------- - :class:`str` - The PostresSQL trigger SQL script - - Raises - ------ - :class:`TypeError` - If `max_users_per_team` is not an integer - """ - # Call at startup and run as script in a transaction - if not isinstance(max_users_per_team, int): - raise TypeError( - f"Expected int but got {type(max_users_per_team)} instance instead" - ) - return _TRIGGER_SCRIPT_TEMPLATE.format(str(max_users_per_team)) From ea9cfd7ad556d9577cbf9515f5fd07e1a162069e Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:15:26 +0530 Subject: [PATCH 12/41] feat: Start docker container --- src/pwncore/container.py | 3 ++ src/pwncore/db/__init__.py | 47 +++++++++++++++++ src/pwncore/routes/ctf/start.py | 93 ++++++++++++++++++++++++++++----- 3 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 src/pwncore/container.py create mode 100644 src/pwncore/db/__init__.py diff --git a/src/pwncore/container.py b/src/pwncore/container.py new file mode 100644 index 0000000..74fcec2 --- /dev/null +++ b/src/pwncore/container.py @@ -0,0 +1,3 @@ +import docker + +docker_client = docker.from_env() diff --git a/src/pwncore/db/__init__.py b/src/pwncore/db/__init__.py new file mode 100644 index 0000000..664050d --- /dev/null +++ b/src/pwncore/db/__init__.py @@ -0,0 +1,47 @@ +from tortoise import Tortoise, fields, run_async +from tortoise.models import Model + +""" +Each user can create only one container. +Only one container for a ctf can be created from a team. +Container: + id (pk) + name + ctf_id + ports # stored as csv + team_id + user_id + flag # TBD + +CTF: + id (pk) + name + image_name + image_config +""" + +# No relationships yet +class Container(Model): + id = fields.TextField(pk=True) + name = fields.TextField() + ctf_id = fields.IntField() + ports = fields.TextField() + team_id = field.IntField() + user_id = field.IntField() + flag = field.TextField() + +class CTF(Model): + id = fields.IntField(pk=True) + name = fields.TextField() + image_name = field.TextField() + image_config = field.TextField() + + +# Might want to keep the schemas and init in separate files +async def init(): + await Tortoise.init( + db_url="sqlite://:memory:", + modules={'models': ['pwncore.db']} + ) + await Tortoise.generate_schemas() +run_async(init()) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index d8924d0..e725af2 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -1,20 +1,89 @@ from __future__ import annotations from pwncore.routes.ctf import router +from pwncore.db import Container, CTF +from pwncore.container import docker_client -@router.get("/start/{ctf_id}") -async def start_the_docker_container( - ctf_id: int, -): # The function name is inferred for the summary - # This is a regular single-line comment. - # Will not be displayed in the documentation. +@router.post("/start/{ctf_id}") +async def start_docker_container(ctf_id: int): + + if not await CTF.filter(id=ctf_id).exists(): + return {"status": "CTF does not exist."} + ctf = await CTF.get(id=ctf_id) + + user_id = get_user_id() # From JWT + if await Container.filter(user_id=user_id).exists(): + user_container = Container.get(user_id=user_id) + return { + "status": "You already have a running container for a CTF.", + "ports": user_container.ports.split(","), + "ctf_id": user_container.ctf_id + } + + team_id = get_team_id() # From JWT + if await Container.filter(team_id=team_id, ctf_id=ctf_id).exists(): + team_container = Container.get(team_id=team_id, ctf_id=ctf_id) + return { + "status": "Your team already has a running container for this CTF.", + "ports": team_container.ports.split(","), + "ctf_id": team_container.ctf_id + } + + # Start a new container + image_config = ctf.image_config.copy() """ - This is a multi-line comment, and will be displayed - in the documentation when the route is expanded. + This image_config will look like: + { + "ports": { + "22/tcp": None, + "80/tcp": None + } + } - The cool thing is that Markdown works here! - # See, Markdown works! - _Pretty_ **cool** right? + `None` gets substitued with the empty ports on the host system. """ - return {"status": "CTF started"} + + # Ports + port_list = get_empty_ports() # Need to implement + + if len(port_list) < len(image_config["ports"]): + # Handle error here + print("AAAAAAAAAAAAAAAAAAA") + return {"status": "Server ran out of ports 💀"} + + ports = [] # Only to save the host ports used to return to the user + for guest_port in image_config["ports"]: + port = port_list.pop() + ports.append(port) + image_config["ports"][guest_port] = port + + # Run + container = docker_client.containers.run( + ctf.image_name, + detach=True, + name=f"{team_id}_{ctf_id}", + **image_config + ) + + flag = f"C0D{{{uuid.uuid4().hex}}}" + container.exec_run( + f"/bin/bash /root/gen_flag '{flag}'", + user="root" + ) + + await Container.create(**{ + "id" : container.id, + "name" : container.name, + "user_id" : user_id + "team_id" : team_id, + "ctf_id" : ctf_id, + "flag" : flag, + "ports" : ','.join(ports) # Save ports as csv + }) + + return { + "status": "Container started.", + "ports": ports, + "ctf_id": ctf_id + } From afdbadc436946caa6d6a223f05c83f4e56c05db9 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:12:39 +0530 Subject: [PATCH 13/41] chore: Get flag format from config --- src/pwncore/routes/ctf/start.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index e725af2..184ac03 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -3,7 +3,7 @@ from pwncore.routes.ctf import router from pwncore.db import Container, CTF from pwncore.container import docker_client - +from pwncore.config import DEV_CONFIG @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int): @@ -66,7 +66,7 @@ async def start_docker_container(ctf_id: int): **image_config ) - flag = f"C0D{{{uuid.uuid4().hex}}}" + flag = f"{DEV_CONFIG.flag}{{{uuid.uuid4().hex}}}" container.exec_run( f"/bin/bash /root/gen_flag '{flag}'", user="root" From 8da9b75917bebcc74f8d20d66b3bb46ad25e81a5 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:13:03 +0530 Subject: [PATCH 14/41] fix: Rename field to fields --- src/pwncore/db/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pwncore/db/__init__.py b/src/pwncore/db/__init__.py index 664050d..ace0fc2 100644 --- a/src/pwncore/db/__init__.py +++ b/src/pwncore/db/__init__.py @@ -26,15 +26,15 @@ class Container(Model): name = fields.TextField() ctf_id = fields.IntField() ports = fields.TextField() - team_id = field.IntField() - user_id = field.IntField() - flag = field.TextField() + team_id = fields.IntField() + user_id = fields.IntField() + flag = fields.TextField() class CTF(Model): id = fields.IntField(pk=True) name = fields.TextField() - image_name = field.TextField() - image_config = field.TextField() + image_name = fields.TextField() + image_config = fields.TextField() # Might want to keep the schemas and init in separate files From d7ce683c84bb0384fd26915454868373db199eeb Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:31:28 +0530 Subject: [PATCH 15/41] chore: Make DB operations atomic --- src/pwncore/routes/ctf/start.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 184ac03..6a6a701 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -1,14 +1,23 @@ from __future__ import annotations +from fastapi import Response +import uuid +from tortoise.transactions import atomic + from pwncore.routes.ctf import router from pwncore.db import Container, CTF from pwncore.container import docker_client from pwncore.config import DEV_CONFIG +# UNTESTED + + +@atomic() @router.post("/start/{ctf_id}") -async def start_docker_container(ctf_id: int): +async def start_docker_container(ctf_id: int, response: Response): if not await CTF.filter(id=ctf_id).exists(): + response.status_code = 404 return {"status": "CTF does not exist."} ctf = await CTF.get(id=ctf_id) @@ -81,6 +90,7 @@ async def start_docker_container(ctf_id: int): "flag" : flag, "ports" : ','.join(ports) # Save ports as csv }) + await Container.save() return { "status": "Container started.", From 5c1eb4c95509cec06905164dfa43401b03287c0c Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:39:08 +0530 Subject: [PATCH 16/41] chore: Add flag to config.py --- src/pwncore/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pwncore/config.py b/src/pwncore/config.py index d514afe..874a702 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -17,6 +17,7 @@ class BaseConfig(Config): def __init__(self, development: bool) -> None: self.development = development + self.flag = "C0D" DEV_CONFIG: t.Final[BaseConfig] = BaseConfig(True) From 5ac130d279fe878c887dc5c4a3099c6a7d81e594 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:00:53 +0530 Subject: [PATCH 17/41] chore: changed status to msg when returning json --- src/pwncore/routes/ctf/start.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 6a6a701..50afa0d 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -18,14 +18,14 @@ async def start_docker_container(ctf_id: int, response: Response): if not await CTF.filter(id=ctf_id).exists(): response.status_code = 404 - return {"status": "CTF does not exist."} + return {"msg": "CTF does not exist."} ctf = await CTF.get(id=ctf_id) user_id = get_user_id() # From JWT if await Container.filter(user_id=user_id).exists(): user_container = Container.get(user_id=user_id) return { - "status": "You already have a running container for a CTF.", + "msg": "You already have a running container for a CTF.", "ports": user_container.ports.split(","), "ctf_id": user_container.ctf_id } @@ -34,7 +34,7 @@ async def start_docker_container(ctf_id: int, response: Response): if await Container.filter(team_id=team_id, ctf_id=ctf_id).exists(): team_container = Container.get(team_id=team_id, ctf_id=ctf_id) return { - "status": "Your team already has a running container for this CTF.", + "msg": "Your team already has a running container for this CTF.", "ports": team_container.ports.split(","), "ctf_id": team_container.ctf_id } @@ -59,7 +59,8 @@ async def start_docker_container(ctf_id: int, response: Response): if len(port_list) < len(image_config["ports"]): # Handle error here print("AAAAAAAAAAAAAAAAAAA") - return {"status": "Server ran out of ports 💀"} + + return {"msg": "Server ran out of ports 💀"} ports = [] # Only to save the host ports used to return to the user for guest_port in image_config["ports"]: @@ -93,7 +94,7 @@ async def start_docker_container(ctf_id: int, response: Response): await Container.save() return { - "status": "Container started.", + "msg": "Container started.", "ports": ports, "ctf_id": ctf_id } From 6f087118b2629145fcc6e551f097a19e92b2a9f1 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:04:33 +0530 Subject: [PATCH 18/41] fix: Add status code when server runs out of ports --- src/pwncore/routes/ctf/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 50afa0d..4699707 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -59,7 +59,7 @@ async def start_docker_container(ctf_id: int, response: Response): if len(port_list) < len(image_config["ports"]): # Handle error here print("AAAAAAAAAAAAAAAAAAA") - + response.status_code = 500 return {"msg": "Server ran out of ports 💀"} ports = [] # Only to save the host ports used to return to the user From 186c9b0bb0980ca1b7d55d2025ef26a418a88c4b Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:34:26 +0530 Subject: [PATCH 19/41] feat: Add /api/ctf/stop and /api/ctf/stopall --- src/pwncore/routes/ctf/start.py | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 4699707..cf28acc 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -98,3 +98,53 @@ async def start_docker_container(ctf_id: int, response: Response): "ports": ports, "ctf_id": ctf_id } + + +@atomic() +@router.post("/stopall") +async def stop_docker_container(response: Response): + + if not await CTF.filter(id=ctf_id).exists(): + response.status_code = 404 + return {"msg": "CTF does not exist."} + ctf = await CTF.get(id=ctf_id) + + user_id = get_user_id() # From JWT + if not await Container.filter(user_id=user_id).exists(): + return {"msg": "You have no running containers."} + + user_container = Container.get(user_id=user_id) + + container = docker_client.containers.get(user_container.id) + container.stop() + container.remove() + + await Container.filter(user_id=user_id).delete() + await Container.save() + + return {"msg": "Container stopped."} + + +@atomic() +@router.post("/stop/{ctf_id}") +async def stop_docker_container(ctf_id: int, response: Response): + + if not await CTF.filter(id=ctf_id).exists(): + response.status_code = 404 + return {"msg": "CTF does not exist."} + ctf = await CTF.get(id=ctf_id) + + user_id = get_user_id() # From JWT + if not await Container.filter(user_id=user_id, ctf_id=ctf_id).exists(): + return {"msg": "You have no running containers for this CTF."} + + user_container = Container.get(user_id=user_id, ctf_id=ctf_id) + + container = docker_client.containers.get(user_container.id) + container.stop() + container.remove() + + await Container.filter(user_id=user_id).delete() + await Container.save() + + return {"msg": "Container stopped."} From 617639806405809f19d79db1576c43d1ba44a993 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:35:53 +0530 Subject: [PATCH 20/41] chore: Remove unneeded ctf_id validation --- src/pwncore/routes/ctf/start.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index cf28acc..7e56ec8 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -104,11 +104,6 @@ async def start_docker_container(ctf_id: int, response: Response): @router.post("/stopall") async def stop_docker_container(response: Response): - if not await CTF.filter(id=ctf_id).exists(): - response.status_code = 404 - return {"msg": "CTF does not exist."} - ctf = await CTF.get(id=ctf_id) - user_id = get_user_id() # From JWT if not await Container.filter(user_id=user_id).exists(): return {"msg": "You have no running containers."} From 218db4175e897e55818ba6204154a0d5be2906ae Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Thu, 9 Nov 2023 22:16:29 +0530 Subject: [PATCH 21/41] chore: Types for Container and CTF --- src/pwncore/db/__init__.py | 21 ++++++++++----------- src/pwncore/routes/ctf/start.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pwncore/db/__init__.py b/src/pwncore/db/__init__.py index ace0fc2..6b7386e 100644 --- a/src/pwncore/db/__init__.py +++ b/src/pwncore/db/__init__.py @@ -22,19 +22,18 @@ # No relationships yet class Container(Model): - id = fields.TextField(pk=True) - name = fields.TextField() - ctf_id = fields.IntField() - ports = fields.TextField() - team_id = fields.IntField() - user_id = fields.IntField() - flag = fields.TextField() + id: str = fields.TextField(pk=True) + name: str = fields.TextField() + ctf_id: int = fields.IntField() + ports: str = fields.TextField() + team_id: int = fields.IntField() + user_id: int = fields.IntField() + flag: str = fields.TextField() class CTF(Model): - id = fields.IntField(pk=True) - name = fields.TextField() - image_name = fields.TextField() - image_config = fields.TextField() + name: str = fields.TextField() + image_name: str = fields.TextField() + image_config: str = fields.TextField() # Might want to keep the schemas and init in separate files diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 7e56ec8..52aa439 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -85,7 +85,7 @@ async def start_docker_container(ctf_id: int, response: Response): await Container.create(**{ "id" : container.id, "name" : container.name, - "user_id" : user_id + "user_id" : user_id, "team_id" : team_id, "ctf_id" : ctf_id, "flag" : flag, From a00395a7fb273a5ea4167488d5f584a674027ab1 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 07:32:03 +0530 Subject: [PATCH 22/41] chore: Optimize DB operations --- src/pwncore/routes/ctf/start.py | 76 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 52aa439..337ae96 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -16,23 +16,14 @@ @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int, response: Response): - if not await CTF.filter(id=ctf_id).exists(): + ctf = await CTF.get_or_none(id=ctf_id) + if not ctf: response.status_code = 404 return {"msg": "CTF does not exist."} - ctf = await CTF.get(id=ctf_id) - - user_id = get_user_id() # From JWT - if await Container.filter(user_id=user_id).exists(): - user_container = Container.get(user_id=user_id) - return { - "msg": "You already have a running container for a CTF.", - "ports": user_container.ports.split(","), - "ctf_id": user_container.ctf_id - } team_id = get_team_id() # From JWT - if await Container.filter(team_id=team_id, ctf_id=ctf_id).exists(): - team_container = Container.get(team_id=team_id, ctf_id=ctf_id) + team_container = Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + if not team_container: return { "msg": "Your team already has a running container for this CTF.", "ports": team_container.ports.split(","), @@ -82,16 +73,25 @@ async def start_docker_container(ctf_id: int, response: Response): user="root" ) - await Container.create(**{ - "id" : container.id, - "name" : container.name, - "user_id" : user_id, - "team_id" : team_id, - "ctf_id" : ctf_id, - "flag" : flag, - "ports" : ','.join(ports) # Save ports as csv - }) - await Container.save() + try: + await Container.create(**{ + "id" : container.id, + "name" : container.name, + "team_id" : team_id, + "ctf_id" : ctf_id, + "flag" : flag, + "ports" : ','.join(ports) # Save ports as csv + }) + await Container.save() + except: # Not sure which exception should be filtered here for + # Stop the container + container.stop() + container.remove() + + response.status_code = 500 + return { + "msg": "An error occured, please try again." + } return { "msg": "Container started.", @@ -104,11 +104,9 @@ async def start_docker_container(ctf_id: int, response: Response): @router.post("/stopall") async def stop_docker_container(response: Response): - user_id = get_user_id() # From JWT - if not await Container.filter(user_id=user_id).exists(): - return {"msg": "You have no running containers."} + team_id = get_team_id() # From JWT - user_container = Container.get(user_id=user_id) + user_container = Container.get(team_id=team_id) container = docker_client.containers.get(user_container.id) container.stop() @@ -124,22 +122,30 @@ async def stop_docker_container(response: Response): @router.post("/stop/{ctf_id}") async def stop_docker_container(ctf_id: int, response: Response): - if not await CTF.filter(id=ctf_id).exists(): + ctf = await CTF.get_or_none(id=ctf_id) + if not ctf: response.status_code = 404 return {"msg": "CTF does not exist."} - ctf = await CTF.get(id=ctf_id) - user_id = get_user_id() # From JWT - if not await Container.filter(user_id=user_id, ctf_id=ctf_id).exists(): + team_id = get_team_id() + team_container = Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + if not team_container: return {"msg": "You have no running containers for this CTF."} - user_container = Container.get(user_id=user_id, ctf_id=ctf_id) + # We first try to delete the record from the DB + # Then we stop the container + try: + await Container.filter(team_id=team_id, ctf_id=ctf_id).delete() + await Container.save() + except: # Not sure which exception should be filtered here for + response.status_code = 500 + return { + "msg": "An error occured, please try again." + } - container = docker_client.containers.get(user_container.id) + container = docker_client.containers.get(team_container.id) container.stop() container.remove() - await Container.filter(user_id=user_id).delete() - await Container.save() return {"msg": "Container stopped."} From f46b90ad828b65f2afdf2505cbf3fe92a5c089d8 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 07:47:14 +0530 Subject: [PATCH 23/41] feat: No more user_id, all start/stop operations are dealt with team --- src/pwncore/db/__init__.py | 1 - src/pwncore/routes/ctf/start.py | 28 +++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/pwncore/db/__init__.py b/src/pwncore/db/__init__.py index 6b7386e..524d49c 100644 --- a/src/pwncore/db/__init__.py +++ b/src/pwncore/db/__init__.py @@ -27,7 +27,6 @@ class Container(Model): ctf_id: int = fields.IntField() ports: str = fields.TextField() team_id: int = fields.IntField() - user_id: int = fields.IntField() flag: str = fields.TextField() class CTF(Model): diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 337ae96..212c1ba 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -30,6 +30,9 @@ async def start_docker_container(ctf_id: int, response: Response): "ctf_id": team_container.ctf_id } + if Container.filter(team_id=team_id).count() >= 3: + + # Start a new container image_config = ctf.image_config.copy() """ @@ -82,7 +85,6 @@ async def start_docker_container(ctf_id: int, response: Response): "flag" : flag, "ports" : ','.join(ports) # Save ports as csv }) - await Container.save() except: # Not sure which exception should be filtered here for # Stop the container container.stop() @@ -106,16 +108,25 @@ async def stop_docker_container(response: Response): team_id = get_team_id() # From JWT - user_container = Container.get(team_id=team_id) + containers = Container.filter(team_id=team_id).values() - container = docker_client.containers.get(user_container.id) - container.stop() - container.remove() + # We first try to delete the record from the DB + # Then we stop the container + try: + await Container.filter(team_id=team_id).delete() + await Container.save() + except: + response.status_code = 500 + return { + "msg": "An error occured, please try again." + } - await Container.filter(user_id=user_id).delete() - await Container.save() + for db_container in containers: + container = docker_client.containers.get(db_container.id) + container.stop() + container.remove() - return {"msg": "Container stopped."} + return {"msg": "All team containers stopped."} @atomic() @@ -147,5 +158,4 @@ async def stop_docker_container(ctf_id: int, response: Response): container.stop() container.remove() - return {"msg": "Container stopped."} From e1fc8cf330d15bb365dbfafad1d1e4a380092552 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 08:04:39 +0530 Subject: [PATCH 24/41] feat: Return messages can now be configured from config.py --- src/pwncore/config.py | 24 +++++++++++++++++++++--- src/pwncore/routes/ctf/start.py | 30 +++++++++++++++++------------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/pwncore/config.py b/src/pwncore/config.py index 874a702..6ab86fa 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -9,15 +9,33 @@ class Config(t.Protocol): - development: bool + development: bool + flag: str + max_containers_per_team: int + messages: dict class BaseConfig(Config): __slots__ = ("development",) + flag = "C0D" + max_containers_per_team = 3 + messages = { + "db_error": "An error occured, please try again.", + + "port_limit_reached": "Server ran out of ports 💀", + + "ctf_not_found": "CTF does not exist.", + + "container_start": "Container started.", + "container_stop": "Container stopped.", + "containers_team_stop": "All team containers stopped.", + "container_not_found": "You have no running containers for this CTF." + "container_already_running": "Your team already has a running container for this CTF.", + "container_limit_reached": "Your team already has reached the maximum number of containers limit, please stop other unused containers." + } + def __init__(self, development: bool) -> None: self.development = development - self.flag = "C0D" - DEV_CONFIG: t.Final[BaseConfig] = BaseConfig(True) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 212c1ba..9dcb96f 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -9,6 +9,8 @@ from pwncore.container import docker_client from pwncore.config import DEV_CONFIG +config = DEV_CONFIG + # UNTESTED @@ -19,19 +21,21 @@ async def start_docker_container(ctf_id: int, response: Response): ctf = await CTF.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 - return {"msg": "CTF does not exist."} + return {"msg": config.messages["ctf_not_found"]} team_id = get_team_id() # From JWT team_container = Container.get_or_none(team_id=team_id, ctf_id=ctf_id) if not team_container: return { - "msg": "Your team already has a running container for this CTF.", + "msg": config.messages["container_already_running"], "ports": team_container.ports.split(","), "ctf_id": team_container.ctf_id } - if Container.filter(team_id=team_id).count() >= 3: - + if Container.filter(team_id=team_id).count() >= config.max_containers_per_team: + return { + "msg": config.messages["container_limit_reached"] + } # Start a new container image_config = ctf.image_config.copy() @@ -54,7 +58,7 @@ async def start_docker_container(ctf_id: int, response: Response): # Handle error here print("AAAAAAAAAAAAAAAAAAA") response.status_code = 500 - return {"msg": "Server ran out of ports 💀"} + return {"msg": config.messages["port_limit_reached"]} ports = [] # Only to save the host ports used to return to the user for guest_port in image_config["ports"]: @@ -92,11 +96,11 @@ async def start_docker_container(ctf_id: int, response: Response): response.status_code = 500 return { - "msg": "An error occured, please try again." + "msg": config.messages["db_error"] } return { - "msg": "Container started.", + "msg": config.messages["container_started"], "ports": ports, "ctf_id": ctf_id } @@ -118,7 +122,7 @@ async def stop_docker_container(response: Response): except: response.status_code = 500 return { - "msg": "An error occured, please try again." + "msg": config.messages["db_error"] } for db_container in containers: @@ -126,7 +130,7 @@ async def stop_docker_container(response: Response): container.stop() container.remove() - return {"msg": "All team containers stopped."} + return {"msg": config.messages["containers_team_stop"]} @atomic() @@ -136,12 +140,12 @@ async def stop_docker_container(ctf_id: int, response: Response): ctf = await CTF.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 - return {"msg": "CTF does not exist."} + return {"msg": config.messages["ctf_not_found"]} team_id = get_team_id() team_container = Container.get_or_none(team_id=team_id, ctf_id=ctf_id) if not team_container: - return {"msg": "You have no running containers for this CTF."} + return {"msg": config.messages["container_not_found"]} # We first try to delete the record from the DB # Then we stop the container @@ -151,11 +155,11 @@ async def stop_docker_container(ctf_id: int, response: Response): except: # Not sure which exception should be filtered here for response.status_code = 500 return { - "msg": "An error occured, please try again." + "msg": config.messages["db_error"] } container = docker_client.containers.get(team_container.id) container.stop() container.remove() - return {"msg": "Container stopped."} + return {"msg": config.messages["container_stop"]} From 0d4ee10cdad32b918c07fc0fe16a657d2c0c40f3 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 08:09:51 +0530 Subject: [PATCH 25/41] docs: Update docs --- README.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dd92f0f..733a1ef 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Tech Stack: - Framework: **FastAPI** -- Database: **PostgreSQL (ORM: SQLAlchemy)** +- Database: **PostgreSQL (ORM: Tortoise)** - Server: **Uvicorn** - Test: **TestClient (in FastAPI) / Tox** - Containerization: **Docker** @@ -39,13 +39,12 @@ All individual routes (`/team/*`, `/ctf/*`) are then put behind `/api` in the `r In case a certain route has multiple complex tasks, they can be separated as a submodule. For example, the route `/api/ctf/start` will perform a lot of tasks (interacting with docker etc.), and hence has a separate file for it. +`src/`: ``` -app.py # Main file docs.py # Takes metadata from each route and compiles it for FastAPI -config.py # Environment variables, could use .env instead -db.py # Database schemas and connector (may do a separate directory if complexity exceeds) +config.py # Configuration variables -Dockerfile +db/... # Database schemas and connector routes/ L team.py @@ -54,10 +53,8 @@ routes/ L __init__.py # Rest of the ctf routes go here L admin.py L leaderboard.py + L team.py L __init__.py # Main router under `/api`, any misc routes go here -helpers/ - L container.py # Specific helper functions - L __init__.py # Contains general helper functions tests/ ``` From 0594759f58c0886607032a0b07d7df26329e3c46 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 08:11:57 +0530 Subject: [PATCH 26/41] docs: Use poetry instead of pip --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 733a1ef..08ebcea 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ ```sh python -m venv .venv # Create a python virtual environment source .venv/bin/activate # Activate it (This command will differ for Windows) -pip instal -r requirements.txt # Install the dependencies +pip install poetry +poetry install # Install the dependencies ``` ## Run: From bff1e18a0d312e6e08f479de4c4655a254832279 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 12:34:24 +0530 Subject: [PATCH 27/41] fix: Fix missing comma --- src/pwncore/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pwncore/config.py b/src/pwncore/config.py index 6ab86fa..6b7d062 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -30,7 +30,7 @@ class BaseConfig(Config): "container_start": "Container started.", "container_stop": "Container stopped.", "containers_team_stop": "All team containers stopped.", - "container_not_found": "You have no running containers for this CTF." + "container_not_found": "You have no running containers for this CTF.", "container_already_running": "Your team already has a running container for this CTF.", "container_limit_reached": "Your team already has reached the maximum number of containers limit, please stop other unused containers." } From 30cdb44dc145ddd47a0d174cdbdd13f47d8122b8 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:18:43 +0530 Subject: [PATCH 28/41] chore: Prevent tortoise from running in multiple event loops --- src/pwncore/__init__.py | 10 +++++++ src/pwncore/db.py | 19 +++++++++++++ src/pwncore/db/__init__.py | 45 ------------------------------ src/pwncore/routes/ctf/__init__.py | 1 + src/pwncore/routes/ctf/start.py | 36 +++++++++++++----------- 5 files changed, 49 insertions(+), 62 deletions(-) create mode 100644 src/pwncore/db.py delete mode 100644 src/pwncore/db/__init__.py diff --git a/src/pwncore/__init__.py b/src/pwncore/__init__.py index 239fc67..2b47838 100644 --- a/src/pwncore/__init__.py +++ b/src/pwncore/__init__.py @@ -1,5 +1,7 @@ from fastapi import FastAPI +from tortoise.contrib.fastapi import register_tortoise + import pwncore.docs as docs import pwncore.routes as routes @@ -8,3 +10,11 @@ ) app.include_router(routes.router) + +register_tortoise( + app, + db_url="sqlite://:memory:", + modules={"models": ["pwncore.db"]}, + generate_schemas=True, + add_exception_handlers=True +) diff --git a/src/pwncore/db.py b/src/pwncore/db.py new file mode 100644 index 0000000..512bae7 --- /dev/null +++ b/src/pwncore/db.py @@ -0,0 +1,19 @@ +from tortoise import fields +from tortoise.models import Model + +# Tortoise is initiated from __main__.py since initiating +# it as a submodule creates multiple event loops. + +# No relationships yet +class Container(Model): + id: str = fields.TextField(pk=True) + name: str = fields.TextField() + ctf_id: int = fields.IntField() + ports: str = fields.TextField() + team_id: int = fields.IntField() + flag: str = fields.TextField() + +class CTF(Model): + name: str = fields.TextField() + image_name: str = fields.TextField() + image_config: str = fields.TextField() diff --git a/src/pwncore/db/__init__.py b/src/pwncore/db/__init__.py deleted file mode 100644 index 524d49c..0000000 --- a/src/pwncore/db/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -from tortoise import Tortoise, fields, run_async -from tortoise.models import Model - -""" -Each user can create only one container. -Only one container for a ctf can be created from a team. -Container: - id (pk) - name - ctf_id - ports # stored as csv - team_id - user_id - flag # TBD - -CTF: - id (pk) - name - image_name - image_config -""" - -# No relationships yet -class Container(Model): - id: str = fields.TextField(pk=True) - name: str = fields.TextField() - ctf_id: int = fields.IntField() - ports: str = fields.TextField() - team_id: int = fields.IntField() - flag: str = fields.TextField() - -class CTF(Model): - name: str = fields.TextField() - image_name: str = fields.TextField() - image_config: str = fields.TextField() - - -# Might want to keep the schemas and init in separate files -async def init(): - await Tortoise.init( - db_url="sqlite://:memory:", - modules={'models': ['pwncore.db']} - ) - await Tortoise.generate_schemas() -run_async(init()) diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index 03c1749..faee46f 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -11,6 +11,7 @@ router = APIRouter(prefix="/ctf", tags=["ctf"]) +from . import start # Routes that do not need a separate submodule for themselves diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 9dcb96f..81d6b33 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -11,7 +11,11 @@ config = DEV_CONFIG -# UNTESTED +# temporary helper functions +def get_team_id(): + return 34 +def get_empty_ports(): + return [4444] @atomic() @@ -32,24 +36,13 @@ async def start_docker_container(ctf_id: int, response: Response): "ctf_id": team_container.ctf_id } - if Container.filter(team_id=team_id).count() >= config.max_containers_per_team: + if await Container.filter(team_id=team_id).count() >= config.max_containers_per_team: return { "msg": config.messages["container_limit_reached"] } # Start a new container - image_config = ctf.image_config.copy() - """ - This image_config will look like: - { - "ports": { - "22/tcp": None, - "80/tcp": None - } - } - - `None` gets substitued with the empty ports on the host system. - """ + image_config = eval(ctf.image_config) # Ports port_list = get_empty_ports() # Need to implement @@ -70,7 +63,7 @@ async def start_docker_container(ctf_id: int, response: Response): container = docker_client.containers.run( ctf.image_name, detach=True, - name=f"{team_id}_{ctf_id}", + name=f"{team_id}_{ctf_id}_{uuid.uuid4().hex}", **image_config ) @@ -87,7 +80,7 @@ async def start_docker_container(ctf_id: int, response: Response): "team_id" : team_id, "ctf_id" : ctf_id, "flag" : flag, - "ports" : ','.join(ports) # Save ports as csv + "ports" : ','.join([str(port) for port in ports]) # Save ports as csv }) except: # Not sure which exception should be filtered here for # Stop the container @@ -100,7 +93,7 @@ async def start_docker_container(ctf_id: int, response: Response): } return { - "msg": config.messages["container_started"], + "msg": config.messages["container_start"], "ports": ports, "ctf_id": ctf_id } @@ -163,3 +156,12 @@ async def stop_docker_container(ctf_id: int, response: Response): container.remove() return {"msg": config.messages["container_stop"]} + + +@router.get("/listaaa") +async def ctf_list(): + # Get list of ctfs + return [ + {"name": "Password Juggling", "ctf_id": 2243}, + {"name": "hexane", "ctf_id": 2242}, + ] From 8cb8ae23e0e46ed76f0a26d0bedac92f3aeb2150 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:25:41 +0530 Subject: [PATCH 29/41] fix: await when fetching from DB --- src/pwncore/routes/ctf/start.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 81d6b33..60b6e75 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -28,7 +28,7 @@ async def start_docker_container(ctf_id: int, response: Response): return {"msg": config.messages["ctf_not_found"]} team_id = get_team_id() # From JWT - team_container = Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) if not team_container: return { "msg": config.messages["container_already_running"], @@ -105,7 +105,7 @@ async def stop_docker_container(response: Response): team_id = get_team_id() # From JWT - containers = Container.filter(team_id=team_id).values() + containers = await Container.filter(team_id=team_id).values() # We first try to delete the record from the DB # Then we stop the container @@ -136,20 +136,20 @@ async def stop_docker_container(ctf_id: int, response: Response): return {"msg": config.messages["ctf_not_found"]} team_id = get_team_id() - team_container = Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + print(team_container) if not team_container: return {"msg": config.messages["container_not_found"]} # We first try to delete the record from the DB # Then we stop the container - try: - await Container.filter(team_id=team_id, ctf_id=ctf_id).delete() - await Container.save() - except: # Not sure which exception should be filtered here for - response.status_code = 500 - return { - "msg": config.messages["db_error"] - } + # try: + await Container.filter(team_id=team_id, ctf_id=ctf_id).delete() + # except: # Not sure which exception should be filtered here for + # response.status_code = 500 + # return { + # "msg": config.messages["db_error"] + # } container = docker_client.containers.get(team_container.id) container.stop() From 57d7c9ec972a5d0340ae210593b8516070fc4417 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:35:00 +0530 Subject: [PATCH 30/41] fix: Change image_config to JSON in tortoise model --- src/pwncore/db.py | 2 +- src/pwncore/routes/ctf/start.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pwncore/db.py b/src/pwncore/db.py index 512bae7..b75162e 100644 --- a/src/pwncore/db.py +++ b/src/pwncore/db.py @@ -16,4 +16,4 @@ class Container(Model): class CTF(Model): name: str = fields.TextField() image_name: str = fields.TextField() - image_config: str = fields.TextField() + image_config: str = fields.JSONField() diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 60b6e75..7ab78f9 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -22,6 +22,16 @@ def get_empty_ports(): @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int, response: Response): + await CTF.create(**{ + "name": "AAA", + "image_name": "key", + "image_config": { + "ports": { + "22/tcp": None + } + } + }) + ctf = await CTF.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 @@ -29,7 +39,7 @@ async def start_docker_container(ctf_id: int, response: Response): team_id = get_team_id() # From JWT team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) - if not team_container: + if team_container: return { "msg": config.messages["container_already_running"], "ports": team_container.ports.split(","), @@ -42,7 +52,7 @@ async def start_docker_container(ctf_id: int, response: Response): } # Start a new container - image_config = eval(ctf.image_config) + image_config = ctf.image_config # Ports port_list = get_empty_ports() # Need to implement From 81c3b27788c03f8716c74dfabb8fd841934002ec Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:59:54 +0530 Subject: [PATCH 31/41] chore: Cleanup and fix types --- src/pwncore/db.py | 3 ++- src/pwncore/routes/ctf/start.py | 35 ++++++++++++--------------------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/pwncore/db.py b/src/pwncore/db.py index b75162e..b2794ea 100644 --- a/src/pwncore/db.py +++ b/src/pwncore/db.py @@ -1,4 +1,5 @@ from tortoise import fields +from tortoise.fields import JSONField from tortoise.models import Model # Tortoise is initiated from __main__.py since initiating @@ -16,4 +17,4 @@ class Container(Model): class CTF(Model): name: str = fields.TextField() image_name: str = fields.TextField() - image_config: str = fields.JSONField() + image_config: JSONField = fields.JSONField() diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 7ab78f9..a95fe24 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -22,15 +22,16 @@ def get_empty_ports(): @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int, response: Response): - await CTF.create(**{ - "name": "AAA", - "image_name": "key", - "image_config": { - "ports": { - "22/tcp": None - } - } - }) + # Testing purposes + # await CTF.create(**{ + # "name": "AAA", + # "image_name": "key", + # "image_config": { + # "ports": { + # "22/tcp": None + # } + # } + # }) ctf = await CTF.get_or_none(id=ctf_id) if not ctf: @@ -55,7 +56,7 @@ async def start_docker_container(ctf_id: int, response: Response): image_config = ctf.image_config # Ports - port_list = get_empty_ports() # Need to implement + port_list = get_empty_ports() # Need to implement if len(port_list) < len(image_config["ports"]): # Handle error here @@ -77,7 +78,7 @@ async def start_docker_container(ctf_id: int, response: Response): **image_config ) - flag = f"{DEV_CONFIG.flag}{{{uuid.uuid4().hex}}}" + flag = f"{config.flag}{{{uuid.uuid4().hex}}}" container.exec_run( f"/bin/bash /root/gen_flag '{flag}'", user="root" @@ -111,7 +112,7 @@ async def start_docker_container(ctf_id: int, response: Response): @atomic() @router.post("/stopall") -async def stop_docker_container(response: Response): +async def stopall_docker_container(response: Response): team_id = get_team_id() # From JWT @@ -121,7 +122,6 @@ async def stop_docker_container(response: Response): # Then we stop the container try: await Container.filter(team_id=team_id).delete() - await Container.save() except: response.status_code = 500 return { @@ -166,12 +166,3 @@ async def stop_docker_container(ctf_id: int, response: Response): container.remove() return {"msg": config.messages["container_stop"]} - - -@router.get("/listaaa") -async def ctf_list(): - # Get list of ctfs - return [ - {"name": "Password Juggling", "ctf_id": 2243}, - {"name": "hexane", "ctf_id": 2242}, - ] From 38b1d8a68a67dae48011341f79b2b4f1eb0ab12f Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:00:43 +0530 Subject: [PATCH 32/41] docs: Update docs --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 08ebcea..9e52f8c 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,7 @@ In case a certain route has multiple complex tasks, they can be separated as a s ``` docs.py # Takes metadata from each route and compiles it for FastAPI config.py # Configuration variables - -db/... # Database schemas and connector +db.py # Database schemas and connector routes/ L team.py From bf7c31b12298a64b074a5919cf63e06c2a46041a Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 14 Nov 2023 08:50:26 +0530 Subject: [PATCH 33/41] chore: remove type annotations from Model --- src/pwncore/db.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pwncore/db.py b/src/pwncore/db.py index b2794ea..918fddb 100644 --- a/src/pwncore/db.py +++ b/src/pwncore/db.py @@ -7,14 +7,14 @@ # No relationships yet class Container(Model): - id: str = fields.TextField(pk=True) - name: str = fields.TextField() - ctf_id: int = fields.IntField() - ports: str = fields.TextField() - team_id: int = fields.IntField() - flag: str = fields.TextField() + id = fields.TextField(pk=True) + name = fields.TextField() + ctf_id = fields.IntField() + ports = fields.TextField() + team_id = fields.IntField() + flag = fields.TextField() class CTF(Model): - name: str = fields.TextField() - image_name: str = fields.TextField() - image_config: JSONField = fields.JSONField() + name = fields.TextField() + image_name = fields.TextField() + image_config = fields.JSONField() From 145ce39ec0f73b79dd6e47fdc3301fc8e47eef3e Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 14 Nov 2023 08:55:42 +0530 Subject: [PATCH 34/41] chore: Add logging for errors on server side --- src/pwncore/routes/ctf/start.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index a95fe24..d067b7c 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -3,6 +3,7 @@ from fastapi import Response import uuid from tortoise.transactions import atomic +import logging from pwncore.routes.ctf import router from pwncore.db import Container, CTF @@ -60,7 +61,7 @@ async def start_docker_container(ctf_id: int, response: Response): if len(port_list) < len(image_config["ports"]): # Handle error here - print("AAAAAAAAAAAAAAAAAAA") + logging.critical("No more free ports available on machine.") response.status_code = 500 return {"msg": config.messages["port_limit_reached"]} From 1f9dba8f8bcc29a95470fc1c1e42475b401d338a Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:04:26 +0530 Subject: [PATCH 35/41] fix: Fix commented try except block and unnecessary print statements --- src/pwncore/routes/ctf/start.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index d067b7c..a9a9c59 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -94,8 +94,8 @@ async def start_docker_container(ctf_id: int, response: Response): "flag" : flag, "ports" : ','.join([str(port) for port in ports]) # Save ports as csv }) - except: # Not sure which exception should be filtered here for - # Stop the container + except Exception: + # Stop the container if failed to make a DB record container.stop() container.remove() @@ -123,7 +123,7 @@ async def stopall_docker_container(response: Response): # Then we stop the container try: await Container.filter(team_id=team_id).delete() - except: + except Exception: response.status_code = 500 return { "msg": config.messages["db_error"] @@ -148,19 +148,18 @@ async def stop_docker_container(ctf_id: int, response: Response): team_id = get_team_id() team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) - print(team_container) if not team_container: return {"msg": config.messages["container_not_found"]} # We first try to delete the record from the DB # Then we stop the container - # try: - await Container.filter(team_id=team_id, ctf_id=ctf_id).delete() - # except: # Not sure which exception should be filtered here for - # response.status_code = 500 - # return { - # "msg": config.messages["db_error"] - # } + try: + await Container.filter(team_id=team_id, ctf_id=ctf_id).delete() + except Exception: + response.status_code = 500 + return { + "msg": config.messages["db_error"] + } container = docker_client.containers.get(team_container.id) container.stop() From 88fa20c7f84ec6c3d6ceeca87138fb71cceb4ee1 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:07:50 +0530 Subject: [PATCH 36/41] docs: Install poetry globally --- README.md | 2 +- src/pwncore/config.py | 2 ++ src/pwncore/routes/ctf/start.py | 4 +--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9e52f8c..3afaa30 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ ## Setup: ```sh +pip install poetry python -m venv .venv # Create a python virtual environment source .venv/bin/activate # Activate it (This command will differ for Windows) -pip install poetry poetry install # Install the dependencies ``` diff --git a/src/pwncore/config.py b/src/pwncore/config.py index 6b7d062..85d0ee6 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -39,3 +39,5 @@ def __init__(self, development: bool) -> None: self.development = development DEV_CONFIG: t.Final[BaseConfig] = BaseConfig(True) + +config = DEV_CONFIG diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index a9a9c59..6d62bd6 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -8,9 +8,7 @@ from pwncore.routes.ctf import router from pwncore.db import Container, CTF from pwncore.container import docker_client -from pwncore.config import DEV_CONFIG - -config = DEV_CONFIG +from pwncore.config import config # temporary helper functions def get_team_id(): From 5d4739771ece6a747ea2155ae05ca0e0640cfd11 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:31:53 +0530 Subject: [PATCH 37/41] chore: Fix config.py --- src/pwncore/__init__.py | 3 +- src/pwncore/config.py | 81 ++++++++++++++++----------------- src/pwncore/routes/ctf/start.py | 24 +++++----- 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/src/pwncore/__init__.py b/src/pwncore/__init__.py index 2b47838..867c3dd 100644 --- a/src/pwncore/__init__.py +++ b/src/pwncore/__init__.py @@ -4,6 +4,7 @@ import pwncore.docs as docs import pwncore.routes as routes +from pwncore.config import config app = FastAPI( title="Pwncore", openapi_tags=docs.tags_metadata, description=docs.description @@ -13,7 +14,7 @@ register_tortoise( app, - db_url="sqlite://:memory:", + db_url=config.db_url, modules={"models": ["pwncore.db"]}, generate_schemas=True, add_exception_handlers=True diff --git a/src/pwncore/config.py b/src/pwncore/config.py index 85d0ee6..48040bd 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -1,43 +1,38 @@ -from __future__ import annotations - -import typing as t - -__all__ = ( - "Config", - "BaseConfig", -) - - -class Config(t.Protocol): - development: bool - flag: str - max_containers_per_team: int - messages: dict - - -class BaseConfig(Config): - __slots__ = ("development",) - - flag = "C0D" - max_containers_per_team = 3 - messages = { - "db_error": "An error occured, please try again.", - - "port_limit_reached": "Server ran out of ports 💀", - - "ctf_not_found": "CTF does not exist.", - - "container_start": "Container started.", - "container_stop": "Container stopped.", - "containers_team_stop": "All team containers stopped.", - "container_not_found": "You have no running containers for this CTF.", - "container_already_running": "Your team already has a running container for this CTF.", - "container_limit_reached": "Your team already has reached the maximum number of containers limit, please stop other unused containers." - } - - def __init__(self, development: bool) -> None: - self.development = development - -DEV_CONFIG: t.Final[BaseConfig] = BaseConfig(True) - -config = DEV_CONFIG +from dataclasses import dataclass + +""" +Sample messages: +"db_error": "An error occurred, please try again.", +"port_limit_reached": "Server ran out of ports 💀", +"ctf_not_found": "CTF does not exist.", +"container_start": "Container started.", +"container_stop": "Container stopped.", +"containers_team_stop": "All team containers stopped.", +"container_not_found": "You have no running containers for this CTF.", +"container_already_running": "Your team already has a running container for this CTF.", +"container_limit_reached": "Your team already has reached the maximum number of containers limit, please stop other unused containers." +""" + +msg_codes = { + "db_error": 0, + "port_limit_reached": 1, + "ctf_not_found": 2, + "container_start": 3, + "container_stop": 4, + "containers_team_stop": 5, + "container_not_found": 6, + "container_already_running": 7, + "container_limit_reached": 8 +} + + +@dataclass +class Config: + development: bool + msg_codes: dict + db_url: str = "sqlite://:memory:" + flag: str = "C0D" + max_containers_per_team: int = 3 + + +config = Config(development=True, msg_codes=msg_codes) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 6d62bd6..a8e0603 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -35,20 +35,20 @@ async def start_docker_container(ctf_id: int, response: Response): ctf = await CTF.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 - return {"msg": config.messages["ctf_not_found"]} + return {"msg_code": config.msg_codes["ctf_not_found"]} team_id = get_team_id() # From JWT team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) if team_container: return { - "msg": config.messages["container_already_running"], + "msg_code": config.msg_codes["container_already_running"], "ports": team_container.ports.split(","), "ctf_id": team_container.ctf_id } if await Container.filter(team_id=team_id).count() >= config.max_containers_per_team: return { - "msg": config.messages["container_limit_reached"] + "msg_code": config.msg_codes["container_limit_reached"] } # Start a new container @@ -61,7 +61,7 @@ async def start_docker_container(ctf_id: int, response: Response): # Handle error here logging.critical("No more free ports available on machine.") response.status_code = 500 - return {"msg": config.messages["port_limit_reached"]} + return {"msg_code": config.msg_codes["port_limit_reached"]} ports = [] # Only to save the host ports used to return to the user for guest_port in image_config["ports"]: @@ -99,11 +99,11 @@ async def start_docker_container(ctf_id: int, response: Response): response.status_code = 500 return { - "msg": config.messages["db_error"] + "msg_code": config.msg_codes["db_error"] } return { - "msg": config.messages["container_start"], + "msg_code": config.msg_codes["container_start"], "ports": ports, "ctf_id": ctf_id } @@ -124,7 +124,7 @@ async def stopall_docker_container(response: Response): except Exception: response.status_code = 500 return { - "msg": config.messages["db_error"] + "msg_code": config.msg_codes["db_error"] } for db_container in containers: @@ -132,7 +132,7 @@ async def stopall_docker_container(response: Response): container.stop() container.remove() - return {"msg": config.messages["containers_team_stop"]} + return {"msg_code": config.msg_codes["containers_team_stop"]} @atomic() @@ -142,12 +142,12 @@ async def stop_docker_container(ctf_id: int, response: Response): ctf = await CTF.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 - return {"msg": config.messages["ctf_not_found"]} + return {"msg_code": config.msg_codes["ctf_not_found"]} team_id = get_team_id() team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) if not team_container: - return {"msg": config.messages["container_not_found"]} + return {"msg_code": config.msg_codes["container_not_found"]} # We first try to delete the record from the DB # Then we stop the container @@ -156,11 +156,11 @@ async def stop_docker_container(ctf_id: int, response: Response): except Exception: response.status_code = 500 return { - "msg": config.messages["db_error"] + "msg_code": config.msg_codes["db_error"] } container = docker_client.containers.get(team_container.id) container.stop() container.remove() - return {"msg": config.messages["container_stop"]} + return {"msg_code": config.msg_codes["container_stop"]} From af70e077d8d22f1a8e2ae22ef17b7ca7e460122c Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:34:48 +0530 Subject: [PATCH 38/41] chore: Fix config.py --- src/pwncore/config.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/pwncore/config.py b/src/pwncore/config.py index 48040bd..34cdb6c 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -30,9 +30,25 @@ class Config: development: bool msg_codes: dict - db_url: str = "sqlite://:memory:" - flag: str = "C0D" - max_containers_per_team: int = 3 + db_url: str + flag: str + max_containers_per_team: int -config = Config(development=True, msg_codes=msg_codes) +config = Config( + development=True, + db_url="sqlite://:memory:", + flag="C0D", + max_containers_per_team=3, + msg_codes={ + "db_error": 0, + "port_limit_reached": 1, + "ctf_not_found": 2, + "container_start": 3, + "container_stop": 4, + "containers_team_stop": 5, + "container_not_found": 6, + "container_already_running": 7, + "container_limit_reached": 8 + } +) From d9db89e39fb6981f58a3d285114ca76bb69d03cf Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:45:28 +0530 Subject: [PATCH 39/41] chore: Use aiodocker and invert ctf router relation --- pyproject.toml | 1 + src/pwncore/container.py | 12 +++- src/pwncore/db.py | 5 +- src/pwncore/routes/ctf/__init__.py | 4 +- src/pwncore/routes/ctf/start.py | 95 ++++++++++++++++++------------ 5 files changed, 72 insertions(+), 45 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f7997c5..dcc0832 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ python = "^3.11" fastapi = "^0.104.1" uvicorn = "^0.24.0" tortoise-orm = {version = "^0.20.0", extras = ["asyncpg", "accel"]} +aiodocker = "^0.21.0" [tool.poetry.group.dev.dependencies] mypy = "^1.6.1" diff --git a/src/pwncore/container.py b/src/pwncore/container.py index 74fcec2..15b5c2a 100644 --- a/src/pwncore/container.py +++ b/src/pwncore/container.py @@ -1,3 +1,11 @@ -import docker +import aiodocker +import atexit +import asyncio -docker_client = docker.from_env() +docker_client = aiodocker.Docker() + + +async def docker_cleanup(): + await docker_client.close() + +atexit.register(lambda: asyncio.run(docker_cleanup())) diff --git a/src/pwncore/db.py b/src/pwncore/db.py index 918fddb..1342b6a 100644 --- a/src/pwncore/db.py +++ b/src/pwncore/db.py @@ -16,5 +16,6 @@ class Container(Model): class CTF(Model): name = fields.TextField() - image_name = fields.TextField() - image_config = fields.JSONField() + docker_config = fields.JSONField() + # image_name = fields.TextField() + # image_config = fields.JSONField() diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index faee46f..4883f9e 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -1,6 +1,7 @@ from __future__ import annotations from fastapi import APIRouter +from pwncore.routes.ctf import start # Metadata at the top for instant accessibility metadata = { @@ -10,8 +11,7 @@ } router = APIRouter(prefix="/ctf", tags=["ctf"]) - -from . import start +router.include_router(start.router) # Routes that do not need a separate submodule for themselves diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index a8e0603..db57bbd 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -1,11 +1,10 @@ from __future__ import annotations -from fastapi import Response +from fastapi import APIRouter, Response import uuid from tortoise.transactions import atomic import logging -from pwncore.routes.ctf import router from pwncore.db import Container, CTF from pwncore.container import docker_client from pwncore.config import config @@ -17,20 +16,32 @@ def get_empty_ports(): return [4444] +router = APIRouter(tags=["ctf"]) + + @atomic() @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int, response: Response): # Testing purposes - # await CTF.create(**{ - # "name": "AAA", - # "image_name": "key", - # "image_config": { - # "ports": { - # "22/tcp": None - # } - # } - # }) + """ + image_config format: + + """ + await CTF.create(**{ + "name": "Invisible-Incursion", + "docker_config": { + "Image": "key:latest", + "AttachStdin": False, + "AttachStdout": False, + "AttachStderr": False, + "Tty": False, + "OpenStdin": False, + "PortBindings": { + "22/tcp": [] + } + } + }) ctf = await CTF.get_or_none(id=ctf_id) if not ctf: @@ -52,51 +63,57 @@ async def start_docker_container(ctf_id: int, response: Response): } # Start a new container - image_config = ctf.image_config + container_name = f"{team_id}_{ctf_id}_{uuid.uuid4().hex}" + container_flag = f"{config.flag}{{{uuid.uuid4().hex}}}" + image_config = ctf.docker_config # Ports port_list = get_empty_ports() # Need to implement - if len(port_list) < len(image_config["ports"]): + if len(port_list) < len(image_config["PortBindings"]): # Handle error here logging.critical("No more free ports available on machine.") response.status_code = 500 return {"msg_code": config.msg_codes["port_limit_reached"]} ports = [] # Only to save the host ports used to return to the user - for guest_port in image_config["ports"]: + for guest_port in image_config["PortBindings"]: port = port_list.pop() ports.append(port) - image_config["ports"][guest_port] = port + image_config["PortBindings"][guest_port] = [{"HostPost": port}] # Run - container = docker_client.containers.run( - ctf.image_name, - detach=True, - name=f"{team_id}_{ctf_id}_{uuid.uuid4().hex}", - **image_config - ) - - flag = f"{config.flag}{{{uuid.uuid4().hex}}}" - container.exec_run( - f"/bin/bash /root/gen_flag '{flag}'", - user="root" - ) + container = await docker_client.containers.run({ + "Image": "key:latest", + "AttachStdin": False, + "AttachStdout": False, + "AttachStderr": False, + "Tty": False, + "OpenStdin": False, + "PortBindings": { + "22/tcp": [{"HostPort": "4444"}] + } + }, name=container_name) + + await ( + await container.exec(["/bin/bash", "/root/gen_flag", container_flag]) + ).start(detach=True) try: await Container.create(**{ "id" : container.id, - "name" : container.name, + "name" : container_name, "team_id" : team_id, "ctf_id" : ctf_id, - "flag" : flag, + "flag" : container_flag, "ports" : ','.join([str(port) for port in ports]) # Save ports as csv }) - except Exception: + except Exception as e: # Stop the container if failed to make a DB record - container.stop() - container.remove() + await container.stop() + await container.delete() + logging.critical(e) response.status_code = 500 return { "msg_code": config.msg_codes["db_error"] @@ -113,7 +130,7 @@ async def start_docker_container(ctf_id: int, response: Response): @router.post("/stopall") async def stopall_docker_container(response: Response): - team_id = get_team_id() # From JWT + team_id = get_team_id() # From JWT containers = await Container.filter(team_id=team_id).values() @@ -128,9 +145,9 @@ async def stopall_docker_container(response: Response): } for db_container in containers: - container = docker_client.containers.get(db_container.id) - container.stop() - container.remove() + container = await docker_client.containers.get(db_container.id) + await container.stop() + await container.delete() return {"msg_code": config.msg_codes["containers_team_stop"]} @@ -159,8 +176,8 @@ async def stop_docker_container(ctf_id: int, response: Response): "msg_code": config.msg_codes["db_error"] } - container = docker_client.containers.get(team_container.id) - container.stop() - container.remove() + container = await docker_client.containers.get(team_container.id) + await container.stop() + await container.delete() return {"msg_code": config.msg_codes["container_stop"]} From da1306aac8f10fe47506530101707dfb3952c8b8 Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:08:12 +0530 Subject: [PATCH 40/41] chore: Let Docker choose ports itself and use the Ports model by WizzyGeek --- src/pwncore/db.py | 16 ++++-- src/pwncore/routes/ctf/start.py | 97 ++++++++++++++++----------------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/pwncore/db.py b/src/pwncore/db.py index 1342b6a..742cc31 100644 --- a/src/pwncore/db.py +++ b/src/pwncore/db.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from tortoise import fields from tortoise.fields import JSONField from tortoise.models import Model @@ -10,12 +12,18 @@ class Container(Model): id = fields.TextField(pk=True) name = fields.TextField() ctf_id = fields.IntField() - ports = fields.TextField() + ports: fields.ReverseRelation[Ports] team_id = fields.IntField() flag = fields.TextField() class CTF(Model): name = fields.TextField() - docker_config = fields.JSONField() - # image_name = fields.TextField() - # image_config = fields.JSONField() + # docker_config = fields.JSONField() + image_name = fields.TextField() + image_config = fields.JSONField() + +class Ports(Model): + container: fields.ForeignKeyRelation[Container] = fields.ForeignKeyField( + "models.Container", related_name="ports", on_delete=fields.OnDelete.CASCADE + ) + port = fields.IntField(pk=True) diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index db57bbd..8602ab8 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -5,7 +5,7 @@ from tortoise.transactions import atomic import logging -from pwncore.db import Container, CTF +from pwncore.db import Container, CTF, Ports from pwncore.container import docker_client from pwncore.config import config @@ -23,25 +23,26 @@ def get_empty_ports(): @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int, response: Response): - # Testing purposes - """ - image_config format: - """ - await CTF.create(**{ - "name": "Invisible-Incursion", - "docker_config": { - "Image": "key:latest", - "AttachStdin": False, - "AttachStdout": False, - "AttachStderr": False, - "Tty": False, - "OpenStdin": False, - "PortBindings": { - "22/tcp": [] - } + image_config contains the raw POST data that gets sent to the Docker Remote API. + For now it just contains the guest ports that need to be opened on the host. + image_config: + { + "PortBindings": { + "22/tcp": [{}] # Let docker randomly assign ports } - }) + } + """ + # Testing purposes + # await CTF.create(**{ + # "name": "Invisible-Incursion", + # "image_name": "key:latest", + # "image_config": { + # "PortBindings": { + # "22/tcp": [{}] + # } + # } + # }) ctf = await CTF.get_or_none(id=ctf_id) if not ctf: @@ -51,9 +52,11 @@ async def start_docker_container(ctf_id: int, response: Response): team_id = get_team_id() # From JWT team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) if team_container: + db_ports = await team_container.ports.all().values('port') # Get ports from DB + ports = [ db_port['port'] for db_port in db_ports ] # Create a list out of it return { "msg_code": config.msg_codes["container_already_running"], - "ports": team_container.ports.split(","), + "ports": ports, "ctf_id": team_container.ctf_id } @@ -65,55 +68,49 @@ async def start_docker_container(ctf_id: int, response: Response): # Start a new container container_name = f"{team_id}_{ctf_id}_{uuid.uuid4().hex}" container_flag = f"{config.flag}{{{uuid.uuid4().hex}}}" - image_config = ctf.docker_config - - # Ports - port_list = get_empty_ports() # Need to implement - - if len(port_list) < len(image_config["PortBindings"]): - # Handle error here - logging.critical("No more free ports available on machine.") - response.status_code = 500 - return {"msg_code": config.msg_codes["port_limit_reached"]} - - ports = [] # Only to save the host ports used to return to the user - for guest_port in image_config["PortBindings"]: - port = port_list.pop() - ports.append(port) - image_config["PortBindings"][guest_port] = [{"HostPost": port}] # Run - container = await docker_client.containers.run({ - "Image": "key:latest", - "AttachStdin": False, - "AttachStdout": False, - "AttachStderr": False, - "Tty": False, - "OpenStdin": False, - "PortBindings": { - "22/tcp": [{"HostPort": "4444"}] + container = await docker_client.containers.run( + name=container_name, + config={ + "Image": ctf.image_name, + "AttachStdin": False, + "AttachStdout": False, + "AttachStderr": False, + "Tty": False, + "OpenStdin": False, + **ctf.image_config } - }, name=container_name) + ) await ( await container.exec(["/bin/bash", "/root/gen_flag", container_flag]) ).start(detach=True) try: - await Container.create(**{ + db_container = await Container.create(**{ "id" : container.id, "name" : container_name, "team_id" : team_id, "ctf_id" : ctf_id, - "flag" : container_flag, - "ports" : ','.join([str(port) for port in ports]) # Save ports as csv + "flag" : container_flag }) - except Exception as e: + + # Get ports and save them + ports = [] # List to return back to frontend + for guest_port in ctf.image_config['PortBindings']: + # Docker assigns the port to the IPv4 and IPv6 addresses + # Since we only require IPv4, we select the zeroth item + # from the returned list. + port = int((await container.port(guest_port))[0]["HostPort"]) + ports.append(port) + host_port = await Ports.create(port=port, container=db_container) + + except Exception: # Stop the container if failed to make a DB record await container.stop() await container.delete() - logging.critical(e) response.status_code = 500 return { "msg_code": config.msg_codes["db_error"] From 9c6ec9d894352bb84c43e305b2ffdeb19993be9c Mon Sep 17 00:00:00 2001 From: mradigen <55953083+mradigen@users.noreply.github.com> Date: Thu, 16 Nov 2023 15:38:46 +0530 Subject: [PATCH 41/41] chore: Add app_lifespan and make start.py adhere with models from #7 --- src/pwncore/__init__.py | 32 +++++--- src/pwncore/config.py | 6 +- src/pwncore/container.py | 8 -- src/pwncore/db.py | 29 ------- src/pwncore/routes/ctf/__init__.py | 6 +- src/pwncore/routes/ctf/start.py | 118 ++++++++++++++--------------- 6 files changed, 86 insertions(+), 113 deletions(-) delete mode 100644 src/pwncore/db.py diff --git a/src/pwncore/__init__.py b/src/pwncore/__init__.py index 867c3dd..48e316c 100644 --- a/src/pwncore/__init__.py +++ b/src/pwncore/__init__.py @@ -1,21 +1,33 @@ from fastapi import FastAPI +from contextlib import asynccontextmanager from tortoise.contrib.fastapi import register_tortoise import pwncore.docs as docs import pwncore.routes as routes +from pwncore.container import docker_client from pwncore.config import config -app = FastAPI( - title="Pwncore", openapi_tags=docs.tags_metadata, description=docs.description -) -app.include_router(routes.router) +@asynccontextmanager +async def app_lifespan(app: FastAPI): + # Startup + register_tortoise( + app, + db_url=config.db_url, + modules={"models": ["pwncore.models"]}, + generate_schemas=True, + add_exception_handlers=True, + ) + yield + # Shutdown + await docker_client.close() -register_tortoise( - app, - db_url=config.db_url, - modules={"models": ["pwncore.db"]}, - generate_schemas=True, - add_exception_handlers=True + +app = FastAPI( + title="Pwncore", + openapi_tags=docs.tags_metadata, + description=docs.description, + lifespan=app_lifespan, ) +app.include_router(routes.router) diff --git a/src/pwncore/config.py b/src/pwncore/config.py index 34cdb6c..ac66190 100644 --- a/src/pwncore/config.py +++ b/src/pwncore/config.py @@ -22,7 +22,7 @@ "containers_team_stop": 5, "container_not_found": 6, "container_already_running": 7, - "container_limit_reached": 8 + "container_limit_reached": 8, } @@ -49,6 +49,6 @@ class Config: "containers_team_stop": 5, "container_not_found": 6, "container_already_running": 7, - "container_limit_reached": 8 - } + "container_limit_reached": 8, + }, ) diff --git a/src/pwncore/container.py b/src/pwncore/container.py index 15b5c2a..5cf5045 100644 --- a/src/pwncore/container.py +++ b/src/pwncore/container.py @@ -1,11 +1,3 @@ import aiodocker -import atexit -import asyncio docker_client = aiodocker.Docker() - - -async def docker_cleanup(): - await docker_client.close() - -atexit.register(lambda: asyncio.run(docker_cleanup())) diff --git a/src/pwncore/db.py b/src/pwncore/db.py deleted file mode 100644 index 742cc31..0000000 --- a/src/pwncore/db.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from tortoise import fields -from tortoise.fields import JSONField -from tortoise.models import Model - -# Tortoise is initiated from __main__.py since initiating -# it as a submodule creates multiple event loops. - -# No relationships yet -class Container(Model): - id = fields.TextField(pk=True) - name = fields.TextField() - ctf_id = fields.IntField() - ports: fields.ReverseRelation[Ports] - team_id = fields.IntField() - flag = fields.TextField() - -class CTF(Model): - name = fields.TextField() - # docker_config = fields.JSONField() - image_name = fields.TextField() - image_config = fields.JSONField() - -class Ports(Model): - container: fields.ForeignKeyRelation[Container] = fields.ForeignKeyField( - "models.Container", related_name="ports", on_delete=fields.OnDelete.CASCADE - ) - port = fields.IntField(pk=True) diff --git a/src/pwncore/routes/ctf/__init__.py b/src/pwncore/routes/ctf/__init__.py index 4883f9e..db0e506 100644 --- a/src/pwncore/routes/ctf/__init__.py +++ b/src/pwncore/routes/ctf/__init__.py @@ -1,7 +1,5 @@ -from __future__ import annotations - from fastapi import APIRouter -from pwncore.routes.ctf import start +from pwncore.routes.ctf.start import router as start_router # Metadata at the top for instant accessibility metadata = { @@ -11,7 +9,7 @@ } router = APIRouter(prefix="/ctf", tags=["ctf"]) -router.include_router(start.router) +router.include_router(start_router) # Routes that do not need a separate submodule for themselves diff --git a/src/pwncore/routes/ctf/start.py b/src/pwncore/routes/ctf/start.py index 8602ab8..eaa012e 100644 --- a/src/pwncore/routes/ctf/start.py +++ b/src/pwncore/routes/ctf/start.py @@ -3,17 +3,17 @@ from fastapi import APIRouter, Response import uuid from tortoise.transactions import atomic -import logging -from pwncore.db import Container, CTF, Ports +# from pwncore.db import Problem, Container, Ports +from pwncore.models import Problem, Container, Ports, Team from pwncore.container import docker_client from pwncore.config import config # temporary helper functions -def get_team_id(): - return 34 -def get_empty_ports(): - return [4444] +if config.development: + + def get_team_id(): + return 1 router = APIRouter(tags=["ctf"]) @@ -22,7 +22,6 @@ def get_empty_ports(): @atomic() @router.post("/start/{ctf_id}") async def start_docker_container(ctf_id: int, response: Response): - """ image_config contains the raw POST data that gets sent to the Docker Remote API. For now it just contains the guest ports that need to be opened on the host. @@ -33,37 +32,45 @@ async def start_docker_container(ctf_id: int, response: Response): } } """ - # Testing purposes - # await CTF.create(**{ - # "name": "Invisible-Incursion", - # "image_name": "key:latest", - # "image_config": { - # "PortBindings": { - # "22/tcp": [{}] - # } - # } - # }) - - ctf = await CTF.get_or_none(id=ctf_id) + if config.development: + await Problem.create( + **{ + "name": "Invisible-Incursion", + "description": "Chod de tujhe se na ho paye", + "author": "Meetesh Saini", + "points": 300, + "image_name": "key:latest", + "image_config": {"PortBindings": {"22/tcp": [{}]}}, + } + ) + await Team.create( + **{ + "name": "CID Squad" + uuid.uuid4().hex, + "secret_hash": "veryverysecret", + } + ) + + ctf = await Problem.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 return {"msg_code": config.msg_codes["ctf_not_found"]} team_id = get_team_id() # From JWT - team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + team_container = await Container.get_or_none(team=team_id, problem=ctf_id) if team_container: - db_ports = await team_container.ports.all().values('port') # Get ports from DB - ports = [ db_port['port'] for db_port in db_ports ] # Create a list out of it + db_ports = await team_container.ports.all().values("port") # Get ports from DB + ports = [db_port["port"] for db_port in db_ports] # Create a list out of it return { "msg_code": config.msg_codes["container_already_running"], "ports": ports, - "ctf_id": team_container.ctf_id + "ctf_id": team_container.problem_id, } - if await Container.filter(team_id=team_id).count() >= config.max_containers_per_team: - return { - "msg_code": config.msg_codes["container_limit_reached"] - } + if ( + await Container.filter(team_id=team_id).count() + >= config.max_containers_per_team + ): + return {"msg_code": config.msg_codes["container_limit_reached"]} # Start a new container container_name = f"{team_id}_{ctf_id}_{uuid.uuid4().hex}" @@ -79,54 +86,52 @@ async def start_docker_container(ctf_id: int, response: Response): "AttachStderr": False, "Tty": False, "OpenStdin": False, - **ctf.image_config - } + **ctf.image_config, + }, ) - await ( - await container.exec(["/bin/bash", "/root/gen_flag", container_flag]) - ).start(detach=True) + await (await container.exec(["/bin/bash", "/root/gen_flag", container_flag])).start( + detach=True + ) try: - db_container = await Container.create(**{ - "id" : container.id, - "name" : container_name, - "team_id" : team_id, - "ctf_id" : ctf_id, - "flag" : container_flag - }) + db_container = await Container.create( + **{ + "docker_id": container.id, + "team_id": team_id, + "problem_id": ctf_id, + "flag": container_flag, + } + ) # Get ports and save them ports = [] # List to return back to frontend - for guest_port in ctf.image_config['PortBindings']: + for guest_port in ctf.image_config["PortBindings"]: # Docker assigns the port to the IPv4 and IPv6 addresses # Since we only require IPv4, we select the zeroth item # from the returned list. port = int((await container.port(guest_port))[0]["HostPort"]) ports.append(port) - host_port = await Ports.create(port=port, container=db_container) + await Ports.create(port=port, container=db_container) - except Exception: + except Exception as e: # Stop the container if failed to make a DB record await container.stop() await container.delete() response.status_code = 500 - return { - "msg_code": config.msg_codes["db_error"] - } + return {"msg_code": config.msg_codes["db_error"]} return { "msg_code": config.msg_codes["container_start"], "ports": ports, - "ctf_id": ctf_id + "ctf_id": ctf_id, } @atomic() @router.post("/stopall") async def stopall_docker_container(response: Response): - team_id = get_team_id() # From JWT containers = await Container.filter(team_id=team_id).values() @@ -137,12 +142,10 @@ async def stopall_docker_container(response: Response): await Container.filter(team_id=team_id).delete() except Exception: response.status_code = 500 - return { - "msg_code": config.msg_codes["db_error"] - } + return {"msg_code": config.msg_codes["db_error"]} for db_container in containers: - container = await docker_client.containers.get(db_container.id) + container = await docker_client.containers.get(db_container["docker_id"]) await container.stop() await container.delete() @@ -152,28 +155,25 @@ async def stopall_docker_container(response: Response): @atomic() @router.post("/stop/{ctf_id}") async def stop_docker_container(ctf_id: int, response: Response): - - ctf = await CTF.get_or_none(id=ctf_id) + ctf = await Problem.get_or_none(id=ctf_id) if not ctf: response.status_code = 404 return {"msg_code": config.msg_codes["ctf_not_found"]} team_id = get_team_id() - team_container = await Container.get_or_none(team_id=team_id, ctf_id=ctf_id) + team_container = await Container.get_or_none(team_id=team_id, problem_id=ctf_id) if not team_container: return {"msg_code": config.msg_codes["container_not_found"]} # We first try to delete the record from the DB # Then we stop the container try: - await Container.filter(team_id=team_id, ctf_id=ctf_id).delete() + await Container.filter(team_id=team_id, problem_id=ctf_id).delete() except Exception: response.status_code = 500 - return { - "msg_code": config.msg_codes["db_error"] - } + return {"msg_code": config.msg_codes["db_error"]} - container = await docker_client.containers.get(team_container.id) + container = await docker_client.containers.get(team_container.docker_id) await container.stop() await container.delete()