aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Touhey <thomas@touhey.fr>2021-09-04 13:35:47 +0200
committerThomas Touhey <thomas@touhey.fr>2021-09-04 13:35:47 +0200
commit5ee802842794f112977b7b9273e35f45f2bf8a88 (patch)
tree5fa3948e850706f7a56a8a94f5290bad19b4cd00
parent2ee7db103f5e391e1131c6e5bbb6d836f024120d (diff)
Reorganized the Python module a tad, gonna rework the docs as well
-rw-r--r--.python-version2
-rwxr-xr-xMakefile14
-rw-r--r--Pipfile6
-rw-r--r--Pipfile.lock244
-rw-r--r--docs/.python-version1
-rw-r--r--docs/Makefile2
-rw-r--r--docs/Pipfile3
-rw-r--r--docs/Pipfile.lock291
-rw-r--r--docs/api/config.rst (renamed from docs/configure.rst)0
-rw-r--r--docs/conf.py2
-rw-r--r--docs/explain.rst6
-rw-r--r--docs/howto.rst7
-rw-r--r--docs/index.rst6
-rw-r--r--docs/onboarding.rst (renamed from docs/run.rst)8
-rwxr-xr-xfingerd/__init__.py82
-rwxr-xr-xfingerd/__main__.py6
-rwxr-xr-xfingerd/_binds.py238
-rwxr-xr-xfingerd/_exceptions.py26
-rwxr-xr-xfingerd/_server.py342
-rwxr-xr-xfingerd/_util.py509
-rw-r--r--fingerd/cli.py63
-rwxr-xr-xfingerd/errors.py52
-rwxr-xr-xfingerd/fiction.py (renamed from fingerd/_fiction.py)212
-rwxr-xr-xfingerd/server.py1064
-rw-r--r--fingerd/version.py10
-rw-r--r--sample-scenarios/actions.toml72
-rw-r--r--setup.cfg3
27 files changed, 1628 insertions, 1643 deletions
diff --git a/.python-version b/.python-version
index a76ccff..11aaa06 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.7.1
+3.9.5
diff --git a/Makefile b/Makefile
index cbf5f4c..503db43 100755
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
#!/usr/bin/make -f
PE := pipenv run
ST := $(PE) ./setup.py
- DNAME := dist/$(shell $(ST) --name)-$(shell $(ST) --version).tar.gz
+ DNAME := dist/$(shell PIPENV_DONT_LOAD_ENV=1 $(ST) --name)-$(shell PIPENV_DONT_LOAD_ENV=1 $(ST) --version).tar.gz
test tests:
$(ST) test
@@ -17,9 +17,14 @@ checkdocs:
@$(ST) checkdocs
run:
- @pipenv run python -m fingerd
+ @pipenv run python -m fingerd --scenario sample-scenarios/actions.toml
+redirect-localhost-ports:
+ iptables -t nat -A OUTPUT -p tcp -s 127.0.0.1 -d 127.0.0.1 \
+ --dport 79 -j DNAT --to 127.0.0.1:3999
+ ip6tables -t nat -A OUTPUT -p tcp -s ::1 -d ::1 \
+ --dport 79 -j DNAT --to '[::1]:3999'
-dist: $(DNAMe)
+dist: $(DNAME)
$(DNAME):
$(ST) sdist
upload: $(DNAME)
@@ -31,5 +36,6 @@ install-user:
$(ST) install --user
.PHONY: test tests prepare update docs checkdocs
-.PHONY: run dist upload install install-user
+.PHONY: run redirect-localhost-ports
+.PHONY: dist upload install install-user
# End of file.
diff --git a/Pipfile b/Pipfile
index b1475b2..37e634e 100644
--- a/Pipfile
+++ b/Pipfile
@@ -4,14 +4,12 @@ verify_ssl = true
name = 'pypi'
[requires]
-python_version = '3.7'
+python_version = '3.9'
[packages]
python-dotenv = '*'
nanomsg = '*'
toml = '*'
+click = '*'
[dev-packages]
-sphinx = '*'
-sphinx_rtd_theme = '*'
-"collective.checkdocs" = '*'
diff --git a/Pipfile.lock b/Pipfile.lock
index 41004d3..5f96c40 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
- "sha256": "9e0f4516a6616f2d7a636e6380ff0a2b074eb58e224d1474c169f794c529e155"
+ "sha256": "f5ee09d66def8c5ee2107e42585d2b58120e45fdcbf0e654c7a3a34acb9979bf"
},
"pipfile-spec": 6,
"requires": {
- "python_version": "3.7"
+ "python_version": "3.9"
},
"sources": [
{
@@ -16,6 +16,14 @@
]
},
"default": {
+ "click": {
+ "hashes": [
+ "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
+ "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
+ ],
+ "index": "pypi",
+ "version": "==8.0.1"
+ },
"nanomsg": {
"hashes": [
"sha256:843be41258219d9d319cf434a68cac7669834ab9c993ea4bab5b3d87f62a7a13"
@@ -25,238 +33,20 @@
},
"python-dotenv": {
"hashes": [
- "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093",
- "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544"
+ "sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544",
+ "sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"
],
"index": "pypi",
- "version": "==0.10.3"
+ "version": "==0.17.1"
},
"toml": {
"hashes": [
- "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
- "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
+ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+ "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"index": "pypi",
- "version": "==0.10.0"
+ "version": "==0.10.2"
}
},
- "develop": {
- "alabaster": {
- "hashes": [
- "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
- "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
- ],
- "version": "==0.7.12"
- },
- "attrs": {
- "hashes": [
- "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
- "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
- ],
- "version": "==19.1.0"
- },
- "babel": {
- "hashes": [
- "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
- "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
- ],
- "version": "==2.7.0"
- },
- "certifi": {
- "hashes": [
- "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939",
- "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695"
- ],
- "version": "==2019.6.16"
- },
- "chardet": {
- "hashes": [
- "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
- "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
- ],
- "version": "==3.0.4"
- },
- "collective.checkdocs": {
- "hashes": [
- "sha256:3a5328257c5224bc72753820c182910d7fb336bc1dba5e09113d48566655e46e"
- ],
- "index": "pypi",
- "version": "==0.2"
- },
- "docutils": {
- "hashes": [
- "sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
- "sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
- "sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
- ],
- "version": "==0.15.2"
- },
- "idna": {
- "hashes": [
- "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
- "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
- ],
- "version": "==2.8"
- },
- "imagesize": {
- "hashes": [
- "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
- "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
- ],
- "version": "==1.1.0"
- },
- "jinja2": {
- "hashes": [
- "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
- "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
- ],
- "version": "==2.10.1"
- },
- "markupsafe": {
- "hashes": [
- "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
- "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
- "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
- "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
- "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
- "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
- "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
- "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
- "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
- "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
- "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
- "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
- "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
- "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
- "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
- "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
- "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
- "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
- "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
- "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
- "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
- "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
- "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
- "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
- "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
- "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
- "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
- "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
- ],
- "version": "==1.1.1"
- },
- "packaging": {
- "hashes": [
- "sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9",
- "sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"
- ],
- "version": "==19.1"
- },
- "pygments": {
- "hashes": [
- "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
- "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
- ],
- "version": "==2.4.2"
- },
- "pyparsing": {
- "hashes": [
- "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
- "sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
- ],
- "version": "==2.4.2"
- },
- "pytz": {
- "hashes": [
- "sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
- "sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
- ],
- "version": "==2019.2"
- },
- "requests": {
- "hashes": [
- "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
- "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
- ],
- "version": "==2.22.0"
- },
- "six": {
- "hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
- ],
- "version": "==1.12.0"
- },
- "snowballstemmer": {
- "hashes": [
- "sha256:713e53b79cbcf97bc5245a06080a33d54a77e7cce2f789c835a143bcdb5c033e"
- ],
- "version": "==1.9.1"
- },
- "sphinx": {
- "hashes": [
- "sha256:0d586b0f8c2fc3cc6559c5e8fd6124628110514fda0e5d7c82e682d749d2e845",
- "sha256:839a3ed6f6b092bb60f492024489cc9e6991360fb9f52ed6361acd510d261069"
- ],
- "index": "pypi",
- "version": "==2.2.0"
- },
- "sphinx-rtd-theme": {
- "hashes": [
- "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
- "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"
- ],
- "index": "pypi",
- "version": "==0.4.3"
- },
- "sphinxcontrib-applehelp": {
- "hashes": [
- "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
- "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
- ],
- "version": "==1.0.1"
- },
- "sphinxcontrib-devhelp": {
- "hashes": [
- "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
- "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
- ],
- "version": "==1.0.1"
- },
- "sphinxcontrib-htmlhelp": {
- "hashes": [
- "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
- "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
- ],
- "version": "==1.0.2"
- },
- "sphinxcontrib-jsmath": {
- "hashes": [
- "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
- "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
- ],
- "version": "==1.0.1"
- },
- "sphinxcontrib-qthelp": {
- "hashes": [
- "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
- "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
- ],
- "version": "==1.0.2"
- },
- "sphinxcontrib-serializinghtml": {
- "hashes": [
- "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
- "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
- ],
- "version": "==1.1.3"
- },
- "urllib3": {
- "hashes": [
- "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
- "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
- ],
- "version": "==1.25.3"
- }
- }
+ "develop": {}
}
diff --git a/docs/.python-version b/docs/.python-version
deleted file mode 100644
index 0b2eb36..0000000
--- a/docs/.python-version
+++ /dev/null
@@ -1 +0,0 @@
-3.7.2
diff --git a/docs/Makefile b/docs/Makefile
index ebf70c7..972c107 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -10,7 +10,7 @@ SPHINXWATCH = $(PE) sphinx-autobuild
SPHINXPROJ = textoutpc
SOURCEDIR = .
BUILDDIR = _build
-WEBROOT = fingerd.touhey.pro:fingerd_doc
+WEBROOT = hercule:fingerd/docs
# Put it first so that "make" without argument is like "make help".
help:
diff --git a/docs/Pipfile b/docs/Pipfile
index 57a81a3..bca103b 100644
--- a/docs/Pipfile
+++ b/docs/Pipfile
@@ -8,6 +8,7 @@ verify_ssl = true
[packages]
sphinx = "*"
sphinx-rtd-theme = "*"
+sphinx-autobuild = "*"
[requires]
-python_version = "3.7"
+python_version = "3.9"
diff --git a/docs/Pipfile.lock b/docs/Pipfile.lock
index 22db0bb..9003dd3 100644
--- a/docs/Pipfile.lock
+++ b/docs/Pipfile.lock
@@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
- "sha256": "ad49cdffc75c09b6abfb00f80a96f9c4967858dfc7153f34febb993592308482"
+ "sha256": "94b3326dd69ba0924a8e8ba1a51f1972bceebacbd4daa2f0c7200a78f86e7f35"
},
"pipfile-spec": 6,
"requires": {
- "python_version": "3.7"
+ "python_version": "3.9"
},
"sources": [
{
@@ -25,200 +25,293 @@
},
"babel": {
"hashes": [
- "sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
- "sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
+ "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
+ "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
],
- "version": "==2.7.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==2.9.1"
},
"certifi": {
"hashes": [
- "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
- "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
+ "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
+ "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
],
- "version": "==2019.3.9"
+ "version": "==2021.5.30"
},
"chardet": {
"hashes": [
- "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
- "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+ "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
+ "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
- "version": "==3.0.4"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==4.0.0"
+ },
+ "colorama": {
+ "hashes": [
+ "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
+ "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
+ ],
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==0.4.4"
},
"docutils": {
"hashes": [
- "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
- "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
- "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
+ "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
+ "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
],
- "version": "==0.14"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==0.16"
},
"idna": {
"hashes": [
- "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
- "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+ "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
- "version": "==2.8"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==2.10"
},
"imagesize": {
"hashes": [
- "sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
- "sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
+ "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
+ "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
],
- "version": "==1.1.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.2.0"
},
"jinja2": {
"hashes": [
- "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
- "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
+ "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4",
+ "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==3.0.1"
+ },
+ "livereload": {
+ "hashes": [
+ "sha256:776f2f865e59fde56490a56bcc6773b6917366bce0c267c60ee8aaf1a0959869"
],
- "version": "==2.10.1"
+ "version": "==2.6.3"
},
"markupsafe": {
"hashes": [
- "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
- "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
- "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
- "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
- "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
- "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
- "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
- "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
- "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
- "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
- "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
- "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
- "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
- "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
- "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
- "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
- "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
- "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
- "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
- "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
- "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
- "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
- "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
- "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
- "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
- "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
- "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
- "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
- ],
- "version": "==1.1.1"
+ "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298",
+ "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64",
+ "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b",
+ "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567",
+ "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff",
+ "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74",
+ "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35",
+ "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26",
+ "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7",
+ "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75",
+ "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f",
+ "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135",
+ "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8",
+ "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a",
+ "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914",
+ "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18",
+ "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8",
+ "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2",
+ "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d",
+ "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b",
+ "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f",
+ "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb",
+ "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833",
+ "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415",
+ "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902",
+ "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9",
+ "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d",
+ "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066",
+ "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f",
+ "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5",
+ "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94",
+ "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509",
+ "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51",
+ "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==2.0.1"
},
"packaging": {
"hashes": [
- "sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
- "sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
+ "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
+ "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
],
- "version": "==19.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==20.9"
},
"pygments": {
"hashes": [
- "sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
- "sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
+ "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
+ "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
],
- "version": "==2.4.2"
+ "markers": "python_version >= '3.5'",
+ "version": "==2.9.0"
},
"pyparsing": {
"hashes": [
- "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
- "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
+ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+ "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
- "version": "==2.4.0"
+ "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==2.4.7"
},
"pytz": {
"hashes": [
- "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
- "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
+ "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
+ "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
],
- "version": "==2019.1"
+ "version": "==2021.1"
},
"requests": {
"hashes": [
- "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
- "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
+ "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
+ "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
- "version": "==2.22.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+ "version": "==2.25.1"
},
"six": {
"hashes": [
- "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
- "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
+ "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
- "version": "==1.12.0"
+ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+ "version": "==1.16.0"
},
"snowballstemmer": {
"hashes": [
- "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
- "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
+ "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2",
+ "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"
],
- "version": "==1.2.1"
+ "version": "==2.1.0"
},
"sphinx": {
"hashes": [
- "sha256:423280646fb37944dd3c85c58fb92a20d745793a9f6c511f59da82fa97cd404b",
- "sha256:de930f42600a4fef993587633984cc5027dedba2464bcf00ddace26b40f8d9ce"
+ "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c",
+ "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4"
],
"index": "pypi",
- "version": "==2.0.1"
+ "version": "==4.0.2"
+ },
+ "sphinx-autobuild": {
+ "hashes": [
+ "sha256:8fe8cbfdb75db04475232f05187c776f46f6e9e04cacf1e49ce81bdac649ccac",
+ "sha256:de1ca3b66e271d2b5b5140c35034c89e47f263f2cd5db302c9217065f7443f05"
+ ],
+ "index": "pypi",
+ "version": "==2021.3.14"
},
"sphinx-rtd-theme": {
"hashes": [
- "sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
- "sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"
+ "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a",
+ "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"
],
"index": "pypi",
- "version": "==0.4.3"
+ "version": "==0.5.2"
},
"sphinxcontrib-applehelp": {
"hashes": [
- "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
- "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
+ "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
+ "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
],
- "version": "==1.0.1"
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.2"
},
"sphinxcontrib-devhelp": {
"hashes": [
- "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
- "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
+ "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
+ "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
],
- "version": "==1.0.1"
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.2"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
- "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
- "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
+ "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07",
+ "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"
],
- "version": "==1.0.2"
+ "markers": "python_version >= '3.6'",
+ "version": "==2.0.0"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
+ "markers": "python_version >= '3.5'",
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
- "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
- "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
+ "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
+ "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
],
- "version": "==1.0.2"
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.3"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
- "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
- "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
+ "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd",
+ "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==1.1.5"
+ },
+ "tornado": {
+ "hashes": [
+ "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb",
+ "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c",
+ "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288",
+ "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95",
+ "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558",
+ "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe",
+ "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791",
+ "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d",
+ "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326",
+ "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b",
+ "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4",
+ "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c",
+ "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910",
+ "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5",
+ "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c",
+ "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0",
+ "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675",
+ "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd",
+ "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f",
+ "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c",
+ "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea",
+ "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6",
+ "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05",
+ "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd",
+ "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575",
+ "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a",
+ "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37",
+ "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795",
+ "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f",
+ "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32",
+ "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c",
+ "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01",
+ "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4",
+ "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2",
+ "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921",
+ "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085",
+ "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df",
+ "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102",
+ "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5",
+ "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68",
+ "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"
],
- "version": "==1.1.3"
+ "markers": "python_version >= '3.5'",
+ "version": "==6.1"
},
"urllib3": {
"hashes": [
- "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
- "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
+ "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
+ "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
],
- "version": "==1.25.3"
+ "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.5"
}
},
"develop": {}
diff --git a/docs/configure.rst b/docs/api/config.rst
index 978d848..978d848 100644
--- a/docs/configure.rst
+++ b/docs/api/config.rst
diff --git a/docs/conf.py b/docs/conf.py
index 5b882e0..fcbd235 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -20,7 +20,7 @@
# -- Project information -----------------------------------------------------
project = 'fingerd'
-copyright = '2019, Thomas Touhey'
+copyright = '2019-2021, Thomas Touhey'
author = 'Thomas Touhey'
# The full version, including alpha/beta/rc tags
diff --git a/docs/explain.rst b/docs/explain.rst
new file mode 100644
index 0000000..c173121
--- /dev/null
+++ b/docs/explain.rst
@@ -0,0 +1,6 @@
+Discussion topics
+=================
+
+.. todo::
+
+ Discuss finger concepts, and stuff.
diff --git a/docs/howto.rst b/docs/howto.rst
new file mode 100644
index 0000000..5ecb176
--- /dev/null
+++ b/docs/howto.rst
@@ -0,0 +1,7 @@
+Howtos
+======
+
+In this section, you will be able to find tutorials for solving specific
+problems using fingerd.
+
+.. toctree::
diff --git a/docs/index.rst b/docs/index.rst
index 0ec0706..87b7ef1 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -11,8 +11,10 @@ project homepage at `fingerd.touhey.pro <https://fingerd.touhey.pro/>`_!
.. toctree::
:maxdepth: 2
- run
- configure
+ onboarding
+ howto
+ explain
+ api
.. _RFC 742: https://tools.ietf.org/html/rfc742
.. _RFC 1288: https://tools.ietf.org/html/rfc1288
diff --git a/docs/run.rst b/docs/onboarding.rst
index 3204d16..767dc47 100644
--- a/docs/run.rst
+++ b/docs/onboarding.rst
@@ -1,5 +1,5 @@
-Running fingerd
-===============
+Onboarding
+==========
``fingerd`` can be run directly and without configuration (while taking its
default option values) with the following command:
@@ -11,3 +11,7 @@ default option values) with the following command:
The server will run while giving native information on TCP port 79 on both
IPv4 and IPv6 (if available). However, you can tweak this behaviour by
configuring it.
+
+.. todo::
+
+ Make a little tutorial about how to run a finger server.
diff --git a/fingerd/__init__.py b/fingerd/__init__.py
index aa779a9..a43600c 100755
--- a/fingerd/__init__.py
+++ b/fingerd/__init__.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
# This file is part of the fingerd project, which is MIT-licensed.
#**************************************************************************
""" finger is both a protocol and a utility to get the information and
@@ -15,22 +15,11 @@ import os.path as _path
from os import environ as _environ
from sys import stderr as _stderr
-from argparse import ArgumentParser as _ArgumentParser
-from ._util import (ConfigurationError, BindError,
- FingerUser, FingerSession, FingerFormatter,
- FingerInterface, FingerLogger)
-from ._fiction import (FingerFiction, FingerLiveInterface,
- FingerFictionInterface)
-from ._server import FingerServer
-
-__all__ = ["run", "version",
- "FingerUser", "FingerSession", "FingerFormatter",
- "FingerInterface", "FingerLogger", "FingerServer", "FingerFiction",
- "FingerNativeInterface", "FingerFictionInterface",
- "ConfigurationError", "BindError"]
-
-version = "0.1"
+from .version import version
+from .errors import *
+from .server import *
+from .fiction import *
# ---
# Native interface definition.
@@ -50,65 +39,4 @@ except (ImportError, ModuleNotFoundError):
FingerNativeInterface = _FingerNoNativeFoundInterface
-# ---
-# Main function.
-# ---
-
-def _get_server():
- """ Get the server through the configuration stored in the
- environment. """
-
- # Load the .env file if present.
-
- try:
- import dotenv as _dotenv
- _dotenv.load_dotenv()
- except ImportError:
- if _path.exists('.env'):
- print("Warning: a `.env` file was found but `python-dotenv`",
- file = _stderr)
- print("isn't installed; consider installing it.",
- file = _stderr)
-
- # Load the environment variables.
-
- bind = _environ.get('BIND', 'localhost:79')
- host = _environ.get('FINGER_HOST', 'LOCALHOST')
- iface = _environ.get('FINGER_TYPE', 'NATIVE').casefold()
- scpt = _environ.get('FINGER_ACTIONS', 'actions.toml')
- src = _environ.get('FINGER_INCOMING', 'ipc:///var/run/fingerd.sock')
-
- if iface == 'native':
- iface = FingerNativeInterface()
- elif iface in ('actions', 'scenario'):
- fic = FingerFiction()
- fic.load(scpt)
-
- iface = FingerFictionInterface(fic)
- elif iface == 'live':
- iface = FingerLiveInterface(src)
- else:
- if iface != 'dummy':
- print("warning: unknown interface type, falling back on dummy",
- file = _stderr)
- iface = FingerInterface()
-
- return FingerServer(bind = bind, host = host, interface = iface)
-
-def run():
- """ Main function for the module. """
-
- # Non-interactive command-line interface.
-
- ap = _ArgumentParser(prog = 'fingerd',
- description = 'Finger (RFC 1288) server-side daemon.')
- args = ap.parse_args()
-
- # Get the server, run it.
-
- server = _get_server()
-
- server.serve_forever()
- server.shutdown()
-
# End of file.
diff --git a/fingerd/__main__.py b/fingerd/__main__.py
index 57714f5..5e39ae9 100755
--- a/fingerd/__main__.py
+++ b/fingerd/__main__.py
@@ -1,16 +1,16 @@
#!/usr/bin/env python3
#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
# This file is part of the fingerd project, which is MIT-licensed.
#**************************************************************************
""" Main script of the module.
Runs the server depending on the configuration content. """
-from . import run as _run
+from .cli import cli as _cli
__all__ = []
if __name__ == '__main__':
- _run()
+ _cli()
# End of file.
diff --git a/fingerd/_binds.py b/fingerd/_binds.py
deleted file mode 100755
index ac391f3..0000000
--- a/fingerd/_binds.py
+++ /dev/null
@@ -1,238 +0,0 @@
-#!/usr/bin/env python3
-#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
-# This file is part of the fingerd project, which is MIT-licensed.
-#**************************************************************************
-""" This file serves for managing binds. """
-
-from enum import Enum as _Enum
-
-import socket as _socket
-
-from ._exceptions import \
- InvalidBindAddressError as _InvalidBindAddressError
-
-__all__ = ["BindAddressType", "BindsDecoder"]
-
-# ---
-# Bind adresses and address types.
-# ---
-
-class BindAddressType(_Enum):
- """ Bind address type. """
-
- """ TCP on IPv4 bind. """
- TCP_IPv4 = 1
-
- """ TCP on IPv6 bind. """
- TCP_IPv6 = 2
-
- """ IPC (Unix socket) bind. """
- IPC = 3
-
-class _BindAddress:
- """ Bind address for fingerd. """
-
- def __init__(self, family):
- self.__family = BindAddressType(family)
-
- def __repr__(self):
- return f"{self.__class__.__name__}(family = {self.__family})"
-
- @property
- def family(self):
- """ Family as one of the `BindAddressType` enumeration values. """
-
- return self.__family
-
-class _TCP4Address(_BindAddress):
- """ IPv4 TCP Address. """
-
- def __init__(self, address, port):
- # XXX: normalize the address to the binary representation in a
- # better way.
-
- try:
- self.__addr = _socket.inet_pton(_socket.AF_INET, address)
- except:
- 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):
- # XXX: normalize the address to the binary representation in a
- # better way.
-
- try:
- self.__addr = _socket.inet_pton(_socket.AF_INET6, address)
- except:
- 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)
-
-# ---
-# Decoding from strings.
-# ---
-
-def _decode_tcp_host(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 _InvalidBindAddressError(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:
- try:
- assert x[1:] != ''
- port = _socket.getservbyname(x[1:])
- except:
- raise _InvalidBindAddressError(addr,
- "expected a valid port number or name " \
- f"(got {repr(x[1:])})") from None
- else:
- raise _InvalidBindAddressError(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('.')
- assert 2 <= len(ip) <= 4
-
- ip = list(map(int, ip))
- assert all(lambda x: 0 <= x < 256, ip)
-
- 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:
- 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
-
-class BindsDecoder:
- """ Binds decoder for fingerd.
- Takes a raw string and the protocol name, either 'finger' (the base
- protocol managed by the class) or 'fingerd-control' (the protocol
- used for controlling the live fingerd interface). """
-
- def __init__(self, raw, proto = 'finger'):
- proto = proto.casefold()
- if proto not in ('finger', 'fingerd-control'):
- raise NotImplementedError(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', 'fingerd-control': 'ipc'}[proto]
- else:
- # just don't add the ':' of ':/' again
- x = '/' + ':/'.join(rest)
-
- if (proto == 'finger' and scheme != 'tcp') \
- or scheme not in ('tcp', 'ipc'):
- raise _InvalidBindError("unsupported scheme "
- f"{repr(scheme)} for protocol {repr(proto)}")
-
- # Decode the address data.
-
- if scheme == "tcp":
- self.__binds.update(_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})"
-
-# End of file.
diff --git a/fingerd/_exceptions.py b/fingerd/_exceptions.py
deleted file mode 100755
index a2ecdd0..0000000
--- a/fingerd/_exceptions.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env python3
-#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
-# This file is part of the fingerd project, which is MIT-licensed.
-#**************************************************************************
-""" This file defines the exceptions used throughout the module. """
-
-class ConfigurationError(Exception):
- """ Generic exception for when an invalid configuration
- exception occurs. """
-
- pass
-
-class BindError(ConfigurationError):
- """ Exception raised when there were errors in the binds. """
-
- def __init__(self):
- super().__init__("invalid bind")
-
-class InvalidBindAddressError(ConfigurationError):
- """ Exception raised when a bind could not be understood. """
-
- def __init__(self, addr, text):
- super().__init__(f"{text} for address {addr}")
-
-# End of file.
diff --git a/fingerd/_server.py b/fingerd/_server.py
deleted file mode 100755
index 01cb4e1..0000000
--- a/fingerd/_server.py
+++ /dev/null
@@ -1,342 +0,0 @@
-#!/usr/bin/env python3
-#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
-# This file is part of the fingerd project, which is MIT-licensed.
-#**************************************************************************
-""" The main server class for the server, with the related utilities that
- will not be presented to the user, is defined in this file. """
-
-import string as _string, socket as _socket, signal as _signal, \
- multiprocessing as _multip, socketserver as _socketserver
-
-from io import TextIOWrapper as _TextIOWrapper, StringIO as _StringIO
-
-from ._util import (FingerInterface as _FingerInterface,
- FingerFormatter as _FingerFormatter, FingerLogger as _FingerLogger,
- ConfigurationError as _InvalidConfError,
- BindError as _InvalidBindError, BindAddressType as _BindAddressType,
- BindsDecoder as _BindsDecoder)
-
-__all__ = ["FingerServer"]
-
-# ---
-# Finger/TCP query decoding.
-# ---
-
-class _FingerQuery:
- """ A finger query. Requests information about connected or specific
- users on a remote server.
-
- There are three types of requests recognized by RFC 1288:
- - {C} is a request for a list of all online users;
- - {Q1} is a request for a local user;
- - {Q2} is a request for a distant user (with hostname).
-
- /W means the RUIP (program answering the query) should be more
- verbose (this token can be ignored). """
-
- # "By default, this program SHOULD filter any unprintable data,
- # leaving only printable 7-bit characters (ASCII 32 through
- # ASCII 126), tabs (ASCII 9) and CRLFs."
-
- allowed_chars = ("\t !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
- + _string.ascii_letters + _string.digits)
-
- def __init__(self, raw):
- """ Initialize the query object by decoding the data. """
-
- # Get a character string out of the query.
-
- raw = ''.join(c for c in raw if c in self.allowed_chars)
- self.line = raw
-
- # Get elements.
-
- self.host = None
- self.username = None
- self.verbose = False
- for element in raw.split():
- if element[0] == '/':
- if 'W' in element[1:]:
- self.verbose = True
- continue
- elif self.username is not None:
- raise Exception
- self.username = element
-
- if self.username is not None and '@' in self.username:
- self.host, *self.username = self.username.split('@')[::-1]
- self.username = '@'.join(self.username[::-1])
-
-# ---
-# TCP request handler.
-# ---
-
-class _FingerTCPHandler(_socketserver.StreamRequestHandler):
- """ The TCP request handler for our finger daemon.
- Instantiated once per connection to the server (request). """
-
- def handle(self):
- """ Request handling. """
-
- inp = _TextIOWrapper(self.rfile, encoding = 'ascii',
- errors = 'ignore', newline = '\r\n')
- outp = _TextIOWrapper(self.wfile, encoding = 'ascii')
-
- # Gather the request line.
-
- try:
- line = inp.readline()
- except ConnectionResetError:
- self.logger.no_query(self.src)
- return
-
- # Decode the request.
-
- ans = _StringIO()
-
- try:
- query = None
- query = _FingerQuery(line)
- except:
- self.logger.bad_query(self.src)
- ans.write(self.fmt.format_query_error(self.host))
- return
-
- if query is not None:
- self.answer(query, ans)
-
- # Write the output.
-
- try:
- outp.write(ans.getvalue())
- except ConnectionResetError:
- self.logger.could_not_answer(self.src)
-
- @property
- def fmt(self):
- """ The answer formatter. """
-
- return self.server.params['formatter']
-
- @property
- def iface(self):
- """ The interface formatter. """
-
- return self.server.params['interface']
-
- @property
- def host(self):
- """ The host. """
-
- return self.server.params['host']
-
- @property
- def logger(self):
- """ The logger. """
-
- return self.server.params['logger']
-
- @property
- def src(self):
- """ The source IP address. """
-
- return self.client_address[0]
-
- def answer(self, query, outp):
- """ Answer the query.
- `query` is the `_FingerQuery` instance.
- `outp` is the output stream.
-
- This method is thought to be overriden someday. """
-
- ans = self.fmt.format_header(self.host, query.line)
-
- # Gather the content.
-
- if query.host is not None:
- if query.username:
- self.logger.transmit(self.src, query.username, query.host)
- else:
- self.logger.transmit_list(self.src, query.host)
-
- cont = self.iface.transmit_query(query.host, query.username,
- query.verbose)
- ans += cont
- else:
- if query.username:
- self.logger.search_users(self.src, query.username)
- check = lambda x: query.username in x.login
- else:
- self.logger.list(self.src)
- check = lambda x: x.sessions
-
- users = self.iface.search_users(check)
-
- if query.username or query.verbose:
- ans += self.fmt.format_long(users)
- else:
- ans += self.fmt.format_short(users)
-
- # Send the answer.
-
- ans += self.fmt.format_footer()
- outp.write(ans)
-
-# ---
-# TCP servers.
-# ---
-
-class _IPv4TCPServer(_socketserver.ThreadingMixIn,
- _socketserver.TCPServer):
- """ The IPv4 TCP Server to be used """
-
- address_family = _socket.AF_INET
- allow_reuse_address = True
-
- def __init__(self, *args, params = {}, **kwargs):
- self.params = params
- super().__init__(*args, **kwargs)
-
-class _IPv6TCPServer(_socketserver.ThreadingMixIn,
- _socketserver.TCPServer):
- """ The IPv6 TCP Server to be used """
-
- address_family = _socket.AF_INET6
- allow_reuse_address = True
-
- def __init__(self, *args, params = {}, **kwargs):
- self.params = params
- super().__init__(*args, **kwargs)
-
-# ---
-# Public server class.
-# ---
-
-def _runserver(family, addr, port, params):
- """ Run the server (will be run in another thread). """
-
- logger = params['logger']
-
- # Make the server corresponding to the entry.
-
- if family == _socket.AF_INET:
- server = _IPv4TCPServer((addr, port), _FingerTCPHandler,
- params = params)
- elif family == _socket.AF_INET6:
- server = _IPv6TCPServer((addr, port), _FingerTCPHandler,
- params = params)
-
- # Run it.
-
- logger.start(addr, port)
- try: server.serve_forever()
- except KeyboardInterrupt:
- pass
- logger.stop(addr, port)
-
-_ldh = _string.ascii_letters + _string.digits + '.-'
-
-class FingerServer:
- """ The Finger Server class. """
-
- def __init__(self, bind = 'localhost:79', host = 'LOCALHOST',
- interface = _FingerInterface(), formatter = _FingerFormatter(),
- logger = _FingerLogger()):
-
- # Check the host.
-
- try:
- host = host.upper()
- assert all(c in _ldh for c in host)
- except:
- raise _InvalidHostError(host)
-
- # Check the interface and formatter classes.
-
- if not isinstance(interface, _FingerInterface):
- raise InvalidConfigurationError("please base your interface "
- "on the base class provided by the fingerd module")
- if not isinstance(formatter, _FingerFormatter):
- raise InvalidConfigurationError("please base your formatter "
- "on the base class provided by the fingerd module")
- if not isinstance(logger, _FingerLogger):
- raise InvalidConfigurationError("please base your logger "
- "on the base class provided by the fingerd module")
-
- # Keep the parameters.
-
- self._host = host
- self._logger = logger
- self._interface = interface
- self._formatter = formatter
-
- # Make what is to send to the handlers.
-
- self._params = {'host': host, 'interface': interface,
- 'formatter': formatter, 'logger': logger}
-
- # Check the addresses.
-
- if not self._makebinds(bind):
- raise _InvalidBindError()
-
- def _makebinds(self, addresses):
- """ Create the servers data using the bind values. """
-
- self._servers = []
-
- for bind in _BindsDecoder(addresses):
- self._servers.append([bind.runserver_params, None])
-
- return bool(self._servers)
-
- def start(self):
- """ Bind and start the underlying servers. """
-
- for entry in self._servers:
- # Check if the thread already exists and runs.
-
- if entry[1] != None and not entry[1].is_alive():
- continue
-
- # Start the thread.
-
- entry[1] = _multip.Process(target = _runserver,
- args = entry[0] + (self._params,))
- entry[1].start()
-
- def stop(self):
- """ Stop the underlying servers. """
-
- for entry in self._servers:
- # Check if the thread is still here.
-
- if entry[1] == None:
- continue
- if not entry[1].is_alive():
- entry[1] = None
- continue
-
- # Stop it.
-
- entry[1].join()
- entry[1] = None
-
- def serve_forever(self):
- """ Serve forever. """
-
- self.start()
- try:
- while True:
- _signal.pause()
- except KeyboardInterrupt:
- pass
- self.stop()
-
- def shutdown(self):
- """ Shutdown the server, alias to `.stop()`. """
-
- self.stop()
-
-# End of file.
diff --git a/fingerd/_util.py b/fingerd/_util.py
deleted file mode 100755
index b51ed34..0000000
--- a/fingerd/_util.py
+++ /dev/null
@@ -1,509 +0,0 @@
-#!/usr/bin/env python3
-#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
-# This file is part of the fingerd project, which is MIT-licensed.
-#**************************************************************************
-""" This file defines the base classes for what is used by the
- finger server, directly or indirectly. """
-
-import sys as _sys, copy as _copy
-import multiprocessing as _multip
-
-from datetime import datetime as _dt, timedelta as _td
-
-from ._exceptions import ConfigurationError, BindError
-from ._binds import BindAddressType, BindsDecoder
-
-__all__ = ["ConfigurationError", "BindError",
- "FingerUser", "FingerSession",
- "FingerFormatter", "FingerInterface", "FingerLogger"
- "BindAddressType", "BindsDecoder"]
-
-# ---
-# User-related classes.
-# ---
-
-class FingerUser:
- """ User description for the finger interface. """
-
- def __init__(self, *_, login = None, name = None,
- home = None, shell = None):
- self.__login = None
- self.__name = ''
- self.__home = None
- self.__shell = None
- self.__office = None
- self.__plan = None
- self.__last_login = None
- self.__sessions = _FingerSessionManager()
-
- self.login = login
- self.name = name
- self.home = home
- self.shell = shell
-
- def __repr__(self):
- p = ('login', 'name', 'home', 'shell', 'office',
- 'last_login', 'sessions')
- p = (f"{x} = {repr(getattr(self, x))}" for x in p \
- if getattr(self, x) is not None)
- return f"{self.__class__.__name__}({', '.join(p)})"
-
- @property
- def login(self):
- """ Login name, e.g. 'cake'. """
-
- return self.__login
-
- @login.setter
- def login(self, value):
- self.__login = value
-
- @property
- def name(self):
- """ Full user name, e.g. 'Jean Dupont'. """
-
- return self.__name
-
- @name.setter
- def name(self, value):
- self.__name = value
-
- @property
- def last_login(self):
- """ Last login date. """
-
- return self.__last_login
-
- @last_login.setter
- def last_login(self, value):
- self.__last_login = None if value is None else \
- value if isinstance(value, _dt) else _dt(value)
-
- @property
- def home(self):
- """ The user's home. """
-
- return self.__home
-
- @home.setter
- def home(self, value):
- self.__home = None if value is None else str(value)
-
- @property
- def shell(self):
- """ The user's shell. """
-
- return self.__shell
-
- @shell.setter
- def shell(self, value):
- self.__shell = None if value is None else str(value)
-
- @property
- def office(self):
- """ The user's office. """
-
- return self.__office
-
- @office.setter
- def office(self, value):
- self.__office = None if value is None else str(value)
-
- @property
- def plan(self):
- """ The user's plan. """
-
- return self.__plan
-
- @plan.setter
- def plan(self, value):
- if value is None:
- self.__plan = None
- else:
- value = str(value)
- self.__plan = '\n'.join(value.splitlines())
-
- @property
- def sessions(self):
- """ Current sessions. """
-
- return self.__sessions
-
-class _FingerSessionManager:
- """ Session manager. """
-
- def __init__(self):
- self.__sessions = []
-
- def __repr__(self):
- return repr(self.__sessions)
-
- def __bool__(self):
- return bool(self.__sessions)
-
- def __iter__(self):
- return iter(self.__sessions[::-1])
-
- def __delitem__(self, key):
- if key is None:
- self.__sessions.pop(0)
- else:
- for i in [i for i, x in enumerate(self.__sessions) \
- if key == x.name][::-1]:
- self.__sessions.pop(i)
-
- def __getitem__(self, key):
- if key is None:
- try:
- return self.__sessions[0]
- except IndexError:
- raise KeyError("could not get latest session") from None
-
- if type(key) is int:
- try:
- return self.__sessions[key]
- except IndexError:
- msg = f"could not get session #{repr(key)}"
- raise IndexError(msg) from None
-
- try:
- return next(x for x in self.__sessions if key == x.name)
- except StopIteration:
- raise KeyError(f"could not get session {repr(key)}") from None
-
- def __setitem__(self, key, value):
- if not isinstance(value, FingerSession):
- raise TypeError("can only add sessions into a session manager")
- value = _copy.deepcopy(value)
-
- if key is None:
- # Check if except the first session, the key of the session,
- # if any, does not override another key.
-
- if value.name is not None:
- try:
- next(i for i, x in self.__sessions[1:] \
- if value.name == x.name)
- except StopIteration:
- pass
- else:
- msg = "value.name overrides another session's key"
- raise ValueError(msg) from None
-
- try:
- self.__sessions[0] = value
- return
- except IndexError:
- raise KeyError("could not set latest session") from None
-
- if type(key) is int:
- # Check if except the key-th session, the key of the session,
- # if any, does not override another key.
-
- if value.name is not None:
- try:
- next(i for i, x in self.__sessions \
- if i != key and value.name == x.name)
- except StopIteration:
- pass
- else:
- msg = "value.name overrides another session's key"
- raise ValueError(msg) from None
-
- try:
- self.__sessions[key] = value
- return
- except IndexError:
- msg = f"could not set session #{repr(key)}"
- raise IndexError(msg) from None
-
- value.name = key
-
- try:
- i = next(i for i, x in enumerate(self.__sessions) \
- if key == x.name)
- except StopIteration:
- raise KeyError(f"could not set session {repr(key)}") from None
-
- self.__sessions[i] = value
-
- def add(self, session):
- """ Add a session. """
-
- if not isinstance(session, FingerSession):
- raise TypeError("can only insert sessions into a session "
- "manager")
- if session.name is not None:
- try:
- next(i for i, x in self.__sessions \
- if session.name == x.name)
- except StopIteration:
- pass
- else:
- msg = "session.name overrides another session's key"
- raise ValueError(msg) from None
-
- self.__sessions.insert(0, session)
-
-class FingerSession:
- """ Session for a user. """
-
- def __init__(self, *_, time = _dt.now()):
- self.__start = time if isinstance(time, _dt) else _dt(time)
- self.__line = None
- self.__host = None
- self.__idle = self.__start
-
- def __repr__(self):
- p = ('start', 'line', 'orig', 'idle')
- p = (f"{x} = {repr(getattr(self, x))}" for x in p \
- if getattr(self, x) is not None)
- return f"{self.__class__.__name__}({', '.join(p)})"
-
- @property
- def start(self):
- """ The session start time. """
-
- return self.__start
-
- @start.setter
- def start(self, value):
- self.__start = value if isinstance(value, _dt) else _dt(value)
-
- @property
- def line(self):
- """ The line on which the user is. """
-
- return self.__line
-
- @line.setter
- def line(self, value):
- self.__line = None if value is None else str(value)
-
- @property
- def host(self):
- """ The host from which the user is connected. """
-
- return self.__host
-
- @host.setter
- def host(self, value):
- self.__host = None if value is None else str(value)
-
-# ---
-# Formatter base class.
-# ---
-
-def _format_idle(idle):
- def _iter_idle(idle):
- days = int(idle.days)
- hours = int(idle.seconds / 3600)
- mins = int(idle.seconds % 3600 / 60)
- secs = int(idle.seconds % 60)
-
- if days:
- yield f"{days} day{('', 's')[days > 1]}"
- if hours:
- yield f"{hours} hour{('', 's')[hours > 1]}"
- if mins:
- yield f"{mins} minute{('', 's')[mins > 1]}"
- if secs:
- yield f"{secs} second{('', 's')[secs > 1]}"
-
- return f"{' '.join(_iter_idle(idle))} idle"
-
-def _format_time(d):
- if d < _td():
- return ""
-
- days = int(d.days)
- hours = int(d.seconds / 3600)
- mins = int(d.seconds % 3600 / 60)
-
- if days:
- return f"{days}d"
- elif hours or mins:
- return f"{hours:02}:{mins:02}"
-
- return ""
-
-def _format_when(d):
- return d.strftime("%a %H:%M")
-
-class FingerFormatter:
- """ Answer formatter.
- Formats the answer following RFC 1288. """
-
- def format_query_error(self, name):
- """ There is an error in your query! """
-
- return "Site: {}\r\n" \
- "You have made a mistake in your query!\r\n".format(name)
-
- def format_header(self, name, request):
- """ Formats the header of each request. """
-
- if request:
- request = ' ' + request
-
- return f"Site: {name}\r\n" f"Command line:{request}\r\n" "\r\n"
-
- def format_footer(self):
- """ Formats the footer of each request (in case the developer
- wants to add something at the end, anything). """
-
- return ""
-
- def format_short(self, users):
- """ Format a user list in a short fashion. """
-
- if not users:
- return "No user list available.\r\n"
-
- lst = []
- for user in users:
- if not user.sessions:
- lst.append((user, None))
- for session in user.sessions:
- lst.append((user, session))
-
- now = _dt.now()
-
- _login = lambda u, s: u.login
- _name = lambda u, s: u.name
- _line = lambda u, s: s.line if s and s.line else ''
- _idle = lambda u, s: _format_time(now - s.idle) if s else ''
- _logt = lambda u, s: _format_when(s.start) if s else ''
- _offic = lambda u, s: u.office if u.office else ''
-
- columns = (
- ('Login',) + tuple(_login(u, s) for u, s in lst),
- ('Name',) + tuple( _name(u, s) for u, s in lst),
- ('TTY',) + tuple( _line(u, s) for u, s in lst),
- ('Idle',) + tuple( _idle(u, s) for u, s in lst),
- ('When',) + tuple( _logt(u, s) for u, s in lst),
- ('Office',) + tuple(_offic(u, s) for u, s in lst))
-
- sizes = tuple(max(map(len, c)) + 1 for i, c in enumerate(columns))
- align = ('<', '<', '<', '^', '^', '<')
-
- lines = []
- for line in range(len(columns[0])):
- lines.append(' '.join(\
- f"{columns[i][line][:sizes[i]]:{align[i]}{sizes[i]}}" \
- for i in range(len(columns))))
-
- return '\r\n'.join(lines) + '\r\n'
-
- def format_long(self, users):
- """ Format a user list in a long fashion. """
-
- if not users:
- return "No user list available.\r\n"
-
- res = ''
-
- for user in users:
- res += (""
- f"Login name: {user.login[:27]:<27} Name: {user.name}\r\n"
- f"Directory: {user.home[:28]:<28} Shell: {user.shell}\r\n"
- f"Office: {user.office if user.office else ''}\r\n")
-
- if user.sessions:
- # List current sessions.
-
- for se in user.sessions:
- since = se.start.strftime("%a %b %e %R")
- tz = str(_dt.utcnow().astimezone().tzinfo)
- res += f"On since {since} ({tz}) on {se.line}"
- if se.host is not None:
- res += f" from {se.host}"
- res += "\r\n"
-
- idle = _dt.now() - se.idle
- if idle >= _td(seconds = 4):
- res += f" {_format_idle(idle)}\r\n"
- elif user.last_login is not None:
- # Show last login.
-
- date = user.last_login.strftime("%a %b %e %R")
- tz = str(_dt.utcnow().astimezone().tzinfo)
- res += f"Last login {date} ({tz}) on console\r\n"
- else:
- res += "Never logged in.\r\n"
-
- if user.plan is None:
- res += "No plan.\r\n"
- else:
- res += "Plan:\r\n"
- res += "\r\n".join(user.plan.splitlines())
- res += "\r\n"
-
- res += "\r\n"
-
- return res
-
-# ---
-# Interface (dummy) base class.
-# ---
-
-class FingerInterface:
- """ Finger interface to the users.
- Expandable (the current class defines the dummy interface). """
-
- def transmit_query(self, user, host, verbose):
- return "This server won't transmit finger queries.\r\n"
-
- def search_users(self, check):
- return []
-
-# ---
-# Server logger base class.
-# ---
-
-class FingerLogger:
- """ Finger logger for the server.
- Expandable, but the base class should be enough for most needs. """
-
- def __init__(self, stream = _sys.stderr):
- self._stream = stream
- self._lock = _multip.Lock()
-
- def _log(self, fmt, *args):
- self._lock.acquire()
- print('\r' + fmt.format(*args), file=self._stream)
- self._lock.release()
-
- def _logr(self, src, fmt, *args):
- self._log('[{}] ' + fmt, src, *args)
-
- def start(self, host, port):
- self._log("Starting fingerd on [{}]:{}.", host, port)
-
- def stop(self, host, port):
- self._log("Stopping fingerd on [{}]:{}.", host, port)
-
- def no_query(self, source):
- self._logr(source, "no query. (possible scan)")
-
- def bad_query(self, source):
- self._logr(source, "bad request.")
-
- def could_not_answer(self, source):
- self._logr(source, "could not write the answer.")
-
- def transmit_list(self, source, host):
- self._logr(source, "transmit user list query to `{}`.", host)
-
- def transmit(self, source, username, host):
- self._logr(source, "transmit user query for `{}` to `{}`.",
- username, host)
-
- def search_users(self, source, username):
- self._logr(source, "look for user `{}`.", username)
-
- def list(self, source):
- self._logr(source, "list connected users.")
-
-# End of file.
diff --git a/fingerd/cli.py b/fingerd/cli.py
new file mode 100644
index 0000000..be665a4
--- /dev/null
+++ b/fingerd/cli.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#**************************************************************************
+# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the fingerd project, which is MIT-licensed.
+#**************************************************************************
+""" fingerd CLI interface. """
+
+import click as _click
+
+from platform import (python_implementation as _python_impl,
+ python_version as _python_version)
+from .version import version as _version
+from . import (FingerNativeInterface as _FingerNativeInterface,
+ FingerFiction as _FingerFiction,
+ FingerFictionInterface as _FingerFictionInterface,
+ FingerLiveInterface as _FingerLiveInterface,
+ FingerInterface as _FingerInterface,
+ FingerServer as _FingerServer)
+
+__all__ = ['cli']
+
+@_click.command(context_settings = {'help_option_names': ['-h', '--help']})
+@_click.version_option(version = _version,
+ message = f"fingerd version {_version}, "
+ f"on {_python_impl()} {_python_version()}")
+@_click.option('-b', '--binds', default = 'localhost:79',
+ envvar = ('BIND', 'BINDS'))
+@_click.option('-H', '--hostname', default = 'localhost',
+ envvar = ('FINGER_HOST',))
+@_click.option('-t', '--type', default = 'native',
+ envvar = ('FINGER_TYPE',))
+@_click.option('-s', '--scenario', default = 'actions.toml',
+ envvar = ('FINGER_ACTIONS',))
+@_click.option('-i', '--incoming', default = 'ipc:///var/run/fingerd.sock',
+ envvar = ('FINGER_INCOMING',))
+def cli(binds, hostname, type, scenario, incoming):
+ """ fingerd is a modern finger (RFC 1288) server.
+ Find out more at <https://fingerd.touhey.pro/>. """
+
+ hostname = hostname.upper()
+ type = type.casefold()
+
+ if type == 'native':
+ iface = _FingerNativeInterface()
+ elif type in ('actions', 'scenario'):
+ fic = _FingerFiction()
+ fic.load(scenario)
+
+ iface = _FingerFictionInterface(fic)
+ elif type == 'live':
+ iface = _FingerLiveInterface(src)
+ else:
+ if type != 'dummy':
+ print("warning: unknown interface type, falling back on dummy",
+ file = _stderr)
+
+ iface = _FingerInterface()
+
+ server = _FingerServer(binds = binds,
+ hostname = hostname, interface = iface)
+ server.serve_forever()
+
+# End of file.
diff --git a/fingerd/errors.py b/fingerd/errors.py
new file mode 100755
index 0000000..98110e8
--- /dev/null
+++ b/fingerd/errors.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#**************************************************************************
+# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the fingerd project, which is MIT-licensed.
+#**************************************************************************
+""" This file defines the exceptions used throughout the module. """
+
+__all__ = [
+ 'ConfigurationError',
+ 'BindError', 'NoBindsError', 'InvalidBindError',
+ 'HostnameError'
+]
+
+# ---
+# Configuration-related errors.
+# ---
+
+class ConfigurationError(Exception):
+ """ Generic exception for when an invalid configuration
+ exception occurs. """
+
+ pass
+
+class BindError(ConfigurationError):
+ """ Exception raised when an error has occurred with the provided
+ binds. """
+
+ def __init__(self, msg):
+ super().__init__("an error has occurred with the provided binds: "
+ + msg)
+
+class NoBindsError(BindError):
+ """ Exception raised when no binds were provided. """
+
+ def __init__(self):
+ super().__init__("no valid bind")
+
+class InvalidBindError(BindError):
+ """ Exception raised when one of the provided binds came out
+ erroneous. """
+
+ def __init__(self, bind, msg = None):
+ super().__init__("one of the provided bind ({}) ".format(repr(bind))
+ + "was invalid{}".format(': ' + msg if msg is not None else ''))
+
+class HostnameError(ConfigurationError):
+ """ Exception raised when a host name is invalid. """
+
+ def __init__(self, hostname):
+ super().__init__("invalid host name {}.".format(repr(hostname)))
+
+# End of file.
diff --git a/fingerd/_fiction.py b/fingerd/fiction.py
index b2bec47..6d63416 100755
--- a/fingerd/_fiction.py
+++ b/fingerd/fiction.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#**************************************************************************
-# Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
+# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
# This file is part of the fingerd project, which is MIT-licensed.
#**************************************************************************
""" Definitions for the finger server fiction interface.
@@ -11,10 +11,14 @@ import copy as _copy, re as _re, math as _math
import itertools as _itertools
from datetime import datetime as _dt, timedelta as _td
-from ._util import FingerInterface as _FingerInterface, \
- FingerUser as _FingerUser, FingerSession as _FingerSession
+from .server import (
+ FingerInterface as _FingerInterface,
+ FingerUser as _FingerUser,
+ FingerSession as _FingerSession)
-__all__ = ["FingerFictionInterface", "FingerLiveInterface",
+__all__ = [
+ "FingerFictionInterface",
+ "FingerLiveInterface",
"FingerFiction"]
_toml = None
@@ -34,36 +38,36 @@ class _FictionalSession(_FingerSession):
def __init__(self, *args, name = None, is_idle = False, **kwargs):
super().__init__(*args, **kwargs)
- self.__name = None if name is None else str(name)
- self.__is_idle = is_idle
- self.__idle_last = self.start
+ self._name = None if name is None else str(name)
+ self._is_idle = is_idle
+ self._idle_last = self.start
def __repr__(self):
p = ('name', 'start', 'line', 'host', 'idle_since', 'active_since')
p = (f"{x} = {repr(getattr(self, x))}" for x in p \
if getattr(self, x) is not None)
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def name(self):
""" Session name (for identifying). """
- return self.__name
+ return self._name
@name.setter
def name(self, value):
- self.__name = None if value is None else str(value)
+ self._name = None if value is None else str(value)
@property
def idle(self):
""" Idle time (simulated). """
- if self.__is_idle:
- return self.__idle_last
+ if self._is_idle:
+ return self._idle_last
# Generate a number of seconds and return it.
- x = (_dt.now() - self.__idle_last).seconds
+ x = (_dt.now() - self._idle_last).seconds
s = lambda x: _math.sin(x * (_math.pi / 2))
randsecs = int(abs(s(x) + s(x / 4)))
@@ -79,27 +83,27 @@ class _FictionalSession(_FingerSession):
def idle_since(self):
""" Idle since the given time. """
- if not self.__is_idle:
+ if not self._is_idle:
return None
- return self.__idle_last
+ return self._idle_last
@idle_since.setter
def idle_since(self, value):
- self.__is_idle = True
- self.__idle_last = value
+ self._is_idle = True
+ self._idle_last = value
@property
def active_since(self):
""" Active since the given time. """
- if self.__is_idle:
+ if self._is_idle:
return None
- return self.__idle_last
+ return self._idle_last
@active_since.setter
def active_since(self, value):
- self.__is_idle = False
- self.__idle_last = value
+ self._is_idle = False
+ self._idle_last = value
# ---
# Parse a delta string.
@@ -151,168 +155,168 @@ class _UserCreationAction(_Action):
def __init__(self, user):
super().__init__()
- self.__user = user
+ self._user = user
def __repr__(self):
p = (f"{x} = {repr(getattr(self, x))}" for x in ('user'))
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def user(self):
""" The user. """
- return _copy.deepcopy(self.__user)
+ return _copy.deepcopy(self._user)
class _UserEditionAction(_Action):
""" A user status has been changed. """
def __init__(self, login, name, home, shell, office, plan):
super().__init__()
- self.__login = login
- self.__name = name
- self.__home = home
- self.__shell = shell
- self.__office = office
- self.__plan = plan
+ self._login = login
+ self._name = name
+ self._home = home
+ self._shell = shell
+ self._office = office
+ self._plan = plan
def __repr__(self):
p = (f"{x} = {repr(getattr(self, x))}" for x in ('login',
'name', 'home', 'shell'))
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def login(self):
""" The user's login. """
- return self.__login
+ return self._login
@property
def name(self):
""" The user's name. """
- return self.__name
+ return self._name
@property
def home(self):
""" The user's home. """
- return self.__home
+ return self._home
@property
def shell(self):
""" The user's shell. """
- return self.__shell
+ return self._shell
@property
def office(self):
""" The user's office. """
- return self.__office
+ return self._office
@property
def plan(self):
""" The user's plan. """
- return self.__plan
+ return self._plan
class _UserLoginAction(_Action):
""" A user has logged in. """
def __init__(self, login, session):
super().__init__()
- self.__login = login
- self.__session = session
+ self._login = login
+ self._session = session
def __repr__(self):
p = (f"{x} = {repr(getattr(self, x))}" for x in ('login',
'session'))
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def login(self):
""" The user's login. """
- return self.__login
+ return self._login
@property
def session(self):
""" The session to create. """
- return self.__session
+ return self._session
class _UserLogoutAction(_Action):
""" A user has logged out. """
def __init__(self, login, name):
super().__init__()
- self.__login = login
- self.__name = name
+ self._login = login
+ self._name = name
def __repr__(self):
p = (f"{x} = {repr(getattr(self, x))}" for x in ('login', 'name'))
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def login(self):
""" The user's login. """
- return self.__login
+ return self._login
@property
def name(self):
""" The name of the session to destroy. """
- return self.__name
+ return self._name
class _UserIdleToggleAction(_Action):
""" A user has changed its idle status. """
def __init__(self, login, name, idle):
super().__init__()
- self.__login = login
- self.__name = name
- self.__idle = bool(idle)
+ self._login = login
+ self._name = name
+ self._idle = bool(idle)
def __repr__(self):
p = (f"{x} = {repr(getattr(self, x))}" for x in ('login', 'name',
'idle'))
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def login(self):
""" The user's login. """
- return self.__login
+ return self._login
@property
def name(self):
""" The name of the session to create. """
- return self.__name
+ return self._name
@property
def idle(self):
""" The idle status. """
- return self.__idle
+ return self._idle
class _UserDeletionAction(_Action):
""" A user has been deleted. """
def __init__(self, login):
super().__init__()
- self.__login = login
+ self._login = login
def __repr__(self):
p = (f"{x} = {repr(getattr(self, x))}" for x in ('login'))
- return f"{self.__class__.__name__}({', '.join(p)})"
+ return f"{self._class__.__name__}({', '.join(p)})"
@property
def login(self):
""" The user's login. """
- return self.__login
+ return self._login
# ---
# Finger interface to actions.
@@ -339,18 +343,18 @@ class FingerFiction:
# Initialize the properties.
- self.__type = 'interrupt'
- self.__start = _td()
- self.__duration = None
+ self._type = 'interrupt'
+ self._start = _td()
+ self._duration = None
- self.__end_type = None
- self.__end_time = None
- self.__actions = []
+ self._end_type = None
+ self._end_time = None
+ self._actions = []
def ready(self):
""" Check if the fiction is ready. """
- return self.__end_time is not None
+ return self._end_time is not None
def load(self, path):
""" Load from an `actions.toml` file. """
@@ -493,23 +497,23 @@ class FingerFiction:
# FIXME: check that incompatible actions, such as double creation
# for a user, doesn't occur.
- self.__type = end_type
- self.__start = actions[0][0] if actions else _td()
- self.__duration = end_time
- self.__actions = actions
+ self._type = end_type
+ self._start = actions[0][0] if actions else _td()
+ self._duration = end_time
+ self._actions = actions
def get(self, to = None, since = None):
""" Get the events. """
if to is None:
- to = self.__duration
+ to = self._duration
if since is None:
- since = self.__start - _td(seconds = 1)
+ since = self._start - _td(seconds = 1)
if since > to:
raise ValueError(f"`since` ({since}) should be " \
f"before `to` ({to}).")
- for time, action in self.__actions:
+ for time, action in self._actions:
if since < time <= to:
yield time, action
@@ -517,13 +521,13 @@ class FingerFiction:
def type(self):
""" Type of action flow, either 'interrupt' or 'stop'. """
- return self.__type
+ return self._type
@property
def duration(self):
""" Maximum offset. """
- return self.__duration
+ return self._duration
class _FingerFictionalInterface(_FingerInterface):
""" Fiction interface, to get a scene as received from inter-process
@@ -536,26 +540,26 @@ class _FingerFictionalInterface(_FingerInterface):
# - `users`: the users.
# - `start`: the start time.
- self.__users = {}
- self.__start = start
+ self._users = {}
+ self._start = start
@property
def start(self):
""" Get the fiction starting point. """
- return self.__start
+ return self._start
def search_users(self, check):
""" Look for users according to a check. """
self.update()
- return [_copy.deepcopy(u) for u in self.__users.values() \
+ return [_copy.deepcopy(u) for u in self._users.values() \
if check(u)]
def reset(self):
""" Reset the interface (revert all actions). """
- self.__users = {}
+ self._users = {}
def apply(self, action, time = _dt.now()):
""" Apply an action. """
@@ -566,20 +570,20 @@ class _FingerFictionalInterface(_FingerInterface):
user = _copy.deepcopy(action.user)
if user.login is None:
raise ValueError("missing login")
- if user.login in self.__users:
+ if user.login in self._users:
raise ValueError("already got a user with that login")
- self.__users[user.login] = user
+ self._users[user.login] = user
elif isinstance(action, _UserEditionAction):
# Edit user `action.user` with the given modifications.
if action.login is None:
raise ValueError("missing login")
- if not action.login in self.__users:
+ if not action.login in self._users:
raise ValueError("got no user with login "
f"{repr(action.login)}")
- user = self.__users[action.login]
+ user = self._users[action.login]
if action.name is not False:
user.name = action.name
if action.shell is not False:
@@ -595,22 +599,22 @@ class _FingerFictionalInterface(_FingerInterface):
if action.login is None:
raise ValueError("missing login")
- if not action.login in self.__users:
+ if not action.login in self._users:
raise ValueError("got no user with login "
f"{repr(action.login)}")
- del self.__users[action.login]
+ del self._users[action.login]
elif isinstance(action, _UserLoginAction):
# Login as user `action.login` with session `action.session`.
session = _copy.deepcopy(action.session)
- session.start = self.__start + time
+ session.start = self._start + time
if action.login is None:
raise ValueError("missing login")
try:
- user = self.__users[action.login]
+ user = self._users[action.login]
except KeyError:
raise ValueError("got no user with login "
f"{repr(action.login)}") from None
@@ -624,7 +628,7 @@ class _FingerFictionalInterface(_FingerInterface):
raise ValueError("missing login")
try:
- user = self.__users[action.login]
+ user = self._users[action.login]
except KeyError:
raise ValueError("got no user with login " \
f"{repr(action.login)}") from None
@@ -641,7 +645,7 @@ class _FingerFictionalInterface(_FingerInterface):
raise ValueError("missing login")
try:
- user = self.__users[action.login]
+ user = self._users[action.login]
except KeyError:
raise ValueError("got no user with login " \
f"{repr(action.login)}") from None
@@ -652,7 +656,7 @@ class _FingerFictionalInterface(_FingerInterface):
raise ValueError(f"got no session {repr(action.name)} "
f"for user {repr(action.login)}") from None
- since = self.__start + time
+ since = self._start + time
if action.idle:
session.idle_since = since
else:
@@ -682,7 +686,7 @@ class FingerFictionInterface(_FingerFictionalInterface):
# Initialize the object properties.
if isinstance(fiction, FingerFiction):
- self.__as = _copy.copy(fiction)
+ self._as = _copy.copy(fiction)
elif fiction is not None:
raise TypeError("fiction should be a FingerFiction or None, "
f"is {fiction.__class__.__name__}.")
@@ -691,24 +695,24 @@ class FingerFictionInterface(_FingerFictionalInterface):
# - `fiction`: the script to follow.
# - `current`: the last update time.
- self.__fiction = fiction
- self.__current = None
+ self._fiction = fiction
+ self._current = None
- def __get_time(self):
+ def _get_time(self):
""" Get the current timedelta using the origin and duration. """
now = _dt.now()
- if self.__fiction is not None:
+ if self._fiction is not None:
# Either 'stop' or 'interrupt'.
# These cases only are distinguishable after the
# end of the fiction.
- if now > self.start + self.__fiction.duration:
- if self.__fiction.type == 'stop':
+ if now > self.start + self._fiction.duration:
+ if self._fiction.type == 'stop':
exit(0)
- return self.__fiction.duration
+ return self._fiction.duration
# We're within the duration of the fiction, so we just return the
# offset from the start.
@@ -718,22 +722,22 @@ class FingerFictionInterface(_FingerFictionalInterface):
def update(self):
""" Update the state according to the script. """
- time = self.__get_time()
+ time = self._get_time()
- if self.__fiction is not None:
+ if self._fiction is not None:
# Check if we need to reset (no users left and apply the
# actions from before the start).
- if self.__current is None or self.__current > time:
+ if self._current is None or self._current > time:
self.reset()
- self.__current = None
+ self._current = None
# Then apply the actions up to the current time.
- for time, action in self.__fiction.get(to = time,
- since = self.__current):
+ for time, action in self._fiction.get(to = time,
+ since = self._current):
self.apply(action, time)
- self.__current = time
+ self._current = time
# End of file.
diff --git a/fingerd/server.py b/fingerd/server.py
new file mode 100755
index 0000000..395a37a
--- /dev/null
+++ b/fingerd/server.py
@@ -0,0 +1,1064 @@
+#!/usr/bin/env python3
+#**************************************************************************
+# Copyright (C) 2017-2021 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the fingerd project, which is MIT-licensed.
+#**************************************************************************
+""" The main server class for the server, with the related utilities that
+ will not be presented to the user, is defined in this file. """
+
+import string as _string
+import socket as _socket
+import signal as _signal
+import sys as _sys
+import copy as _copy
+import multiprocessing as _multip
+import socketserver as _socketserver
+
+from enum import Enum as _Enum
+from io import TextIOWrapper as _TextIOWrapper, StringIO as _StringIO
+from datetime import datetime as _dt, timedelta as _td
+
+from .errors import (
+ NoBindsError as _NoBindsError,
+ InvalidBindError as _InvalidBindError,
+ HostnameError as _HostnameError)
+
+__all__ = [
+ 'FingerServer',
+ 'FingerInterface', 'FingerFormatter', 'FingerLogger',
+ 'FingerUser', 'FingerSession'
+]
+
+# ---
+# Basic representations.
+# ---
+
+class FingerUser:
+ """ User description for the finger interface. """
+
+ def __init__(self, *_, login = None, name = None,
+ home = None, shell = None):
+ self._login = None
+ self._name = ''
+ self._home = None
+ self._shell = None
+ self._office = None
+ self._plan = None
+ self._last_login = None
+ self._sessions = _FingerSessionManager()
+
+ self.login = login
+ self.name = name
+ self.home = home
+ self.shell = shell
+
+ def __repr__(self):
+ p = ('login', 'name', 'home', 'shell', 'office',
+ 'last_login', 'sessions')
+ p = (f"{x} = {repr(getattr(self, x))}" for x in p \
+ if getattr(self, x) is not None)
+ return f"{self._class__.__name__}({', '.join(p)})"
+
+ @property
+ def login(self):
+ """ Login name, e.g. 'cake'. """
+
+ return self._login
+
+ @login.setter
+ def login(self, value):
+ self._login = value
+
+ @property
+ def name(self):
+ """ Full user name, e.g. 'Jean Dupont'. """
+
+ return self._name
+
+ @name.setter
+ def name(self, value):
+ self._name = value
+
+ @property
+ def last_login(self):
+ """ Last login date. """
+
+ return self._last_login
+
+ @last_login.setter
+ def last_login(self, value):
+ self._last_login = None if value is None else \
+ value if isinstance(value, _dt) else _dt(value)
+
+ @property
+ def home(self):
+ """ The user's home. """
+
+ return self._home
+
+ @home.setter
+ def home(self, value):
+ self._home = None if value is None else str(value)
+
+ @property
+ def shell(self):
+ """ The user's shell. """
+
+ return self._shell
+
+ @shell.setter
+ def shell(self, value):
+ self._shell = None if value is None else str(value)
+
+ @property
+ def office(self):
+ """ The user's office. """
+
+ return self._office
+
+ @office.setter
+ def office(self, value):
+ self._office = None if value is None else str(value)
+
+ @property
+ def plan(self):
+ """ The user's plan. """
+
+ return self._plan
+
+ @plan.setter
+ def plan(self, value):
+ if value is None:
+ self._plan = None
+ else:
+ value = str(value)
+ self._plan = '\n'.join(value.splitlines())
+
+ @property
+ def sessions(self):
+ """ Current sessions. """
+
+ return self._sessions
+
+
+class _FingerSessionManager:
+ """ Session manager. """
+
+ def __init__(self):
+ self._sessions = []
+
+ def __repr__(self):
+ return repr(self._sessions)
+
+ def __bool__(self):
+ return bool(self._sessions)
+
+ def __iter__(self):
+ return iter(self._sessions[::-1])
+
+ def __delitem__(self, key):
+ if key is None:
+ self._sessions.pop(0)
+ else:
+ for i in [i for i, x in enumerate(self._sessions) \
+ if key == x.name][::-1]:
+ self._sessions.pop(i)
+
+ def __getitem__(self, key):
+ if key is None:
+ try:
+ return self._sessions[0]
+ except IndexError:
+ raise KeyError("could not get latest session") from None
+
+ if type(key) is int:
+ try:
+ return self._sessions[key]
+ except IndexError:
+ msg = f"could not get session #{repr(key)}"
+ raise IndexError(msg) from None
+
+ try:
+ return next(x for x in self._sessions if key == x.name)
+ except StopIteration:
+ raise KeyError(f"could not get session {repr(key)}") from None
+
+ def __setitem__(self, key, value):
+ if not isinstance(value, FingerSession):
+ raise TypeError("can only add sessions into a session manager")
+ value = _copy.deepcopy(value)
+
+ if key is None:
+ # Check if except the first session, the key of the session,
+ # if any, does not override another key.
+
+ if value.name is not None:
+ try:
+ next(i for i, x in self._sessions[1:] \
+ if value.name == x.name)
+ except StopIteration:
+ pass
+ else:
+ msg = "value.name overrides another session's key"
+ raise ValueError(msg) from None
+
+ try:
+ self._sessions[0] = value
+ return
+ except IndexError:
+ raise KeyError("could not set latest session") from None
+
+ if type(key) is int:
+ # Check if except the key-th session, the key of the session,
+ # if any, does not override another key.
+
+ if value.name is not None:
+ try:
+ next(i for i, x in self._sessions \
+ if i != key and value.name == x.name)
+ except StopIteration:
+ pass
+ else:
+ msg = "value.name overrides another session's key"
+ raise ValueError(msg) from None
+
+ try:
+ self._sessions[key] = value
+ return
+ except IndexError:
+ msg = f"could not set session #{repr(key)}"
+ raise IndexError(msg) from None
+
+ value.name = key
+
+ try:
+ i = next(i for i, x in enumerate(self._sessions) \
+ if key == x.name)
+ except StopIteration:
+ raise KeyError(f"could not set session {repr(key)}") from None
+
+ self._sessions[i] = value
+
+ def add(self, session):
+ """ Add a session. """
+
+ if not isinstance(session, FingerSession):
+ raise TypeError("can only insert sessions into a session "
+ "manager")
+ if session.name is not None:
+ try:
+ next(i for i, x in self._sessions \
+ if session.name == x.name)
+ except StopIteration:
+ pass
+ else:
+ msg = "session.name overrides another session's key"
+ raise ValueError(msg) from None
+
+ self._sessions.insert(0, session)
+
+
+class FingerSession:
+ """ Session for a user. """
+
+ def __init__(self, *_, time = _dt.now()):
+ self._start = time if isinstance(time, _dt) else _dt(time)
+ self._line = None
+ self._host = None
+ self._idle = self._start
+
+ def __repr__(self):
+ p = ('start', 'line', 'orig', 'idle')
+ p = (f"{x} = {repr(getattr(self, x))}" for x in p \
+ if getattr(self, x) is not None)
+ return f"{self._class__.__name__}({', '.join(p)})"
+
+ @property
+ def start(self):
+ """ The session start time. """
+
+ return self._start
+
+ @start.setter
+ def start(self, value):
+ self._start = value if isinstance(value, _dt) else _dt(value)
+
+ @property
+ def line(self):
+ """ The line on which the user is. """
+
+ return self._line
+
+ @line.setter
+ def line(self, value):
+ self._line = None if value is None else str(value)
+
+ @property
+ def host(self):
+ """ The host from which the user is connected. """
+
+ return self._host
+
+ @host.setter
+ def host(self, value):
+ self._host = None if value is None else str(value)
+
+# ---
+# Formatter base class.
+# ---
+
+class FingerFormatter:
+ """ Answer formatter.
+ Formats the answer following RFC 1288. """
+
+ # ---
+ # Internal formatting utilities.
+ # ---
+
+ def _format_idle(self, idle):
+ """ Format an idle time delta. """
+
+ def _iter_idle(idle):
+ days = int(idle.days)
+ hours = int(idle.seconds / 3600)
+ mins = int(idle.seconds % 3600 / 60)
+ secs = int(idle.seconds % 60)
+
+ if days:
+ yield f"{days} day{('', 's')[days > 1]}"
+ if hours:
+ yield f"{hours} hour{('', 's')[hours > 1]}"
+ if mins:
+ yield f"{mins} minute{('', 's')[mins > 1]}"
+ if secs:
+ yield f"{secs} second{('', 's')[secs > 1]}"
+
+ return f"{' '.join(_iter_idle(idle))} idle"
+
+ def _format_time(self, d):
+ """ Format a date and time. """
+
+ if d < _td():
+ return ""
+
+ days = int(d.days)
+ hours = int(d.seconds / 3600)
+ mins = int(d.seconds % 3600 / 60)
+
+ if days:
+ return f"{days}d"
+ elif hours or mins:
+ return f"{hours:02}:{mins:02}"
+
+ return ""
+
+ def _format_when(self, d):
+ """ Format a date and time for 'when'. """
+
+ return d.strftime("%a %H:%M")
+
+ # ---
+ # Used formatting functions.
+ # ---
+
+ def format_query_error(self, name):
+ """ There is an error in your query! """
+
+ return "Site: {}\r\n" \
+ "You have made a mistake in your query!\r\n".format(name)
+
+ def format_header(self, name, request):
+ """ Formats the header of each request. """
+
+ if request:
+ request = ' ' + request
+
+ return f"Site: {name}\r\n" f"Command line:{request}\r\n" "\r\n"
+
+ def format_footer(self):
+ """ Formats the footer of each request (in case the developer
+ wants to add something at the end, anything). """
+
+ return ""
+
+ def format_short(self, users):
+ """ Format a user list in a short fashion. """
+
+ if not users:
+ return "No user list available.\r\n"
+
+ lst = []
+ for user in users:
+ if not user.sessions:
+ lst.append((user, None))
+ for session in user.sessions:
+ lst.append((user, session))
+
+ now = _dt.now()
+
+ _login = lambda u, s: u.login
+ _name = lambda u, s: u.name
+ _line = lambda u, s: s.line if s and s.line else ''
+ _idle = lambda u, s: self._format_time(now - s.idle) if s else ''
+ _logt = lambda u, s: self._format_when(s.start) if s else ''
+ _offic = lambda u, s: u.office if u.office else ''
+
+ columns = (
+ ('Login',) + tuple(_login(u, s) for u, s in lst),
+ ('Name',) + tuple( _name(u, s) for u, s in lst),
+ ('TTY',) + tuple( _line(u, s) for u, s in lst),
+ ('Idle',) + tuple( _idle(u, s) for u, s in lst),
+ ('When',) + tuple( _logt(u, s) for u, s in lst),
+ ('Office',) + tuple(_offic(u, s) for u, s in lst))
+
+ sizes = tuple(max(map(len, c)) + 1 for i, c in enumerate(columns))
+ align = ('<', '<', '<', '^', '^', '<')
+
+ lines = []
+ for line in range(len(columns[0])):
+ lines.append(' '.join(\
+ f"{columns[i][line][:sizes[i]]:{align[i]}{sizes[i]}}" \
+ for i in range(len(columns))))
+
+ return '\r\n'.join(lines) + '\r\n'
+
+ def format_long(self, users):
+ """ Format a user list in a long fashion. """
+
+ if not users:
+ return "No user list available.\r\n"
+
+ res = ''
+
+ for user in users:
+ res += (""
+ f"Login name: {user.login[:27]:<27} Name: {user.name}\r\n"
+ f"Directory: {user.home[:28]:<28} Shell: {user.shell}\r\n"
+ f"Office: {user.office if user.office else ''}\r\n")
+
+ if user.sessions:
+ # List current sessions.
+
+ for se in user.sessions:
+ since = se.start.strftime("%a %b %e %R")
+ tz = str(_dt.utcnow().astimezone().tzinfo)
+ res += f"On since {since} ({tz}) on {se.line}"
+ if se.host is not None:
+ res += f" from {se.host}"
+ res += "\r\n"
+
+ idle = _dt.now() - se.idle
+ if idle >= _td(seconds = 4):
+ res += f" {self._format_idle(idle)}\r\n"
+ elif user.last_login is not None:
+ # Show last login.
+
+ date = user.last_login.strftime("%a %b %e %R")
+ tz = str(_dt.utcnow().astimezone().tzinfo)
+ res += f"Last login {date} ({tz}) on console\r\n"
+ else:
+ res += "Never logged in.\r\n"
+
+ if user.plan is None:
+ res += "No plan.\r\n"
+ else:
+ res += "Plan:\r\n"
+ res += "\r\n".join(user.plan.splitlines())
+ res += "\r\n"
+
+ res += "\r\n"
+
+ return res
+
+# ---
+# Interface (dummy) base class.
+# ---
+
+class FingerInterface:
+ """ Finger interface to the users.
+ Expandable (the current class defines the dummy interface). """
+
+ def transmit_query(self, user, host, verbose):
+ return "This server won't transmit finger queries.\r\n"
+
+ def search_users(self, check):
+ return []
+
+# ---
+# Server logger base class.
+# ---
+
+class FingerLogger:
+ """ Finger logger for the server.
+ Expandable, but the base class should be enough for most needs. """
+
+ def __init__(self, stream = _sys.stderr):
+ self._stream = stream
+ self._lock = _multip.Lock()
+
+ def _log(self, fmt, *args):
+ self._lock.acquire()
+ print('\r' + fmt.format(*args), file=self._stream)
+ self._lock.release()
+
+ def _logr(self, src, fmt, *args):
+ self._log('[{}] ' + fmt, src, *args)
+
+ def start(self, host, port):
+ self._log("Starting fingerd on [{}]:{}.", host, port)
+
+ def stop(self, host, port):
+ self._log("Stopping fingerd on [{}]:{}.", host, port)
+
+ def no_query(self, source):
+ self._logr(source, "no query. (possible scan)")
+
+ def bad_query(self, source):
+ self._logr(source, "bad request.")
+
+ def could_not_answer(self, source):
+ self._logr(source, "could not write the answer.")
+
+ def transmit_list(self, source, host):
+ self._logr(source, "transmit user list query to `{}`.", host)
+
+ def transmit(self, source, username, host):
+ self._logr(source, "transmit user query for `{}` to `{}`.",
+ username, host)
+
+ def search_users(self, source, username):
+ self._logr(source, "look for user `{}`.", username)
+
+ def list(self, source):
+ self._logr(source, "list connected users.")
+
+# ---
+# Bind-related configuration.
+# ---
+
+class _BindAddress:
+ """ Bind address for fingerd. """
+
+ class Type(_Enum):
+ """ Bind address type. """
+
+ """ TCP on IPv4 bind. """
+ TCP_IPv4 = 1
+
+ """ TCP on IPv6 bind. """
+ TCP_IPv6 = 2
+
+ """ IPC (Unix socket) bind. """
+ IPC = 3
+
+ def __init__(self, family):
+ self._family = self.Type(family)
+
+ def __repr__(self):
+ return f"{self._class__.__name__}(family = {self._family})"
+
+ @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:
+ 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:
+ 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 fingerd.
+ Takes a raw string and the protocol name, either 'finger' (the base
+ protocol managed by the class) or 'fingerd-control' (the protocol
+ used for controlling the live fingerd interface). """
+
+ def __init__(self, raw, proto = 'finger'):
+ proto = proto.casefold()
+ if proto not in ('finger', 'fingerd-control'):
+ 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', 'fingerd-control': 'ipc'}[proto]
+ else:
+ # just don't add the ':' of ':/' again
+ x = '/' + ':/'.join(rest)
+
+ if (proto == 'finger' and scheme != 'tcp') \
+ or scheme not in ('tcp', 'ipc'):
+ raise _InvalidBindError(addr, "unsupported scheme "
+ f"{repr(scheme)} for protocol {repr(proto)}")
+
+ # 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 _InvalidBindAddressError(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:
+ try:
+ assert x[1:] != ''
+ port = _socket.getservbyname(x[1:])
+ except:
+ raise _InvalidBindError(addr,
+ "expected a valid port number or name " \
+ f"(got {repr(x[1:])})") from None
+ else:
+ raise _InvalidBindAddressError(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('.')
+ assert 2 <= len(ip) <= 4
+
+ ip = list(map(int, ip))
+ assert all(lambda x: 0 <= x < 256, ip)
+
+ 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:
+ 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.
+# ---
+
+class _FingerQuery:
+ """ A finger query. Requests information about connected or specific
+ users on a remote server.
+
+ There are three types of requests recognized by RFC 1288:
+
+ * {C} is a request for a list of all online users.
+ * {Q1} is a request for a local user.
+ * {Q2} is a request for a distant user (with hostname).
+
+ /W means the RUIP (program answering the query) should be more
+ verbose (this token can be ignored). """
+
+ # "By default, this program SHOULD filter any unprintable data,
+ # leaving only printable 7-bit characters (ASCII 32 through
+ # ASCII 126), tabs (ASCII 9) and CRLFs."
+
+ allowed_chars = ("\t !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
+ + _string.ascii_letters + _string.digits)
+
+ def __init__(self, raw):
+ """ Initialize the query object by decoding the data. """
+
+ # Get a character string out of the query.
+
+ raw = ''.join(c for c in raw if c in self.allowed_chars)
+ self.line = raw
+
+ # Get elements.
+
+ self.host = None
+ self.username = None
+ self.verbose = False
+ for element in raw.split():
+ if element[0] == '/':
+ if 'W' in element[1:]:
+ self.verbose = True
+ continue
+ elif self.username is not None:
+ raise Exception
+ self.username = element
+
+ if self.username is not None and '@' in self.username:
+ self.host, *self.username = self.username.split('@')[::-1]
+ self.username = '@'.join(self.username[::-1])
+
+
+class _FingerTCPHandler(_socketserver.StreamRequestHandler):
+ """ The TCP request handler for our finger daemon.
+ Instantiated once per connection to the server (request). """
+
+ def handle(self):
+ """ Request handling. """
+
+ inp = _TextIOWrapper(self.rfile, encoding = 'ascii',
+ errors = 'ignore', newline = '\r\n')
+ outp = _TextIOWrapper(self.wfile, encoding = 'ascii')
+
+ # Gather the request line.
+
+ try:
+ line = inp.readline()
+ except ConnectionResetError:
+ self.logger.no_query(self.src)
+ return
+
+ # Decode the request.
+
+ ans = _StringIO()
+
+ try:
+ query = None
+ query = _FingerQuery(line)
+ except:
+ self.logger.bad_query(self.src)
+ ans.write(self.fmt.format_query_error(self.host))
+ return
+
+ if query is not None:
+ self.answer(query, ans)
+
+ # Write the output.
+
+ try:
+ outp.write(ans.getvalue())
+ except ConnectionResetError:
+ self.logger.could_not_answer(self.src)
+
+ @property
+ def fmt(self):
+ """ The answer formatter. """
+
+ return self.server.params['formatter']
+
+ @property
+ def iface(self):
+ """ The interface formatter. """
+
+ return self.server.params['interface']
+
+ @property
+ def host(self):
+ """ The host. """
+
+ return self.server.params['host']
+
+ @property
+ def logger(self):
+ """ The logger. """
+
+ return self.server.params['logger']
+
+ @property
+ def src(self):
+ """ The source IP address. """
+
+ return self.client_address[0]
+
+ def answer(self, query, outp):
+ """ Answer the query.
+ `query` is the `_FingerQuery` instance.
+ `outp` is the output stream.
+
+ This method is thought to be overriden someday. """
+
+ ans = self.fmt.format_header(self.host, query.line)
+
+ # Gather the content.
+
+ if query.host is not None:
+ if query.username:
+ self.logger.transmit(self.src, query.username, query.host)
+ else:
+ self.logger.transmit_list(self.src, query.host)
+
+ cont = self.iface.transmit_query(query.host, query.username,
+ query.verbose)
+ ans += cont
+ else:
+ if query.username:
+ self.logger.search_users(self.src, query.username)
+ check = lambda x: query.username in x.login
+ else:
+ self.logger.list(self.src)
+ check = lambda x: x.sessions
+
+ users = self.iface.search_users(check)
+
+ if query.username or query.verbose:
+ ans += self.fmt.format_long(users)
+ else:
+ ans += self.fmt.format_short(users)
+
+ # Send the answer.
+
+ ans += self.fmt.format_footer()
+ outp.write(ans)
+
+
+class _IPv4TCPServer(_socketserver.ThreadingMixIn,
+ _socketserver.TCPServer):
+ """ The IPv4 TCP Server to be used """
+
+ address_family = _socket.AF_INET
+ allow_reuse_address = True
+
+ def __init__(self, *args, params = {}, **kwargs):
+ self.params = params
+ super().__init__(*args, **kwargs)
+
+
+class _IPv6TCPServer(_socketserver.ThreadingMixIn,
+ _socketserver.TCPServer):
+ """ The IPv6 TCP Server to be used """
+
+ address_family = _socket.AF_INET6
+ allow_reuse_address = True
+
+ def __init__(self, *args, params = {}, **kwargs):
+ self.params = params
+ super().__init__(*args, **kwargs)
+
+
+class FingerServer:
+ """ The Finger Server class. """
+
+ def __init__(self, binds = 'localhost:79', hostname = 'LOCALHOST',
+ interface = FingerInterface(), formatter = FingerFormatter(),
+ logger = FingerLogger()):
+ # Check the host name.
+
+ try:
+ hostname = hostname.upper()
+ assert all(c in _string.ascii_letters + _string.digits + '.-'
+ for c in hostname)
+ except:
+ raise _HostnameError(hostname)
+
+ # Check the interface and formatter classes.
+
+ if not isinstance(interface, FingerInterface):
+ raise TypeError("please base your interface "
+ "on the base class provided by the fingerd module")
+
+ if not isinstance(formatter, FingerFormatter):
+ raise TypeError("please base your formatter "
+ "on the base class provided by the fingerd module")
+
+ if not isinstance(logger, FingerLogger):
+ raise TypeError("please base your logger "
+ "on the base class provided by the fingerd module")
+
+ # Keep the parameters.
+
+ self._host = hostname
+ self._logger = logger
+ self._interface = interface
+ self._formatter = formatter
+
+ # Check the addresses.
+
+ self._servers = []
+
+ for bind in _BindsDecoder(binds):
+ self._servers.append([bind.runserver_params, None])
+
+ if not self._servers:
+ raise _NoBindsError()
+
+ def start(self):
+ """ Bind and start the underlying servers. """
+
+ def run_server(family, addr, port):
+ """ Run the server (will be run in another thread). """
+
+ logger = self._logger
+
+ # Make the server corresponding to the entry.
+
+ params = {
+ 'host': self._host,
+ 'interface': self._interface,
+ 'formatter': self._formatter,
+ 'logger': self._logger
+ }
+
+ if family == _socket.AF_INET:
+ server = _IPv4TCPServer((addr, port), _FingerTCPHandler,
+ params = params)
+ elif family == _socket.AF_INET6:
+ server = _IPv6TCPServer((addr, port), _FingerTCPHandler,
+ params = params)
+
+ # Run it.
+
+ logger.start(addr, port)
+ try: server.serve_forever()
+ except KeyboardInterrupt:
+ pass
+ logger.stop(addr, port)
+
+ for entry in self._servers:
+ # Check if the thread already exists and runs.
+
+ if entry[1] != None and not entry[1].is_alive():
+ continue
+
+ # Start the thread.
+
+ entry[1] = _multip.Process(target = run_server,
+ args = entry[0])
+ entry[1].start()
+
+ def stop(self):
+ """ Stop the underlying servers. """
+
+ for entry in self._servers:
+ # Check if the thread is still here.
+
+ if entry[1] == None:
+ continue
+ if not entry[1].is_alive():
+ entry[1] = None
+ continue
+
+ # Stop it.
+
+ entry[1].join()
+ entry[1] = None
+
+ def serve_forever(self):
+ """ Serve forever. """
+
+ self.start()
+
+ try:
+ while True:
+ _signal.pause()
+ except KeyboardInterrupt:
+ pass
+
+ self.stop()
+
+ def shutdown(self):
+ """ Shutdown the server, alias to `.stop()`. """
+
+ self.stop()
+
+# End of file.
diff --git a/fingerd/version.py b/fingerd/version.py
new file mode 100644
index 0000000..4c7f345
--- /dev/null
+++ b/fingerd/version.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python3
+#**************************************************************************
+# Copyright (C) 2021 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the fingerd project, which is MIT-licensed.
+#**************************************************************************
+""" fingerd version definition. """
+
+version = "0.1"
+
+# End of file.
diff --git a/sample-scenarios/actions.toml b/sample-scenarios/actions.toml
new file mode 100644
index 0000000..30f851b
--- /dev/null
+++ b/sample-scenarios/actions.toml
@@ -0,0 +1,72 @@
+[[-120j]]
+type = "create"
+login = "rinehart"
+name = "Mark J. Rinehart"
+shell = "/bin/bash"
+home = "/home/rinehart"
+office = "019 Hill"
+
+[[-24j]]
+type = "create"
+login = "cake"
+name = "Thomas Touhey"
+shell = "/bin/zsh"
+home = "/home/cake"
+office = "none, +33621612972"
+plan = "../.plan"
+
+[[-22j]]
+type = "update"
+login = "cake"
+name = "Thomas Adrian Touhey"
+office = false
+plan = false
+
+[[-6j]]
+type = "create"
+login = "blebneh"
+name = "Bleb K. Neh"
+shell = "/bin/sh"
+home = "/home/bleb"
+office = "019 Hill 908-932-"
+
+[[-2j6h20m]]
+type = "login"
+login = "cake"
+line = "pts/0"
+host = "192.168.1.77"
+
+[[-2j5h20m]]
+type = "idle"
+login = "cake"
+
+[[-2h38s]]
+type = "login"
+login = "cake"
+line = "pts/1"
+host = "192.168.1.77"
+
+[[-10s]]
+type = "login"
+login = "rinehart"
+line = "ttyS3"
+
+[[10s]]
+type = "delete"
+login = "blebneh"
+
+[[30s]]
+type = "logout"
+login = "rinehart"
+
+[[40s]]
+type = "logout"
+login = "cake"
+
+[[45s]]
+type = "active"
+login = "cake"
+
+[[1m30s]]
+type = "idle"
+login = "cake"
diff --git a/setup.cfg b/setup.cfg
index 651ed7a..4ac1313 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = fingerd
-version = attr: fingerd.version
+version = attr: fingerd.version.version
url = https://fingerd.touhey.pro/
author = Thomas Touhey
author_email = thomas@touhey.fr
@@ -26,6 +26,7 @@ scripts =
scripts/fingerd-control
install_requires =
toml
+ click
[options.package_data]
* = *.txt, *.rst