diff --git a/plinth/modules/i2p/helpers.py b/plinth/modules/i2p/helpers.py new file mode 100644 index 000000000..76b1301cd --- /dev/null +++ b/plinth/modules/i2p/helpers.py @@ -0,0 +1,119 @@ +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import os +import re + +import augeas + +I2P_CONF_DIR = '/var/lib/i2p/i2p-config' +FILE_TUNNEL_CONF = os.path.join(I2P_CONF_DIR, 'i2ptunnel.config') +TUNNEL_IDX_REGEX = re.compile(r'tunnel.(\d+).name$') + + +class TunnelEditor(object): + """ + + :type aug: augeas.Augeas + """ + + def __init__(self, conf_filename=None, idx=None): + self.conf_filename = conf_filename or FILE_TUNNEL_CONF + self.idx = idx + self.aug = None + + @property + def lines(self): + if self.aug: + return self.aug.match('/files{}/*'.format(self.conf_filename)) + else: + return [] + + def read_conf(self): + """Return an instance of Augeaus for processing APT configuration.""" + self.aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + + augeas.Augeas.NO_MODL_AUTOLOAD) + self.aug.set('/augeas/load/Properties/lens', 'Properties.lns') + self.aug.set('/augeas/load/Properties/incl[last() + 1]', self.conf_filename) + self.aug.load() + + return self + + def write_conf(self): + self.aug.save() + return self + + def set_tunnel_idx(self, name): + + """ + Finds the index of the tunnel with the given name. + + Chainable method. + + :type name: basestring + """ + + for prop in self.aug.match('/files{}/*'.format(self.conf_filename)): + match = TUNNEL_IDX_REGEX.search(prop) + + if match and self.aug.get(prop) == name: + self.idx = int(match.group(1)) + return self + raise ValueError('No tunnel with that name') + + def calc_prop_path(self, tunnel_prop): + """ + Calculates the property name as found in the properties files + :type tunnel_prop: str + :rtype: basestring + """ + calced_prop_path = '/files{filepath}/tunnel.{idx}.{tunnel_prop}'.format( + idx=self.idx, tunnel_prop=tunnel_prop, + filepath=self.conf_filename + ) + return calced_prop_path + + def set_tunnel_prop(self, tunnel_prop, value): + """ + Updates a tunnel's property. + + The idx has to be set and the property has to exist in the config file and belong to the tunnel's properties. + + see calc_prop_path + + Chainable method. + + :param tunnel_prop: + :type tunnel_prop: str + :param value: + :type value: basestring | int + :return: + :rtype: + """ + if self.idx is None: + raise ValueError('Please init the tunnel index before calling this method') + + calc_prop_path = self.calc_prop_path(tunnel_prop) + self.aug.set(calc_prop_path, value) + return self + + def __getitem__(self, tunnel_prop): + ret = self.aug.get(self.calc_prop_path(tunnel_prop)) + if ret is None: + raise KeyError('Unknown property {}'.format(tunnel_prop)) + return ret + + def __setitem__(self, tunnel_prop, value): + self.aug.set(self.calc_prop_path(tunnel_prop), value) diff --git a/plinth/modules/i2p/tests/__init__.py b/plinth/modules/i2p/tests/__init__.py new file mode 100644 index 000000000..019e3bfd7 --- /dev/null +++ b/plinth/modules/i2p/tests/__init__.py @@ -0,0 +1,16 @@ +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + diff --git a/plinth/modules/i2p/tests/data/i2ptunnel.config b/plinth/modules/i2p/tests/data/i2ptunnel.config new file mode 100644 index 000000000..467980059 --- /dev/null +++ b/plinth/modules/i2p/tests/data/i2ptunnel.config @@ -0,0 +1,192 @@ +# NOTE: This I2P config file must use UTF-8 encoding +tunnel.0.description=HTTP proxy for browsing eepsites and the web +tunnel.0.interface=127.0.0.1 +tunnel.0.listenPort=4444 +tunnel.0.name=I2P HTTP Proxy +tunnel.0.option.i2cp.closeIdleTime=1800000 +tunnel.0.option.i2cp.closeOnIdle=false +tunnel.0.option.i2cp.delayOpen=false +tunnel.0.option.i2cp.destination.sigType=EdDSA_SHA512_Ed25519 +tunnel.0.option.i2cp.newDestOnResume=false +tunnel.0.option.i2cp.reduceIdleTime=900000 +tunnel.0.option.i2cp.reduceOnIdle=true +tunnel.0.option.i2cp.reduceQuantity=1 +tunnel.0.option.i2p.streaming.connectDelay=1000 +tunnel.0.option.i2ptunnel.httpclient.SSLOutproxies=false.i2p +tunnel.0.option.i2ptunnel.httpclient.allowInternalSSL=true +tunnel.0.option.i2ptunnel.httpclient.jumpServers=http://stats.i2p/cgi-bin/jump.cgi?a=,http://no.i2p/jump/,http://i2pjump.i2p/jump/ +tunnel.0.option.i2ptunnel.httpclient.sendAccept=false +tunnel.0.option.i2ptunnel.httpclient.sendReferer=false +tunnel.0.option.i2ptunnel.httpclient.sendUserAgent=false +tunnel.0.option.i2ptunnel.useLocalOutproxy=false +tunnel.0.option.inbound.backupQuantity=0 +tunnel.0.option.inbound.length=3 +tunnel.0.option.inbound.lengthVariance=0 +tunnel.0.option.inbound.nickname=shared clients +tunnel.0.option.inbound.quantity=2 +tunnel.0.option.outbound.backupQuantity=0 +tunnel.0.option.outbound.length=3 +tunnel.0.option.outbound.lengthVariance=0 +tunnel.0.option.outbound.nickname=shared clients +tunnel.0.option.outbound.priority=10 +tunnel.0.option.outbound.quantity=2 +tunnel.0.option.outproxyAuth=false +tunnel.0.option.persistentClientKey=false +tunnel.0.option.sslManuallySet=true +tunnel.0.option.useSSL=false +tunnel.0.proxyList=false.i2p +tunnel.0.sharedClient=true +tunnel.0.startOnLoad=true +tunnel.0.type=httpclient +tunnel.1.description=IRC tunnel to access the Irc2P network +tunnel.1.i2cpHost=127.0.0.1 +tunnel.1.i2cpPort=7654 +tunnel.1.interface=127.0.0.1 +tunnel.1.listenPort=6668 +tunnel.1.name=Irc2P +tunnel.1.option.crypto.lowTagThreshold=14 +tunnel.1.option.crypto.tagsToSend=20 +tunnel.1.option.i2cp.closeIdleTime=1200000 +tunnel.1.option.i2cp.closeOnIdle=true +tunnel.1.option.i2cp.delayOpen=true +tunnel.1.option.i2cp.destination.sigType=ECDSA_SHA256_P256 +tunnel.1.option.i2cp.newDestOnResume=false +tunnel.1.option.i2cp.reduceIdleTime=600000 +tunnel.1.option.i2cp.reduceOnIdle=true +tunnel.1.option.i2cp.reduceQuantity=1 +tunnel.1.option.i2p.streaming.connectDelay=1000 +tunnel.1.option.i2p.streaming.maxWindowSize=16 +tunnel.1.option.inbound.length=3 +tunnel.1.option.inbound.lengthVariance=0 +tunnel.1.option.inbound.nickname=Irc2P +tunnel.1.option.outbound.length=3 +tunnel.1.option.outbound.lengthVariance=0 +tunnel.1.option.outbound.nickname=Irc2P +tunnel.1.option.outbound.priority=15 +tunnel.1.sharedClient=false +tunnel.1.startOnLoad=true +tunnel.1.targetDestination=irc.00.i2p:6667,irc.postman.i2p:6667,irc.echelon.i2p:6667 +tunnel.1.type=ircclient +tunnel.2.description=I2P Monotone Server +tunnel.2.i2cpHost=127.0.0.1 +tunnel.2.i2cpPort=7654 +tunnel.2.interface=127.0.0.1 +tunnel.2.listenPort=8998 +tunnel.2.name=mtn.i2p-projekt.i2p +tunnel.2.option.i2cp.destination.sigType=EdDSA_SHA512_Ed25519 +tunnel.2.option.i2cp.reduceIdleTime=900000 +tunnel.2.option.i2cp.reduceOnIdle=true +tunnel.2.option.i2cp.reduceQuantity=1 +tunnel.2.option.inbound.backupQuantity=0 +tunnel.2.option.inbound.length=3 +tunnel.2.option.inbound.lengthVariance=0 +tunnel.2.option.inbound.nickname=shared clients +tunnel.2.option.inbound.quantity=2 +tunnel.2.option.outbound.backupQuantity=0 +tunnel.2.option.outbound.length=3 +tunnel.2.option.outbound.lengthVariance=0 +tunnel.2.option.outbound.nickname=shared clients +tunnel.2.option.outbound.quantity=2 +tunnel.2.sharedClient=true +tunnel.2.startOnLoad=false +tunnel.2.targetDestination=mtn.i2p-projekt.i2p:4691 +tunnel.2.type=client +tunnel.3.description=My eepsite +tunnel.3.i2cpHost=127.0.0.1 +tunnel.3.i2cpPort=7654 +tunnel.3.name=I2P webserver +tunnel.3.option.i2cp.destination.sigType=7 +tunnel.3.option.i2p.streaming.limitAction=http +tunnel.3.option.i2p.streaming.maxConcurrentStreams=20 +tunnel.3.option.i2p.streaming.maxConnsPerDay=100 +tunnel.3.option.i2p.streaming.maxConnsPerHour=40 +tunnel.3.option.i2p.streaming.maxConnsPerMinute=10 +tunnel.3.option.i2p.streaming.maxTotalConnsPerMinute=25 +tunnel.3.option.inbound.length=3 +tunnel.3.option.inbound.lengthVariance=0 +tunnel.3.option.inbound.nickname=eepsite +tunnel.3.option.maxPosts=3 +tunnel.3.option.maxTotalPosts=10 +tunnel.3.option.outbound.length=3 +tunnel.3.option.outbound.lengthVariance=0 +tunnel.3.option.outbound.nickname=eepsite +tunnel.3.option.shouldBundleReplyInfo=false +tunnel.3.privKeyFile=eepsite/eepPriv.dat +tunnel.3.spoofedHost=mysite.i2p +tunnel.3.startOnLoad=false +tunnel.3.targetHost=127.0.0.1 +tunnel.3.targetPort=7658 +tunnel.3.type=httpserver +tunnel.4.description=smtp server +tunnel.4.i2cpHost=127.0.0.1 +tunnel.4.i2cpPort=7654 +tunnel.4.interface=127.0.0.1 +tunnel.4.listenPort=7659 +tunnel.4.name=smtp.postman.i2p +tunnel.4.option.i2cp.destination.sigType=EdDSA_SHA512_Ed25519 +tunnel.4.option.i2cp.reduceIdleTime=900000 +tunnel.4.option.i2cp.reduceOnIdle=true +tunnel.4.option.i2cp.reduceQuantity=1 +tunnel.4.option.inbound.backupQuantity=0 +tunnel.4.option.inbound.length=3 +tunnel.4.option.inbound.lengthVariance=0 +tunnel.4.option.inbound.nickname=shared clients +tunnel.4.option.inbound.quantity=2 +tunnel.4.option.outbound.backupQuantity=0 +tunnel.4.option.outbound.length=3 +tunnel.4.option.outbound.lengthVariance=0 +tunnel.4.option.outbound.nickname=shared clients +tunnel.4.option.outbound.quantity=2 +tunnel.4.sharedClient=true +tunnel.4.startOnLoad=true +tunnel.4.targetDestination=smtp.postman.i2p:25 +tunnel.4.type=client +tunnel.5.description=pop3 server +tunnel.5.i2cpHost=127.0.0.1 +tunnel.5.i2cpPort=7654 +tunnel.5.interface=127.0.0.1 +tunnel.5.listenPort=7660 +tunnel.5.name=pop3.postman.i2p +tunnel.5.option.i2cp.destination.sigType=EdDSA_SHA512_Ed25519 +tunnel.5.option.i2cp.reduceIdleTime=900000 +tunnel.5.option.i2cp.reduceOnIdle=true +tunnel.5.option.i2cp.reduceQuantity=1 +tunnel.5.option.i2p.streaming.connectDelay=1000 +tunnel.5.option.inbound.backupQuantity=0 +tunnel.5.option.inbound.length=3 +tunnel.5.option.inbound.lengthVariance=0 +tunnel.5.option.inbound.nickname=shared clients +tunnel.5.option.inbound.quantity=2 +tunnel.5.option.outbound.backupQuantity=0 +tunnel.5.option.outbound.length=3 +tunnel.5.option.outbound.lengthVariance=0 +tunnel.5.option.outbound.nickname=shared clients +tunnel.5.option.outbound.quantity=2 +tunnel.5.sharedClient=true +tunnel.5.startOnLoad=true +tunnel.5.targetDestination=pop.postman.i2p:110 +tunnel.5.type=client +tunnel.6.description=HTTPS proxy for browsing eepsites and the web +tunnel.6.i2cpHost=127.0.0.1 +tunnel.6.i2cpPort=7654 +tunnel.6.interface=127.0.0.1 +tunnel.6.listenPort=4445 +tunnel.6.name=I2P HTTPS Proxy +tunnel.6.option.i2cp.reduceIdleTime=900000 +tunnel.6.option.i2cp.reduceOnIdle=true +tunnel.6.option.i2cp.reduceQuantity=1 +tunnel.6.option.i2p.streaming.connectDelay=1000 +tunnel.6.option.inbound.backupQuantity=0 +tunnel.6.option.inbound.length=3 +tunnel.6.option.inbound.lengthVariance=0 +tunnel.6.option.inbound.nickname=shared clients +tunnel.6.option.inbound.quantity=2 +tunnel.6.option.outbound.backupQuantity=0 +tunnel.6.option.outbound.length=3 +tunnel.6.option.outbound.lengthVariance=0 +tunnel.6.option.outbound.nickname=shared clients +tunnel.6.option.outbound.quantity=2 +tunnel.6.proxyList=outproxy-tor.meeh.i2p +tunnel.6.sharedClient=true +tunnel.6.startOnLoad=true +tunnel.6.type=connectclient diff --git a/plinth/modules/i2p/tests/test_helpers.py b/plinth/modules/i2p/tests/test_helpers.py new file mode 100644 index 000000000..dc817c396 --- /dev/null +++ b/plinth/modules/i2p/tests/test_helpers.py @@ -0,0 +1,64 @@ +# This file is part of FreedomBox. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import unittest + +from pathlib import Path + +from plinth.modules.i2p.helpers import TunnelEditor + +DATA_DIR = Path(__file__).parent / 'data' +TUNNEL_CONF_PATH = DATA_DIR / 'i2ptunnel.config' +TUNNEL_HTTP_NAME = 'I2P HTTP Proxy' + + +class TunnelEditorTests(unittest.TestCase): + + def setUp(self): + self.editor = TunnelEditor(str(TUNNEL_CONF_PATH)) + + def test_reading_conf(self): + self.editor.read_conf() + self.assertGreater(len(self.editor.lines), 1) + + def test_setting_idx(self): + self.editor.read_conf() + self.assertIsNone(self.editor.idx) + self.editor.set_tunnel_idx(TUNNEL_HTTP_NAME) + self.assertEqual(self.editor.idx, 0) + + def test_setting_tunnel_props(self): + self.editor.read_conf() + self.editor.set_tunnel_idx('I2P HTTP Proxy') + interface = '0.0.0.0' + self.editor.set_tunnel_prop('interface', interface) + self.assertEqual(self.editor['interface'], interface) + + def test_getting_inexistent_props(self): + self.editor.read_conf() + self.editor.idx = 0 + self.assertRaises(KeyError, self.editor.__getitem__, 'blabla') + + def test_setting_new_props(self): + self.editor.read_conf() + self.editor.idx = 0 + value = 'lol' + prop = 'blablabla' + self.editor[prop] = value + self.assertEqual(self.editor[prop], value) + + +if __name__ == '__main__': + unittest.main()