aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas "Cakeisalie5" Touhey <thomas@touhey.fr>2019-02-07 11:16:47 +0100
committerThomas "Cakeisalie5" Touhey <thomas@touhey.fr>2019-02-07 11:16:47 +0100
commit425681857d9d2af3d2e27994b8a3a00439c579f5 (patch)
treee14f93b71e1267e0b54cad190b01ee38a9a7ae1c
parenta378220e143d461897c3d964e5567a0272c68e28 (diff)
Finally got around to correct the binds decoder.
-rw-r--r--.python-version1
-rw-r--r--LICENSE.txt2
-rw-r--r--Pipfile.lock6
-rw-r--r--README.rst3
-rwxr-xr-xfingerd/__main__.py60
-rwxr-xr-xfingerd/_server.py66
-rwxr-xr-xfingerd/_util.py195
-rw-r--r--fingerd/client/__init__.py48
-rwxr-xr-xsetup.py6
9 files changed, 265 insertions, 122 deletions
diff --git a/.python-version b/.python-version
new file mode 100644
index 0000000..a76ccff
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.7.1
diff --git a/LICENSE.txt b/LICENSE.txt
index fedf678..2b2178c 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,7 +1,7 @@
The MIT License (MIT)
=====================
-Copyright (C) 2017-2018 Thomas Touhey <thomas@touhey.fr>
+Copyright (C) 2017-2019 Thomas Touhey <thomas@touhey.fr>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
diff --git a/Pipfile.lock b/Pipfile.lock
index b3706b4..f01ccff 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -18,11 +18,11 @@
"default": {
"python-dotenv": {
"hashes": [
- "sha256:122290a38ece9fe4f162dc7c95cae3357b983505830a154d3c98ef7f6c6cea77",
- "sha256:4a205787bc829233de2a823aa328e44fd9996fedb954989a21f1fc67c13d7a77"
+ "sha256:a84569d0e00d178bc5b957f7ff208bf49287cbf61857c31c258c4a91f571527b",
+ "sha256:c9b1ddd3cdbe75c7d462cb84674d87130f4b948f090f02c7d7144779afb99ae0"
],
"index": "pypi",
- "version": "==0.9.1"
+ "version": "==0.10.1"
},
"toml": {
"hashes": [
diff --git a/README.rst b/README.rst
index 41d2811..5cfbec2 100644
--- a/README.rst
+++ b/README.rst
@@ -56,7 +56,8 @@ its comments.
``BIND``
This variable is mandatory and contains the endpoints to bind on,
- separated with commas. Each endpoint can be of the following format:
+ separated with commas (``,``). Each endpoint can have the following
+ format:
``example.com``
Bind on all addresses that ``example.com`` resolve as (IPv4 and IPv6),
diff --git a/fingerd/__main__.py b/fingerd/__main__.py
index 8aac80c..3910283 100755
--- a/fingerd/__main__.py
+++ b/fingerd/__main__.py
@@ -21,53 +21,9 @@ from . import FingerServer as _FingerServer, \
FingerLiveInterface as _FingerLiveInterface, \
FingerNativeInterface as _FingerNativeInterface
-def get_old_style_server(config_path = None):
- """ Get the server through the deprecated configuration style. """
-
- _cpaths = ('/etc/fingerd/fingerd.ini', '/etc/fingerd.ini',
- _path.normpath(_path.join(_path.dirname(__file__), '..',
- 'fingerd.ini')))
-
- # Load the configuration.
-
- c = _ConfigParser()
- for path in [config_path] if config_path is not None else _cpaths:
- if c.read(path):
- break
- else:
- print("error: could not find a suitable configuration file",
- file = _stderr)
- exit(1)
-
- dirname = _path.dirname(path)
-
- bind = c['server']['bind']
- host = c['server']['host']
- iface = c['server']['type']
-
- if iface == 'native':
- iface = _FingerNativeInterface()
- elif iface == 'actions':
- fic = _FingerFiction()
- fic.load(c['actions']['path'])
-
- iface = _FingerFictionInterface(fic)
- 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 get_server():
""" Get the server through the configuration stored in the environment. """
- if not 'BIND' in _environ:
- # XXX: deprecated.
-
- return get_old_style_server()
-
bind = _environ.get('BIND', 'localhost:79')
host = _environ.get('FINGER_HOST', 'LOCALHOST')
iface = _environ.get('FINGER_TYPE', 'NATIVE').casefold()
@@ -100,24 +56,20 @@ def main():
import dotenv as _dotenv
_dotenv.load_dotenv()
except ImportError:
- pass
+ if _path.exists('.env'):
+ print("Warning: a `.env` file was found but `python-dotenv`",
+ file = _stderr)
+ print("didn't exist, consider installing it.",
+ file = _stderr)
# Non-interactive command line interface.
ap = _ArgumentParser()
- ap.add_argument('-c', dest = 'config_path',
- help = 'configuration file path',
- default = _environ.get('CONFIG', None))
args = ap.parse_args()
# Get the server, run it.
- if args.config_path is not None:
- # XXX: deprecated.
-
- server = get_old_style_server(args.config_path)
- else:
- server = get_server()
+ server = get_server()
server.serve_forever()
server.shutdown()
diff --git a/fingerd/_server.py b/fingerd/_server.py
index 84f8412..2772586 100755
--- a/fingerd/_server.py
+++ b/fingerd/_server.py
@@ -11,9 +11,10 @@ import string as _string, socket as _socket, signal as _signal, \
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
+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"]
@@ -282,61 +283,14 @@ class FingerServer:
self._servers = []
- # Extract the addresses as (family, host, port) entries.
-
- values = set()
- for x in map(lambda x: x.strip(), addresses.split(',')):
- host = ''
- if x[0] == '[':
- # Extract the host using this.
-
- to = x.find(']')
- if to < 0:
- continue
- host = x[1:to]
- x = x[to + 1:]
-
- # Extract the port.
-
- if x in ('', ':'):
- port = 79
- else:
- try:
- assert x[0] == ':'
- port = int(x[1:])
- except:
- continue
- else:
- # Extract the host and the port.
-
- x = x.split(':')
- if len(x) == 1:
- host = x[0]
- else:
- try:
- port = int(x[-1])
- except:
- continue
- host = ':'.join(x[:-1])
-
- try:
- entries = _socket.getaddrinfo(host, port,
- proto = _socket.IPPROTO_TCP,
- type = _socket.SOCK_STREAM)
- except:
- continue
-
- for ent in entries:
- fam = ent[0]
- if not fam in (_socket.AF_INET, _socket.AF_INET6):
- continue
+ for bind in _BindsDecoder(addresses):
+ if bind.family == _BindAddressType.TCP_IPv4:
+ fam = _socket.AF_INET
+ elif bind.family == _BindAddressType.TCP_IPv6:
+ fam = _socket.AF_INET6
- ip = ent[4][0]
- #ip = _socket.inet_pton(fam, ip)
- port = ent[4][1]
- values.add((fam, ip, port, None))
+ self._servers.append([fam, bind.data[0], bind.data[1], None])
- self._servers = list(list(i) for i in values)
return bool(self._servers)
def start(self):
diff --git a/fingerd/_util.py b/fingerd/_util.py
index 5e73c39..9e8a971 100755
--- a/fingerd/_util.py
+++ b/fingerd/_util.py
@@ -6,14 +6,18 @@
""" This file defines the base classes for what is used by the
finger server, directly or indirectly. """
-__all__ = ["ConfigurationError", "BindError",
- "FingerUser", "FingerSession",
- "FingerFormatter", "FingerInterface", "FingerLogger"]
-
import sys as _sys, copy as _copy
import multiprocessing as _multip
+import socket as _socket
+
+from enum import Enum as _Enum
from datetime import datetime as _dt, timedelta as _td
+__all__ = ["ConfigurationError", "BindError",
+ "FingerUser", "FingerSession",
+ "FingerFormatter", "FingerInterface", "FingerLogger"
+ "BindAddressType", "BindsDecoder"]
+
# ---
# Exceptions.
# ---
@@ -30,6 +34,12 @@ class BindError(ConfigurationError):
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}")
+
# ---
# User-related classes.
# ---
@@ -515,4 +525,181 @@ class FingerLogger:
def list(self, source):
self._logr(source, "list connected users.")
+# ---
+# Bind decoders.
+# ---
+
+class BindAddressType(_Enum):
+ """ Bind address type. """
+
+ """ TCP on IPv4 bind. """
+ TCP_IPv4 = 1
+
+ """ TCP on IPv6 bind. """
+ TCP_IPv6 = 2
+
+ """ IPC bind. """
+ IPC = 3
+
+class _BindAddress:
+ """ Bind address for fingerd. """
+
+ def __init__(self, family, data):
+ self.__family = BindAddressType(family)
+ self.__data = data
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}(family = {self.__family}, " \
+ f"data = {self.__data})"
+
+ @property
+ def family(self):
+ """ Family as one of the `BindAddressType` enumeration values. """
+
+ return self.__family
+
+ @property
+ def data(self):
+ """ Data to be sent for binding. """
+
+ return self.__data
+
+def _decode_tcp_host(x):
+ """ Decode suitable hosts for a TCP bind. """
+
+ addrs = ()
+ addr = x
+
+ # 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:
+ port = None if x[1:] == "" else _socket.getservbyname(x[1:])
+ if port is None:
+ raise InvalidBindAddressError(addr,
+ "expected a port number or name") 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 += (_BindAddress(BindAddressType.TCP_IPv6, (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 += (_BindAddress(BindAddressType.TCP_IPv4, (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 += (_BindAddress(BindAddressType.TCP_IPv4,
+ (ip, port)),)
+ else:
+ ip6 = ent[4][0]
+ _socket.inet_pton(_socket.AF_INET6, ent[4][0])
+ addrs += (_BindAddress(BindAddressType.TCP_IPv6,
+ (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:
+ 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 {repr(scheme)} " \
+ f"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/client/__init__.py b/fingerd/client/__init__.py
new file mode 100644
index 0000000..ca92dca
--- /dev/null
+++ b/fingerd/client/__init__.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+#******************************************************************************
+# Copyright (C) 2017-2018 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the fingerd Python 3.x module, which is MIT-licensed.
+#******************************************************************************
+""" The `fingerd.client` submodule is a module for adding actions to the
+ fingerd live interface. """
+
+__all__ = ["FingerLiveClient", "DELETE"]
+
+from .. import FingerUser as _FingerUser, FingerSession as _FingerSession
+
+# ---
+# The special DELETE value.
+# ---
+
+class _DeleteClass:
+ pass
+
+DELETE = _DeleteClass()
+
+# ---
+# The client.
+# ---
+
+class FingerLiveClient:
+ """ Client for sending actions to the fingerd live interface. """
+
+ def __init__(self, url):
+ # FIXME: open the connection.
+
+ raise NotImplementedError
+
+ def create_user(self, user):
+ """ 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 login(self, login, session):
+ """ Login to a finger session. """
+
+# End of file.
diff --git a/setup.py b/setup.py
index 7e10325..4ae4872 100755
--- a/setup.py
+++ b/setup.py
@@ -1,11 +1,11 @@
#!/usr/bin/env python3
#******************************************************************************
-# Copyright (C) 2018 Thomas Touhey <thomas@touhey.fr>
-# This file is part of the textoutpc project, which is MIT-licensed.
+# Copyright (C) 2018-2019 Thomas Touhey <thomas@touhey.fr>
+# This file is part of the fingerd project, which is MIT-licensed.
#******************************************************************************
""" Setup script for the fingerd Python package. """
-from setuptools import setup as _setup, find_packages as _find_packages
+from setuptools import setup as _setup
_setup()