3
0
mirror of https://github.com/jlu5/PyLink.git synced 2025-02-26 02:10:39 +01:00

Merge branch 'devel'

SSL support - Closes #80.
This commit is contained in:
James Lu 2015-08-12 07:20:27 -07:00
commit d3e335edee
8 changed files with 141 additions and 22 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ __pycache__/
*.save* *.save*
*.db *.db
*.pid *.pid
*.pem

View File

@ -21,6 +21,7 @@ class IrcUser():
self.identified = False self.identified = False
self.channels = set() self.channels = set()
self.away = ''
def __repr__(self): def __repr__(self):
return repr(self.__dict__) return repr(self.__dict__)

View File

@ -53,6 +53,17 @@ servers:
# PyLink might introduce a nick that is too long and cause netsplits! # PyLink might introduce a nick that is too long and cause netsplits!
maxnicklen: 30 maxnicklen: 30
# Toggles SSL for this network. Defaults to false if not specified, and requires the
# ssl_certfile and ssl_keyfile options to work.
# ssl: true
# ssl_certfile: pylink-cert.pem
# ssl_keyfile: pylink-key.pem
# Optionally, you can set this option to verify the SSL certificate
# fingerprint (SHA1) of your uplink.
# ssl_fingerprint: "e0fee1adf795c84eec4735f039503eb18d9c35cc"
ts6net: ts6net:
ip: 127.0.0.1 ip: 127.0.0.1
port: 7000 port: 7000

77
main.py
View File

@ -7,6 +7,8 @@ import time
import sys import sys
from collections import defaultdict from collections import defaultdict
import threading import threading
import ssl
import hashlib
from log import log from log import log
import conf import conf
@ -73,20 +75,73 @@ class Irc():
ip = self.serverdata["ip"] ip = self.serverdata["ip"]
port = self.serverdata["port"] port = self.serverdata["port"]
while True: while True:
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.initVars() self.initVars()
checks_ok = True
try: try:
self.socket = socket.socket()
self.socket.setblocking(0)
# Initial connection timeout is a lot smaller than the timeout after # Initial connection timeout is a lot smaller than the timeout after
# we've connected; this is intentional. # we've connected; this is intentional.
self.socket = socket.create_connection((ip, port), timeout=self.pingfreq) self.socket.settimeout(self.pingfreq)
self.socket.setblocking(0) self.ssl = self.serverdata.get('ssl')
if self.ssl:
log.info('(%s) Attempting SSL for this connection...', self.name)
certfile = self.serverdata.get('ssl_certfile')
keyfile = self.serverdata.get('ssl_keyfile')
if certfile and keyfile:
try:
self.socket = ssl.wrap_socket(self.socket,
certfile=certfile,
keyfile=keyfile)
except OSError:
log.exception('(%s) Caught OSError trying to '
'initialize the SSL connection; '
'are "ssl_certfile" and '
'"ssl_keyfile" set correctly?',
self.name)
checks_ok = False
else:
log.error('(%s) SSL certfile/keyfile was not set '
'correctly, aborting... ', self.name)
checks_ok = False
log.info("Connecting to network %r on %s:%s", self.name, ip, port)
self.socket.connect((ip, port))
self.socket.settimeout(self.pingtimeout) self.socket.settimeout(self.pingtimeout)
self.proto.connect(self)
self.spawnMain() if self.ssl and checks_ok:
log.info('(%s) Starting ping schedulers....', self.name) peercert = self.socket.getpeercert(binary_form=True)
self.schedulePing() sha1fp = hashlib.sha1(peercert).hexdigest()
log.info('(%s) Server ready; listening for data.', self.name) expected_fp = self.serverdata.get('ssl_fingerprint')
self.run() if expected_fp:
if sha1fp != expected_fp:
log.error('(%s) Uplink\'s SSL certificate '
'fingerprint (SHA1) does not match the '
'one configured: expected %r, got %r; '
'disconnecting...', self.name,
expected_fp, sha1fp)
checks_ok = False
else:
log.info('(%s) Uplink SSL certificate fingerprint '
'(SHA1) verified: %r', self.name, sha1fp)
else:
log.info('(%s) Uplink\'s SSL certificate fingerprint '
'is %r. You can enhance the security of your '
'link by specifying this in a "ssl_fingerprint"'
' option in your server block.', self.name,
sha1fp)
if checks_ok:
self.proto.connect(self)
self.spawnMain()
log.info('(%s) Starting ping schedulers....', self.name)
self.schedulePing()
log.info('(%s) Server ready; listening for data.', self.name)
self.run()
else:
log.error('(%s) A configuration error was encountered '
'trying to set up this connection. Please check'
' your configuration file and try again.',
self.name)
except (socket.error, classes.ProtocolError, ConnectionError) as e: except (socket.error, classes.ProtocolError, ConnectionError) as e:
log.warning('(%s) Disconnected from IRC: %s: %s', log.warning('(%s) Disconnected from IRC: %s: %s',
self.name, type(e).__name__, str(e)) self.name, type(e).__name__, str(e))
@ -117,8 +172,8 @@ class Irc():
log.debug('(%s) self.pingtimeout: %s', self.name, self.pingtimeout) log.debug('(%s) self.pingtimeout: %s', self.name, self.pingtimeout)
data = self.socket.recv(2048) data = self.socket.recv(2048)
buf += data buf += data
if self.connected and not data: if self.connected.is_set() and not data:
log.warn('(%s) No data received and self.connected is not set; disconnecting!', self.name) log.warn('(%s) No data received and self.connected is set; disconnecting!', self.name)
break break
while b'\n' in buf: while b'\n' in buf:
line, buf = buf.split(b'\n', 1) line, buf = buf.split(b'\n', 1)

View File

@ -109,7 +109,8 @@ def getPrefixModes(irc, remoteirc, channel, user):
for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'): for pmode in ('owner', 'admin', 'op', 'halfop', 'voice'):
if pmode in remoteirc.cmodes: # Mode supported by IRCd if pmode in remoteirc.cmodes: # Mode supported by IRCd
mlist = irc.channels[channel].prefixmodes[pmode+'s'] mlist = irc.channels[channel].prefixmodes[pmode+'s']
log.debug('(%s) getPrefixModes: checking if %r is in %r', irc.name, user, mlist) log.debug('(%s) getPrefixModes: checking if %r is in %s list: %r',
irc.name, user, pmode, mlist)
if user in mlist: if user in mlist:
modes += remoteirc.cmodes[pmode] modes += remoteirc.cmodes[pmode]
return modes return modes
@ -142,6 +143,9 @@ def getRemoteUser(irc, remoteirc, user, spawnIfMissing=True):
host=host, realname=realname, host=host, realname=realname,
modes=modes, ts=userobj.ts).uid modes=modes, ts=userobj.ts).uid
remoteirc.users[u].remote = irc.name remoteirc.users[u].remote = irc.name
away = userobj.away
if away:
remoteirc.proto.awayClient(remoteirc, u, away)
relayusers[(irc.name, user)][remoteirc.name] = u relayusers[(irc.name, user)][remoteirc.name] = u
return u return u
@ -158,11 +162,9 @@ def getLocalUser(irc, user, targetirc=None):
# First, iterate over everyone! # First, iterate over everyone!
remoteuser = None remoteuser = None
for k, v in relayusers.items(): for k, v in relayusers.items():
log.debug('(%s) getLocalUser: processing %s, %s in relayusers', irc.name, k, v)
if k[0] == irc.name: if k[0] == irc.name:
# We don't need to do anything if the target users is on # We don't need to do anything if the target users is on
# the same network as us. # the same network as us.
log.debug('(%s) getLocalUser: skipping %s since the target network matches the source network.', irc.name, k)
continue continue
if v.get(irc.name) == user: if v.get(irc.name) == user:
# If the stored pseudoclient UID for the kicked user on # If the stored pseudoclient UID for the kicked user on
@ -718,9 +720,9 @@ def removeChannel(irc, channel):
irc.proto.partClient(irc, user, channel, 'Channel delinked.') irc.proto.partClient(irc, user, channel, 'Channel delinked.')
# Don't ever quit it either... # Don't ever quit it either...
if user != irc.pseudoclient.uid and not irc.users[user].channels: if user != irc.pseudoclient.uid and not irc.users[user].channels:
irc.proto.quitClient(irc, user, 'Left all shared channels.')
remoteuser = getLocalUser(irc, user) remoteuser = getLocalUser(irc, user)
del relayusers[remoteuser][irc.name] del relayusers[remoteuser][irc.name]
irc.proto.quitClient(irc, user, 'Left all shared channels.')
@utils.add_cmd @utils.add_cmd
def create(irc, source, args): def create(irc, source, args):
@ -847,7 +849,10 @@ def delink(irc, source, args):
if entry: if entry:
if entry[0] == irc.name: # We own this channel. if entry[0] == irc.name: # We own this channel.
if not remotenet: if not remotenet:
utils.msg(irc, source, "Error: you must select a network to delink, or use the 'destroy' command no remove this relay entirely.") utils.msg(irc, source, "Error: You must select a network to "
"delink, or use the 'destroy' command to remove "
"this relay entirely (it was created on the current "
"network).")
return return
else: else:
for link in db[entry]['links'].copy(): for link in db[entry]['links'].copy():
@ -933,3 +938,9 @@ def linked(irc, source, args):
else: else:
s += '(no relays yet)' s += '(no relays yet)'
utils.msg(irc, source, s) utils.msg(irc, source, s)
def handle_away(irc, numeric, command, args):
for netname, user in relayusers[(irc.name, numeric)].items():
remoteirc = utils.networkobjects[netname]
remoteirc.proto.awayClient(remoteirc, user, args['text'])
utils.add_hook(handle_away, 'AWAY')

View File

@ -80,7 +80,8 @@ def sjoinServer(irc, server, channel, users, ts=None):
else: else:
utils.applyModes(irc, channel, modes) utils.applyModes(irc, channel, modes)
''' '''
modes = irc.channels[channel].modes # Strip out list-modes, they shouldn't be ever sent in FJOIN.
modes = [m for m in irc.channels[channel].modes if m[0] not in irc.cmodes['*A']]
uids = [] uids = []
changedmodes = [] changedmodes = []
namelist = [] namelist = []
@ -297,6 +298,16 @@ def numericServer(irc, source, numeric, text):
"locally by InspIRCd servers, so there is no " "locally by InspIRCd servers, so there is no "
"need for PyLink to send numerics directly yet.") "need for PyLink to send numerics directly yet.")
def awayClient(irc, source, text):
"""<irc object> <numeric> <text>
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text:
_send(irc, source, 'AWAY %s :%s' % (int(time.time()), text))
else:
_send(irc, source, 'AWAY')
def connect(irc): def connect(irc):
ts = irc.start_ts ts = irc.start_ts
@ -664,3 +675,13 @@ def handle_fname(irc, numeric, command, args):
def handle_endburst(irc, numeric, command, args): def handle_endburst(irc, numeric, command, args):
return {} return {}
def handle_away(irc, numeric, command, args):
# <- :1MLAAAAIG AWAY 1439371390 :Auto-away
try:
ts = args[0]
irc.users[numeric].away = text = args[1]
return {'text': text, 'ts': ts}
except IndexError: # User is unsetting away status
irc.users[numeric].away = ''
return {'text': ''}

View File

@ -231,6 +231,16 @@ def pingServer(irc, source=None, target=None):
def numericServer(irc, source, numeric, target, text): def numericServer(irc, source, numeric, target, text):
_send(irc, source, '%s %s %s' % (numeric, target, text)) _send(irc, source, '%s %s %s' % (numeric, target, text))
def awayClient(irc, source, text):
"""<irc object> <numeric> <text>
Sends an AWAY message with text <text> from PyLink client <numeric>.
<text> can be an empty string to unset AWAY status."""
if text:
_send(irc, source, 'AWAY :%s' % text)
else:
_send(irc, source, 'AWAY')
def connect(irc): def connect(irc):
ts = irc.start_ts ts = irc.start_ts
@ -649,3 +659,13 @@ def handle_472(irc, numeric, command, args):
' desyncs, try adding the line "loadmodule "extensions/%s.so";" to ' ' desyncs, try adding the line "loadmodule "extensions/%s.so";" to '
'your IRCd configuration.', irc.name, setter, badmode, 'your IRCd configuration.', irc.name, setter, badmode,
charlist[badmode]) charlist[badmode])
def handle_away(irc, numeric, command, args):
# <- :6ELAAAAAB AWAY :Auto-away
try:
irc.users[numeric].away = text = args[0]
except IndexError: # User is unsetting away status
irc.users[numeric].away = text = ''
return {'text': text}

View File

@ -1,18 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Shell script to start PyLink under CPUlimit, killing it if it starts abusing the CPU. # Shell script to start PyLink under CPUlimit, throttling it if it starts abusing the CPU.
# Set this to whatever you want. cpulimit --help # Set this to whatever you want. cpulimit --help
LIMIT=20 LIMIT=35
# Change to the PyLink root directory. # Change to the PyLink root directory.
WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) WRAPPER_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
cd "$WRAPPER_DIR" cd "$WRAPPER_DIR"
if [[ ! -z "$(which cpulimit)" ]]; then if [[ ! -z "$(which cpulimit)" ]]; then
# -k kills the PyLink daemon if it goes over $LIMIT
# -z makes cpulimit exit when PyLink dies. # -z makes cpulimit exit when PyLink dies.
cpulimit -l $LIMIT -z -k ./main.py cpulimit -l $LIMIT -z ./main.py
echo "PyLink has been started (daemonized) under cpulimit, and will automatically be killed if it goes over the CPU limit of ${LIMIT}%." echo "PyLink has been started (daemonized) under cpulimit, and will automatically be throttled if it goes over the CPU limit of ${LIMIT}%."
echo "To kill the process manually, run ./kill.sh" echo "To kill the process manually, run ./kill.sh"
else else
echo 'cpulimit not found in $PATH! Aborting.' echo 'cpulimit not found in $PATH! Aborting.'