ZeroNet/src/util/helper.py

357 lines
10 KiB
Python

import os
import stat
import socket
import struct
import re
import collections
import time
import logging
import base64
import json
import gevent
from Config import config
def atomicWrite(dest, content, mode="wb"):
try:
with open(dest + "-tmpnew", mode) as f:
f.write(content)
f.flush()
os.fsync(f.fileno())
if os.path.isfile(dest + "-tmpold"): # Previous incomplete write
os.rename(dest + "-tmpold", dest + "-tmpold-%s" % time.time())
if os.path.isfile(dest): # Rename old file to -tmpold
os.rename(dest, dest + "-tmpold")
os.rename(dest + "-tmpnew", dest)
if os.path.isfile(dest + "-tmpold"):
os.unlink(dest + "-tmpold") # Remove old file
return True
except Exception as err:
from Debug import Debug
logging.error(
"File %s write failed: %s, (%s) reverting..." %
(dest, Debug.formatException(err), Debug.formatStack())
)
if os.path.isfile(dest + "-tmpold") and not os.path.isfile(dest):
os.rename(dest + "-tmpold", dest)
return False
def jsonDumps(data):
content = json.dumps(data, indent=1, sort_keys=True)
# Make it a little more compact by removing unnecessary white space
def compact_dict(match):
if "\n" in match.group(0):
return match.group(0).replace(match.group(1), match.group(1).strip())
else:
return match.group(0)
content = re.sub(r"\{(\n[^,\[\{]{10,100000}?)\}[, ]{0,2}\n", compact_dict, content, flags=re.DOTALL)
def compact_list(match):
if "\n" in match.group(0):
stripped_lines = re.sub("\n[ ]*", "", match.group(1))
return match.group(0).replace(match.group(1), stripped_lines)
else:
return match.group(0)
content = re.sub(r"\[([^\[\{]{2,100000}?)\][, ]{0,2}\n", compact_list, content, flags=re.DOTALL)
# Remove end of line whitespace
content = re.sub(r"(?m)[ ]+$", "", content)
return content
def openLocked(path, mode="wb"):
try:
if os.name == "posix":
import fcntl
f = open(path, mode)
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
elif os.name == "nt":
import msvcrt
f = open(path, mode)
msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
else:
f = open(path, mode)
except (IOError, PermissionError, BlockingIOError) as err:
raise BlockingIOError("Unable to lock file: %s" % err)
return f
def getFreeSpace():
free_space = -1
if "statvfs" in dir(os): # Unix
statvfs = os.statvfs(config.data_dir.encode("utf8"))
free_space = statvfs.f_frsize * statvfs.f_bavail
else: # Windows
try:
import ctypes
free_space_pointer = ctypes.c_ulonglong(0)
ctypes.windll.kernel32.GetDiskFreeSpaceExW(
ctypes.c_wchar_p(config.data_dir), None, None, ctypes.pointer(free_space_pointer)
)
free_space = free_space_pointer.value
except Exception as err:
logging.error("GetFreeSpace error: %s" % err)
return free_space
def sqlquote(value):
if type(value) is int:
return str(value)
else:
return "'%s'" % value.replace("'", "''")
def shellquote(*args):
if len(args) == 1:
return '"%s"' % args[0].replace('"', "")
else:
return tuple(['"%s"' % arg.replace('"', "") for arg in args])
def packPeers(peers):
packed_peers = {"ipv4": [], "ipv6": [], "onion": []}
for peer in peers:
try:
ip_type = getIpType(peer.ip)
if ip_type in packed_peers:
packed_peers[ip_type].append(peer.packMyAddress())
except Exception:
logging.debug("Error packing peer address: %s" % peer)
return packed_peers
# ip, port to packed 6byte or 18byte format
def packAddress(ip, port):
if ":" in ip:
return socket.inet_pton(socket.AF_INET6, ip) + struct.pack("H", port)
else:
return socket.inet_aton(ip) + struct.pack("H", port)
# From 6byte or 18byte format to ip, port
def unpackAddress(packed):
if len(packed) == 18:
return socket.inet_ntop(socket.AF_INET6, packed[0:16]), struct.unpack_from("H", packed, 16)[0]
else:
if len(packed) != 6:
raise Exception("Invalid length ip4 packed address: %s" % len(packed))
return socket.inet_ntoa(packed[0:4]), struct.unpack_from("H", packed, 4)[0]
# onion, port to packed 12byte format
def packOnionAddress(onion, port):
onion = onion.replace(".onion", "")
return base64.b32decode(onion.upper()) + struct.pack("H", port)
# From 12byte format to ip, port
def unpackOnionAddress(packed):
return base64.b32encode(packed[0:-2]).lower().decode() + ".onion", struct.unpack("H", packed[-2:])[0]
# Get dir from file
# Return: data/site/content.json -> data/site/
def getDirname(path):
if "/" in path:
return path[:path.rfind("/") + 1].lstrip("/")
else:
return ""
# Get dir from file
# Return: data/site/content.json -> content.json
def getFilename(path):
return path[path.rfind("/") + 1:]
def getFilesize(path):
try:
s = os.stat(path)
except Exception:
return None
if stat.S_ISREG(s.st_mode): # Test if it's file
return s.st_size
else:
return None
# Convert hash to hashid for hashfield
def toHashId(hash):
return int(hash[0:4], 16)
# Merge dict values
def mergeDicts(dicts):
back = collections.defaultdict(set)
for d in dicts:
for key, val in d.items():
back[key].update(val)
return dict(back)
# Request https url using gevent SSL error workaround
def httpRequest(url, as_file=False):
if url.startswith("http://"):
import urllib.request
response = urllib.request.urlopen(url)
else: # Hack to avoid Python gevent ssl errors
import socket
import http.client
import ssl
host, request = re.match("https://(.*?)(/.*?)$", url).groups()
conn = http.client.HTTPSConnection(host)
sock = socket.create_connection((conn.host, conn.port), conn.timeout, conn.source_address)
conn.sock = ssl.wrap_socket(sock, conn.key_file, conn.cert_file)
conn.request("GET", request)
response = conn.getresponse()
if response.status in [301, 302, 303, 307, 308]:
logging.info("Redirect to: %s" % response.getheader('Location'))
response = httpRequest(response.getheader('Location'))
if as_file:
import io
data = io.BytesIO()
while True:
buff = response.read(1024 * 16)
if not buff:
break
data.write(buff)
return data
else:
return response
def timerCaller(secs, func, *args, **kwargs):
gevent.spawn_later(secs, timerCaller, secs, func, *args, **kwargs)
func(*args, **kwargs)
def timer(secs, func, *args, **kwargs):
return gevent.spawn_later(secs, timerCaller, secs, func, *args, **kwargs)
def create_connection(address, timeout=None, source_address=None):
if address in config.ip_local:
sock = socket.create_connection_original(address, timeout, source_address)
else:
sock = socket.create_connection_original(address, timeout, socket.bind_addr)
return sock
def socketBindMonkeyPatch(bind_ip, bind_port):
import socket
logging.info("Monkey patching socket to bind to: %s:%s" % (bind_ip, bind_port))
socket.bind_addr = (bind_ip, int(bind_port))
socket.create_connection_original = socket.create_connection
socket.create_connection = create_connection
def limitedGzipFile(*args, **kwargs):
import gzip
class LimitedGzipFile(gzip.GzipFile):
def read(self, size=-1):
return super(LimitedGzipFile, self).read(1024 * 1024 * 25)
return LimitedGzipFile(*args, **kwargs)
def avg(items):
if len(items) > 0:
return sum(items) / len(items)
else:
return 0
def isIp(ip):
if ":" in ip: # IPv6
try:
socket.inet_pton(socket.AF_INET6, ip)
return True
except Exception:
return False
else: # IPv4
try:
socket.inet_aton(ip)
return True
except Exception:
return False
local_ip_pattern = re.compile(r"^127\.|192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[0-1]\.|169\.254\.|::1$|fe80")
def isPrivateIp(ip):
return local_ip_pattern.match(ip)
def getIpType(ip):
if ip.endswith(".onion"):
return "onion"
elif ":" in ip:
return "ipv6"
elif re.match(r"[0-9\.]+$", ip):
return "ipv4"
else:
return "unknown"
def createSocket(ip, sock_type=socket.SOCK_STREAM):
ip_type = getIpType(ip)
if ip_type == "ipv6":
return socket.socket(socket.AF_INET6, sock_type)
else:
return socket.socket(socket.AF_INET, sock_type)
def getInterfaceIps(ip_type="ipv4"):
res = []
if ip_type == "ipv6":
test_ips = ["ff0e::c", "2606:4700:4700::1111"]
else:
test_ips = ['239.255.255.250', "8.8.8.8"]
for test_ip in test_ips:
try:
s = createSocket(test_ip, sock_type=socket.SOCK_DGRAM)
s.connect((test_ip, 1))
res.append(s.getsockname()[0])
except Exception:
pass
try:
res += [ip[4][0] for ip in socket.getaddrinfo(socket.gethostname(), 1)]
except Exception:
pass
res = [re.sub("%.*", "", ip) for ip in res if getIpType(ip) == ip_type and isIp(ip)]
return list(set(res))
def cmp(a, b):
return (a > b) - (a < b)
def encodeResponse(func): # Encode returned data from utf8 to bytes
def wrapper(*args, **kwargs):
back = func(*args, **kwargs)
if "__next__" in dir(back):
for part in back:
if type(part) == bytes:
yield part
else:
yield part.encode()
else:
if type(back) == bytes:
yield back
else:
yield back.encode()
return wrapper