diff options
author | Thomas Touhey <thomas@touhey.fr> | 2021-09-04 17:18:41 +0200 |
---|---|---|
committer | Thomas Touhey <thomas@touhey.fr> | 2021-09-04 17:18:41 +0200 |
commit | 99c8256289363fd437a1f350eeb42faf35a2a317 (patch) | |
tree | 31896a5fbbc323f49940ea06f4fa12f86848f006 | |
parent | 5ee802842794f112977b7b9273e35f45f2bf8a88 (diff) |
Continued documenting and stuff.
-rw-r--r-- | .env.template | 38 | ||||
-rw-r--r-- | Pipfile | 1 | ||||
-rw-r--r-- | docs/Makefile | 3 | ||||
-rw-r--r-- | docs/Pipfile | 1 | ||||
-rw-r--r-- | docs/Pipfile.lock | 82 | ||||
-rw-r--r-- | docs/api.rst | 9 | ||||
-rw-r--r-- | docs/api/config.rst | 231 | ||||
-rw-r--r-- | docs/api/core.rst | 54 | ||||
-rw-r--r-- | docs/cli.rst | 110 | ||||
-rw-r--r-- | docs/conf.py | 15 | ||||
-rw-r--r-- | docs/discuss.rst | 11 | ||||
-rw-r--r-- | docs/discuss/fictional-interfaces.rst | 132 | ||||
-rw-r--r-- | docs/discuss/fingerd-structure.rst | 36 | ||||
-rw-r--r-- | docs/explain.rst | 6 | ||||
-rw-r--r-- | docs/howto.rst | 7 | ||||
-rw-r--r-- | docs/index.rst | 4 | ||||
-rw-r--r-- | docs/onboarding.rst | 20 | ||||
-rw-r--r-- | docs/onboarding/installing.rst | 41 | ||||
-rw-r--r-- | docs/onboarding/running.rst | 81 | ||||
-rw-r--r-- | docs/onboarding/tweaking.rst | 16 | ||||
-rwxr-xr-x | fingerd/__init__.py | 2 | ||||
-rw-r--r-- | fingerd/cli.py | 7 | ||||
-rwxr-xr-x | fingerd/control/__init__.py | 182 | ||||
-rwxr-xr-x | fingerd/control/__main__.py | 16 | ||||
-rwxr-xr-x | fingerd/core.py (renamed from fingerd/server.py) | 548 | ||||
-rwxr-xr-x | fingerd/fiction.py | 14 | ||||
-rw-r--r-- | fingerd/version.py | 2 | ||||
-rw-r--r-- | setup.cfg | 2 |
28 files changed, 969 insertions, 702 deletions
diff --git a/.env.template b/.env.template deleted file mode 100644 index 869fd37..0000000 --- a/.env.template +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh -# FINGERD ENVIRONMENT -# =================== -# Address and port on which to open the server. -# The BIND variable is required, otherwise old-style configuration is -# assumed (although old-style configuration is deprecated). -# -# It is recommended to use another port than 79 as described in the README, -# but the default port is still 79 as it is the standard port for -# the finger protocol. - -BIND=localhost:79 -#FINGER_HOST=LOCALHOST - -# Interface type, i.e. what we actually want to have as the data behind the -# server. There are three possible interface types: -# - DUMMY: no users are connected. -# - NATIVE: the data is gathered from the system. -# - SCENARIO: the data is gathered from a scenario. -# - LIVE: the data is gathered from a live source, here a nanomsg endpoint. - -#FINGER_TYPE=NATIVE - -# For actions, the scenario path must be given in the following variable, -# absolute or relative to the current working directory. - -#FINGER_SCENARIO=actions.toml - -# For a live server receiving events, there must be an endpoint where -# endpoints are received. This endpoint is using nanomsg's REQREP protocol, -# so a nanomsg URI is expected, amongst: - -#FINGER_INCOMING=ipc:///var/run/fingerd.sock -#FINGER_INCOMING=tcp://127.0.0.1:5560 -#FINGER_INCOMING=tcp://[::1]:5560 -#FINGER_INCOMING=tcp://localhost:5560 - -# That's it, your done with the finger server configuration! :-) @@ -8,7 +8,6 @@ python_version = '3.9' [packages] python-dotenv = '*' -nanomsg = '*' toml = '*' click = '*' diff --git a/docs/Makefile b/docs/Makefile index 972c107..f754cdf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -27,7 +27,8 @@ help: # Livehtml build. livehtml: - $(SPHINXWATCH) -b html $(SPHINXOPTS) . $(BUILDDIR)/html + $(SPHINXWATCH) -b html $(SPHINXOPTS) . $(BUILDDIR)/html \ + --ignore "**/.*.kate-swp" --watch ../fingerd .PHONY: livehtml diff --git a/docs/Pipfile b/docs/Pipfile index bca103b..efdb5a1 100644 --- a/docs/Pipfile +++ b/docs/Pipfile @@ -9,6 +9,7 @@ verify_ssl = true sphinx = "*" sphinx-rtd-theme = "*" sphinx-autobuild = "*" +sphinx-autodoc-typehints = "*" [requires] python_version = "3.9" diff --git a/docs/Pipfile.lock b/docs/Pipfile.lock index 9003dd3..6a33722 100644 --- a/docs/Pipfile.lock +++ b/docs/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "94b3326dd69ba0924a8e8ba1a51f1972bceebacbd4daa2f0c7200a78f86e7f35" + "sha256": "9e53fd6e9c8d06e445ecf8cdf0d568ef01a8e987bb88ea841878ddf582def9d1" }, "pipfile-spec": 6, "requires": { @@ -38,13 +38,13 @@ ], "version": "==2021.5.30" }, - "chardet": { + "charset-normalizer": { "hashes": [ - "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", - "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", + "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==4.0.0" + "markers": "python_version >= '3'", + "version": "==2.0.4" }, "colorama": { "hashes": [ @@ -64,11 +64,11 @@ }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", + "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.10" + "markers": "python_version >= '3'", + "version": "==3.2" }, "imagesize": { "hashes": [ @@ -99,30 +99,50 @@ "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", @@ -134,19 +154,19 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.0" }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], "markers": "python_version >= '3.5'", - "version": "==2.9.0" + "version": "==2.10.0" }, "pyparsing": { "hashes": [ @@ -165,11 +185,11 @@ }, "requests": { "hashes": [ - "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", - "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.25.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==2.26.0" }, "six": { "hashes": [ @@ -188,11 +208,11 @@ }, "sphinx": { "hashes": [ - "sha256:b5c2ae4120bf00c799ba9b3699bc895816d272d120080fbc967292f29b52b48c", - "sha256:d1cb10bee9c4231f1700ec2e24a91be3f3a3aba066ea4ca9f3bbe47e59d5a1d4" + "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", + "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" ], "index": "pypi", - "version": "==4.0.2" + "version": "==4.1.2" }, "sphinx-autobuild": { "hashes": [ @@ -202,6 +222,14 @@ "index": "pypi", "version": "==2021.3.14" }, + "sphinx-autodoc-typehints": { + "hashes": [ + "sha256:193617d9dbe0847281b1399d369e74e34cd959c82e02c7efde077fca908a9f52", + "sha256:5e81776ec422dd168d688ab60f034fccfafbcd94329e9537712c93003bddc04a" + ], + "index": "pypi", + "version": "==1.12.0" + }, "sphinx-rtd-theme": { "hashes": [ "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a", @@ -307,11 +335,11 @@ }, "urllib3": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" ], "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" + "version": "==1.26.6" } }, "develop": {} diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..f257cca --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,9 @@ +API Reference +============= + +If you are looking for information on a specific function, class or method, +this part of the documentation is for you. + +.. toctree:: + + api/core diff --git a/docs/api/config.rst b/docs/api/config.rst deleted file mode 100644 index 978d848..0000000 --- a/docs/api/config.rst +++ /dev/null @@ -1,231 +0,0 @@ -Configuring fingerd -=================== - -``fingerd`` stores its configuration in the environment. For commodity, -a ``.env`` configuration is proposed, install the ``python-dotenv`` module -and copy the ``.env.template`` file into ``.env`` and edit it following -its comments. - -``BIND`` - This variable is mandatory and contains the endpoints to bind on, - separated with commas (``,``). Each endpoint can have the following - format: - - ``example.com`` - Bind on all addresses that ``example.com`` resolve as (IPv4 and IPv6), - port 79. - - ``example.com:1234`` - Bind on all addresses that ``example.com`` resolve as (IPv4 and IPv6), - port 1234. - - ``1.2.3.4`` or ``[1.2.3.4]`` - Bind on ``1.2.3.4`` (IPv4), port 79. - - ``1.2.3.4:1234`` or ``[1.2.3.4]:1234`` - Bind on ``1.2.3.4`` (IPv4), port 1234. - - ``::1:2:3:4`` or ``[::1:2:3:4]`` - Bind on ``::1:2:3:4`` (IPv6) port 79. - - ``[::1:2:3:4]:1234`` - Bind on ``::1:2:3:4`` (IPv6) port 1234. - - Here are some examples: - - .. code-block:: bash - - # On modern platforms, binds on 127.0.0.1:79 (IPv4) - # and [::1]:79 (IPv6). - BIND=localhost - - # Binds on 1.2.3.4:3331 and [2001:41d0:302:2200::3b2]:79. - BIND="1.2.3.4:3331,[2001:41d0:302:2200::3b2]:79" - BIND="1.2.3.4:3331,2001:41d0:302:2200::3b2" - -``FINGER_HOST`` - The host as which fingerd identifies itself (domain name), ``LOCALHOST`` - by default. - -``FINGER_TYPE`` - The interface type, or where the displayed data comes from. There are - several interface types: - - ``DUMMY`` - There is no data (no users, no sessions). - - ``NATIVE`` - The data is gathered from the system. - - ``SCENARIO`` - The data is gathered from a scenario (see :ref:`scenario` below). - - ``LIVE`` - The data is gathered from actions given in an IPC endpoint (see - :ref:`live`). - - By default, the interface type is ``NATIVE``. - -If the interface type is ``SCENARIO``, then the following variable is -required: - -``FINGER_SCENARIO`` - The scenario path (see :ref:`actions` for the format). - -``FINGER_INCOMING`` - The finger live action source (see :ref:`live`). - -The finger server should be available through the TCP port 79, which can only -be opened by ``root`` on UNIX-like machines. Instead of using this port -directly, which forces the use of the ``root`` user to manage the service, -you can redirect the incoming transmissions from an interface on TCP port 79 -to another TCP port which you can open as a user (port number over 1024) -by appending a rule in ``iptables``: - -.. code-block:: sh - - iptables -t nat -A OUTPUT -p tcp [-s <source ip>] [-d <destination ip>] \ - --dport 79 -j DNAT --to '<ip:port>' - -Where ``<source ip>`` is the source IP address or network that are redirected, -``<destination ip>`` is the destination IP address or network for which the -requests are redirected and ``<ip:port>`` is the IP and port -you want to forward the packets to, e.g. ``127.0.0.1:4000``. - -For example, for running the server locally on port 3999 and only accepting -requests from the same machine (on IPv4 and IPv6): - -.. code-block:: sh - - 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' - -.. _actions: - -Actions -------- - -While interfaces provide still information to the server, some use actions -underneath. In this section, the actions themselves are described. - -Flow-related actions -~~~~~~~~~~~~~~~~~~~~ - -The following are related to the action flow: - -``interrupt`` - The server freezes on the latest situation. - -``stop`` - The server stops on the event. - -All the actions after the time of any of these will be ignored. - -User-related actions -~~~~~~~~~~~~~~~~~~~~ - -User-related actions' types can be of the following: - -``create`` - A user has been created. - -``update`` - A user has been updated. - -``delete`` - A user has been deleted. - -As all of these actions are about users, they all take an additional -``login`` argument which is the affected user's name, e.g. ``rinehart``. - -The ``create`` and ``update`` event takes some more arguments: - -``name`` - The user full name, e.g. “Mark J. Rinehart”. - -``shell`` - The selected login shell. - -``home`` - The home directory. - -``office`` - The user's office name, e.g. “B121 on second floor”. - -``plan`` - The plan path. - -For an ``update`` action, setting properties to ``false`` will erase their -previous value without setting a new one. - -Session-related actions -~~~~~~~~~~~~~~~~~~~~~~~ - -Session-related actions' types can be of the following: - -``login`` - A user has logged in and is active. - -``logout`` - A user has logged out. - -``idle`` - A user is now idle (not typing on the keyboard anymore). - -``active`` - A user is now active (typing every now and then). - -These actions take an additional argument ``login`` which is the user to -which the session belongs, and an optional other ``session`` argument to -identify the session for which the event is in the case of multiple sessions -for the user. - -The ``login`` operation takes the information about the originating shell: - -``line`` - The physical line on which the user is connected. - -``host`` - The remote host from which the physical line is opened, if any. - -.. _scenario: - -Scenario (scripted actions) ---------------------------- - -Scenarios, represented by the ``SCENARIO`` server type, are scripted -sequences of actions. - -Scenarios use a TOML document describing actions, which are points in time -where something happens. Every action has a time offset, using a TOML -array section (``[[something]]``), and properties describing what's -happening. Time offsets are represented the following way: - -:: - - -?(<weeks>w)?(<days>[jd])?(<hours>h)?(<minutes>m)?(<seconds>s)? - -Where negative times, starting with a dash (``-``), are the initial situation, -what is supposed to have happened before the beginning. - -For example, ``-1w5d2h`` means “1 week, 5 days and 2 hours before the -origin” and ``2j`` means “2 days after the origin”. So if we want to make -an action that takes place 5 seconds after the origin, the first line of the -action will be the following one: - -.. code-block:: - - [[5s]] - -All actions have a type represented by the ``type`` property, and other -properties depending on the type. Types and related properties are -described in the :ref:`actions` section. - -.. _live: - -Live actions ------------- - -TODO diff --git a/docs/api/core.rst b/docs/api/core.rst new file mode 100644 index 0000000..1fea63c --- /dev/null +++ b/docs/api/core.rst @@ -0,0 +1,54 @@ +Core classes +============ + +.. py:module:: fingerd.core + +These classes constitute the core of the fingerd module. + +Base representations +-------------------- + +The following classes are objects used by other classes to represent users and +sessions. + +.. autoclass:: FingerUser + :members: login, name, last_login, home, shell, office, plan, + sessions + +.. autoclass:: FingerSession + :members: start, line, host, idle + +The base interface class +------------------------ + +The following class is subclassed for making server interface classes. +For more information, consult :ref:`discuss-interfaces`. + +.. autoclass:: FingerInterface + :members: transmit_query, search_users + +The base formatter class +------------------------ + +The following class is subclassed for making formatter classes. +For more information, consult :ref:`discuss-formatters`. + +.. autoclass:: FingerFormatter + :members: format_query_error, format_short, format_long, + _format_header, _format_footer + +The base logger class +--------------------- + +The following class is subclassed for making logger classes. +For more information, consult :ref:`discuss-loggers`. + +.. autoclass:: FingerLogger + :members: start, stop, no_query, bad_query, could_not_answer, + transmit_list, transmit, search_users, list + +The server object +----------------- + +.. autoclass:: FingerServer + :members: start, stop, serve_forever diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 0000000..caa70a8 --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,110 @@ +.. _cli: + +Configuration options +===================== + +When run through CLI, fingerd exposes a number of configuration options using +its own code. Each configuration option is configurable through three +cumulative means: + + * A UNIX-style command-line option. + * An environment variable set directly. + * An environment variable set through a dotenv file (``.env``). + +When a configuration option is given through multiple means, the resolution +order of such the value is the one given above. + +Binding +------- + +As for all network servers, fingerd will bind one or more network ports and +answer to queries from all of the bound ports. These network ports are +defined through the following option: + + * The ``-b`` or ``--binds`` CLI option. + * The ``BIND`` or ``BINDS`` environment variable. + +This option must be defined one way or another, and must contain the list +of ports to bind separated with commas (``,``). Each port must have one of +the following formats: + +``example.com`` + Bind on all addresses that ``example.com`` resolve as (IPv4 and IPv6), + port 79. + +``example.com:1234`` + Bind on all addresses that ``example.com`` resolve as (IPv4 and IPv6), + port 1234. + +``1.2.3.4`` or ``[1.2.3.4]`` + Bind on ``1.2.3.4`` (IPv4), port 79. + +``1.2.3.4:1234`` or ``[1.2.3.4]:1234`` + Bind on ``1.2.3.4`` (IPv4), port 1234. + +``::1:2:3:4`` or ``[::1:2:3:4]`` + Bind on ``::1:2:3:4`` (IPv6) port 79. + +``[::1:2:3:4]:1234`` + Bind on ``::1:2:3:4`` (IPv6) port 1234. + +Here are some examples: + +.. code-block:: bash + + # On modern platforms, binds on 127.0.0.1:79 (IPv4) + # and [::1]:79 (IPv6). + BIND=localhost + + # Binds on 1.2.3.4:3331 and [2001:41d0:302:2200::3b2]:79. + BIND="1.2.3.4:3331,[2001:41d0:302:2200::3b2]:79" + BIND="1.2.3.4:3331,2001:41d0:302:2200::3b2" + +Setting the hostname +-------------------- + +The fingerd server answers queries with a given hostname to identify itself. +This hostname can be configured through the following option: + + * The ``-H`` or ``--hostname`` CLI option. + * The ``FINGER_HOST`` environment variable. + +It is optional and defined to ``LOCALHOST`` by default. + +Defining the server type +------------------------ + +For answering queries, the information provided by the server can come from +different sources. The server type represents the source of the information, +and can be configured through the following option: + + * The ``-t`` or ``--type`` CLI option. + * The ``FINGER_TYPE`` environment variable. + +The possible values for this option are the following: + +``DUMMY`` + There is no data (no users, no sessions). + +``NATIVE`` + The data is gathered from the system. + +``SCENARIO`` + The data is gathered from a scenario (see :ref:`scenario` below). + +By default, the interface type is ``NATIVE``. If an invalid server type is +provided, the server is considered a dummy server. + +Defining the scenario +--------------------- + +.. todo:: + + Link to the format description for the scenario format? + +When the server is a scenario server, the path to a scenario in the TOML +format must be provided. This path must then be configured through the +following option: + + * The ``-s`` or ``--scenario`` CLI option. + * The ``FINGER_ACTIONS`` environment variable. diff --git a/docs/conf.py b/docs/conf.py index fcbd235..9f30bd2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,16 +6,15 @@ # full list see the documentation: # http://www.sphinx-doc.org/en/master/config +import os, sys + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- @@ -45,7 +44,9 @@ release = _get_release() # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc' + 'sphinx.ext.autodoc', + 'sphinx_autodoc_typehints', + 'sphinx.ext.todo' ] # Add any paths that contain templates here, relative to this directory. @@ -70,11 +71,13 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', '.goutput*', '*.kate-swp'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' +todo_include_todos = True + # -- Options for HTML output ------------------------------------------------- diff --git a/docs/discuss.rst b/docs/discuss.rst new file mode 100644 index 0000000..513b249 --- /dev/null +++ b/docs/discuss.rst @@ -0,0 +1,11 @@ +Discussion topics +================= + +fingerd has a number of concepts necessary for a full understanding of its +conception and an efficient use of it. You can find these concepts and +discussion topics in the following sections. + +.. toctree:: + + discuss/fingerd-structure + discuss/fictional-interfaces diff --git a/docs/discuss/fictional-interfaces.rst b/docs/discuss/fictional-interfaces.rst new file mode 100644 index 0000000..0113ea1 --- /dev/null +++ b/docs/discuss/fictional-interfaces.rst @@ -0,0 +1,132 @@ +.. _fictional-interfaces: + +Fictional interfaces +==================== + +One of the ambitions for fingerd was to be able to run scenarios for inclusion +in alternate-reality games (ARG). + +.. todo:: + + Describe live actions as well? + +.. _actions: + +Scenario +-------- + +While interfaces provide still information to the server, some use actions +underneath. In this section, the actions themselves are described. + +Flow-related actions +~~~~~~~~~~~~~~~~~~~~ + +The following are related to the action flow: + +``interrupt`` + The server freezes on the latest situation. + +``stop`` + The server stops on the event. + +All the actions after the time of any of these will be ignored. + +User-related actions +~~~~~~~~~~~~~~~~~~~~ + +User-related actions' types can be of the following: + +``create`` + A user has been created. + +``update`` + A user has been updated. + +``delete`` + A user has been deleted. + +As all of these actions are about users, they all take an additional +``login`` argument which is the affected user's name, e.g. ``rinehart``. + +The ``create`` and ``update`` event takes some more arguments: + +``name`` + The user full name, e.g. “Mark J. Rinehart”. + +``shell`` + The selected login shell. + +``home`` + The home directory. + +``office`` + The user's office name, e.g. “B121 on second floor”. + +``plan`` + The plan path. + +For an ``update`` action, setting properties to ``false`` will erase their +previous value without setting a new one. + +Session-related actions +~~~~~~~~~~~~~~~~~~~~~~~ + +Session-related actions' types can be of the following: + +``login`` + A user has logged in and is active. + +``logout`` + A user has logged out. + +``idle`` + A user is now idle (not typing on the keyboard anymore). + +``active`` + A user is now active (typing every now and then). + +These actions take an additional argument ``login`` which is the user to +which the session belongs, and an optional other ``session`` argument to +identify the session for which the event is in the case of multiple sessions +for the user. + +The ``login`` operation takes the information about the originating shell: + +``line`` + The physical line on which the user is connected. + +``host`` + The remote host from which the physical line is opened, if any. + +.. _scenario: + +Scenario (scripted actions) +--------------------------- + +Scenarios, represented by the ``SCENARIO`` server type, are scripted +sequences of actions. + +Scenarios use a TOML document describing actions, which are points in time +where something happens. Every action has a time offset, using a TOML +array section (``[[something]]``), and properties describing what's +happening. Time offsets are represented the following way: + +:: + + -?(<weeks>w)?(<days>[jd])?(<hours>h)?(<minutes>m)?(<seconds>s)? + +Where negative times, starting with a dash (``-``), are the initial situation, +what is supposed to have happened before the beginning. + +For example, ``-1w5d2h`` means “1 week, 5 days and 2 hours before the +origin” and ``2j`` means “2 days after the origin”. So if we want to make +an action that takes place 5 seconds after the origin, the first line of the +action will be the following one: + +.. code-block:: + + [[5s]] + +All actions have a type represented by the ``type`` property, and other +properties depending on the type. Types and related properties are +described in the :ref:`actions` section. diff --git a/docs/discuss/fingerd-structure.rst b/docs/discuss/fingerd-structure.rst new file mode 100644 index 0000000..f0e2e90 --- /dev/null +++ b/docs/discuss/fingerd-structure.rst @@ -0,0 +1,36 @@ +fingerd structure +================= + +fingerd's conception is centered around the server class, +:py:class:`fingerd.server.FingerServer`, which itself makes use of +three other classes: + + * An interface, which provides the data presented by the server to the client. + * A formatter, which takes data obtained by the server through its interface + and represents it using text. + * A logger, which is called by the server for each operation to log server + and request events. + +.. _discuss-interfaces: + +Interfaces +---------- + +Interfaces in fingerd are subclasses of +:py:class:`fingerd.server.FingerInterface`. + +.. _discuss-formatters: + +Formatters +---------- + +Formatters in fingerd are subclasses of +:py:class:`fingerd.server.FingerFormatter`. + +.. _discuss-loggers: + +Loggers +------- + +Loggers in fingerd are subclasses of +:py:class:`fingerd.server.FingerLogger`. diff --git a/docs/explain.rst b/docs/explain.rst deleted file mode 100644 index c173121..0000000 --- a/docs/explain.rst +++ /dev/null @@ -1,6 +0,0 @@ -Discussion topics -================= - -.. todo:: - - Discuss finger concepts, and stuff. diff --git a/docs/howto.rst b/docs/howto.rst deleted file mode 100644 index 5ecb176..0000000 --- a/docs/howto.rst +++ /dev/null @@ -1,7 +0,0 @@ -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 87b7ef1..4c0795f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,8 +12,8 @@ project homepage at `fingerd.touhey.pro <https://fingerd.touhey.pro/>`_! :maxdepth: 2 onboarding - howto - explain + discuss + cli api .. _RFC 742: https://tools.ietf.org/html/rfc742 diff --git a/docs/onboarding.rst b/docs/onboarding.rst index 767dc47..c47ebd3 100644 --- a/docs/onboarding.rst +++ b/docs/onboarding.rst @@ -1,17 +1,13 @@ Onboarding ========== -``fingerd`` can be run directly and without configuration (while taking its -default option values) with the following command: +You're a new user trying to figure out what you can and cannot do with +fingerd, and you're willing to experiment? You're at the right place! +In this section, you will be able to install, run and start tweaking +fingerd to better suit your needs. -.. code-block:: sh +.. toctree:: - python3 -m fingerd <command line options> - -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. + onboarding/installing + onboarding/running + onboarding/tweaking diff --git a/docs/onboarding/installing.rst b/docs/onboarding/installing.rst new file mode 100644 index 0000000..4996516 --- /dev/null +++ b/docs/onboarding/installing.rst @@ -0,0 +1,41 @@ +Installing fingerd +================== + +In order to run and tweak fingerd, you must first install it; this section +will cover the need. + +Dependencies +------------ + +fingerd dependencies are pure Python dependencies, automatically installed +when using a package manager such as pip: + + * `python-dotenv`_, used for loading ``.env`` files when the CLI is not + a suitable option for configuration and the environment is harder to + manipulate from the program running fingerd. + * `toml`_, used for reading scenarios for :ref:`fiction interfaces <fictional-interfaces>`. + * `click`_, used for implementing the command-line interfaces. + +Installing fingerd using pip +---------------------------- + +To install fingerd, you can use pip with the following command: + +.. code-block:: sh + + python -m pip install fingerd + +Some notes on this command: + + * On most Linux distributions, you can directly call ``pip`` (or ``pip3`` + on those where Python 2.x is still the default); I personnally prefer + to call it through Python as a module. + * On Linux and other UNIX-like distributions where Python 2.x is still the + default, when Python 3.x is installed, you must usually call it using + ``python3`` instead of ``python``. + * On Microsoft Windows, the Python executable, when added to the PATH, + goes by the name ``py`` instead of ``python``. + +.. _python-dotenv: https://pypi.org/project/python-dotenv/ +.. _toml: https://pypi.org/project/toml/ +.. _click: https://pypi.org/project/click/ diff --git a/docs/onboarding/running.rst b/docs/onboarding/running.rst new file mode 100644 index 0000000..7d7e38c --- /dev/null +++ b/docs/onboarding/running.rst @@ -0,0 +1,81 @@ +Running fingerd +=============== + +Once installed, you can directly run fingerd with its default options +through the following command: + +.. code-block:: sh + + python3 -m fingerd + +By default, this will run a finger server on TCP port 79 for both Internet +protocols (IPv4 and IPv6) if available, returning native information if +implemented or dummy information (no users) if not. + +Using a different port +---------------------- + +Due to historical reasons, on UNIX-like systems, by default, TCP ports below +1024 are considered "privileged ports", which means a program needs to be +run as root (uid 0) to bind that port. Although on Linux, this is configurable +(see `ip_unprivileged_port_start`_), it is rarely done in practice. + +However, running a network server program in root is considered a bad security +practice. Usually, application servers are run on a custom unprivileged port +(usually 3000, 5000 or 8000 in my experience) and a load balancer, usually +Apache or nginx for HTTP and HTTPS services, redirects traffic to that custom +port. + +Although fingerd seems harmless, it is recommended to run it as an unprivileged +user that only can access the required data, usually session information from +the host in its default configuration. In order for it to still be able to +listen to requests and answer on TCP port 79, one possibility is to attribute +a custom port to it, such as 3999, and redirect all inbound traffic on port 79 +to that custom port using iptables. This can be done by appending a rule to +the NAT table using a command such as the following: + +.. code-block:: sh + + iptables -t nat -A OUTPUT -p tcp [-s <source ip>] [-d <destination ip>] --dport 79 -j DNAT --to '<ip:port>' + +For example, if the fingerd server is running on port 3999, we can use both +these commands to redirect traffic to that server: + +.. code-block:: sh + + iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 79 -j DNAT --to 127.0.0.1:3999 + ip6tables -t nat -A OUTPUT -p tcp -d ::1 --dport 79 -j DNAT --to '[::1]:3999' + +Or, if you only want to accept request from localhost, you can also make use of +the ``-s`` option to only accept traffic from localhost: + +.. code-block:: sh + + 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' + +However, you must think of listening on port 3999 on the command-line, by +using the following command-line option: + +.. code-block:: sh + + python3 -m fingerd -b localhost:3999 + # OR + BIND=localhost:3999 python3 -m fingerd + +See :ref:`cli` for more information. + +Setting the hostname +-------------------- + +By default, fingerd uses the hostname ``localhost`` when answering to requests. +Say you want the server to answer with the ``EXAMPLE.ORG`` hostname. You can +do so using the following command-line option: + +.. code-block:: sh + + python3 -m fingerd -H example.org + # OR + FINGER_HOST=example.org python3 -m fingerd + +.. _`ip_unprivileged_port_start`: https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt diff --git a/docs/onboarding/tweaking.rst b/docs/onboarding/tweaking.rst new file mode 100644 index 0000000..cab8e12 --- /dev/null +++ b/docs/onboarding/tweaking.rst @@ -0,0 +1,16 @@ +Tweaking fingerd +================ + +In order to start tweaking fingerd using Python instead of the CLI, you +can import utilities from the module. The minimal code for running the +server is the following: + +.. code-block:: python + + from fingerd import FingerServer + + server = FingerServer() + server.serve_forever() + +For more information, please consult the discussion topics and API reference +on the current documentation. diff --git a/fingerd/__init__.py b/fingerd/__init__.py index a43600c..29d5afd 100755 --- a/fingerd/__init__.py +++ b/fingerd/__init__.py @@ -18,7 +18,7 @@ from sys import stderr as _stderr from .version import version from .errors import * -from .server import * +from .core import * from .fiction import * # --- diff --git a/fingerd/cli.py b/fingerd/cli.py index be665a4..d7b5b12 100644 --- a/fingerd/cli.py +++ b/fingerd/cli.py @@ -13,7 +13,6 @@ 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) @@ -31,9 +30,7 @@ __all__ = ['cli'] 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): +def cli(binds, hostname, type, scenario): """ fingerd is a modern finger (RFC 1288) server. Find out more at <https://fingerd.touhey.pro/>. """ @@ -47,8 +44,6 @@ def cli(binds, hostname, type, scenario, incoming): 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", diff --git a/fingerd/control/__init__.py b/fingerd/control/__init__.py deleted file mode 100755 index ee2a493..0000000 --- a/fingerd/control/__init__.py +++ /dev/null @@ -1,182 +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 `fingerd.control` submodule is a module for adding actions to the - fingerd live interface. """ - -from sys import stderr as _stderr -from argparse import ArgumentParser as _ArgumentParser - -__all__ = ["run", - "FingerLiveClient", "DELETE"] - -# --- -# The special DELETE value. -# --- - -class _DeleteClass: - pass - -DELETE = _DeleteClass() - -# --- -# The client. -# --- - -class FingerControlClient: - """ Client for sending actions to the fingerd live interface. """ - - def __init__(self, url): - # FIXME: open the connection. - - pass - - def create_user(self, login, name = None, home = None, shell = None, - office = None, plan = None): - """ Create a user. """ - - raise NotImplementedError - - def edit_user(self, login, name = None, home = None, shell = None, - office = None, plan = None): - """ Edit properties for a user. """ - - raise NotImplementedError - - def delete_user(self, login): - """ Delete a user. """ - - raise NotImplementedError - - def login(self, login, session = None, line = None, host = None): - """ Login to a finger session. """ - - raise NotImplementedError - - def idle(self, login, session = None): - """ Make the user idle on a finger session. """ - - raise NotImplementedError - - def active(self, login, session = None): - """ Make the user active again on a finger session. """ - - raise NotImplementedError - - def logout(self, login, session = None): - """ Log a user out of a finger session. """ - - raise NotImplementedError - -# --- -# The main function for the module. -# --- - -def run(): - """ Main function for the submodule. """ - - # Non-interactive command-line interface. - - ap = _ArgumentParser(prog = 'fingerd-control', - description = 'Control the fingerd daemon live interface.') - ap.add_argument('-t', '--to', help = ('interface provided by the ' - 'daemon'), default = 'ipc:///var/run/fingerd.sock') - usap = ap.add_subparsers(metavar = 'any command', dest = 'command', - required = True) - ssap = usap # until i find out how to have “menus”, sorta… - - cap = usap.add_parser('create_user', help = 'create a user') - cap.add_argument('login', help = 'the user\'s login') - cap.add_argument('-n', '--name', help = "the user's name", - default = None) - cap.add_argument('-H', '--home', help = "the user's home directory", - default = None) - cap.add_argument('-s', '--shell', help = "the user's shell", - default = None) - cap.add_argument('-o', '--office', help = "the user's office", - default = None) - cap.add_argument('-p', '--plan', help = "the user's plan", - default = None) - - eap = usap.add_parser('edit_user', help = 'edit a user') - eap.add_argument('login', help = 'the user\'s login') - eap.add_argument('-n', '--name', help = "the user's name", - default = None) - eap.add_argument('-H', '--home', help = "the user's home", - default = None) - eap.add_argument('-s', '--shell', help = "the user's shell", - default = None) - eap.add_argument('-o', '--office', help = "the user's office", - default = None) - eap.add_argument('-p', '--plan', help = "the user's plan", - default = None) - - dap = usap.add_parser('delete_user', help = 'delete a user') - dap.add_argument('login', help = "the user's login") - - lap = ssap.add_parser('login', help = 'log in a user') - lap.add_argument('login', help = "the user's login") - lap.add_argument('session', help = "the session's identifier", - default = None, nargs = '?') - lap.add_argument('-l', '--line', help = "the user's physical line") - lap.add_argument('-H', '--host', help = "the remote host of the line") - - gap = ssap.add_parser('logout', help = 'log out a user') - gap.add_argument('login', help = "the user's login") - gap.add_argument('session', help = "the session's identifier", - default = None, nargs = '?') - - iap = ssap.add_parser('idle', help = "make a user go idle") - iap.add_argument('login', help = "the user's login") - iap.add_argument('session', help = "the session's identifier", - default = None, nargs = '?') - - aap = ssap.add_parser('active', help = "make a user go active") - aap.add_argument('login', help = "the user's login") - aap.add_argument('session', help = "the session's identifier", - default = None, nargs = '?') - - args = ap.parse_args() - args = vars(args) - - # Get the client and execute the command. - - ua = {} - if 'home' in args and args['home'] is not None: - ua['home'] = args['home'] - if 'name' in args and args['name'] is not None: - ua['name'] = args['name'] - if 'office' in args and args['office'] is not None: - ua['office'] = args['office'] - if 'plan' in args and args['plan'] is not None: - ua['plan'] = args['plan'] - if 'shell' in args and args['shell'] is not None: - ua['shell'] = args['shell'] - - sa = {} - if 'session' in args and args['session'] is not None: - sa['session'] = args['session'] - if 'line' in args and args['line'] is not None: - sa['line'] = args['line'] - if 'host' in args and args['host'] is not None: - sa['host'] = args['host'] - - clnt = FingerControlClient(args['to']) - if args['command'] == 'create_user': - clnt.create_user(args['login'], **ua) - elif args['command'] == 'edit_user': - clnt.edit_user(args['login'], **ua) - elif args['command'] == 'delete_user': - clnt.delete_user(args['login']) - elif args['command'] == 'login': - clnt.login(args['login'], **sa) - elif args['command'] == 'logout': - clnt.logout(args['login'], **sa) - elif args['command'] == 'idle': - clnt.idle(args['login'], **sa) - elif args['command'] == 'active': - clnt.active(args['logout'], **sa) - -# End of file. diff --git a/fingerd/control/__main__.py b/fingerd/control/__main__.py deleted file mode 100755 index df21d37..0000000 --- a/fingerd/control/__main__.py +++ /dev/null @@ -1,16 +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. -#************************************************************************** -""" Main script of the control client module. - Runs the client depending on the configuration content. """ - -from . import run as _run - -__all__ = [] - -if __name__ == '__main__': - _run() - -# End of file. diff --git a/fingerd/server.py b/fingerd/core.py index 395a37a..e3bc08b 100755 --- a/fingerd/server.py +++ b/fingerd/core.py @@ -17,6 +17,8 @@ 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 typing import Optional as _Optional +from collections.abc import Sequence as _Sequence from .errors import ( NoBindsError as _NoBindsError, @@ -33,11 +35,102 @@ __all__ = [ # Basic representations. # --- +class FingerSession: + """ Active session on the system for a given user. + + :param time: The start time of the given session; + by default, the current datetime. """ + + __slots__ = ('_start', '_line', '_host', '_idle') + + def __init__(self, *_, time: _Optional[_dt] = None): + self._start = _dt.now() + self._idle = None + self._line = None + self._host = None + + self.start = time + self._idle = self._start + + def __repr__(self): + p = ('start', 'idle', 'line', 'orig') + 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) -> _dt: + """ The timestamp at which the session has started. + + Note that when set, the timezone is stripped out of the + provided datetime. """ + + return self._start + + @start.setter + def start(self, value): + value = value if isinstance(value, _dt) else _dt(value) + value = value.replace(tzinfo = None) + + self._start = value + + @property + def idle(self) -> _dt: + """ The timestamp since which the user is idle on the session. + + Note that when set, the timezone is stripped out of the + provided datetime; also, if the provided datetime is before + the session start timestamp, it is set to it. """ + + return self._idle + + @idle.setter + def idle(self, value): + value = value if isinstance(value, _dt) else _dt(value) + value = value.replace(tzinfo = None) + + if value < self._start: + value = self._start + + self._idle = value + + @property + def line(self) -> _Optional[str]: + """ 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) -> _Optional[str]: + """ 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) + + class FingerUser: - """ User description for the finger interface. """ + """ User description, returned by subclasses of + :py:class:`FingerInterface` and used by subclasses of + :py:class:`FingerFormatter`. + + :param login: The login of the user. + :param name: The display name of the user. + :param home: The path to the home of the user. + :param shell: The path to the user's default shell. """ + + __slots__ = ('_login', '_name', '_home', '_shell', '_office', + '_plan', '_last_login', '_sessions') - def __init__(self, *_, login = None, name = None, - home = None, shell = None): + def __init__(self, *_, login: _Optional[str] = None, + name: _Optional[str] = None, home: _Optional[str] = None, + shell: _Optional[str] = None): self._login = None self._name = '' self._home = None @@ -60,74 +153,79 @@ class FingerUser: return f"{self._class__.__name__}({', '.join(p)})" @property - def login(self): - """ Login name, e.g. 'cake'. """ + def login(self) -> _Optional[str]: + """ The login name of the user, e.g. 'cake' or 'gaben'. """ return self._login @login.setter - def login(self, value): + def login(self, value: _Optional[str]) -> None: self._login = value @property - def name(self): - """ Full user name, e.g. 'Jean Dupont'. """ + def name(self) -> _Optional[str]: + """ The display name of the user, e.g. 'Jean Dupont'. """ return self._name @name.setter - def name(self, value): + def name(self, value: _Optional[str]) -> None: self._name = value @property - def last_login(self): - """ Last login date. """ + def last_login(self) -> _Optional[str]: + """ The last login date for the user, None if not known. """ return self._last_login @last_login.setter - def last_login(self, value): + def last_login(self, value: _Optional[str]) -> None: 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. """ + def home(self) -> _Optional[str]: + """ The path to the user's home on the given system, None if + not known or defined. """ return self._home @home.setter - def home(self, value): + def home(self, value: _Optional[str]) -> None: self._home = None if value is None else str(value) @property - def shell(self): - """ The user's shell. """ + def shell(self) -> _Optional[str]: + """ The path to the user's shell on the given system, None if + not known or defined. """ return self._shell @shell.setter - def shell(self, value): + def shell(self, value: _Optional[str]) -> None: self._shell = None if value is None else str(value) @property - def office(self): - """ The user's office. """ + def office(self) -> _Optional[str]: + """ The display name of the user's office, None if not known + or defined. """ return self._office @office.setter - def office(self, value): + def office(self, value: _Optional[str]) -> None: self._office = None if value is None else str(value) @property - def plan(self): - """ The user's plan. """ + def plan(self) -> _Optional[str]: + """ The plan of the user, usually the content of the ``.plan`` + file in the user's home on real (and kind of obsolete) UNIX-like + systems. """ return self._plan @plan.setter - def plan(self, value): + def plan(self, value: _Optional[str]) -> None: if value is None: self._plan = None else: @@ -135,8 +233,8 @@ class FingerUser: self._plan = '\n'.join(value.splitlines()) @property - def sessions(self): - """ Current sessions. """ + def sessions(self) -> _Sequence[FingerSession]: + """ The current sessions array for the user, always defined. """ return self._sessions @@ -144,6 +242,8 @@ class FingerUser: class _FingerSessionManager: """ Session manager. """ + __slots__ = ('_sessions') + def __init__(self): self._sessions = [] @@ -257,65 +357,31 @@ class _FingerSessionManager: 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. """ + """ Formatter for :py:class:`FingerServer`. + Provides text-formatted (as strings limited to ASCII) + answers for given queries with given results as objects. + + This class must be subclassed by other formatters. + Only methods not starting with an underscore are called by + instances of :py:class:`FingerServer`; others are utilities + called by these. + + Unless methods are overridden to have a different behaviour, + this formatter aims at RFC 1288 compliance. """ + + def __repr__(self): + return f"{self.__class__.__name__}()" # --- # Internal formatting utilities. # --- - def _format_idle(self, idle): + def _format_idle(self, idle: _dt) -> str: """ Format an idle time delta. """ def _iter_idle(idle): @@ -335,7 +401,7 @@ class FingerFormatter: return f"{' '.join(_iter_idle(idle))} idle" - def _format_time(self, d): + def _format_time(self, d: _dt) -> str: """ Format a date and time. """ if d < _td(): @@ -352,37 +418,56 @@ class FingerFormatter: return "" - def _format_when(self, d): + def _format_when(self, d: _dt) -> str: """ 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! """ + def _format_header(self, hostname: str, raw_query: str) -> str: + """ Returns the header of the formatted answer for every request, + except when an error has occurred in the user's query. - return "Site: {}\r\n" \ - "You have made a mistake in your query!\r\n".format(name) + :param hostname: The hostname configured for the server. + :param raw_query: The raw query given by the user. + :return: The header of the formatted answer as text. """ - def format_header(self, name, request): - """ Formats the header of each request. """ + if raw_query: + raw_query = ' ' + raw_query - if request: - request = ' ' + request + return f"Site: {hostname}\r\n" f"Command line:{raw_query}\r\n" "\r\n" - return f"Site: {name}\r\n" f"Command line:{request}\r\n" "\r\n" + def _format_footer(self) -> str: + """ Returns the footer of the formatted answer for every request, + except when an error has occurred in the user's query. - def format_footer(self): - """ Formats the footer of each request (in case the developer - wants to add something at the end, anything). """ + :return: The footer of the formatted answer as text. """ return "" - def format_short(self, users): - """ Format a user list in a short fashion. """ + # --- + # Used formatting functions. + # --- + + def format_query_error(self, hostname: str, raw_query: str) -> str: + """ Returns the formatted answer for when an error has occurred + in the user's query. + + :param hostname: The hostname configured for the server. + :param raw_query: The raw query given by the user. + :return: The formatted answer as text. """ + + return "Site: {}\r\n" \ + "You have made a mistake in your query!\r\n".format(hostname) + + def format_short(self, hostname: str, raw_query: str, + users: _Sequence[FingerUser]) -> str: + """ Returns the formatted answer for a user list in the 'short' + format. + + :param hostname: The hostname configured for the server. + :param raw_query: The raw query given by the user. + :param users: The user list. + :return: The formatted answer as text. """ if not users: return "No user list available.\r\n" @@ -420,10 +505,20 @@ class FingerFormatter: f"{columns[i][line][:sizes[i]]:{align[i]}{sizes[i]}}" \ for i in range(len(columns)))) - return '\r\n'.join(lines) + '\r\n' + return ( + self._format_header(hostname, raw_query) + + '\r\n'.join(lines) + '\r\n' + + self._format_footer()) - def format_long(self, users): - """ Format a user list in a long fashion. """ + def format_long(self, hostname: str, raw_query: str, + users: _Sequence[FingerUser]) -> str: + """ Returns the formatted answer for a user list in the 'long' + format. + + :param hostname: The hostname configured for the server. + :param raw_query: The raw query given by the user. + :param users: The user list. + :return: The formatted answer as text. """ if not users: return "No user list available.\r\n" @@ -468,20 +563,57 @@ class FingerFormatter: res += "\r\n" - return res + return ( + self._format_header(hostname, raw_query) + + res + + self._format_footer()) # --- # Interface (dummy) base class. # --- class FingerInterface: - """ Finger interface to the users. - Expandable (the current class defines the dummy interface). """ + """ Data source for :py:class:`FingerServer`. + Provides users and answers for the various queries received + from the clients by the server. + + This class must be subclassed by other interfaces. + Only methods not starting with an underscore are called by + instances of :py:class:`FingerServer`; others are utilities + called by these. + + By default, it behaves like a dummy interface. """ + + def __repr__(self): + return f"{self.__class__.__name__}()" + + def transmit_query(self, query: _Optional[str], host: str, + verbose: bool) -> str: + """ Transmit a user query to a foreign host, and return + the answer formatted by it. + + If used directly (not overridden by subclasses), this + method will refuse to transmit finger queries. + + :param query: The user query, set to None in case of + no query provided by the client. + :param host: The distant host to which to transmit the + query. + :param verbose: Whether the verbose flag (``/W``, long format) + has been passed by the current client + or not. + :return: The answer formatted by the distant server. """ - def transmit_query(self, user, host, verbose): return "This server won't transmit finger queries.\r\n" - def search_users(self, check): + def search_users(self, query: _Optional[str]) -> _Sequence[FingerUser]: + """ Search for users on the current host using the given query. + + :param query: The user query, set to None in case of no + query provided by the client. + :return: The list of users found using the query provided + by the client. """ + return [] # --- @@ -489,48 +621,149 @@ class FingerInterface: # --- class FingerLogger: - """ Finger logger for the server. - Expandable, but the base class should be enough for most needs. """ + """ Logger class for :py:class:`FingerServer`. + Provides methods for allowing custom behaviours on server and + request events. + + This class must be subclassed by other loggers. + Only methods not starting with an underscore are called by + instances of :py:class:`FingerServer`; others are utilities called + by these. + + Unless methods are overridden to have a different behaviour, + this logger emits messages on the given text stream, by default + stderr (standard error output). """ def __init__(self, stream = _sys.stderr): self._stream = stream self._lock = _multip.Lock() - def _log(self, fmt, *args): + def __repr__(self): + return f"{self.__class__.__name__}()" + + # --- + # Internal methods. + # --- + + def _write(self, msg: str) -> None: + """ Write the formatted string on the given stream. """ + self._lock.acquire() - print('\r' + fmt.format(*args), file=self._stream) + print('\r' + msg, file=self._stream) self._lock.release() - def _logr(self, src, fmt, *args): - self._log('[{}] ' + fmt, src, *args) + def _write_s(self, src: str, msg: str) -> None: + """ Write the formatted string and add the source address of + the host to the given stream using + :py:meth:`FingerLogger._write`. """ + + self._write(f'[{src}] {msg}') + + # --- + # Public methods. + # --- + + def start(self, host: str, port: str) -> None: + """ This method is called when the server starts listening on + the given port of the given host. + + :param host: The host on which the server has started + listening (usually an IPv4 or IPv6 address). + :param port: The TCP port on which the server has started + listening. """ + + self._write(f"Starting fingerd on [{host}]:{port}.") - def start(self, host, port): - self._log("Starting fingerd on [{}]:{}.", host, port) + def stop(self, host: str, port: str) -> None: + """ This method is called when the server stops listening on + the given port of the given host. - def stop(self, host, port): - self._log("Stopping fingerd on [{}]:{}.", host, port) + :param host: The host on which the server has stopped + listening (usually an IPv4 or IPv6 address). + :param port: The TCP port on which the server has stopped + listening. """ - def no_query(self, source): - self._logr(source, "no query. (possible scan)") + self._write(f"Stopping fingerd on [{host}]:{port}.") - def bad_query(self, source): - self._logr(source, "bad request.") + def no_query(self, source: str) -> None: + """ This method is called when a connection has been opened with + the server by a client, but immediately closed without + emitting a query. - def could_not_answer(self, source): - self._logr(source, "could not write the answer.") + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). """ - def transmit_list(self, source, host): - self._logr(source, "transmit user list query to `{}`.", host) + self._write_s(source, "no query. (possible scan)") - def transmit(self, source, username, host): - self._logr(source, "transmit user query for `{}` to `{}`.", - username, host) + def bad_query(self, source: str) -> None: + """ This method is called when a client has emitted a bad request. - def search_users(self, source, username): - self._logr(source, "look for user `{}`.", username) + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). """ - def list(self, source): - self._logr(source, "list connected users.") + self._write_s(source, "bad request.") + + def could_not_answer(self, source: str) -> None: + """ This method is called when a client has closed the connection + before or while the server was answering. + + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). """ + + self._write_s(source, "could not write the answer.") + + def transmit_list(self, source: str, host: str): + """ This method is called when a client has requested the server + to transmit a finger request to another finger server, without + a user query. + + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). + :param host: The host to which the user wants the request + to be transmitted. """ + + self._write_s(source, f"transmit user list query to `{host}`.") + + def transmit(self, source: str, username: str, host: str) -> None: + """ This method is called when a client has requested the server + to transmit a finger request to another finger server, with + a user query. + + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). + :param username: The query the user wants transmitted to + the provided host. + :param host: The host to which the user wants the request + to be transmitted. """ + + self._write_s(source, f"transmit user query for `{username}` " + f"to `{host}`.") + + def search_users(self, source: str, username: str) -> None: + """ This method is called when a client has requested a user list + to the current host, with a query provided. + + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). + :param username: The provided user query. """ + + self._write_s(source, f"look for user `{username}`.") + + def list(self, source: str) -> None: + """ This method is called when a client has requested a user list + to the current host, without any provided queries. + + :param source: The source address from which the client + has started the connection (usually an + IPv4 or IPv6 address). """ + + self._write_s(source, "list connected users.") # --- # Bind-related configuration. @@ -548,9 +781,6 @@ class _BindAddress: """ TCP on IPv6 bind. """ TCP_IPv6 = 2 - """ IPC (Unix socket) bind. """ - IPC = 3 - def __init__(self, family): self._family = self.Type(family) @@ -607,13 +837,14 @@ class _TCP6Address(_BindAddress): 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'): + if proto not in ('finger',): raise ValueError(f"unsupported protocol {proto}") self._binds = set() @@ -629,13 +860,13 @@ class _BindsDecoder: # the situation. x = scheme - scheme = {'finger': 'tcp', 'fingerd-control': 'ipc'}[proto] + scheme = {'finger': 'tcp'}[proto] else: # just don't add the ':' of ':/' again x = '/' + ':/'.join(rest) if (proto == 'finger' and scheme != 'tcp') \ - or scheme not in ('tcp', 'ipc'): + or scheme not in ('tcp',): raise _InvalidBindError(addr, "unsupported scheme " f"{repr(scheme)} for protocol {repr(proto)}") @@ -828,7 +1059,7 @@ class _FingerTCPHandler(_socketserver.StreamRequestHandler): query = _FingerQuery(line) except: self.logger.bad_query(self.src) - ans.write(self.fmt.format_query_error(self.host)) + ans.write(self.fmt.format_query_error(self.host, line)) return if query is not None: @@ -878,8 +1109,6 @@ class _FingerTCPHandler(_socketserver.StreamRequestHandler): 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: @@ -902,13 +1131,12 @@ class _FingerTCPHandler(_socketserver.StreamRequestHandler): users = self.iface.search_users(check) if query.username or query.verbose: - ans += self.fmt.format_long(users) + ans = self.fmt.format_long(self.host, query.line, users) else: - ans += self.fmt.format_short(users) + ans = self.fmt.format_short(self.host, query.line, users) # Send the answer. - ans += self.fmt.format_footer() outp.write(ans) @@ -937,11 +1165,24 @@ class _IPv6TCPServer(_socketserver.ThreadingMixIn, class FingerServer: - """ The Finger Server class. """ - - def __init__(self, binds = 'localhost:79', hostname = 'LOCALHOST', - interface = FingerInterface(), formatter = FingerFormatter(), - logger = FingerLogger()): + """ The main finger server class. + + :param binds: The hosts and ports on which the server should + listen to and answer finger requests. + :param hostname: The hostname to be included in answers sent + to clients. + :param interface: The interface to use for querying + users and sessions. + :param formatter: The formatter to use for formatting + answers sent to clients. + :param logger: The logger to use for logging server and + request events. """ + + def __init__(self, binds: str = 'localhost:79', + hostname: str = 'LOCALHOST', + interface: FingerInterface = FingerInterface(), + formatter: FingerFormatter = FingerFormatter(), + logger: FingerLogger = FingerLogger()): # Check the host name. try: @@ -982,8 +1223,9 @@ class FingerServer: if not self._servers: raise _NoBindsError() - def start(self): - """ Bind and start the underlying servers. """ + def start(self) -> None: + """ Bind all ports for the given hosts and start the underlying + servers in separate processes. """ def run_server(family, addr, port): """ Run the server (will be run in another thread). """ @@ -1026,8 +1268,9 @@ class FingerServer: args = entry[0]) entry[1].start() - def stop(self): - """ Stop the underlying servers. """ + def stop(self) -> None: + """ Unbind all ports for the given hosts and stop the underlying + server processes. """ for entry in self._servers: # Check if the thread is still here. @@ -1043,8 +1286,11 @@ class FingerServer: entry[1].join() entry[1] = None - def serve_forever(self): - """ Serve forever. """ + def serve_forever(self) -> None: + """ Shortcut for synchronously starting all servers using + :py:meth:`FingerServer.start`, waiting for an interrupt + signal, and stopping all servers using + :py:meth:`FingerServer.stop`. """ self.start() diff --git a/fingerd/fiction.py b/fingerd/fiction.py index 6d63416..d07b865 100755 --- a/fingerd/fiction.py +++ b/fingerd/fiction.py @@ -11,14 +11,13 @@ import copy as _copy, re as _re, math as _math import itertools as _itertools from datetime import datetime as _dt, timedelta as _td -from .server import ( +from .core import ( FingerInterface as _FingerInterface, FingerUser as _FingerUser, FingerSession as _FingerSession) __all__ = [ "FingerFictionInterface", - "FingerLiveInterface", "FingerFiction"] _toml = None @@ -662,17 +661,6 @@ class _FingerFictionalInterface(_FingerInterface): else: session.active_since = since -class FingerLiveInterface(_FingerFictionalInterface): - """ Fiction interface, where actions are gathered live using an - IPC or inter-host endpoint. """ - - def __init__(self, source, start = _dt.now()): - super().__init__(start) - - # Initialize the object properties. - # FIXME: use the `source` to get the source. - - raise NotImplementedError() class FingerFictionInterface(_FingerFictionalInterface): """ Fiction interface, to repeat a scene which the script diff --git a/fingerd/version.py b/fingerd/version.py index 4c7f345..c368fc7 100644 --- a/fingerd/version.py +++ b/fingerd/version.py @@ -5,6 +5,6 @@ #************************************************************************** """ fingerd version definition. """ -version = "0.1" +version = "0.2" # End of file. @@ -20,7 +20,7 @@ classifiers = [options] zip_safe = False include_package_data = True -packages = fingerd, fingerd.control +packages = fingerd scripts = scripts/fingerd scripts/fingerd-control |