ellinetircd 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ellinetircd-1.0.0/LICENSE +8 -0
- ellinetircd-1.0.0/MANIFEST.in +2 -0
- ellinetircd-1.0.0/PKG-INFO +60 -0
- ellinetircd-1.0.0/README.md +38 -0
- ellinetircd-1.0.0/ellinetircd/__init__.py +34 -0
- ellinetircd-1.0.0/ellinetircd/__main__.py +75 -0
- ellinetircd-1.0.0/ellinetircd/channel.py +85 -0
- ellinetircd-1.0.0/ellinetircd/config.py +14 -0
- ellinetircd-1.0.0/ellinetircd/exceptions.py +86 -0
- ellinetircd-1.0.0/ellinetircd/sdnotify.py +32 -0
- ellinetircd-1.0.0/ellinetircd/server.py +77 -0
- ellinetircd-1.0.0/ellinetircd/states.py +497 -0
- ellinetircd-1.0.0/ellinetircd/user.py +199 -0
- ellinetircd-1.0.0/ellinetircd.egg-info/PKG-INFO +60 -0
- ellinetircd-1.0.0/ellinetircd.egg-info/SOURCES.txt +24 -0
- ellinetircd-1.0.0/ellinetircd.egg-info/dependency_links.txt +1 -0
- ellinetircd-1.0.0/ellinetircd.egg-info/requires.txt +1 -0
- ellinetircd-1.0.0/ellinetircd.egg-info/top_level.txt +1 -0
- ellinetircd-1.0.0/pyproject.toml +6 -0
- ellinetircd-1.0.0/setup.cfg +29 -0
- ellinetircd-1.0.0/setup.py +3 -0
- ellinetircd-1.0.0/tests/__init__.py +0 -0
- ellinetircd-1.0.0/tests/common.py +138 -0
- ellinetircd-1.0.0/tests/test_protocole.py +199 -0
- ellinetircd-1.0.0/version.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright Julien Castiaux 2020
|
|
2
|
+
Copyright ElliNet13 2026
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ellinetircd
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ElliNetIRCd - an asynchronous IRC server
|
|
5
|
+
Home-page: https://github.com/ElliNet13/ElliNetIRCd
|
|
6
|
+
Download-URL: https://pypi.org/project/ellinetircd/
|
|
7
|
+
Author: ElliNet13
|
|
8
|
+
Author-email: your@email.com
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Environment :: No Input/Output (Daemon)
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Topic :: Communications :: Chat :: Internet Relay Chat
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: trio
|
|
20
|
+
Dynamic: download-url
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
ellinetircd
|
|
24
|
+
=======
|
|
25
|
+
|
|
26
|
+
A python asynchronous IRC server based on aioirc.
|
|
27
|
+
|
|
28
|
+
### Installation and Usage
|
|
29
|
+
|
|
30
|
+
Download and install the latest stable version using pip.
|
|
31
|
+
Windows users might replace `python3` by `py`.
|
|
32
|
+
```bash
|
|
33
|
+
python3 -m pip install ellinetircd
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then run the server:
|
|
37
|
+
```bash
|
|
38
|
+
HOST=0.0.0.0 LOGLEVEL=INFO python3 -m ellinetircd
|
|
39
|
+
```
|
|
40
|
+
If you are using powershell you can use the following command:
|
|
41
|
+
```powershell
|
|
42
|
+
$env:HOST="0.0.0.0"
|
|
43
|
+
$env:LOGLEVEL="INFO"
|
|
44
|
+
python -m ellinetircd
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The configuration is done via environment variables, see `--help`:
|
|
48
|
+
|
|
49
|
+
optional arguments:
|
|
50
|
+
-h, --help show this help message and exit
|
|
51
|
+
-V, --version show program's version number and exit
|
|
52
|
+
|
|
53
|
+
environment variables:
|
|
54
|
+
HOST public domain name (default: julien-UX410UAR)
|
|
55
|
+
ADDR ip address to bind (default: 127.0.1.1)
|
|
56
|
+
PORT port to bind (default: 6667)
|
|
57
|
+
PASS server password (default: )
|
|
58
|
+
TIMEOUT kick inactive users after x seconds (default: 60)
|
|
59
|
+
PING_TIMEOUT PING inactive users x seconds before timeout (default: 5)
|
|
60
|
+
LOGLEVEL logging verbosity (default: WARNING)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
ellinetircd
|
|
2
|
+
=======
|
|
3
|
+
|
|
4
|
+
A python asynchronous IRC server based on aioirc.
|
|
5
|
+
|
|
6
|
+
### Installation and Usage
|
|
7
|
+
|
|
8
|
+
Download and install the latest stable version using pip.
|
|
9
|
+
Windows users might replace `python3` by `py`.
|
|
10
|
+
```bash
|
|
11
|
+
python3 -m pip install ellinetircd
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then run the server:
|
|
15
|
+
```bash
|
|
16
|
+
HOST=0.0.0.0 LOGLEVEL=INFO python3 -m ellinetircd
|
|
17
|
+
```
|
|
18
|
+
If you are using powershell you can use the following command:
|
|
19
|
+
```powershell
|
|
20
|
+
$env:HOST="0.0.0.0"
|
|
21
|
+
$env:LOGLEVEL="INFO"
|
|
22
|
+
python -m ellinetircd
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The configuration is done via environment variables, see `--help`:
|
|
26
|
+
|
|
27
|
+
optional arguments:
|
|
28
|
+
-h, --help show this help message and exit
|
|
29
|
+
-V, --version show program's version number and exit
|
|
30
|
+
|
|
31
|
+
environment variables:
|
|
32
|
+
HOST public domain name (default: julien-UX410UAR)
|
|
33
|
+
ADDR ip address to bind (default: 127.0.1.1)
|
|
34
|
+
PORT port to bind (default: 6667)
|
|
35
|
+
PASS server password (default: )
|
|
36
|
+
TIMEOUT kick inactive users after x seconds (default: 60)
|
|
37
|
+
PING_TIMEOUT PING inactive users x seconds before timeout (default: 5)
|
|
38
|
+
LOGLEVEL logging verbosity (default: WARNING)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
import logging
|
|
3
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("ellinetircd")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "unknown"
|
|
9
|
+
|
|
10
|
+
from ellinetircd.config import config as cfg
|
|
11
|
+
logger = logging.getLogger(__package__)
|
|
12
|
+
IO = logging.INFO - 5
|
|
13
|
+
SECURITY = logging.ERROR + 5
|
|
14
|
+
logging.addLevelName(IO, 'IO')
|
|
15
|
+
logging.addLevelName(SECURITY, 'SECURITY')
|
|
16
|
+
logger.setLevel(cfg.LOGLEVEL)
|
|
17
|
+
servlocal = contextvars.ContextVar('servlocal')
|
|
18
|
+
MAXLINELEN = 512
|
|
19
|
+
|
|
20
|
+
import ellinetircd.channel
|
|
21
|
+
import ellinetircd.exceptions
|
|
22
|
+
import ellinetircd.server
|
|
23
|
+
import ellinetircd.sdnotify
|
|
24
|
+
import ellinetircd.states
|
|
25
|
+
import ellinetircd.user
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def update_status() -> None:
|
|
29
|
+
sl = servlocal.get()
|
|
30
|
+
ellinetircd.sdnotify.status(
|
|
31
|
+
f"Listening on {cfg.ADDR} ({cfg.HOST}) port {cfg.PORT}. "
|
|
32
|
+
f"Currently {len(sl.users)} registered users"
|
|
33
|
+
f" in {len(sl.channels)} channels."
|
|
34
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import textwrap
|
|
6
|
+
import trio
|
|
7
|
+
from socket import gethostname, gethostbyname
|
|
8
|
+
|
|
9
|
+
import ellinetircd
|
|
10
|
+
from ellinetircd.config import config as cfg
|
|
11
|
+
from ellinetircd.server import Server
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Color the [LEVEL] part of messages, need new terminal on Windows
|
|
16
|
+
# https://github.com/odoo/odoo/blob/13.0/odoo/netsvc.py#L57-L100
|
|
17
|
+
class ColoredFormatter(logging.Formatter):
|
|
18
|
+
colors = {
|
|
19
|
+
logging.DEBUG: (34, 49), # blue
|
|
20
|
+
ellinetircd.IO: (37, 49), # white
|
|
21
|
+
logging.INFO: (32, 49), # green
|
|
22
|
+
logging.WARNING: (33, 49), # yellow
|
|
23
|
+
logging.ERROR: (31, 49), # red
|
|
24
|
+
ellinetircd.SECURITY: (31, 49), # red
|
|
25
|
+
logging.CRITICAL: (37, 41), # white fg, red bg
|
|
26
|
+
}
|
|
27
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
28
|
+
fg, bg = type(self).colors.get(record.levelno, (32, 49))
|
|
29
|
+
record.levelname = f'\033[1;{fg}m\033[1;{bg}m{record.levelname}\033[0m'
|
|
30
|
+
return super().format(record)
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
stderr = logging.StreamHandler()
|
|
34
|
+
stderr.formatter = (
|
|
35
|
+
ColoredFormatter('%(asctime)s [%(levelname)s] %(message)s')
|
|
36
|
+
if hasattr(sys.stderr, 'fileno') and os.isatty(sys.stderr.fileno()) else
|
|
37
|
+
logging.Formatter('[%(levelname)s] %(message)s')
|
|
38
|
+
)
|
|
39
|
+
root_logger = logging.getLogger('')
|
|
40
|
+
root_logger.handlers.clear()
|
|
41
|
+
root_logger.addHandler(stderr)
|
|
42
|
+
|
|
43
|
+
server = Server(cfg.HOST, cfg.ADDR, cfg.PORT, cfg.PASS)
|
|
44
|
+
try:
|
|
45
|
+
trio.run(server.serve)
|
|
46
|
+
except Exception:
|
|
47
|
+
logger.critical("Dead", exc_info=True)
|
|
48
|
+
finally:
|
|
49
|
+
logging.shutdown()
|
|
50
|
+
|
|
51
|
+
# Dummy argparse, used only for --help and --version
|
|
52
|
+
parser = argparse.ArgumentParser(
|
|
53
|
+
prog=ellinetircd.__name__,
|
|
54
|
+
usage=f"{sys.executable} -m {ellinetircd.__name__}",
|
|
55
|
+
description="single-server minimalist IRC",
|
|
56
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
57
|
+
epilog=textwrap.dedent(f"""\
|
|
58
|
+
environment variables:
|
|
59
|
+
HOST public domain name (default: {gethostname()})
|
|
60
|
+
ADDR ip address to bind (default: {gethostbyname(cfg.HOST)})
|
|
61
|
+
PORT port to bind (default: 6667)
|
|
62
|
+
PASS server password (default: )
|
|
63
|
+
TIMEOUT kick inactive users after x seconds (default: 60)
|
|
64
|
+
PING_TIMEOUT PING inactive users x seconds before timeout (default: 5)
|
|
65
|
+
LOGLEVEL logging verbosity (default: WARNING)
|
|
66
|
+
""")
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
'-V', '--version',
|
|
70
|
+
action='version',
|
|
71
|
+
version=f'{ellinetircd.__name__} {ellinetircd.__version__}',
|
|
72
|
+
)
|
|
73
|
+
parser.parse_args()
|
|
74
|
+
|
|
75
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import trio
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import List, Set, Union
|
|
5
|
+
|
|
6
|
+
import ellinetircd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Channel:
|
|
13
|
+
"""
|
|
14
|
+
The Channel class represents a chat room.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
name: str
|
|
18
|
+
the channel name.
|
|
19
|
+
users: Set[User]
|
|
20
|
+
a set of user who is subscribe to the channel.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, name: str) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Parameters:
|
|
26
|
+
name: str
|
|
27
|
+
the channel name.
|
|
28
|
+
"""
|
|
29
|
+
self._name = name
|
|
30
|
+
self.users: Set["ellinetircd.user.User"] = set()
|
|
31
|
+
|
|
32
|
+
def __str__(self) -> str:
|
|
33
|
+
"""
|
|
34
|
+
We represents the channel object by his name.
|
|
35
|
+
|
|
36
|
+
Return: str
|
|
37
|
+
the name of the channel.
|
|
38
|
+
"""
|
|
39
|
+
return self._name
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def name(self) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Getter for private attributes channel name.
|
|
45
|
+
|
|
46
|
+
Returns: str
|
|
47
|
+
the name of the channel.
|
|
48
|
+
"""
|
|
49
|
+
return self._name
|
|
50
|
+
|
|
51
|
+
def _log_messages(self, messages: List[str]) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Log all messages sent to a client.
|
|
54
|
+
|
|
55
|
+
Parameters: List[str]
|
|
56
|
+
A list of messages send to the client.
|
|
57
|
+
"""
|
|
58
|
+
for message in messages:
|
|
59
|
+
logger.log(ellinetircd.IO, "send to %s: %s", self, message)
|
|
60
|
+
|
|
61
|
+
async def send(
|
|
62
|
+
self,
|
|
63
|
+
messages: Union[str, List[str]],
|
|
64
|
+
skipusers: Set["ellinetircd.user.User"] = set(),
|
|
65
|
+
) -> None:
|
|
66
|
+
""""
|
|
67
|
+
Send many messages to each user subscribed to this channel.
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
messages: Union[str, List[str]]
|
|
71
|
+
Messages to send to the users.
|
|
72
|
+
skipusers: Set[User]
|
|
73
|
+
A set of users to skip sending messages to.
|
|
74
|
+
"""
|
|
75
|
+
if isinstance(messages, str):
|
|
76
|
+
messages = [messages]
|
|
77
|
+
|
|
78
|
+
async with trio.open_nursery() as self._nursery:
|
|
79
|
+
self._log_messages(messages)
|
|
80
|
+
for user in self.users.difference(skipusers):
|
|
81
|
+
self._nursery.start_soon(partial(
|
|
82
|
+
user.send,
|
|
83
|
+
messages,
|
|
84
|
+
log=logger.isEnabledFor(logging.DEBUG)
|
|
85
|
+
))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from socket import gethostname, gethostbyname
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
config = SimpleNamespace()
|
|
8
|
+
config.HOST = os.getenv('HOST', gethostname())
|
|
9
|
+
config.ADDR = os.getenv('ADDR', gethostbyname(config.HOST))
|
|
10
|
+
config.PORT = int(os.getenv('PORT', 6667))
|
|
11
|
+
config.PASS = os.getenv('PASS')
|
|
12
|
+
config.TIMEOUT = int(os.getenv('TIMEOUT', 60))
|
|
13
|
+
config.PING_TIMEOUT = int(os.getenv('PING_TIMEOUT', 5))
|
|
14
|
+
config.LOGLEVEL = os.getenv('LOGLEVEL', 'WARNING')
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
__ALL__ = [
|
|
2
|
+
'Disconnect', 'IRCException', 'ErrNoSuchChannel', 'ErrNoRecipient',
|
|
3
|
+
'ErrNoTextToSend', 'ErrUnknownCommand', 'ErrNoNicknameGiven',
|
|
4
|
+
'ErrErroneusNickname', 'ErrNicknameInUse', 'ErrNotOnChannel',
|
|
5
|
+
'ErrNoLogin', 'ErrNeedMoreParams', 'ErrAlreadyRegistred',
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
import ellinetircd
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
class Disconnect(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class IRCException(Exception):
|
|
15
|
+
"""
|
|
16
|
+
Abstract IRC exception
|
|
17
|
+
|
|
18
|
+
They are excepted by dispatch() and forwarded to the user.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, *args: Any) -> None:
|
|
21
|
+
super().__init__(type(self).format(*args))
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def format(cls, *args: Any) -> str:
|
|
25
|
+
return ":{host} {code} {error}".format(
|
|
26
|
+
host=ellinetircd.servlocal.get().host,
|
|
27
|
+
code=cls.code,
|
|
28
|
+
error=cls.msg.format(*args),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
class ErrUnknownError(IRCException):
|
|
32
|
+
# <user> <command> :<info>
|
|
33
|
+
msg = "{} {} :{}"
|
|
34
|
+
code = 400
|
|
35
|
+
|
|
36
|
+
class ErrNoSuchNick(IRCException):
|
|
37
|
+
msg = "{} :No such nick/channel"
|
|
38
|
+
code = 401
|
|
39
|
+
|
|
40
|
+
class ErrNoSuchChannel(IRCException):
|
|
41
|
+
msg = "{} :No such channel"
|
|
42
|
+
code = 403
|
|
43
|
+
|
|
44
|
+
class ErrNoRecipient(IRCException):
|
|
45
|
+
msg = ":No recipient given ({})"
|
|
46
|
+
code = 411
|
|
47
|
+
|
|
48
|
+
class ErrNoTextToSend(IRCException):
|
|
49
|
+
msg = ":No text to send"
|
|
50
|
+
code = 412
|
|
51
|
+
|
|
52
|
+
class ErrUnknownCommand(IRCException):
|
|
53
|
+
msg = "{} :Unknown command"
|
|
54
|
+
code = 421
|
|
55
|
+
|
|
56
|
+
class ErrNoNicknameGiven(IRCException):
|
|
57
|
+
msg = ":No nickname given"
|
|
58
|
+
code = 431
|
|
59
|
+
|
|
60
|
+
class ErrErroneusNickname(IRCException):
|
|
61
|
+
msg = "{} :Erroneous nickname"
|
|
62
|
+
code = 432
|
|
63
|
+
|
|
64
|
+
class ErrNicknameInUse(IRCException):
|
|
65
|
+
msg = "{} :Nickname is already in use"
|
|
66
|
+
code = 433
|
|
67
|
+
|
|
68
|
+
class ErrNotOnChannel(IRCException):
|
|
69
|
+
msg = "{} :You're not on that channel"
|
|
70
|
+
code = 442
|
|
71
|
+
|
|
72
|
+
class ErrNoLogin(IRCException):
|
|
73
|
+
msg = ":User not logged in"
|
|
74
|
+
code = 444
|
|
75
|
+
|
|
76
|
+
class ErrNeedMoreParams(IRCException):
|
|
77
|
+
msg = "{} :Not enough parameters"
|
|
78
|
+
code = 461
|
|
79
|
+
|
|
80
|
+
class ErrAlreadyRegistred(IRCException):
|
|
81
|
+
msg = ":Unauthorized command (already registered)"
|
|
82
|
+
code = 462
|
|
83
|
+
|
|
84
|
+
class ErrPasswdMismatch(IRCException):
|
|
85
|
+
msg = ":Password incorrect"
|
|
86
|
+
code = 464
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def notify(payload: bytes) -> None:
|
|
5
|
+
if _sdsocket:
|
|
6
|
+
_sdsocket.sendall(payload)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def ready() -> None:
|
|
10
|
+
notify(b"READY=1")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def reloading() -> None:
|
|
14
|
+
notify(b"RELOADING=1")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def stopping() -> None:
|
|
18
|
+
notify(b"STOPPING=1")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def status(line: str) -> None:
|
|
22
|
+
notify(b"STATUS=" + line.encode())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Setup
|
|
26
|
+
_sdsocket = None
|
|
27
|
+
_notify_socket = os.getenv('NOTIFY_SOCKET', '')
|
|
28
|
+
if _notify_socket:
|
|
29
|
+
if _notify_socket.startswith('@'):
|
|
30
|
+
_notify_socket = f'\0{_notify_socket[1:]}'
|
|
31
|
+
_sdsocket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
32
|
+
_sdsocket.connect(_notify_socket)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import logging
|
|
3
|
+
import signal
|
|
4
|
+
import trio
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import ellinetircd
|
|
8
|
+
from ellinetircd import sdnotify
|
|
9
|
+
from ellinetircd.exceptions import Disconnect
|
|
10
|
+
from ellinetircd.user import User
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Server:
|
|
17
|
+
def __init__(self, host: str, addr: str, port: int, pwd: Optional[str]) -> None:
|
|
18
|
+
self.host = host
|
|
19
|
+
self.addr = addr
|
|
20
|
+
self.port = port
|
|
21
|
+
self.pwd = pwd
|
|
22
|
+
|
|
23
|
+
async def handle(self, stream: trio.abc.Stream) -> None:
|
|
24
|
+
servlocal = ellinetircd.servlocal.get()
|
|
25
|
+
async with trio.open_nursery() as nursery:
|
|
26
|
+
user = User(stream, nursery)
|
|
27
|
+
logger.info("Connection with %s established.", user)
|
|
28
|
+
nursery.start_soon(user.ping_forever)
|
|
29
|
+
try:
|
|
30
|
+
await user.serve()
|
|
31
|
+
except Disconnect as exc:
|
|
32
|
+
logger.warning("Protocol violation while serving %s, %s.", user, repr(exc.__cause__ or exc))
|
|
33
|
+
await user.terminate(exc.args[0] if exc.args else "Protocol violation")
|
|
34
|
+
except Exception:
|
|
35
|
+
logger.exception("Error while serving %s.", user)
|
|
36
|
+
await user.terminate("Internal host error")
|
|
37
|
+
else:
|
|
38
|
+
await user.terminate()
|
|
39
|
+
|
|
40
|
+
logger.info("Connection with %s closed.", user)
|
|
41
|
+
|
|
42
|
+
def started(self, _listeners: Any) -> None:
|
|
43
|
+
sdnotify.ready()
|
|
44
|
+
ellinetircd.update_status()
|
|
45
|
+
|
|
46
|
+
async def _onterm(self) -> None:
|
|
47
|
+
with trio.open_signal_receiver(signal.SIGTERM, signal.SIGINT) as signal_aiter:
|
|
48
|
+
async for _ in signal_aiter:
|
|
49
|
+
if self._nursery.cancel_scope.cancel_called:
|
|
50
|
+
raise KeyboardInterrupt()
|
|
51
|
+
sdnotify.stopping()
|
|
52
|
+
sdnotify.status("Terminating connections...")
|
|
53
|
+
self._nursery.cancel_scope.cancel()
|
|
54
|
+
|
|
55
|
+
async def serve(self) -> None:
|
|
56
|
+
ellinetircd.servlocal.set(ServLocal(self.host, self.pwd, {}, {}))
|
|
57
|
+
async with trio.open_nursery() as self._nursery:
|
|
58
|
+
self._nursery.start_soon(self._onterm)
|
|
59
|
+
logger.info("Listening on %s port %s.", self.addr, self.port)
|
|
60
|
+
await trio.serve_tcp(self.handle, self.port, host=self.addr, task_status=self)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclasses.dataclass(eq=False)
|
|
64
|
+
class ServLocal:
|
|
65
|
+
host: str
|
|
66
|
+
pwd: str # password, "pass" is a reserved keyword
|
|
67
|
+
users: Dict[str, User]
|
|
68
|
+
channels: Dict[str, "ellinetircd.channel.Channel"]
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return (
|
|
72
|
+
f'{self.__name__}('
|
|
73
|
+
f'host: {self.host!r}, '
|
|
74
|
+
f'pass: {"yes" if self.pwd else "no"}, '
|
|
75
|
+
f'users: {len(self.users)}, '
|
|
76
|
+
f'channels: {len(self.channels)})'
|
|
77
|
+
)
|