diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .python-version | 1 | ||||
-rw-r--r-- | LICENSE.txt | 2 | ||||
-rw-r--r-- | docs/Pipfile | 10 | ||||
-rw-r--r-- | docs/Pipfile.lock | 166 | ||||
-rw-r--r-- | docs/api/fiction.rst | 2 | ||||
-rw-r--r-- | docs/config/pyfingerd.service | 12 | ||||
-rwxr-xr-x | pyfingerd/__init__.py | 2 | ||||
-rwxr-xr-x | pyfingerd/__main__.py | 2 | ||||
-rw-r--r-- | pyfingerd/binds.py | 233 | ||||
-rwxr-xr-x | pyfingerd/cli.py | 4 | ||||
-rwxr-xr-x | pyfingerd/core.py | 250 | ||||
-rwxr-xr-x | pyfingerd/errors.py | 2 | ||||
-rwxr-xr-x | pyfingerd/fiction.py | 7 | ||||
-rwxr-xr-x | pyfingerd/native.py | 2 | ||||
-rwxr-xr-x | pyfingerd/posix.py | 5 | ||||
-rw-r--r-- | pyfingerd/utils.py | 2 | ||||
-rwxr-xr-x | pyfingerd/version.py | 4 | ||||
-rw-r--r-- | setup.cfg | 4 | ||||
-rw-r--r-- | tests/test_binds.py | 44 | ||||
-rw-r--r-- | tests/test_server.py | 78 |
21 files changed, 492 insertions, 341 deletions
@@ -5,6 +5,7 @@ __pycache__ *.pyc *.spec +/.python-version /.env /actions*.* diff --git a/.python-version b/.python-version deleted file mode 100644 index 11aaa06..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.9.5 diff --git a/LICENSE.txt b/LICENSE.txt index f730817..65feab7 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,7 +1,7 @@ The MIT License (MIT) ===================== -Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), diff --git a/docs/Pipfile b/docs/Pipfile index efdb5a1..d8bf3cb 100644 --- a/docs/Pipfile +++ b/docs/Pipfile @@ -6,10 +6,18 @@ verify_ssl = true [dev-packages] [packages] -sphinx = "*" sphinx-rtd-theme = "*" sphinx-autobuild = "*" sphinx-autodoc-typehints = "*" +sphinx-autodoc-annotation = "*" +click = "*" +coloredlogs = "*" +croniter = "*" +python-dateutil = "*" +pytz = "*" +pyutmpx = "*" +toml = "*" +Sphinx = "*" [requires] python_version = "3.9" diff --git a/docs/Pipfile.lock b/docs/Pipfile.lock index 6a33722..7658342 100644 --- a/docs/Pipfile.lock +++ b/docs/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9e53fd6e9c8d06e445ecf8cdf0d568ef01a8e987bb88ea841878ddf582def9d1" + "sha256": "b3940e1cfb50d6595efe5dfd8d5e5f975f9c18b5a8dcd277c0cfcb8b54c4471e" }, "pipfile-spec": 6, "requires": { @@ -33,18 +33,26 @@ }, "certifi": { "hashes": [ - "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", - "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" ], - "version": "==2021.5.30" + "version": "==2021.10.8" }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" ], "markers": "python_version >= '3'", - "version": "==2.0.4" + "version": "==2.0.9" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "index": "pypi", + "version": "==8.0.3" }, "colorama": { "hashes": [ @@ -54,37 +62,61 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, + "coloredlogs": { + "hashes": [ + "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", + "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0" + ], + "index": "pypi", + "version": "==15.0.1" + }, + "croniter": { + "hashes": [ + "sha256:4023e4d18ced979332369964351e8f4f608c1f7c763e146b1d740002c4245247", + "sha256:d30dd147d1daec39d015a15b8cceb3069b9780291b9c141e869c32574a8eeacb" + ], + "index": "pypi", + "version": "==1.1.0" + }, "docutils": { "hashes": [ - "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af", - "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc" + "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125", + "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.17.1" + }, + "humanfriendly": { + "hashes": [ + "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", + "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==0.16" + "version": "==10.0" }, "idna": { "hashes": [ - "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", - "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" ], "markers": "python_version >= '3'", - "version": "==3.2" + "version": "==3.3" }, "imagesize": { "hashes": [ - "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1", - "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1" + "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c", + "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.0" + "version": "==1.3.0" }, "jinja2": { "hashes": [ - "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", - "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", + "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" ], "markers": "python_version >= '3.6'", - "version": "==3.0.1" + "version": "==3.0.3" }, "livereload": { "hashes": [ @@ -97,6 +129,7 @@ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", @@ -104,6 +137,7 @@ "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", @@ -111,27 +145,36 @@ "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", + "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", + "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", @@ -139,10 +182,14 @@ "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", + "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", @@ -154,34 +201,50 @@ }, "packaging": { "hashes": [ - "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", - "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" ], "markers": "python_version >= '3.6'", - "version": "==21.0" + "version": "==21.3" }, "pygments": { "hashes": [ - "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", - "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + "sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4", + "sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c" ], "markers": "python_version >= '3.5'", - "version": "==2.10.0" + "version": "==2.11.1" }, "pyparsing": { "hashes": [ - "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", - "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.6" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.7" + "index": "pypi", + "version": "==2.8.2" }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", + "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" ], - "version": "==2021.1" + "index": "pypi", + "version": "==2021.3" + }, + "pyutmpx": { + "hashes": [ + "sha256:95f9631bfaf464b38a5dc5452d951d604c566ab64b3a437b5d65cefecdc90bcc" + ], + "index": "pypi", + "version": "==0.4.1" }, "requests": { "hashes": [ @@ -201,18 +264,18 @@ }, "snowballstemmer": { "hashes": [ - "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2", - "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914" + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" ], - "version": "==2.1.0" + "version": "==2.2.0" }, "sphinx": { "hashes": [ - "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", - "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" + "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c", + "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851" ], "index": "pypi", - "version": "==4.1.2" + "version": "==4.3.2" }, "sphinx-autobuild": { "hashes": [ @@ -222,6 +285,13 @@ "index": "pypi", "version": "==2021.3.14" }, + "sphinx-autodoc-annotation": { + "hashes": [ + "sha256:4a3d03081efe1e5f2bc9b9d00746550f45b9f543b0c79519c523168ca7f7d89a" + ], + "index": "pypi", + "version": "==1.0.post1" + }, "sphinx-autodoc-typehints": { "hashes": [ "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52", @@ -232,11 +302,11 @@ }, "sphinx-rtd-theme": { "hashes": [ - "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", - "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f" + "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8", + "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c" ], "index": "pypi", - "version": "==0.5.2" + "version": "==1.0.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -286,6 +356,14 @@ "markers": "python_version >= '3.5'", "version": "==1.1.5" }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "index": "pypi", + "version": "==0.10.2" + }, "tornado": { "hashes": [ "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", @@ -335,11 +413,11 @@ }, "urllib3": { "hashes": [ - "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", - "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.6" + "version": "==1.26.7" } }, "develop": {} diff --git a/docs/api/fiction.rst b/docs/api/fiction.rst index cefd359..6d6386a 100644 --- a/docs/api/fiction.rst +++ b/docs/api/fiction.rst @@ -46,7 +46,7 @@ Playing fictions ---------------- .. autoclass:: FingerFictionInterface - :members: reset, apply, update + :members: reset, apply Playing scenarios ----------------- diff --git a/docs/config/pyfingerd.service b/docs/config/pyfingerd.service deleted file mode 100644 index 035a49c..0000000 --- a/docs/config/pyfingerd.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=pyfingerd -After=network.target - -[Service] -Type=simple -ExecStart=/usr/bin/pyfingerd -ExecStop=/bin/kill -s TERM $MAINPID -TimeoutSec=15 - -[Install] -WantedBy=multi-user.target diff --git a/pyfingerd/__init__.py b/pyfingerd/__init__.py index aad1d8a..b6dcfec 100755 --- a/pyfingerd/__init__.py +++ b/pyfingerd/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ Pure Python finger protocol implementation. diff --git a/pyfingerd/__main__.py b/pyfingerd/__main__.py index d8e58e2..a9f8150 100755 --- a/pyfingerd/__main__.py +++ b/pyfingerd/__main__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ Main script of the module. """ diff --git a/pyfingerd/binds.py b/pyfingerd/binds.py new file mode 100644 index 0000000..4d22c93 --- /dev/null +++ b/pyfingerd/binds.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# ***************************************************************************** +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> +# This file is part of the pyfingerd project, which is MIT-licensed. +# ***************************************************************************** +""" Binds decoder for the finger server. """ + +import socket as _socket + +from typing import Sequence as _Sequence + +from .errors import InvalidBindError as _InvalidBindError + +__all__ = [ + 'FingerBind', 'FingerBindsDecoder', + 'FingerTCPv4Bind', 'FingerTCPv6Bind', +] + + +class FingerBind: + """ Bind address for pyfingerd. """ + + @property + def runserver_params(self): + """ Return the data as ``_runserver`` arguments. """ + + raise NotImplementedError + + +class FingerTCPv4Bind(FingerBind): + """ IPv4 TCP Address. """ + + def __init__(self, address, port): + try: + self._addr = _socket.inet_pton(_socket.AF_INET, address) + except Exception: + self._addr = address + + self._port = port + + @property + def runserver_params(self): + """ Return the data as `_runserver` parameters. """ + + return ( + _socket.AF_INET, + _socket.inet_ntop(_socket.AF_INET, self._addr), + self._port, + ) + + +class FingerTCPv6Bind(FingerBind): + """ IPv6 TCP Address. """ + + def __init__(self, address, port): + try: + self._addr = _socket.inet_pton(_socket.AF_INET6, address) + except Exception: + self._addr = address + + self._port = port + + @property + def runserver_params(self): + """ Return the data as `_runserver` parameters. """ + + return ( + _socket.AF_INET6, + _socket.inet_ntop(_socket.AF_INET6, self._addr), + self._port, + ) + + +class FingerBindsDecoder: + """ Binds decoder for pyfingerd. """ + + def __init__(self, proto: str = 'finger'): + proto = proto.casefold() + if proto not in ('finger',): + raise ValueError(f'unsupported protocol {proto!r}') + + self._proto = proto + + def decode(self, raw: str) -> _Sequence[FingerBind]: + """ Get binds for the server, using a given string. """ + + binds = set() + + for addr in map(lambda x: x.strip(), raw.split(',')): + if not addr: + continue + + # Try to find a scheme. + + scheme, *rest = addr.split(':/') + if not rest: + # No scheme found, let's just guess the scheme based on + # the situation. + + raw = scheme + scheme = {'finger': 'tcp'}[self._proto] + else: + # just don't add the ':' of ':/' again + raw = '/' + ':/'.join(rest) + + if ( + (self._proto == 'finger' and scheme != 'tcp') + or scheme not in ('tcp',) + ): + raise _InvalidBindError( + addr, + f'Unsupported scheme {scheme!r} for ' + f'protocol {self._proto!r}', + ) + + # Decode the address data. + + if scheme == 'tcp': + binds.update(self._decode_tcp_host(raw)) + + return tuple(binds) + + def __repr__(self): + return f'{self._class__.__name__}()' + + def _decode_tcp_host(self, x): + """ Decode suitable hosts for a TCP bind. """ + + addrs = () + addr = x + + # TODO: manage the '*' case. + # TODO: decode hosts without the default host. + + # Get the host part first, we'll decode it later. + + if x[0] == '[': + # The host part is an IPv6, look for the closing ']' and + # decode it later. + + to = x.find(']') + if to < 0: + raise _InvalidBindError( + addr, "Expected closing ']'") + + host = x[1:to] + x = x[to + 1:] + + is_ipv6 = True + else: + # The host part is either an IPv4 or a host name, look for + # the ':' and decode it later. + + host, *x = x.split(':') + x = ':' + ':'.join(x) + + is_ipv6 = False + + # Decode the port part. + + if x in ('', ':'): + port = 79 + elif x[0] == ':': + try: + port = int(x[1:]) + except ValueError: + try: + if x[1:] != '': + raise AssertionError('Expected a port number') + port = _socket.getservbyname(x[1:]) + except Exception: + raise _InvalidBindError( + addr, + 'Expected a valid port number or name ' + f'(got {x[1:]!r})', + ) from None + else: + raise _InvalidBindError( + addr, 'Garbage found after the host', + ) + + # Decode the host part and get the addresses. + + addrs = () + if is_ipv6: + # Decode the IPv6 address (validate it using `_socket.inet_pton`). + + ip6 = host + _socket.inet_pton(_socket.AF_INET6, host) + addrs += (FingerTCPv6Bind(ip6, port),) + else: + # Decode the host (try IPv4, otherwise, resolve domain). + + try: + ip = host.split('.') + if len(ip) < 2 or len(ip) > 4: + raise AssertionError('2 <= len(ip) <= 4') + + ip = list(map(int, ip)) + if not all(lambda x: 0 <= x < 256, ip): + raise AssertionError('non-8-bit component') + + if len(ip) == 2: + ip = [ip[0], 0, 0, ip[1]] + elif len(ip) == 3: + ip = [ip[0], 0, ip[1], ip[2]] + + addrs += (FingerTCPv4Bind(ip, port),) + except Exception: + entries = _socket.getaddrinfo( + host, port, + proto=_socket.IPPROTO_TCP, + type=_socket.SOCK_STREAM) + + for ent in entries: + if ( + ent[0] not in (_socket.AF_INET, _socket.AF_INET6) + or ent[1] not in (_socket.SOCK_STREAM,) + ): + continue + + if ent[0] == _socket.AF_INET: + ip = ent[4][0] + _socket.inet_pton(_socket.AF_INET, ent[4][0]) + addrs += (FingerTCPv4Bind(ip, port),) + else: + ip6 = ent[4][0] + _socket.inet_pton(_socket.AF_INET6, ent[4][0]) + addrs += (FingerTCPv6Bind(ip6, port),) + + return addrs + +# End of file. diff --git a/pyfingerd/cli.py b/pyfingerd/cli.py index e184c0c..409b376 100755 --- a/pyfingerd/cli.py +++ b/pyfingerd/cli.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2021-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ pyfingerd CLI interface. """ @@ -69,7 +69,7 @@ __all__ = ['cli'] @_click.option( '-S', '--start', 'scenario_start', type=_click.DateTime(), - default=_datetime.now(), + default=_datetime.now().strftime('%Y-%m-%dT%H:%M:%S'), envvar=('FINGER_START',), help=( 'Date and time at which the scenario starts or has started ' diff --git a/pyfingerd/core.py b/pyfingerd/core.py index b9e8b57..8a3d65b 100755 --- a/pyfingerd/core.py +++ b/pyfingerd/core.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ Main classes for the finger server, interfaces and formatters. @@ -13,21 +13,18 @@ import asyncio as _asyncio import copy as _copy import multiprocessing as _multip import signal as _signal -import socket as _socket import string as _string -from collections.abc import Sequence as _Sequence from datetime import datetime as _dt, timedelta as _td, tzinfo as _tzinfo -from enum import Enum as _Enum from errno import errorcode as _errorcode -from typing import Optional as _Optional +from typing import Optional as _Optional, Sequence as _Sequence from croniter import croniter as _croniter from pytz import utc as _utc +from .binds import FingerBindsDecoder as _FingerBindsDecoder from .errors import ( HostnameError as _HostnameError, - InvalidBindError as _InvalidBindError, MalformedQueryError as _MalformedQueryError, NoBindsError as _NoBindsError, ) @@ -757,243 +754,6 @@ class FingerInterface: # --- -# Bind-related configuration. -# --- - - -class _BindAddress: - """ Bind address for pyfingerd. """ - - class Type(_Enum): - """ Bind address type. """ - - """ TCP on IPv4 bind. """ - TCP_IPv4 = 1 - - """ TCP on IPv6 bind. """ - TCP_IPv6 = 2 - - def __init__(self, family): - self._family = self.Type(family) - - def __repr__(self): - return f'{self._class__.__name__}(family={self._family!r})' - - @property - def family(self): - """ Family as one of the `BindAddress.Type` enumeration values. """ - - return self._family - - -class _TCP4Address(_BindAddress): - """ IPv4 TCP Address. """ - - def __init__(self, address, port): - super().__init__(_BindAddress.Type.TCP_IPv4) - - try: - self._addr = _socket.inet_pton(_socket.AF_INET, address) - except Exception: - self._addr = address - - self._port = port - - @property - def runserver_params(self): - """ Return the data as `_runserver` parameters. """ - - return ( - _socket.AF_INET, - _socket.inet_ntop(_socket.AF_INET, self._addr), - self._port) - - -class _TCP6Address(_BindAddress): - """ IPv6 TCP Address. """ - - def __init__(self, address, port): - super().__init__(_BindAddress.Type.TCP_IPv6) - - try: - self._addr = _socket.inet_pton(_socket.AF_INET6, address) - except Exception: - self._addr = address - - self._port = port - - @property - def runserver_params(self): - """ Return the data as `_runserver` parameters. """ - - return ( - _socket.AF_INET6, - _socket.inet_ntop(_socket.AF_INET6, self._addr), - self._port, - ) - - -class _BindsDecoder: - """ Binds decoder for pyfingerd. - - Takes a raw string and the protocol name, either 'finger' (the base - protocol managed by the class) or 'pyfingerd-control' (the protocol - used for controlling the live pyfingerd interface). - """ - - def __init__(self, raw, proto='finger'): - proto = proto.casefold() - if proto not in ('finger',): - raise ValueError(f'unsupported protocol {proto}') - - self._binds = set() - - for x in map(lambda x: x.strip(), raw.split(',')): - addr = x - - # Try to find a scheme. - - scheme, *rest = x.split(':/') - if not rest: - # No scheme found, let's just guess the scheme based on - # the situation. - - x = scheme - scheme = {'finger': 'tcp'}[proto] - else: - # just don't add the ':' of ':/' again - x = '/' + ':/'.join(rest) - - if ( - (proto == 'finger' and scheme != 'tcp') - or scheme not in ('tcp',) - ): - raise _InvalidBindError( - addr, - f'Unsupported scheme {scheme!r} for protocol {proto!r}', - ) - - # Decode the address data. - - if scheme == 'tcp': - self._binds.update(self._decode_tcp_host(x)) - - self._binds = tuple(self._binds) - - def __iter__(self): - return iter(self._binds) - - def __repr__(self): - return f'{self._class__.__name__}(binds={self._binds})' - - def _decode_tcp_host(self, x): - """ Decode suitable hosts for a TCP bind. """ - - addrs = () - addr = x - - # TODO: manage the '*' case. - - # Get the host part first, we'll decode it later. - - if x[0] == '[': - # The host part is an IPv6, look for the closing ']' and - # decode it later. - - to = x.find(']') - if to < 0: - raise _InvalidBindError( - addr, "Expected closing ']'") - - host = x[1:to] - x = x[to + 1:] - - is_ipv6 = True - else: - # The host part is either an IPv4 or a host name, look for - # the ':' and decode it later. - - host, *x = x.split(':') - x = ':' + ':'.join(x) - - is_ipv6 = False - - # Decode the port part. - - if x == '': - port = 79 - elif x[0] == ':': - try: - port = int(x[1:]) - except ValueError: - try: - if x[1:] != '': - raise AssertionError('Expected a port number') - port = _socket.getservbyname(x[1:]) - except Exception: - raise _InvalidBindError( - addr, - 'Expected a valid port number or name ' - f'(got {x[1:]!r})', - ) from None - else: - raise _InvalidBindError( - addr, 'Garbage found after the host', - ) - - # Decode the host part and get the addresses. - - addrs = () - if is_ipv6: - # Decode the IPv6 address (validate it using `_socket.inet_pton`). - - ip6 = host - _socket.inet_pton(_socket.AF_INET6, host) - addrs += (_TCP6Address(ip6, port),) - else: - # Decode the host (try IPv4, otherwise, resolve domain). - - try: - ip = host.split('.') - if len(ip) < 2 or len(ip) > 4: - raise AssertionError('2 <= len(ip) <= 4') - - ip = list(map(int, ip)) - if not all(lambda x: 0 <= x < 256, ip): - raise AssertionError('non-8-bit component') - - if len(ip) == 2: - ip = [ip[0], 0, 0, ip[1]] - elif len(ip) == 3: - ip = [ip[0], 0, ip[1], ip[2]] - - addrs += (_TCP4Address(ip, port),) - except Exception: - entries = _socket.getaddrinfo( - host, port, - proto=_socket.IPPROTO_TCP, - type=_socket.SOCK_STREAM) - - for ent in entries: - if ( - ent[0] not in (_socket.AF_INET, _socket.AF_INET6) - or ent[1] not in (_socket.SOCK_STREAM,) - ): - continue - - if ent[0] == _socket.AF_INET: - ip = ent[4][0] - _socket.inet_pton(_socket.AF_INET, ent[4][0]) - addrs += (_TCP4Address(ip, port),) - else: - ip6 = ent[4][0] - _socket.inet_pton(_socket.AF_INET6, ent[4][0]) - addrs += (_TCP6Address(ip6, port),) - - return addrs - - -# --- # Finger/TCP server implementation. # --- @@ -1117,7 +877,7 @@ class FingerServer: # Check the binds. - self._binds = [b for b in _BindsDecoder(binds)] + self._binds = [b for b in _FingerBindsDecoder().decode(binds)] if not self._binds: raise _NoBindsError() @@ -1290,7 +1050,7 @@ class FingerServer: if seconds >= 0: break - await _asyncio.sleep(seconds) # TODO + await _asyncio.sleep(seconds) async def start_servers(): """ Start the servers. """ diff --git a/pyfingerd/errors.py b/pyfingerd/errors.py index f5ae0f6..6810df5 100755 --- a/pyfingerd/errors.py +++ b/pyfingerd/errors.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ This file defines the exceptions used throughout the module. """ diff --git a/pyfingerd/fiction.py b/pyfingerd/fiction.py index 7130845..1dc61b8 100755 --- a/pyfingerd/fiction.py +++ b/pyfingerd/fiction.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ Definitions for the finger server fiction interface. @@ -14,10 +14,11 @@ import math as _math import os.path as _path from collections import defaultdict as _defaultdict -from collections.abc import Sequence as _Sequence from datetime import datetime as _dt, timedelta as _td from enum import Enum as _Enum -from typing import Optional as _Optional, Union as _Union +from typing import ( + Optional as _Optional, Sequence as _Sequence, Union as _Union, +) from .core import ( FingerInterface as _FingerInterface, diff --git a/pyfingerd/native.py b/pyfingerd/native.py index b1be94c..8fbdd34 100755 --- a/pyfingerd/native.py +++ b/pyfingerd/native.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ Defining the native interface. """ diff --git a/pyfingerd/posix.py b/pyfingerd/posix.py index 3ca1849..265fa06 100755 --- a/pyfingerd/posix.py +++ b/pyfingerd/posix.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2017-2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# Copyright (C) 2017-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd Python 3.x module, which is MIT-licensed. # ***************************************************************************** """ Make use of the utmp/x file to read the user data. @@ -10,13 +10,12 @@ import pwd as _pwd -from collections.abc import Sequence as _Sequence from copy import copy as _copy from datetime import datetime as _dt from multiprocessing import Lock as _Lock from os import stat as _stat from os.path import exists as _exists, join as _joinpaths -from typing import Optional as _Optional +from typing import Optional as _Optional, Sequence as _Sequence from pytz import utc as _utc import pyutmpx as _pyutmpx diff --git a/pyfingerd/utils.py b/pyfingerd/utils.py index c96412a..99d8546 100644 --- a/pyfingerd/utils.py +++ b/pyfingerd/utils.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2021 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr> +# Copyright (C) 2021-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd Python 3.x module, which is MIT-licensed. # ***************************************************************************** """ Utilities for the pyfingerd module. """ diff --git a/pyfingerd/version.py b/pyfingerd/version.py index 46cf96c..23685b0 100755 --- a/pyfingerd/version.py +++ b/pyfingerd/version.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # ***************************************************************************** -# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr> +# Copyright (C) 2021-2022 Thomas Touhey <thomas@touhey.fr> # This file is part of the pyfingerd project, which is MIT-licensed. # ***************************************************************************** """ pyfingerd version definition. """ -version = '0.4.1' +version = '0.4.2' # End of file. @@ -20,7 +20,7 @@ classifiers = [options] zip_safe = False include_package_data = True -packages = find: +packages = pyfingerd scripts = scripts/pyfingerd @@ -31,7 +31,7 @@ scripts = universal = True [flake8] -ignore = D105,D107,D202,D208,D210,D401,W503 +ignore = D105,D107,D202,D208,D210,D401,F405,W503 per-file-ignores = tests/*:S101,D102 rst-roles = diff --git a/tests/test_binds.py b/tests/test_binds.py new file mode 100644 index 0000000..e1fac43 --- /dev/null +++ b/tests/test_binds.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# ***************************************************************************** +# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr> +# This file is part of the pyfingerd project, which is MIT-licensed. +# ***************************************************************************** +""" Tests for the pyfingerd server. """ + +import socket + +from pyfingerd.binds import FingerBindsDecoder, FingerTCPv4Bind +import pytest + + +class TestFingerDecoder: + """ Test binds. """ + + @pytest.fixture + def decoder(self): + return FingerBindsDecoder(proto='finger') + + def test_no_binds(self, decoder): + assert decoder.decode('') == () + + @pytest.mark.parametrize('raw,cls,params', ( + ('127.0.0.1:79', FingerTCPv4Bind, ( + socket.AF_INET, + '127.0.0.1', + 79, + )), + ('127.0.2.3', FingerTCPv4Bind, ( + socket.AF_INET, + '127.0.2.3', + 79, + )), + )) + def test_binds(self, decoder, raw, cls, params): + binds = decoder.decode(raw) + assert len(binds) == 1 + + bind = binds[0] + assert isinstance(bind, cls) + assert bind.runserver_params == params + +# End of file. diff --git a/tests/test_server.py b/tests/test_server.py index 180c643..de944f8 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -10,11 +10,8 @@ import socket from datetime import timedelta from time import sleep -from pyfingerd.core import FingerServer -from pyfingerd.fiction import ( - FingerScenario, FingerScenarioInterface, - FingerUserCreationAction, FingerUserLoginAction, FingerUserLogoutAction, -) +from pyfingerd.core import * # NOQA +from pyfingerd.fiction import * # NOQA import pytest @@ -22,7 +19,7 @@ import pytest class TestFingerConnection: """ Test basic finger connections. """ - @pytest.fixture + @pytest.fixture(autouse=True) def fingerserver(self): """ Start a finger server. @@ -54,10 +51,44 @@ class TestFingerConnection: ), timedelta(seconds=1)) + class TestFormatter(FingerFormatter): + """ Test formatter, uncomplicated to test. """ + + def _format_users(self, users): + result = f'{len(users)}\n' + for user in users: + result += ( + f'{user.login}|{user.name}|{user.home}|' + f'{user.shell}|{user.office or ""}|' + f'{len(user.sessions)}\n' + ) + for session in user.sessions: + result += ( + f'{session.line or ""}|{session.host or ""}\n' + ) + + return result + + def format_query_error(self, hostname, raw_query): + return f'{hostname}\n{raw_query}\nerror\n' + + def format_short(self, hostname, raw_query, users): + return ( + f'{hostname}\n{raw_query}\nshort\n' + + self._format_users(users) + ) + + def format_long(self, hostname, raw_query, users): + return ( + f'{hostname}\n{raw_query}\nlong\n' + + self._format_users(users) + ) + server = FingerServer( 'localhost:3099', hostname='example.org', interface=FingerScenarioInterface(scenario), + formatter=TestFormatter(), ) server.start() @@ -67,31 +98,40 @@ class TestFingerConnection: server.stop() def _send_command(self, command): - conn = socket.create_connection(('localhost', 3099)) - conn.send(command) - return conn.recv(1024) + conn = socket.create_connection(('localhost', 3099), 1) + conn.send(command.encode('ascii') + b'\r\n') + + result = tuple( + conn.recv(1024).decode('ascii') + .rstrip('\r\n').split('\r\n') + ) + + assert result[:2] == ( + 'EXAMPLE.ORG', + command, + ) + return result[2:] # --- # Tests. # --- - def test_no_user_list(self, fingerserver): + def test_no_user_list(self): """ Test if an unknown user returns an empty result. """ - result = self._send_command(b'user\r\n') - assert result == b'No user list available.\r\n' + assert self._send_command('user') == ('long', '0') - def test_existing_user_list(self, fingerserver): + def test_existing_user_list(self): """ Test the user list before and after the cron is executed. """ - result = self._send_command(b'\r\n') - - assert result != b'' - assert result != b'No user list available.\r\n' + assert self._send_command('') == ( + 'short', '1', + 'john|John Doe|/home/john|/bin/bash|84.6|1', + 'tty1|', + ) sleep(2) - result = self._send_command(b'\r\n') - assert result == b'No user list available.\r\n' + assert self._send_command('') == ('short', '0') # End of file. |