Changes based on style and lint checks. (final_code_quality_10)

This commit is contained in:
coffeedogs 2018-10-10 12:22:07 +01:00
parent a7a21e79ed
commit b27b7239a5
No known key found for this signature in database
GPG Key ID: 9D818C503D0B7E70
6 changed files with 259 additions and 211 deletions

View File

@ -1,84 +0,0 @@
#!/usr/bin/env python2.7
from PyQt4 import QtCore, QtGui
class MigrationWizardIntroPage(QtGui.QWizardPage):
def __init__(self):
super(QtGui.QWizardPage, self).__init__()
self.setTitle("Migrating configuration")
label = QtGui.QLabel("This wizard will help you to migrate your configuration. "
"You can still keep using PyBitMessage once you migrate, the changes are backwards compatible.")
label.setWordWrap(True)
layout = QtGui.QVBoxLayout()
layout.addWidget(label)
self.setLayout(layout)
def nextId(self):
return 1
class MigrationWizardAddressesPage(QtGui.QWizardPage):
def __init__(self, addresses):
super(QtGui.QWizardPage, self).__init__()
self.setTitle("Addresses")
label = QtGui.QLabel("Please select addresses that you are already using with mailchuck. ")
label.setWordWrap(True)
layout = QtGui.QVBoxLayout()
layout.addWidget(label)
self.setLayout(layout)
def nextId(self):
return 10
class MigrationWizardGPUPage(QtGui.QWizardPage):
def __init__(self):
super(QtGui.QWizardPage, self).__init__()
self.setTitle("GPU")
label = QtGui.QLabel("Are you using a GPU? ")
label.setWordWrap(True)
layout = QtGui.QVBoxLayout()
layout.addWidget(label)
self.setLayout(layout)
def nextId(self):
return 10
class MigrationWizardConclusionPage(QtGui.QWizardPage):
def __init__(self):
super(QtGui.QWizardPage, self).__init__()
self.setTitle("All done!")
label = QtGui.QLabel("You successfully migrated.")
label.setWordWrap(True)
layout = QtGui.QVBoxLayout()
layout.addWidget(label)
self.setLayout(layout)
class Ui_MigrationWizard(QtGui.QWizard):
def __init__(self, addresses):
super(QtGui.QWizard, self).__init__()
self.pages = {}
page = MigrationWizardIntroPage()
self.setPage(0, page)
self.setStartId(0)
page = MigrationWizardAddressesPage(addresses)
self.setPage(1, page)
page = MigrationWizardGPUPage()
self.setPage(2, page)
page = MigrationWizardConclusionPage()
self.setPage(10, page)
self.setWindowTitle("Migration from PyBitMessage wizard")
self.adjustSize()
self.show()

View File

@ -1,10 +1,18 @@
from HTMLParser import HTMLParser
"""
src/bitmessageqt/safehtmlparser.py
==================================
"""
# pylint: disable=attribute-defined-outside-init
import inspect
import re
from urllib import quote, quote_plus
from HTMLParser import HTMLParser
from urllib import quote_plus
from urlparse import urlparse
class SafeHTMLParser(HTMLParser):
"""HTML parser with sanitisation"""
# from html5lib.sanitiser
acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area',
'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button',
@ -20,21 +28,29 @@ class SafeHTMLParser(HTMLParser):
'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot',
'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video']
replaces_pre = [["&", "&amp;"], ["\"", "&quot;"], ["<", "&lt;"], [">", "&gt;"]]
replaces_post = [["\n", "<br/>"], ["\t", "&nbsp;&nbsp;&nbsp;&nbsp;"], [" ", "&nbsp; "], [" ", "&nbsp; "], ["<br/> ", "<br/>&nbsp;"]]
src_schemes = [ "data" ]
#uriregex1 = re.compile(r'(?i)\b((?:(https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?]))')
uriregex1 = re.compile(r'((https?|ftp|bitcoin):(?:/{1,3}|[a-z0-9%])(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
replaces_post = [
["\n", "<br/>"],
["\t", "&nbsp;&nbsp;&nbsp;&nbsp;"],
[" ", "&nbsp; "],
[" ", "&nbsp; "],
["<br/> ", "<br/>&nbsp;"]]
src_schemes = ["data"]
uriregex1 = re.compile(
r'((https?|ftp|bitcoin):'
r'(?:/{1,3}|[a-z0-9%])(?:[a-zA-Z]|[0-9]|[$-_@.&+#]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
uriregex2 = re.compile(r'<a href="([^"]+)&amp;')
emailregex = re.compile(r'\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b')
@staticmethod
def replace_pre(text):
"""Perform substring replacement before regex replacements"""
for a in SafeHTMLParser.replaces_pre:
text = text.replace(a[0], a[1])
return text
@staticmethod
def replace_post(text):
"""Perform substring replacement after regex replacements"""
for a in SafeHTMLParser.replaces_post:
text = text.replace(a[0], a[1])
if len(text) > 1 and text[0] == " ":
@ -46,6 +62,7 @@ class SafeHTMLParser(HTMLParser):
self.reset_safe()
def reset_safe(self):
"""Reset runtime variables specific to this class"""
self.elements = set()
self.raw = u""
self.sanitised = u""
@ -53,7 +70,8 @@ class SafeHTMLParser(HTMLParser):
self.allow_picture = False
self.allow_external_src = False
def add_if_acceptable(self, tag, attrs = None):
def add_if_acceptable(self, tag, attrs=None):
"""Add tag if it passes sanitisation"""
if tag not in SafeHTMLParser.acceptable_elements:
return
self.sanitised += "<"
@ -69,35 +87,42 @@ class SafeHTMLParser(HTMLParser):
if url.scheme not in SafeHTMLParser.src_schemes:
val = ""
self.sanitised += " " + quote_plus(attr)
if not (val is None):
if val is not None:
self.sanitised += "=\"" + val + "\""
if inspect.stack()[1][3] == "handle_startendtag":
self.sanitised += "/"
self.sanitised += ">"
def handle_starttag(self, tag, attrs):
""""""
if tag in SafeHTMLParser.acceptable_elements:
self.has_html = True
self.add_if_acceptable(tag, attrs)
def handle_endtag(self, tag):
""""""
self.add_if_acceptable(tag)
def handle_startendtag(self, tag, attrs):
""""""
if tag in SafeHTMLParser.acceptable_elements:
self.has_html = True
self.add_if_acceptable(tag, attrs)
def handle_data(self, data):
""""""
self.sanitised += data
def handle_charref(self, name):
""""""
self.sanitised += "&#" + name + ";"
def handle_entityref(self, name):
""""""
self.sanitised += "&" + name + ";"
def feed(self, data):
""""""
try:
data = unicode(data, 'utf-8')
except UnicodeDecodeError:
@ -112,7 +137,8 @@ class SafeHTMLParser(HTMLParser):
tmp = SafeHTMLParser.replace_post(tmp)
self.raw += tmp
def is_html(self, text = None, allow_picture = False):
def is_html(self, text=None, allow_picture=False):
"""Detect if string contains HTML tags"""
if text:
self.reset()
self.reset_safe()

View File

@ -1,24 +1,31 @@
"""
src/bitmessageqt/support.py
===========================
"""
import ctypes
from PyQt4 import QtCore, QtGui
import ssl
import sys
import time
from PyQt4 import QtCore, QtGui
from version import softwareVersion
import account
from bmconfigparser import BMConfigParser
from debug import logger
import defaults
from foldertree import AccountMixin
from helper_sql import *
from l10n import getTranslationLanguage
from openclpow import openclAvailable, openclEnabled
import network.stats
import paths
import proofofwork
from pyelliptic.openssl import OpenSSL
import queues
import network.stats
import state
from version import softwareVersion
from bmconfigparser import BMConfigParser
from foldertree import AccountMixin
from helper_sql import sqlExecute, sqlQuery
from l10n import getTranslationLanguage
from openclpow import openclEnabled
from pyelliptic.openssl import OpenSSL
# this is BM support address going to Peter Surda
OLD_SUPPORT_ADDRESS = 'BM-2cTkCtMYkrSPwFTpgcBrMrf5d8oZwvMZWK'
@ -26,7 +33,10 @@ SUPPORT_ADDRESS = 'BM-2cUdgkDDAahwPAU6oD2A7DnjqZz3hgY832'
SUPPORT_LABEL = 'PyBitmessage support'
SUPPORT_MY_LABEL = 'My new address'
SUPPORT_SUBJECT = 'Support request'
SUPPORT_MESSAGE = '''You can use this message to send a report to one of the PyBitmessage core developers regarding PyBitmessage or the mailchuck.com email service. If you are using PyBitmessage involuntarily, for example because your computer was infected with ransomware, this is not an appropriate venue for resolving such issues.
SUPPORT_MESSAGE = (
'''You can use this message to send a report to one of the PyBitmessage core developers regarding PyBitmessage'''
''' or the mailchuck.com email service. If you are using PyBitmessage involuntarily, for example because your'''
''' computer was infected with ransomware, this is not an appropriate venue for resolving such issues.
Please describe what you are trying to do:
@ -51,38 +61,63 @@ Locale: {}
SOCKS: {}
UPnP: {}
Connected hosts: {}
'''
''')
def checkAddressBook(myapp):
"""
Check if the support bitmessage address is in addressbook, and add it if necessary. Also delete the old one which
is considered compromised.
"""
sqlExecute('''DELETE from addressbook WHERE address=?''', OLD_SUPPORT_ADDRESS)
queryreturn = sqlQuery('''SELECT * FROM addressbook WHERE address=?''', SUPPORT_ADDRESS)
if queryreturn == []:
sqlExecute('''INSERT INTO addressbook VALUES (?,?)''', str(QtGui.QApplication.translate("Support", SUPPORT_LABEL)), SUPPORT_ADDRESS)
sqlExecute(
'''INSERT INTO addressbook VALUES (?,?)''', str(
QtGui.QApplication.translate(
"Support", SUPPORT_LABEL)), SUPPORT_ADDRESS)
myapp.rerenderAddressBook()
def checkHasNormalAddress():
"""Check if a normal (non-chan, non-subscription) address exists"""
for address in account.getSortedAccounts():
acct = account.accountClass(address)
if acct.type == AccountMixin.NORMAL and BMConfigParser().safeGetBoolean(address, 'enabled'):
return address
return False
def createAddressIfNeeded(myapp):
"""Create a normal address (for sending the support request) in case it doesn't exist already"""
if not checkHasNormalAddress():
queues.addressGeneratorQueue.put(('createRandomAddress', 4, 1, str(QtGui.QApplication.translate("Support", SUPPORT_MY_LABEL)), 1, "", False, defaults.networkDefaultProofOfWorkNonceTrialsPerByte, defaults.networkDefaultPayloadLengthExtraBytes))
queues.addressGeneratorQueue.put(('createRandomAddress',
4,
1,
str(QtGui.QApplication.translate("Support",
SUPPORT_MY_LABEL)),
1,
"",
False,
defaults.networkDefaultProofOfWorkNonceTrialsPerByte,
defaults.networkDefaultPayloadLengthExtraBytes))
while state.shutdown == 0 and not checkHasNormalAddress():
time.sleep(.2)
myapp.rerenderComboBoxSendFrom()
return checkHasNormalAddress()
def createSupportMessage(myapp):
"""Create a support message (pre-fill from system info)"""
# pylint: disable=too-many-locals
checkAddressBook(myapp)
address = createAddressIfNeeded(myapp)
if state.shutdown:
return
myapp.ui.lineEditSubject.setText(str(QtGui.QApplication.translate("Support", SUPPORT_SUBJECT)))
addrIndex = myapp.ui.comboBoxSendFrom.findData(address, QtCore.Qt.UserRole, QtCore.Qt.MatchFixedString | QtCore.Qt.MatchCaseSensitive)
addrIndex = myapp.ui.comboBoxSendFrom.findData(
address, QtCore.Qt.UserRole, QtCore.Qt.MatchFixedString | QtCore.Qt.MatchCaseSensitive)
if addrIndex == -1: # something is very wrong
return
myapp.ui.comboBoxSendFrom.setCurrentIndex(addrIndex)
@ -95,7 +130,7 @@ def createSupportMessage(myapp):
os = sys.platform
if os == "win32":
windowsversion = sys.getwindowsversion()
windowsversion = sys.getwindowsversion() # pylint: disable=no-member
os = "Windows " + str(windowsversion[0]) + "." + str(windowsversion[1])
else:
try:
@ -107,7 +142,8 @@ def createSupportMessage(myapp):
architecture = "32" if ctypes.sizeof(ctypes.c_voidp) == 4 else "64"
pythonversion = sys.version
opensslversion = "%s (Python internal), %s (external for PyElliptic)" % (ssl.OPENSSL_VERSION, OpenSSL._version)
opensslversion = "%s (Python internal), %s (external for PyElliptic)" % (
ssl.OPENSSL_VERSION, OpenSSL._version) # pylint: disable=protected-access
frozen = "N/A"
if paths.frozen:
@ -123,7 +159,9 @@ def createSupportMessage(myapp):
upnp = BMConfigParser().safeGet('bitmessagesettings', 'upnp', "N/A")
connectedhosts = len(network.stats.connectedHostsList())
myapp.ui.textEditMessage.setText(str(QtGui.QApplication.translate("Support", SUPPORT_MESSAGE)).format(version, os, architecture, pythonversion, opensslversion, frozen, portablemode, cpow, openclpow, locale, socks, upnp, connectedhosts))
myapp.ui.textEditMessage.setText(str(QtGui.QApplication.translate("Support", SUPPORT_MESSAGE)).format(
version, os, architecture, pythonversion, opensslversion, frozen,
portablemode, cpow, openclpow, locale, socks, upnp, connectedhosts))
# single msg tab
myapp.ui.tabWidgetSend.setCurrentIndex(

View File

@ -1,27 +1,33 @@
from ConfigParser import NoOptionError, NoSectionError
"""
src/network/connectionpool.py
=============================
"""
import errno
import re
import socket
import time
import random
import re
from ConfigParser import NoOptionError, NoSectionError
import helper_bootstrap
import helper_random
import knownnodes
import protocol
import state
from bmconfigparser import BMConfigParser
from debug import logger
import helper_bootstrap
import knownnodes
from network.proxy import Proxy
from network.tcp import TCPServer, Socks5BMConnection, Socks4aBMConnection, TCPConnection
from network.udp import UDPSocket
from network.connectionchooser import chooseConnection
import network.asyncore_pollchoose as asyncore
import protocol
from network.connectionchooser import chooseConnection
from network.proxy import Proxy
from network.tcp import Socks4aBMConnection, Socks5BMConnection, TCPConnection, TCPServer
from network.udp import UDPSocket
from singleton import Singleton
import state
import helper_random
@Singleton
class BMConnectionPool(object):
"""Pool of all existing connections"""
# pylint: disable=too-many-instance-attributes
def __init__(self):
asyncore.set_rates(
BMConfigParser().safeGetInt("bitmessagesettings", "maxdownloadrate"),
@ -36,9 +42,11 @@ class BMConnectionPool(object):
self.bootstrapped = False
def connectToStream(self, streamNumber):
"""Connect to a bitmessage stream"""
self.streams.append(streamNumber)
def getConnectionByAddr(self, addr):
"""Return an (existing) connection object based on a `Peer` object (IP and port)"""
if addr in self.inboundConnections:
return self.inboundConnections[addr]
try:
@ -56,6 +64,7 @@ class BMConnectionPool(object):
raise KeyError
def isAlreadyConnected(self, nodeid):
"""Check if we're already connected to this peer"""
for i in self.inboundConnections.values() + self.outboundConnections.values():
try:
if nodeid == i.nodeid:
@ -65,6 +74,7 @@ class BMConnectionPool(object):
return False
def addConnection(self, connection):
"""Add a connection object to our internal dict"""
if isinstance(connection, UDPSocket):
return
if connection.isOutbound:
@ -76,6 +86,7 @@ class BMConnectionPool(object):
self.inboundConnections[connection.destination.host] = connection
def removeConnection(self, connection):
"""Remove a connection from our internal dict"""
if isinstance(connection, UDPSocket):
del self.udpSockets[connection.listening.host]
elif isinstance(connection, TCPServer):
@ -96,18 +107,19 @@ class BMConnectionPool(object):
connection.close()
def getListeningIP(self):
"""What IP are we supposed to be listening on?"""
# pylint: disable=no-self-use
if BMConfigParser().safeGet("bitmessagesettings", "onionhostname").endswith(".onion"):
host = BMConfigParser().safeGet("bitmessagesettings", "onionbindip")
else:
host = '127.0.0.1'
if BMConfigParser().safeGetBoolean("bitmessagesettings", "sockslisten") or \
BMConfigParser().get("bitmessagesettings", "socksproxytype") == "none":
# python doesn't like bind + INADDR_ANY?
#host = socket.INADDR_ANY
host = BMConfigParser().get("network", "bind")
return host
def startListening(self, bind=None):
"""Open a listening socket and start accepting connections on it"""
if bind is None:
bind = self.getListeningIP()
port = BMConfigParser().safeGetInt("bitmessagesettings", "port")
@ -116,6 +128,10 @@ class BMConnectionPool(object):
self.listeningSockets[ls.destination] = ls
def startUDPSocket(self, bind=None):
"""
Open an UDP socket. Depending on settings, it can either only accept incoming UDP packets, or also be able to
send them.
"""
if bind is None:
host = self.getListeningIP()
udpSocket = UDPSocket(host=host, announcing=True)
@ -127,7 +143,8 @@ class BMConnectionPool(object):
self.udpSockets[udpSocket.listening.host] = udpSocket
def loop(self):
# defaults to empty loop if outbound connections are maxed
"""defaults to empty loop if outbound connections are maxed"""
# pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks
spawnConnections = False
acceptConnections = True
if BMConfigParser().safeGetBoolean('bitmessagesettings', 'dontconnect'):
@ -135,7 +152,7 @@ class BMConnectionPool(object):
elif BMConfigParser().safeGetBoolean('bitmessagesettings', 'sendoutgoingconnections'):
spawnConnections = True
if BMConfigParser().get('bitmessagesettings', 'socksproxytype')[0:5] == 'SOCKS' and \
(not BMConfigParser().getboolean('bitmessagesettings', 'sockslisten') and \
(not BMConfigParser().getboolean('bitmessagesettings', 'sockslisten') and
".onion" not in BMConfigParser().get('bitmessagesettings', 'onionhostname')):
acceptConnections = False
@ -146,8 +163,8 @@ class BMConnectionPool(object):
self.bootstrapped = True
Proxy.proxy = (BMConfigParser().safeGet("bitmessagesettings", "sockshostname"),
BMConfigParser().safeGetInt("bitmessagesettings", "socksport"))
# TODO AUTH
# TODO reset based on GUI settings changes
# .. todo:: AUTH
# .. todo:: reset based on GUI settings changes
try:
if not BMConfigParser().get("network", "onionsocksproxytype").startswith("SOCKS"):
raise NoOptionError
@ -171,12 +188,6 @@ class BMConnectionPool(object):
if chosen in state.ownAddresses:
continue
#for c in self.outboundConnections:
# if chosen == c.destination:
# continue
#for c in self.inboundConnections:
# if chosen.host == c.destination.host:
# continue
try:
if chosen.host.endswith(".onion") and Proxy.onionproxy is not None:
if BMConfigParser().get("network", "onionsocksproxytype") == "SOCKS5":
@ -203,7 +214,7 @@ class BMConnectionPool(object):
self.outboundConnections.values()
):
i.set_state("close")
# FIXME: rating will be increased after next connection
# .. todo:: FIXME: rating will be increased after next connection
i.handle_close()
if acceptConnections:
@ -211,14 +222,14 @@ class BMConnectionPool(object):
if BMConfigParser().safeGet("network", "bind") == '':
self.startListening()
else:
for bind in re.sub("[^\w.]+", " ", BMConfigParser().safeGet("network", "bind")).split():
for bind in re.sub(r"[^\w.]+", " ", BMConfigParser().safeGet("network", "bind")).split():
self.startListening(bind)
logger.info('Listening for incoming connections.')
if not self.udpSockets:
if BMConfigParser().safeGet("network", "bind") == '':
self.startUDPSocket()
else:
for bind in re.sub("[^\w.]+", " ", BMConfigParser().safeGet("network", "bind")).split():
for bind in re.sub(r"[^\w.]+", " ", BMConfigParser().safeGet("network", "bind")).split():
self.startUDPSocket(bind)
self.startUDPSocket(False)
logger.info('Starting UDP socket(s).')
@ -239,7 +250,6 @@ class BMConnectionPool(object):
loopTime = 2.0
asyncore.loop(timeout=loopTime, count=1000)
reaper = []
for i in self.inboundConnections.values() + self.outboundConnections.values():
minTx = time.time() - 20
if i.fullyEstablished:
@ -250,14 +260,18 @@ class BMConnectionPool(object):
else:
i.close_reason = "Timeout (%is)" % (time.time() - i.lastTx)
i.set_state("close")
for i in self.inboundConnections.values() + self.outboundConnections.values() + self.listeningSockets.values() + self.udpSockets.values():
all_connections = list()
all_connections.extend(self.inboundConnections.values())
all_connections.extend(self.outboundConnections.values())
all_connections.extend(self.listeningSockets.values())
all_connections.extend(self.udpSockets.values())
for i in all_connections:
if not (i.accepting or i.connecting or i.connected):
reaper.append(i)
self.removeConnection(i)
else:
try:
if i.state == "close":
reaper.append(i)
self.removeConnection(i)
except AttributeError:
pass
for i in reaper:
self.removeConnection(i)

View File

@ -1,14 +1,22 @@
"""
src/network/proxy.py
====================
"""
# pylint: disable=protected-access
import socket
import time
from advanceddispatcher import AdvancedDispatcher
import asyncore_pollchoose as asyncore
import state
from advanceddispatcher import AdvancedDispatcher
from bmconfigparser import BMConfigParser
from debug import logger
import network.connectionpool
import state
class ProxyError(Exception):
"""Base proxy exception class"""
errorCodes = ("UnknownError")
def __init__(self, code=-1):
@ -21,6 +29,7 @@ class ProxyError(Exception):
class GeneralProxyError(ProxyError):
"""General proxy error class (not specfic to an implementation)"""
errorCodes = ("Success",
"Invalid data",
"Not connected",
@ -34,6 +43,7 @@ class GeneralProxyError(ProxyError):
class Proxy(AdvancedDispatcher):
"""Base proxy class for AdvancedDispatcher"""
# these are global, and if you change config during runtime, all active/new
# instances should change too
_proxy = ("127.0.0.1", 9050)
@ -44,10 +54,12 @@ class Proxy(AdvancedDispatcher):
@property
def proxy(self):
"""Return proxy IP and port"""
return self.__class__._proxy
@proxy.setter
def proxy(self, address):
"""Set proxy IP and port"""
if not isinstance(address, tuple) or (len(address) < 2) or \
(not isinstance(address[0], str) or not isinstance(address[1], int)):
raise ValueError
@ -55,29 +67,35 @@ class Proxy(AdvancedDispatcher):
@property
def auth(self):
"""Return proxy authentication settings"""
return self.__class__._auth
@auth.setter
def auth(self, authTuple):
"""Set proxy authentication (username and password)"""
self.__class__._auth = authTuple
@property
def onion_proxy(self):
"""Return separate proxy IP and port for use only with onion addresses. Untested."""
return self.__class__._onion_proxy
@onion_proxy.setter
def onion_proxy(self, address):
if address is not None and (not isinstance(address, tuple) or (len(address) < 2) or \
"""Set onion proxy address"""
if address is not None and (not isinstance(address, tuple) or (len(address) < 2) or
(not isinstance(address[0], str) or not isinstance(address[1], int))):
raise ValueError
self.__class__._onion_proxy = address
@property
def onion_auth(self):
"""Set proxy authentication for onion hosts only. Untested"""
return self.__class__._onion_auth
@onion_auth.setter
def onion_auth(self, authTuple):
"""Set proxy authentication for onion hosts only. Untested"""
self.__class__._onion_auth = authTuple
def __init__(self, address):
@ -99,6 +117,7 @@ class Proxy(AdvancedDispatcher):
self.connect(self.proxy)
def handle_connect(self):
"""Handle connection event (to the proxy)"""
self.set_state("init")
try:
AdvancedDispatcher.handle_connect(self)
@ -109,5 +128,7 @@ class Proxy(AdvancedDispatcher):
self.state_init()
def state_proxy_handshake_done(self):
"""Handshake is complete at this point"""
# pylint: disable=attribute-defined-outside-init
self.connectedAt = time.time()
return False

View File

@ -1,21 +1,29 @@
"""
src/storage/filesystem.py
=========================
"""
import string
import time
from binascii import hexlify, unhexlify
from os import listdir, makedirs, path, remove, rmdir
import string
from threading import RLock
import time
import traceback
from paths import lookupAppdataFolder
from storage import InventoryStorage, InventoryItem
from storage import InventoryItem, InventoryStorage
class FilesystemInventory(InventoryStorage):
class FilesystemInventory(InventoryStorage): # pylint: disable=too-many-ancestors
"""Module for using filesystem (directory with files) for inventory storage"""
# pylint: disable=abstract-method
topDir = "inventory"
objectDir = "objects"
metadataFilename = "metadata"
dataFilename = "data"
def __init__(self):
super(self.__class__, self).__init__()
super(FilesystemInventory, self).__init__()
self.baseDir = path.join(lookupAppdataFolder(), FilesystemInventory.topDir)
for createDir in [self.baseDir, path.join(self.baseDir, "objects")]:
if path.exists(createDir):
@ -23,72 +31,85 @@ class FilesystemInventory(InventoryStorage):
raise IOError("%s exists but it's not a directory" % (createDir))
else:
makedirs(createDir)
self.lock = RLock() # Guarantees that two receiveDataThreads don't receive and process the same message concurrently (probably sent by a malicious individual)
self.lock = RLock()
# Guarantees that two receiveDataThreads don't receive and process the
# same message concurrently (probably sent by a malicious individual)
self._inventory = {}
self._load()
def __contains__(self, hash):
retval = False
def __contains__(self, hash_):
for streamDict in self._inventory.values():
if hash in streamDict:
if hash_ in streamDict:
return True
return False
def __getitem__(self, hash):
def __getitem__(self, hash_):
for streamDict in self._inventory.values():
try:
retval = streamDict[hash]
retval = streamDict[hash_]
except KeyError:
continue
if retval.payload is None:
retval = InventoryItem(retval.type, retval.stream, self.getData(hash), retval.expires, retval.tag)
retval = InventoryItem(retval.type, retval.stream, self.getData(hash_), retval.expires, retval.tag)
return retval
raise KeyError(hash)
raise KeyError(hash_)
def __setitem__(self, hash, value):
def __setitem__(self, hash_, value):
with self.lock:
value = InventoryItem(*value)
try:
makedirs(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash)))
makedirs(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash_)))
except OSError:
pass
try:
with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.metadataFilename), 'w') as f:
inv_path = path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash_))
with open(path.join(inv_path, FilesystemInventory.metadataFilename), 'w') as f:
f.write("%s,%s,%s,%s," % (value.type, value.stream, value.expires, hexlify(value.tag)))
with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.dataFilename), 'w') as f:
with open(path.join(inv_path, FilesystemInventory.dataFilename), 'w') as f:
f.write(value.payload)
except IOError:
raise KeyError
try:
self._inventory[value.stream][hash] = value
self._inventory[value.stream][hash_] = value
except KeyError:
self._inventory[value.stream] = {}
self._inventory[value.stream][hash] = value
self._inventory[value.stream][hash_] = value
def delHashId(self, hash):
for stream in self._inventory.keys():
def delHashId(self, hash_):
"""Remove object from inventory"""
for stream in self._inventory:
try:
del self._inventory[stream][hash]
del self._inventory[stream][hash_]
except KeyError:
pass
with self.lock:
try:
remove(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.metadataFilename))
remove(
path.join(
self.baseDir,
FilesystemInventory.objectDir,
hexlify(hash_),
FilesystemInventory.metadataFilename))
except IOError:
pass
try:
remove(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash), FilesystemInventory.dataFilename))
remove(
path.join(
self.baseDir,
FilesystemInventory.objectDir,
hexlify(hash_),
FilesystemInventory.dataFilename))
except IOError:
pass
try:
rmdir(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash)))
rmdir(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hash_)))
except IOError:
pass
def __iter__(self):
elems = []
for streamDict in self._inventory.values():
elems.extend (streamDict.keys())
elems.extend(streamDict.keys())
return elems.__iter__()
def __len__(self):
@ -103,39 +124,47 @@ class FilesystemInventory(InventoryStorage):
try:
objectType, streamNumber, expiresTime, tag = self.getMetadata(hashId)
try:
newInventory[streamNumber][hashId] = InventoryItem(objectType, streamNumber, None, expiresTime, tag)
newInventory[streamNumber][hashId] = InventoryItem(
objectType, streamNumber, None, expiresTime, tag)
except KeyError:
newInventory[streamNumber] = {}
newInventory[streamNumber][hashId] = InventoryItem(objectType, streamNumber, None, expiresTime, tag)
newInventory[streamNumber][hashId] = InventoryItem(
objectType, streamNumber, None, expiresTime, tag)
except KeyError:
print "error loading %s" % (hexlify(hashId))
pass
self._inventory = newInventory
# for i, v in self._inventory.items():
# print "loaded stream: %s, %i items" % (i, len(v))
def stream_list(self):
"""Return list of streams"""
return self._inventory.keys()
def object_list(self):
"""Return inventory vectors (hashes) from a directory"""
return [unhexlify(x) for x in listdir(path.join(self.baseDir, FilesystemInventory.objectDir))]
def getData(self, hashId):
"""Get object data"""
try:
with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId), FilesystemInventory.dataFilename), 'r') as f:
inv_path = path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId))
with open(path.join(inv_path, FilesystemInventory.dataFilename), 'r') as f:
return f.read()
except IOError:
raise AttributeError
def getMetadata(self, hashId):
"""Get object metadata"""
# pylint: disable=unused-variable
try:
with open(path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId), FilesystemInventory.metadataFilename), 'r') as f:
inv_path = path.join(self.baseDir, FilesystemInventory.objectDir, hexlify(hashId))
with open(path.join(inv_path, FilesystemInventory.metadataFilename), 'r') as f:
objectType, streamNumber, expiresTime, tag, undef = string.split(f.read(), ",", 4)
return [int(objectType), int(streamNumber), int(expiresTime), unhexlify(tag)]
except IOError:
raise KeyError
def by_type_and_tag(self, objectType, tag):
"""Get object type and tag"""
# pylint: disable=unused-variable
retval = []
for stream, streamDict in self._inventory:
for hashId, item in streamDict:
@ -149,12 +178,14 @@ class FilesystemInventory(InventoryStorage):
return retval
def hashes_by_stream(self, stream):
"""Return inventory vectors (hashes) for a stream"""
try:
return self._inventory[stream].keys()
except KeyError:
return []
def unexpired_hashes_by_stream(self, stream):
"""Return unexpired hashes in the inventory for a particular stream"""
t = int(time.time())
try:
return [x for x, value in self._inventory[stream].items() if value.expires > t]
@ -162,12 +193,14 @@ class FilesystemInventory(InventoryStorage):
return []
def flush(self):
"""Flush the inventory and create a new, empty one"""
self._load()
def clean(self):
"""Clean out old items from the inventory"""
minTime = int(time.time()) - (60 * 60 * 30)
deletes = []
for stream, streamDict in self._inventory.items():
for _, streamDict in self._inventory.items():
for hashId, item in streamDict.items():
if item.expires < minTime:
deletes.append(hashId)