Changes based on style and lint checks. (final_code_quality_2) #1357
|
@ -1,79 +1,108 @@
|
||||||
#!/usr/bin/python2.7
|
#!/usr/bin/python2.7
|
||||||
|
"""
|
||||||
|
src/settingsmixin.py
|
||||||
|
====================
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
|
|
||||||
class SettingsMixin(object):
|
class SettingsMixin(object):
|
||||||
|
|||||||
|
"""Mixin for adding geometry and state saving between restarts."""
|
||||||
def warnIfNoObjectName(self):
|
def warnIfNoObjectName(self):
|
||||||
Handle objects which don't have a name. Currently it ignores them. Objects without a name can't have their state/geometry saved as they don't have an identifier. Handle objects which don't have a name. Currently it ignores them. Objects without a name can't have their state/geometry saved as they don't have an identifier.
|
|||||||
|
"""
|
||||||
|
Handle objects which don't have a name. Currently it ignores them. Objects without a name can't have their
|
||||||
|
state/geometry saved as they don't have an identifier.
|
||||||
|
"""
|
||||||
if self.objectName() == "":
|
if self.objectName() == "":
|
||||||
# TODO: logger
|
# .. todo:: logger
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def writeState(self, source):
|
def writeState(self, source):
|
||||||
|
"""Save object state (e.g. relative position of a splitter)"""
|
||||||
self.warnIfNoObjectName()
|
self.warnIfNoObjectName()
|
||||||
Save object state (e.g. relative position of a splitter) Save object state (e.g. relative position of a splitter)
|
|||||||
settings = QtCore.QSettings()
|
settings = QtCore.QSettings()
|
||||||
settings.beginGroup(self.objectName())
|
settings.beginGroup(self.objectName())
|
||||||
settings.setValue("state", source.saveState())
|
settings.setValue("state", source.saveState())
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
|
|
||||||
def writeGeometry(self, source):
|
def writeGeometry(self, source):
|
||||||
|
"""Save object geometry (e.g. window size and position)"""
|
||||||
self.warnIfNoObjectName()
|
self.warnIfNoObjectName()
|
||||||
settings = QtCore.QSettings()
|
settings = QtCore.QSettings()
|
||||||
Save object geometry (e.g. window size and position) Save object geometry (e.g. window size and position)
|
|||||||
settings.beginGroup(self.objectName())
|
settings.beginGroup(self.objectName())
|
||||||
settings.setValue("geometry", source.saveGeometry())
|
settings.setValue("geometry", source.saveGeometry())
|
||||||
settings.endGroup()
|
settings.endGroup()
|
||||||
|
|
||||||
def readGeometry(self, target):
|
def readGeometry(self, target):
|
||||||
|
"""Load object geometry"""
|
||||||
self.warnIfNoObjectName()
|
self.warnIfNoObjectName()
|
||||||
settings = QtCore.QSettings()
|
settings = QtCore.QSettings()
|
||||||
try:
|
try:
|
||||||
Load object geometry Load object geometry
|
|||||||
geom = settings.value("/".join([str(self.objectName()), "geometry"]))
|
geom = settings.value("/".join([str(self.objectName()), "geometry"]))
|
||||||
target.restoreGeometry(geom.toByteArray() if hasattr(geom, 'toByteArray') else geom)
|
target.restoreGeometry(geom.toByteArray() if hasattr(geom, 'toByteArray') else geom)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def readState(self, target):
|
def readState(self, target):
|
||||||
|
"""Load object state"""
|
||||||
self.warnIfNoObjectName()
|
self.warnIfNoObjectName()
|
||||||
settings = QtCore.QSettings()
|
settings = QtCore.QSettings()
|
||||||
try:
|
try:
|
||||||
state = settings.value("/".join([str(self.objectName()), "state"]))
|
state = settings.value("/".join([str(self.objectName()), "state"]))
|
||||||
Load object state Load object state
|
|||||||
target.restoreState(state.toByteArray() if hasattr(state, 'toByteArray') else state)
|
target.restoreState(state.toByteArray() if hasattr(state, 'toByteArray') else state)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SMainWindow(QtGui.QMainWindow, SettingsMixin):
|
class SMainWindow(QtGui.QMainWindow, SettingsMixin):
|
||||||
|
"""Main window with Settings functionality."""
|
||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
|
"""Load main window settings."""
|
||||||
self.readGeometry(self)
|
self.readGeometry(self)
|
||||||
self.readState(self)
|
self.readState(self)
|
||||||
|
|
||||||
def saveSettings(self):
|
def saveSettings(self):
|
||||||
Main window with Settings functionality. Main window with Settings functionality.
|
|||||||
|
"""Save main window settings"""
|
||||||
self.writeState(self)
|
self.writeState(self)
|
||||||
Load main window settings. Load main window settings.
|
|||||||
self.writeGeometry(self)
|
self.writeGeometry(self)
|
||||||
|
|
||||||
|
|
||||||
class STableWidget(QtGui.QTableWidget, SettingsMixin):
|
class STableWidget(QtGui.QTableWidget, SettingsMixin):
|
||||||
|
"""Table widget with Settings functionality"""
|
||||||
|
# pylint: disable=too-many-ancestors
|
||||||
Save main window settings Save main window settings
|
|||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
|
"""Load table settings."""
|
||||||
self.readState(self.horizontalHeader())
|
self.readState(self.horizontalHeader())
|
||||||
|
|
||||||
def saveSettings(self):
|
def saveSettings(self):
|
||||||
|
"""Save table settings."""
|
||||||
Table widget with Settings functionality Table widget with Settings functionality
|
|||||||
self.writeState(self.horizontalHeader())
|
self.writeState(self.horizontalHeader())
|
||||||
|
|
||||||
|
|
||||||
Load table settings. Load table settings.
|
|||||||
class SSplitter(QtGui.QSplitter, SettingsMixin):
|
class SSplitter(QtGui.QSplitter, SettingsMixin):
|
||||||
|
"""Splitter with Settings functionality."""
|
||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
|
"""Load splitter settings"""
|
||||||
Save table settings. Save table settings.
|
|||||||
self.readState(self)
|
self.readState(self)
|
||||||
|
|
||||||
def saveSettings(self):
|
def saveSettings(self):
|
||||||
|
"""Save splitter settings."""
|
||||||
self.writeState(self)
|
self.writeState(self)
|
||||||
|
|
||||||
Splitter with Settings functionality. Splitter with Settings functionality.
|
|||||||
|
|
||||||
class STreeWidget(QtGui.QTreeWidget, SettingsMixin):
|
class STreeWidget(QtGui.QTreeWidget, SettingsMixin):
|
||||||
Load splitter settings Load splitter settings
|
|||||||
|
"""Tree widget with settings functionality."""
|
||||||
|
# pylint: disable=too-many-ancestors
|
||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
#recurse children
|
"""Load tree settings."""
|
||||||
Save splitter settings. Save splitter settings.
|
|||||||
#self.readState(self)
|
# recurse children
|
||||||
|
# self.readState(self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def saveSettings(self):
|
def saveSettings(self):
|
||||||
#recurse children
|
"""Save tree settings"""
|
||||||
Tree widget with settings functionality. Tree widget with settings functionality.
|
|||||||
#self.writeState(self)
|
# recurse children
|
||||||
|
# self.writeState(self)
|
||||||
pass
|
pass
|
||||||
Load tree settings. Load tree settings.
|
|||||||
|
|
|
@ -1,36 +1,48 @@
|
||||||
typo typo
typo typo
|
|||||||
|
"""
|
||||||
typo typo
|
|||||||
|
src/network/socks5.py
|
||||||
typo typo
|
|||||||
|
=====================
|
||||||
typo typo
|
|||||||
|
|
||||||
typo typo
|
|||||||
|
"""
|
||||||
typo typo
|
|||||||
|
# pylint: disable=attribute-defined-outside-init
|
||||||
typo typo
|
|||||||
|
|
||||||
typo typo
|
|||||||
import socket
|
import socket
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from proxy import Proxy, ProxyError, GeneralProxyError
|
from proxy import GeneralProxyError, Proxy, ProxyError
|
||||||
typo typo
typo typo
|
|||||||
|
|
||||||
typo typo
|
|||||||
|
|
||||||
class Socks5AuthError(ProxyError):
|
class Socks5AuthError(ProxyError):
|
||||||
|
"""Thrown when the socks5 protocol encounters an authentication error"""
|
||||||
Thrown when the socks5 protocol encounters an authentication error Thrown when the socks5 protocol encounters an authentication error
typo typo
|
|||||||
errorCodes = ("Succeeded",
|
errorCodes = ("Succeeded",
|
||||||
"Authentication is required",
|
"Authentication is required",
|
||||||
typo typo
typo typo
|
|||||||
"All offered authentication methods were rejected",
|
"All offered authentication methods were rejected",
|
||||||
typo typo
typo typo
|
|||||||
"Unknown username or invalid password",
|
"Unknown username or invalid password",
|
||||||
typo typo
typo typo
|
|||||||
"Unknown error")
|
"Unknown error")
|
||||||
typo typo
typo typo
|
|||||||
|
|
||||||
|
|
||||||
class Socks5Error(ProxyError):
|
class Socks5Error(ProxyError):
|
||||||
|
"""Thrown when socks5 protocol encounters an error"""
|
||||||
typo typo
|
|||||||
errorCodes = ("Succeeded",
|
errorCodes = ("Succeeded",
|
||||||
"General SOCKS server failure",
|
"General SOCKS server failure",
|
||||||
typo typo
typo typo
|
|||||||
"Connection not allowed by ruleset",
|
"Connection not allowed by ruleset",
|
||||||
typo typo
typo typo
|
|||||||
"Network unreachable",
|
"Network unreachable",
|
||||||
typo typo
Thrown when socks5 protocol encounters an error Thrown when socks5 protocol encounters an error
typo typo
|
|||||||
"Host unreachable",
|
"Host unreachable",
|
||||||
typo typo
typo typo
|
|||||||
"Connection refused",
|
"Connection refused",
|
||||||
typo typo
typo typo
|
|||||||
"TTL expired",
|
"TTL expired",
|
||||||
typo typo
typo typo
|
|||||||
"Command not supported",
|
"Command not supported",
|
||||||
typo typo
typo typo
|
|||||||
"Address type not supported",
|
"Address type not supported",
|
||||||
typo typo
typo typo
|
|||||||
"Unknown error")
|
"Unknown error")
|
||||||
typo typo
typo typo
|
|||||||
|
|
||||||
|
|
||||||
class Socks5(Proxy):
|
class Socks5(Proxy):
|
||||||
|
"""A socks5 proxy base class"""
|
||||||
typo typo
|
|||||||
def __init__(self, address=None):
|
def __init__(self, address=None):
|
||||||
Proxy.__init__(self, address)
|
Proxy.__init__(self, address)
|
||||||
self.ipaddr = None
|
self.ipaddr = None
|
||||||
self.destport = address[1]
|
self.destport = address[1]
|
||||||
|
|
||||||
def state_init(self):
|
def state_init(self):
|
||||||
|
"""Protocol initialisation (before connection is established)"""
|
||||||
typo typo
|
|||||||
if self._auth:
|
if self._auth:
|
||||||
self.append_write_buf(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
|
self.append_write_buf(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
|
||||||
else:
|
else:
|
||||||
|
@ -39,6 +51,7 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def state_auth_1(self):
|
def state_auth_1(self):
|
||||||
|
"""Perform authentication if peer is requesting it."""
|
||||||
typo typo
|
|||||||
ret = struct.unpack('BB', self.read_buf[:2])
|
ret = struct.unpack('BB', self.read_buf[:2])
|
||||||
if ret[0] != 5:
|
if ret[0] != 5:
|
||||||
# general error
|
# general error
|
||||||
|
@ -48,9 +61,9 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
self.set_state("auth_done", length=2)
|
self.set_state("auth_done", length=2)
|
||||||
elif ret[1] == 2:
|
elif ret[1] == 2:
|
||||||
# username/password
|
# username/password
|
||||||
self.append_write_buf(struct.pack('BB', 1, len(self._auth[0])) + \
|
self.append_write_buf(struct.pack('BB', 1, len(self._auth[0])) +
|
||||||
typo typo
typo typo
|
|||||||
self._auth[0] + struct.pack('B', len(self._auth[1])) + \
|
self._auth[0] + struct.pack('B', len(self._auth[1])) +
|
||||||
typo typo
typo typo
|
|||||||
self._auth[1])
|
self._auth[1])
|
||||||
typo typo
typo typo
|
|||||||
self.set_state("auth_needed", length=2, expectBytes=2)
|
self.set_state("auth_needed", length=2, expectBytes=2)
|
||||||
else:
|
else:
|
||||||
if ret[1] == 0xff:
|
if ret[1] == 0xff:
|
||||||
|
@ -62,6 +75,7 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def state_auth_needed(self):
|
def state_auth_needed(self):
|
||||||
|
"""Handle response to authentication attempt"""
|
||||||
typo typo
|
|||||||
ret = struct.unpack('BB', self.read_buf[0:2])
|
ret = struct.unpack('BB', self.read_buf[0:2])
|
||||||
if ret[0] != 1:
|
if ret[0] != 1:
|
||||||
# general error
|
# general error
|
||||||
|
@ -74,6 +88,7 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def state_pre_connect(self):
|
def state_pre_connect(self):
|
||||||
|
"""Handle feedback from socks5 while it is connecting on our behalf."""
|
||||||
typo typo
|
|||||||
# Get the response
|
# Get the response
|
||||||
if self.read_buf[0:1] != chr(0x05).encode():
|
if self.read_buf[0:1] != chr(0x05).encode():
|
||||||
self.close()
|
self.close()
|
||||||
|
@ -81,7 +96,7 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
elif self.read_buf[1:2] != chr(0x00).encode():
|
elif self.read_buf[1:2] != chr(0x00).encode():
|
||||||
# Connection failed
|
# Connection failed
|
||||||
self.close()
|
self.close()
|
||||||
if ord(self.read_buf[1:2])<=8:
|
if ord(self.read_buf[1:2]) <= 8:
|
||||||
typo typo
typo typo
|
|||||||
raise Socks5Error(ord(self.read_buf[1:2]))
|
raise Socks5Error(ord(self.read_buf[1:2]))
|
||||||
else:
|
else:
|
||||||
raise Socks5Error(9)
|
raise Socks5Error(9)
|
||||||
|
@ -96,21 +111,31 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def state_proxy_addr_1(self):
|
def state_proxy_addr_1(self):
|
||||||
|
"""Handle IPv4 address returned for peer"""
|
||||||
typo typo
|
|||||||
self.boundaddr = self.read_buf[0:4]
|
self.boundaddr = self.read_buf[0:4]
|
||||||
self.set_state("proxy_port", length=4, expectBytes=2)
|
self.set_state("proxy_port", length=4, expectBytes=2)
|
||||||
Perform authentication if peer is requesting it. Perform authentication if peer is requesting it.
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def state_proxy_addr_2_1(self):
|
def state_proxy_addr_2_1(self):
|
||||||
|
"""
|
||||||
typo typo
|
|||||||
|
Handle other addresses than IPv4 returned for peer (e.g. IPv6, onion, ...). This is part 1 which retrieves the
|
||||||
typo typo
|
|||||||
|
length of the data.
|
||||||
typo typo
|
|||||||
|
"""
|
||||||
typo typo
|
|||||||
self.address_length = ord(self.read_buf[0:1])
|
self.address_length = ord(self.read_buf[0:1])
|
||||||
self.set_state("proxy_addr_2_2", length=1, expectBytes=self.address_length)
|
self.set_state("proxy_addr_2_2", length=1, expectBytes=self.address_length)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def state_proxy_addr_2_2(self):
|
def state_proxy_addr_2_2(self):
|
||||||
|
"""
|
||||||
typo typo
|
|||||||
|
Handle other addresses than IPv4 returned for peer (e.g. IPv6, onion, ...). This is part 2 which retrieves the
|
||||||
typo typo
|
|||||||
|
data.
|
||||||
typo typo
|
|||||||
|
"""
|
||||||
typo typo
|
|||||||
self.boundaddr = self.read_buf[0:self.address_length]
|
self.boundaddr = self.read_buf[0:self.address_length]
|
||||||
self.set_state("proxy_port", length=self.address_length, expectBytes=2)
|
self.set_state("proxy_port", length=self.address_length, expectBytes=2)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def state_proxy_port(self):
|
def state_proxy_port(self):
|
||||||
|
"""Handle peer's port being returned."""
|
||||||
typo typo
|
|||||||
self.boundport = struct.unpack(">H", self.read_buf[0:2])[0]
|
self.boundport = struct.unpack(">H", self.read_buf[0:2])[0]
|
||||||
Init Init
Went for """Child socks5 class used for making outbound connections.""", assuming you were thinking about commenting the Went for """Child socks5 class used for making outbound connections.""", assuming you were thinking about commenting the `__init__` method with your comment
|
|||||||
self.__proxysockname = (self.boundaddr, self.boundport)
|
self.__proxysockname = (self.boundaddr, self.boundport)
|
||||||
if self.ipaddr is not None:
|
if self.ipaddr is not None:
|
||||||
|
@ -121,14 +146,17 @@ class Socks5(Proxy):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def proxy_sock_name(self):
|
def proxy_sock_name(self):
|
||||||
|
"""Handle return value when using SOCKS5 for DNS resolving instead of connecting."""
|
||||||
typo typo
|
|||||||
return socket.inet_ntoa(self.__proxysockname[0])
|
return socket.inet_ntoa(self.__proxysockname[0])
|
||||||
|
|
||||||
|
|
||||||
class Socks5Connection(Socks5):
|
class Socks5Connection(Socks5):
|
||||||
|
"""Child socks5 class used for making outbound connections."""
|
||||||
typo typo
|
|||||||
def __init__(self, address):
|
def __init__(self, address):
|
||||||
Socks5.__init__(self, address=address)
|
Socks5.__init__(self, address=address)
|
||||||
|
|
||||||
def state_auth_done(self):
|
def state_auth_done(self):
|
||||||
|
"""Request connection to be made"""
|
||||||
typo typo
|
|||||||
# Now we can request the actual connection
|
# Now we can request the actual connection
|
||||||
self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00))
|
self.append_write_buf(struct.pack('BBB', 0x05, 0x01, 0x00))
|
||||||
Handle response to authentication attempt Handle response to authentication attempt
|
|||||||
# If the given destination address is an IP address, we'll
|
# If the given destination address is an IP address, we'll
|
||||||
|
@ -138,10 +166,12 @@ class Socks5Connection(Socks5):
|
||||||
typo typo
typo typo
|
|||||||
self.append_write_buf(chr(0x01).encode() + self.ipaddr)
|
self.append_write_buf(chr(0x01).encode() + self.ipaddr)
|
||||||
except socket.error:
|
except socket.error:
|
||||||
# Well it's not an IP number, so it's probably a DNS name.
|
# Well it's not an IP number, so it's probably a DNS name.
|
||||||
if Proxy._remote_dns:
|
if Proxy._remote_dns: # pylint: disable=protected-access
|
||||||
typo typo
typo typo
|
|||||||
# Resolve remotely
|
# Resolve remotely
|
||||||
self.ipaddr = None
|
self.ipaddr = None
|
||||||
self.append_write_buf(chr(0x03).encode() + chr(len(self.destination[0])).encode() + self.destination[0])
|
self.append_write_buf(chr(0x03).encode() +
|
||||||
typo typo
typo typo
|
|||||||
|
chr(len(self.destination[0])).encode() +
|
||||||
typo typo
|
|||||||
|
self.destination[0])
|
||||||
typo typo
|
|||||||
else:
|
else:
|
||||||
# Resolve locally
|
# Resolve locally
|
||||||
self.ipaddr = socket.inet_aton(socket.gethostbyname(self.destination[0]))
|
self.ipaddr = socket.inet_aton(socket.gethostbyname(self.destination[0]))
|
||||||
|
@ -151,6 +181,7 @@ class Socks5Connection(Socks5):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
Handle feedback from socks5 while it is connecting on our behalf. Handle feedback from socks5 while it is connecting on our behalf.
|
|||||||
def state_pre_connect(self):
|
def state_pre_connect(self):
|
||||||
|
"""Tell socks5 to initiate a connection"""
|
||||||
typo typo
|
|||||||
try:
|
try:
|
||||||
return Socks5.state_pre_connect(self)
|
return Socks5.state_pre_connect(self)
|
||||||
except Socks5Error as e:
|
except Socks5Error as e:
|
||||||
|
@ -159,12 +190,14 @@ class Socks5Connection(Socks5):
|
||||||
typo typo
typo typo
|
|||||||
|
|
||||||
|
|
||||||
class Socks5Resolver(Socks5):
|
class Socks5Resolver(Socks5):
|
||||||
|
"""DNS resolver class using socks5"""
|
||||||
typo typo
|
|||||||
def __init__(self, host):
|
def __init__(self, host):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.port = 8444
|
self.port = 8444
|
||||||
Socks5.__init__(self, address=(self.host, self.port))
|
Socks5.__init__(self, address=(self.host, self.port))
|
||||||
|
|
||||||
def state_auth_done(self):
|
def state_auth_done(self):
|
||||||
|
"""Perform resolving"""
|
||||||
typo typo
|
|||||||
# Now we can request the actual connection
|
# Now we can request the actual connection
|
||||||
self.append_write_buf(struct.pack('BBB', 0x05, 0xF0, 0x00))
|
self.append_write_buf(struct.pack('BBB', 0x05, 0xF0, 0x00))
|
||||||
self.append_write_buf(chr(0x03).encode() + chr(len(self.host)).encode() + str(self.host))
|
self.append_write_buf(chr(0x03).encode() + chr(len(self.host)).encode() + str(self.host))
|
||||||
|
@ -173,4 +206,8 @@ class Socks5Resolver(Socks5):
|
||||||
typo typo
typo typo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def resolved(self):
|
def resolved(self):
|
||||||
|
"""
|
||||||
typo typo
|
|||||||
|
Resolving is done, process the return value. To use this within PyBitmessage, a callback needs to be
|
||||||
typo typo
|
|||||||
|
implemented which hasn't been done yet.
|
||||||
typo typo
|
|||||||
|
"""
|
||||||
typo typo
|
|||||||
print "Resolved %s as %s" % (self.host, self.proxy_sock_name())
|
print "Resolved %s as %s" % (self.host, self.proxy_sock_name())
|
||||||
|
|
||||||
typo typo
typo typo
|
Mixin for adding geometry and state saving between restarts.