progress in removing core
parent
e12781a49d
commit
e1676ef168
|
@ -41,7 +41,6 @@ class PrivateAPI:
|
||||||
self.config = config
|
self.config = config
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.startTime = epoch.get_epoch()
|
self.startTime = epoch.get_epoch()
|
||||||
self._crypto = onionrInst.onionrCrypto
|
|
||||||
app = flask.Flask(__name__)
|
app = flask.Flask(__name__)
|
||||||
bindPort = int(config.get('client.client.port', 59496))
|
bindPort = int(config.get('client.client.port', 59496))
|
||||||
self.bindPort = bindPort
|
self.bindPort = bindPort
|
||||||
|
|
|
@ -34,7 +34,7 @@ class PublicAPI:
|
||||||
self.i2pEnabled = config.get('i2p.host', False)
|
self.i2pEnabled = config.get('i2p.host', False)
|
||||||
self.hideBlocks = [] # Blocks to be denied sharing
|
self.hideBlocks = [] # Blocks to be denied sharing
|
||||||
self.host = apiutils.setbindip.set_bind_IP(filepaths.public_API_host_file)
|
self.host = apiutils.setbindip.set_bind_IP(filepaths.public_API_host_file)
|
||||||
self.torAdder = gettransports.get_transports[0]
|
self.torAdder = gettransports.transports[0]
|
||||||
self.bindPort = config.get('client.public.port')
|
self.bindPort = config.get('client.public.port')
|
||||||
self.lastRequest = 0
|
self.lastRequest = 0
|
||||||
self.hitCount = 0 # total rec requests to public api since server started
|
self.hitCount = 0 # total rec requests to public api since server started
|
||||||
|
|
351
onionr/core.py
351
onionr/core.py
|
@ -1,351 +0,0 @@
|
||||||
'''
|
|
||||||
Onionr - Private P2P Communication
|
|
||||||
|
|
||||||
Core Onionr library, useful for external programs. Handles peer & data processing
|
|
||||||
'''
|
|
||||||
'''
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU 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 General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
'''
|
|
||||||
import os, sys, json
|
|
||||||
import logger, netcontroller, config
|
|
||||||
from onionrblockapi import Block
|
|
||||||
import coredb
|
|
||||||
import deadsimplekv as simplekv
|
|
||||||
import onionrcrypto, onionrproofs, onionrevents as events, onionrexceptions
|
|
||||||
import onionrblacklist
|
|
||||||
from onionrusers import onionrusers
|
|
||||||
from onionrstorage import removeblock, setdata
|
|
||||||
import dbcreator, onionrstorage, serializeddata, subprocesspow
|
|
||||||
from etc import onionrvalues, powchoice
|
|
||||||
from onionrutils import localcommand, stringvalidators, bytesconverter, epoch
|
|
||||||
from onionrutils import blockmetadata
|
|
||||||
from utils import identifyhome
|
|
||||||
import storagecounter
|
|
||||||
|
|
||||||
class Core:
|
|
||||||
def __init__(self, torPort=0):
|
|
||||||
'''
|
|
||||||
Initialize Core Onionr library
|
|
||||||
'''
|
|
||||||
# set data dir
|
|
||||||
self.dataDir = identifyhome.identify_home()
|
|
||||||
|
|
||||||
self.usageFile = self.dataDir + 'disk-usage.txt'
|
|
||||||
self.config = config
|
|
||||||
self.maxBlockSize = 10000000 # max block size in bytes
|
|
||||||
|
|
||||||
self.onionrInst = None
|
|
||||||
self.hsAddress = ''
|
|
||||||
self.i2pAddress = config.get('i2p.own_addr', None)
|
|
||||||
self.bootstrapFileLocation = 'static-data/bootstrap-nodes.txt'
|
|
||||||
self.bootstrapList = []
|
|
||||||
self.requirements = onionrvalues.OnionrValues()
|
|
||||||
self.torPort = torPort
|
|
||||||
self.dataNonceFile = self.dataDir + 'block-nonces.dat'
|
|
||||||
self.forwardKeysFile = self.dataDir + 'forward-keys.db'
|
|
||||||
self.keyStore = simplekv.DeadSimpleKV(self.dataDir + 'cachedstorage.dat', refresh_seconds=5)
|
|
||||||
self.storage_counter = storagecounter.StorageCounter(self)
|
|
||||||
|
|
||||||
# Socket data, defined here because of multithreading constraints with gevent
|
|
||||||
self.killSockets = False
|
|
||||||
self.startSocket = {}
|
|
||||||
self.socketServerConnData = {}
|
|
||||||
self.socketReasons = {}
|
|
||||||
self.socketServerResponseData = {}
|
|
||||||
|
|
||||||
if os.path.exists(self.dataDir + '/hs/hostname'):
|
|
||||||
with open(self.dataDir + '/hs/hostname', 'r') as hs:
|
|
||||||
self.hsAddress = hs.read().strip()
|
|
||||||
|
|
||||||
# Load bootstrap address list
|
|
||||||
if os.path.exists(self.bootstrapFileLocation):
|
|
||||||
with open(self.bootstrapFileLocation, 'r') as bootstrap:
|
|
||||||
bootstrap = bootstrap.read()
|
|
||||||
for i in bootstrap.split('\n'):
|
|
||||||
self.bootstrapList.append(i)
|
|
||||||
else:
|
|
||||||
logger.warn('Warning: address bootstrap file not found ' + self.bootstrapFileLocation)
|
|
||||||
|
|
||||||
self.use_subprocess = powchoice.use_subprocess(self)
|
|
||||||
# Initialize the crypto object
|
|
||||||
self._crypto = onionrcrypto.OnionrCrypto(self)
|
|
||||||
self._blacklist = onionrblacklist.OnionrBlackList(self)
|
|
||||||
self.serializer = serializeddata.SerializedData(self)
|
|
||||||
|
|
||||||
def addPeer(self, peerID, name=''):
|
|
||||||
'''
|
|
||||||
Adds a public key to the key database (misleading function name)
|
|
||||||
'''
|
|
||||||
return coredb.keydb.addkeys.add_peer(self, peerID, name)
|
|
||||||
|
|
||||||
def addAddress(self, address):
|
|
||||||
'''
|
|
||||||
Add an address to the address database (only tor currently)
|
|
||||||
'''
|
|
||||||
return coredb.keydb.addkeys.add_address(self, address)
|
|
||||||
|
|
||||||
def removeAddress(self, address):
|
|
||||||
'''
|
|
||||||
Remove an address from the address database
|
|
||||||
'''
|
|
||||||
return coredb.keydb.removekeys.remove_address(self, address)
|
|
||||||
|
|
||||||
def removeBlock(self, block):
|
|
||||||
'''
|
|
||||||
remove a block from this node (does not automatically blacklist)
|
|
||||||
|
|
||||||
**You may want blacklist.addToDB(blockHash)
|
|
||||||
'''
|
|
||||||
removeblock.remove_block(self, block)
|
|
||||||
|
|
||||||
def createAddressDB(self):
|
|
||||||
'''
|
|
||||||
Generate the address database
|
|
||||||
'''
|
|
||||||
self.dbCreate.createAddressDB()
|
|
||||||
|
|
||||||
def createPeerDB(self):
|
|
||||||
'''
|
|
||||||
Generate the peer sqlite3 database and populate it with the peers table.
|
|
||||||
'''
|
|
||||||
self.dbCreate.createPeerDB()
|
|
||||||
|
|
||||||
def createBlockDB(self):
|
|
||||||
'''
|
|
||||||
Create a database for blocks
|
|
||||||
'''
|
|
||||||
self.dbCreate.createBlockDB()
|
|
||||||
|
|
||||||
def setData(self, data):
|
|
||||||
'''
|
|
||||||
Set the data assciated with a hash
|
|
||||||
'''
|
|
||||||
return onionrstorage.setdata.set_data(self, data)
|
|
||||||
|
|
||||||
def getData(self, hash):
|
|
||||||
'''
|
|
||||||
Simply return the data associated to a hash
|
|
||||||
'''
|
|
||||||
return onionrstorage.getData(self, hash)
|
|
||||||
|
|
||||||
def listAdders(self, randomOrder=True, i2p=True, recent=0):
|
|
||||||
'''
|
|
||||||
Return a list of addresses
|
|
||||||
'''
|
|
||||||
return coredb.keydb.listkeys.list_adders(self, randomOrder, i2p, recent)
|
|
||||||
|
|
||||||
def listPeers(self, randomOrder=True, getPow=False, trust=0):
|
|
||||||
'''
|
|
||||||
Return a list of public keys (misleading function name)
|
|
||||||
|
|
||||||
randomOrder determines if the list should be in a random order
|
|
||||||
trust sets the minimum trust to list
|
|
||||||
'''
|
|
||||||
return coredb.keydb.listkeys.list_peers(self, randomOrder, getPow, trust)
|
|
||||||
|
|
||||||
def getPeerInfo(self, peer, info):
|
|
||||||
'''
|
|
||||||
Get info about a peer from their database entry
|
|
||||||
|
|
||||||
id text 0
|
|
||||||
name text, 1
|
|
||||||
adders text, 2
|
|
||||||
dateSeen not null, 3
|
|
||||||
trust int 4
|
|
||||||
hashID text 5
|
|
||||||
'''
|
|
||||||
return coredb.keydb.userinfo.get_user_info(self, peer, info)
|
|
||||||
|
|
||||||
def setPeerInfo(self, peer, key, data):
|
|
||||||
'''
|
|
||||||
Update a peer for a key
|
|
||||||
'''
|
|
||||||
return coredb.keydb.userinfo.set_peer_info(self, peer, key, data)
|
|
||||||
|
|
||||||
def getAddressInfo(self, address, info):
|
|
||||||
'''
|
|
||||||
Get info about an address from its database entry
|
|
||||||
|
|
||||||
address text, 0
|
|
||||||
type int, 1
|
|
||||||
knownPeer text, 2
|
|
||||||
speed int, 3
|
|
||||||
success int, 4
|
|
||||||
powValue 5
|
|
||||||
failure int 6
|
|
||||||
lastConnect 7
|
|
||||||
trust 8
|
|
||||||
introduced 9
|
|
||||||
'''
|
|
||||||
return coredb.keydb.transportinfo.get_address_info(self, address, info)
|
|
||||||
|
|
||||||
def setAddressInfo(self, address, key, data):
|
|
||||||
'''
|
|
||||||
Update an address for a key
|
|
||||||
'''
|
|
||||||
return coredb.keydb.transportinfo.set_address_info(self, address, key, data)
|
|
||||||
|
|
||||||
def insertBlock(self, data, header='txt', sign=False, encryptType='', symKey='', asymPeer='', meta = {}, expire=None, disableForward=False):
|
|
||||||
'''
|
|
||||||
Inserts a block into the network
|
|
||||||
encryptType must be specified to encrypt a block
|
|
||||||
'''
|
|
||||||
allocationReachedMessage = 'Cannot insert block, disk allocation reached.'
|
|
||||||
if self.storage_counter.isFull():
|
|
||||||
logger.error(allocationReachedMessage)
|
|
||||||
return False
|
|
||||||
retData = False
|
|
||||||
|
|
||||||
if type(data) is None:
|
|
||||||
raise ValueError('Data cannot be none')
|
|
||||||
|
|
||||||
createTime = epoch.get_epoch()
|
|
||||||
|
|
||||||
dataNonce = bytesconverter.bytes_to_str(self._crypto.sha3Hash(data))
|
|
||||||
try:
|
|
||||||
with open(self.dataNonceFile, 'r') as nonces:
|
|
||||||
if dataNonce in nonces:
|
|
||||||
return retData
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
# record nonce
|
|
||||||
with open(self.dataNonceFile, 'a') as nonceFile:
|
|
||||||
nonceFile.write(dataNonce + '\n')
|
|
||||||
|
|
||||||
if type(data) is bytes:
|
|
||||||
data = data.decode()
|
|
||||||
data = str(data)
|
|
||||||
plaintext = data
|
|
||||||
plaintextMeta = {}
|
|
||||||
plaintextPeer = asymPeer
|
|
||||||
|
|
||||||
retData = ''
|
|
||||||
signature = ''
|
|
||||||
signer = ''
|
|
||||||
metadata = {}
|
|
||||||
# metadata is full block metadata, meta is internal, user specified metadata
|
|
||||||
|
|
||||||
# only use header if not set in provided meta
|
|
||||||
|
|
||||||
meta['type'] = str(header)
|
|
||||||
|
|
||||||
if encryptType in ('asym', 'sym', ''):
|
|
||||||
metadata['encryptType'] = encryptType
|
|
||||||
else:
|
|
||||||
raise onionrexceptions.InvalidMetadata('encryptType must be asym or sym, or blank')
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = data.encode()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if encryptType == 'asym':
|
|
||||||
meta['rply'] = createTime # Duplicate the time in encrypted messages to prevent replays
|
|
||||||
if not disableForward and sign and asymPeer != self._crypto.pubKey:
|
|
||||||
try:
|
|
||||||
forwardEncrypted = onionrusers.OnionrUser(self, asymPeer).forwardEncrypt(data)
|
|
||||||
data = forwardEncrypted[0]
|
|
||||||
meta['forwardEnc'] = True
|
|
||||||
expire = forwardEncrypted[2] # Expire time of key. no sense keeping block after that
|
|
||||||
except onionrexceptions.InvalidPubkey:
|
|
||||||
pass
|
|
||||||
#onionrusers.OnionrUser(self, asymPeer).generateForwardKey()
|
|
||||||
fsKey = onionrusers.OnionrUser(self, asymPeer).generateForwardKey()
|
|
||||||
#fsKey = onionrusers.OnionrUser(self, asymPeer).getGeneratedForwardKeys().reverse()
|
|
||||||
meta['newFSKey'] = fsKey
|
|
||||||
jsonMeta = json.dumps(meta)
|
|
||||||
plaintextMeta = jsonMeta
|
|
||||||
if sign:
|
|
||||||
signature = self._crypto.edSign(jsonMeta.encode() + data, key=self._crypto.privKey, encodeResult=True)
|
|
||||||
signer = self._crypto.pubKey
|
|
||||||
|
|
||||||
if len(jsonMeta) > 1000:
|
|
||||||
raise onionrexceptions.InvalidMetadata('meta in json encoded form must not exceed 1000 bytes')
|
|
||||||
|
|
||||||
user = onionrusers.OnionrUser(self, symKey)
|
|
||||||
|
|
||||||
# encrypt block metadata/sig/content
|
|
||||||
if encryptType == 'sym':
|
|
||||||
|
|
||||||
if len(symKey) < self.requirements.passwordLength:
|
|
||||||
raise onionrexceptions.SecurityError('Weak encryption key')
|
|
||||||
jsonMeta = self._crypto.symmetricEncrypt(jsonMeta, key=symKey, returnEncoded=True).decode()
|
|
||||||
data = self._crypto.symmetricEncrypt(data, key=symKey, returnEncoded=True).decode()
|
|
||||||
signature = self._crypto.symmetricEncrypt(signature, key=symKey, returnEncoded=True).decode()
|
|
||||||
signer = self._crypto.symmetricEncrypt(signer, key=symKey, returnEncoded=True).decode()
|
|
||||||
elif encryptType == 'asym':
|
|
||||||
if stringvalidators.validate_pub_key(asymPeer):
|
|
||||||
# Encrypt block data with forward secrecy key first, but not meta
|
|
||||||
jsonMeta = json.dumps(meta)
|
|
||||||
jsonMeta = self._crypto.pubKeyEncrypt(jsonMeta, asymPeer, encodedData=True).decode()
|
|
||||||
data = self._crypto.pubKeyEncrypt(data, asymPeer, encodedData=True).decode()
|
|
||||||
signature = self._crypto.pubKeyEncrypt(signature, asymPeer, encodedData=True).decode()
|
|
||||||
signer = self._crypto.pubKeyEncrypt(signer, asymPeer, encodedData=True).decode()
|
|
||||||
try:
|
|
||||||
onionrusers.OnionrUser(self, asymPeer, saveUser=True)
|
|
||||||
except ValueError:
|
|
||||||
# if peer is already known
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise onionrexceptions.InvalidPubkey(asymPeer + ' is not a valid base32 encoded ed25519 key')
|
|
||||||
|
|
||||||
# compile metadata
|
|
||||||
metadata['meta'] = jsonMeta
|
|
||||||
metadata['sig'] = signature
|
|
||||||
metadata['signer'] = signer
|
|
||||||
metadata['time'] = createTime
|
|
||||||
|
|
||||||
# ensure expire is integer and of sane length
|
|
||||||
if type(expire) is not type(None):
|
|
||||||
assert len(str(int(expire))) < 14
|
|
||||||
metadata['expire'] = expire
|
|
||||||
|
|
||||||
# send block data (and metadata) to POW module to get tokenized block data
|
|
||||||
if self.use_subprocess:
|
|
||||||
payload = subprocesspow.SubprocessPOW(data, metadata, self).start()
|
|
||||||
else:
|
|
||||||
payload = onionrproofs.POW(metadata, data).waitForResult()
|
|
||||||
if payload != False:
|
|
||||||
try:
|
|
||||||
retData = self.setData(payload)
|
|
||||||
except onionrexceptions.DiskAllocationReached:
|
|
||||||
logger.error(allocationReachedMessage)
|
|
||||||
retData = False
|
|
||||||
else:
|
|
||||||
# Tell the api server through localCommand to wait for the daemon to upload this block to make statistical analysis more difficult
|
|
||||||
if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!':
|
|
||||||
if self.config.get('general.security_level', 1) == 0:
|
|
||||||
localcommand.local_command(self, '/waitforshare/' + retData, post=True, maxWait=5)
|
|
||||||
coredb.daemonqueue.daemon_queue_add('uploadBlock', retData)
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
coredb.blockmetadb.add_to_block_DB(retData, selfInsert=True, dataSaved=True)
|
|
||||||
coredb.blockmetadata.process_block_metadata(self, retData)
|
|
||||||
|
|
||||||
if retData != False:
|
|
||||||
if plaintextPeer == onionrvalues.DENIABLE_PEER_ADDRESS:
|
|
||||||
events.event('insertdeniable', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': bytesconverter.bytes_to_str(asymPeer)}, onionr = self.onionrInst, threaded = True)
|
|
||||||
else:
|
|
||||||
events.event('insertblock', {'content': plaintext, 'meta': plaintextMeta, 'hash': retData, 'peer': bytesconverter.bytes_to_str(asymPeer)}, onionr = self.onionrInst, threaded = True)
|
|
||||||
return retData
|
|
||||||
|
|
||||||
def introduceNode(self):
|
|
||||||
'''
|
|
||||||
Introduces our node into the network by telling X many nodes our HS address
|
|
||||||
'''
|
|
||||||
if localcommand.local_command(self, '/ping', maxWait=10) == 'pong!':
|
|
||||||
coredb.daemonqueue.daemon_queue_add('announceNode')
|
|
||||||
logger.info('Introduction command will be processed.', terminal=True)
|
|
||||||
else:
|
|
||||||
logger.warn('No running node detected. Cannot introduce.', terminal=True)
|
|
|
@ -2,8 +2,8 @@ import json
|
||||||
import onionrblockapi
|
import onionrblockapi
|
||||||
from onionrutils import bytesconverter, stringvalidators
|
from onionrutils import bytesconverter, stringvalidators
|
||||||
class GetBlockData:
|
class GetBlockData:
|
||||||
def __init__(self, client_api_inst):
|
def __init__(self, client_api_inst=None):
|
||||||
self.client_api_inst = client_api_inst
|
return
|
||||||
|
|
||||||
def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False):
|
def get_block_data(self, bHash, decrypt=False, raw=False, headerOnly=False):
|
||||||
assert stringvalidators.validate_hash(bHash)
|
assert stringvalidators.validate_hash(bHash)
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
'''
|
'''
|
||||||
from flask import Blueprint, Response, abort
|
from flask import Blueprint, Response, abort
|
||||||
import onionrblockapi
|
import onionrblockapi
|
||||||
from httpapi import apiutils
|
from .. import apiutils
|
||||||
from onionrutils import stringvalidators
|
from onionrutils import stringvalidators
|
||||||
from coredb import blockmetadb
|
from coredb import blockmetadb
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ HiddenServicePort 80 ''' + self.apiServerIP + ''':''' + str(self.hsPort)
|
||||||
logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running? This can also be a result of file permissions being too open', terminal=True)
|
logger.fatal('Failed to start Tor. Maybe a stray instance of Tor used by Onionr is still running? This can also be a result of file permissions being too open', terminal=True)
|
||||||
return False
|
return False
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, level = logger.LEVEL_IMPORTANT, terminal=True)
|
logger.fatal('Got keyboard interrupt. Onionr will exit soon.', timestamp = False, terminal=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info('Finished starting Tor.', terminal=True)
|
logger.info('Finished starting Tor.', terminal=True)
|
||||||
|
|
|
@ -19,14 +19,14 @@
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import logger, config, onionrexceptions, nacl.exceptions
|
import logger, config, onionrexceptions, nacl.exceptions
|
||||||
import json, os, sys, datetime, base64, onionrstorage, onionrcrypto
|
import json, os, sys, datetime, base64, onionrstorage
|
||||||
from onionrusers import onionrusers
|
from onionrusers import onionrusers
|
||||||
from onionrutils import stringvalidators, epoch
|
from onionrutils import stringvalidators, epoch
|
||||||
from coredb import blockmetadb
|
from coredb import blockmetadb
|
||||||
from onionrstorage import removeblock
|
from onionrstorage import removeblock
|
||||||
import onionrblocks
|
import onionrblocks
|
||||||
|
from onionrcrypto import encryption, cryptoutils as cryptoutils, signing
|
||||||
class Block:
|
class Block:
|
||||||
crypto = onionrcrypto.OnionrCrypto()
|
|
||||||
blockCacheOrder = list() # NEVER write your own code that writes to this!
|
blockCacheOrder = list() # NEVER write your own code that writes to this!
|
||||||
blockCache = dict() # should never be accessed directly, look at Block.getCache()
|
blockCache = dict() # should never be accessed directly, look at Block.getCache()
|
||||||
|
|
||||||
|
@ -71,23 +71,23 @@ class Block:
|
||||||
# decrypt data
|
# decrypt data
|
||||||
if self.getHeader('encryptType') == 'asym':
|
if self.getHeader('encryptType') == 'asym':
|
||||||
try:
|
try:
|
||||||
self.bcontent = crypto.pubKeyDecrypt(self.bcontent, encodedData=encodedData)
|
self.bcontent = encryption.pub_key_decrypt(self.bcontent, encodedData=encodedData)
|
||||||
bmeta = crypto.pubKeyDecrypt(self.bmetadata, encodedData=encodedData)
|
bmeta = encryption.pub_key_decrypt(self.bmetadata, encodedData=encodedData)
|
||||||
try:
|
try:
|
||||||
bmeta = bmeta.decode()
|
bmeta = bmeta.decode()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# yet another bytes fix
|
# yet another bytes fix
|
||||||
pass
|
pass
|
||||||
self.bmetadata = json.loads(bmeta)
|
self.bmetadata = json.loads(bmeta)
|
||||||
self.signature = crypto.pubKeyDecrypt(self.signature, encodedData=encodedData)
|
self.signature = encryption.pub_key_decrypt(self.signature, encodedData=encodedData)
|
||||||
self.signer = crypto.pubKeyDecrypt(self.signer, encodedData=encodedData)
|
self.signer = encryption.pub_key_decrypt(self.signer, encodedData=encodedData)
|
||||||
self.bheader['signer'] = self.signer.decode()
|
self.bheader['signer'] = self.signer.decode()
|
||||||
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
|
self.signedData = json.dumps(self.bmetadata) + self.bcontent.decode()
|
||||||
|
|
||||||
# Check for replay attacks
|
# Check for replay attacks
|
||||||
try:
|
try:
|
||||||
if epoch.get_epoch() - blockmetadb.get_block_date(self.hash) > 60:
|
if epoch.get_epoch() - blockmetadb.get_block_date(self.hash) > 60:
|
||||||
assert self.crypto.replayTimestampValidation(self.bmetadata['rply'])
|
assert cryptoutils.replay_validator(self.bmetadata['rply'])
|
||||||
except (AssertionError, KeyError, TypeError) as e:
|
except (AssertionError, KeyError, TypeError) as e:
|
||||||
if not self.bypassReplayCheck:
|
if not self.bypassReplayCheck:
|
||||||
# Zero out variables to prevent reading of replays
|
# Zero out variables to prevent reading of replays
|
||||||
|
@ -124,7 +124,7 @@ class Block:
|
||||||
Verify if a block's signature is signed by its claimed signer
|
Verify if a block's signature is signed by its claimed signer
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if crypto.edVerify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
|
if signing.ed_verify(data=self.signedData, key=self.signer, sig=self.signature, encodedData=True):
|
||||||
self.validSig = True
|
self.validSig = True
|
||||||
else:
|
else:
|
||||||
self.validSig = False
|
self.validSig = False
|
||||||
|
@ -425,7 +425,7 @@ class Block:
|
||||||
if (not self.isSigned()) or (not stringvalidators.validate_pub_key(signer)):
|
if (not self.isSigned()) or (not stringvalidators.validate_pub_key(signer)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return bool(crypto.edVerify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData))
|
return bool(signing.ed_verify(self.getSignedData(), signer, self.getSignature(), encodedData = encodedData))
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ from netcontroller import NetController
|
||||||
from onionrutils import localcommand
|
from onionrutils import localcommand
|
||||||
import filepaths
|
import filepaths
|
||||||
from coredb import daemonqueue
|
from coredb import daemonqueue
|
||||||
|
from onionrcrypto import getourkeypair
|
||||||
def _proper_shutdown(o_inst):
|
def _proper_shutdown(o_inst):
|
||||||
localcommand.local_command('shutdown')
|
localcommand.local_command('shutdown')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
@ -72,7 +72,7 @@ def daemon(o_inst):
|
||||||
logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID))
|
logger.debug('Started .onion service: %s' % (logger.colors.underline + net.myID))
|
||||||
else:
|
else:
|
||||||
logger.debug('.onion service disabled')
|
logger.debug('.onion service disabled')
|
||||||
logger.info('Using public key: %s' % (logger.colors.underline + o_inst._crypto.pubKey[:52]), terminal=True)
|
logger.info('Using public key: %s' % (logger.colors.underline + getourkeypair.get_keypair()[0][:52]), terminal=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
|
@ -22,6 +22,7 @@ import sys, getpass
|
||||||
import logger, onionrexceptions
|
import logger, onionrexceptions
|
||||||
from onionrutils import stringvalidators, bytesconverter
|
from onionrutils import stringvalidators, bytesconverter
|
||||||
from onionrusers import onionrusers, contactmanager
|
from onionrusers import onionrusers, contactmanager
|
||||||
|
from coredb import keydb
|
||||||
import unpaddedbase32
|
import unpaddedbase32
|
||||||
def add_ID(o_inst):
|
def add_ID(o_inst):
|
||||||
try:
|
try:
|
||||||
|
@ -82,22 +83,22 @@ def friend_command(o_inst):
|
||||||
action = action.lower()
|
action = action.lower()
|
||||||
if action == 'list':
|
if action == 'list':
|
||||||
# List out peers marked as our friend
|
# List out peers marked as our friend
|
||||||
for friend in contactmanager.ContactManager.list_friends(o_inst.):
|
for friend in contactmanager.ContactManager.list_friends():
|
||||||
logger.info(friend.publicKey + ' - ' + friend.get_info('name'), terminal=True)
|
logger.info(friend.publicKey + ' - ' + friend.get_info('name'), terminal=True)
|
||||||
elif action in ('add', 'remove'):
|
elif action in ('add', 'remove'):
|
||||||
try:
|
try:
|
||||||
friend = sys.argv[3]
|
friend = sys.argv[3]
|
||||||
if not stringvalidators.validate_pub_key(friend):
|
if not stringvalidators.validate_pub_key(friend):
|
||||||
raise onionrexceptions.InvalidPubkey('Public key is invalid')
|
raise onionrexceptions.InvalidPubkey('Public key is invalid')
|
||||||
if friend not in o_inst..listPeers():
|
if friend not in keydb.listkeys.list_peers():
|
||||||
raise onionrexceptions.KeyNotKnown
|
raise onionrexceptions.KeyNotKnown
|
||||||
friend = onionrusers.OnionrUser(o_inst., friend)
|
friend = onionrusers.OnionrUser(friend)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
logger.warn('Friend ID is required.', terminal=True)
|
logger.warn('Friend ID is required.', terminal=True)
|
||||||
action = 'error' # set to 'error' so that the finally block does not process anything
|
action = 'error' # set to 'error' so that the finally block does not process anything
|
||||||
except onionrexceptions.KeyNotKnown:
|
except onionrexceptions.KeyNotKnown:
|
||||||
o_inst..addPeer(friend)
|
o_inst.addPeer(friend)
|
||||||
friend = onionrusers.OnionrUser(o_inst., friend)
|
friend = onionrusers.OnionrUser(friend)
|
||||||
finally:
|
finally:
|
||||||
if action == 'add':
|
if action == 'add':
|
||||||
friend.setTrust(1)
|
friend.setTrust(1)
|
||||||
|
|
|
@ -36,7 +36,7 @@ class OnionrCrypto:
|
||||||
self.secrets = secrets
|
self.secrets = secrets
|
||||||
self.deterministicRequirement = 25 # Min deterministic password/phrase length
|
self.deterministicRequirement = 25 # Min deterministic password/phrase length
|
||||||
self.HASH_ID_ROUNDS = 2000
|
self.HASH_ID_ROUNDS = 2000
|
||||||
self.keyManager = keymanager.KeyManager(self)
|
self.keyManager = keymanager.KeyManager()
|
||||||
|
|
||||||
# Load our own pub/priv Ed25519 keys, gen & save them if they don't exist
|
# Load our own pub/priv Ed25519 keys, gen & save them if they don't exist
|
||||||
if os.path.exists(self._keyFile):
|
if os.path.exists(self._keyFile):
|
||||||
|
@ -52,47 +52,6 @@ class OnionrCrypto:
|
||||||
self.keyManager.addKey(self.pubKey, self.privKey)
|
self.keyManager.addKey(self.pubKey, self.privKey)
|
||||||
return
|
return
|
||||||
|
|
||||||
def edVerify(self, data, key, sig, encodedData=True):
|
|
||||||
'''Verify signed data (combined in nacl) to an ed25519 key'''
|
|
||||||
try:
|
|
||||||
key = nacl.signing.VerifyKey(key=key, encoder=nacl.encoding.Base32Encoder)
|
|
||||||
except nacl.exceptions.ValueError:
|
|
||||||
#logger.debug('Signature by unknown key (cannot reverse hash)')
|
|
||||||
return False
|
|
||||||
except binascii.Error:
|
|
||||||
logger.warn('Could not load key for verification, invalid padding')
|
|
||||||
return False
|
|
||||||
retData = False
|
|
||||||
sig = base64.b64decode(sig)
|
|
||||||
try:
|
|
||||||
data = data.encode()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
if encodedData:
|
|
||||||
try:
|
|
||||||
retData = key.verify(data, sig) # .encode() is not the same as nacl.encoding
|
|
||||||
except nacl.exceptions.BadSignatureError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
retData = key.verify(data, sig)
|
|
||||||
except nacl.exceptions.BadSignatureError:
|
|
||||||
pass
|
|
||||||
return retData
|
|
||||||
|
|
||||||
def edSign(self, data, key, encodeResult=False):
|
|
||||||
'''Ed25519 sign data'''
|
|
||||||
try:
|
|
||||||
data = data.encode()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
key = nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder)
|
|
||||||
retData = ''
|
|
||||||
if encodeResult:
|
|
||||||
retData = key.sign(data, encoder=nacl.encoding.Base64Encoder).signature.decode() # .encode() is not the same as nacl.encoding
|
|
||||||
else:
|
|
||||||
retData = key.sign(data).signature
|
|
||||||
return retData
|
|
||||||
|
|
||||||
def pubKeyEncrypt(self, data, pubkey, encodedData=False):
|
def pubKeyEncrypt(self, data, pubkey, encodedData=False):
|
||||||
'''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)'''
|
'''Encrypt to a public key (Curve25519, taken from base32 Ed25519 pubkey)'''
|
||||||
|
@ -113,25 +72,6 @@ class OnionrCrypto:
|
||||||
|
|
||||||
return retVal
|
return retVal
|
||||||
|
|
||||||
def pubKeyDecrypt(self, data, pubkey='', privkey='', encodedData=False):
|
|
||||||
'''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)'''
|
|
||||||
decrypted = False
|
|
||||||
if encodedData:
|
|
||||||
encoding = nacl.encoding.Base64Encoder
|
|
||||||
else:
|
|
||||||
encoding = nacl.encoding.RawEncoder
|
|
||||||
if privkey == '':
|
|
||||||
privkey = self.privKey
|
|
||||||
ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
|
||||||
|
|
||||||
if stringvalidators.validate_pub_key(privkey):
|
|
||||||
privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
|
||||||
anonBox = nacl.public.SealedBox(privkey)
|
|
||||||
else:
|
|
||||||
anonBox = nacl.public.SealedBox(ownKey)
|
|
||||||
decrypted = anonBox.decrypt(data, encoder=encoding)
|
|
||||||
return decrypted
|
|
||||||
|
|
||||||
def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True):
|
def symmetricEncrypt(self, data, key, encodedKey=False, returnEncoded=True):
|
||||||
'''Encrypt data with a 32-byte key (Salsa20-Poly1305 MAC)'''
|
'''Encrypt data with a 32-byte key (Salsa20-Poly1305 MAC)'''
|
||||||
if encodedKey:
|
if encodedKey:
|
||||||
|
@ -251,38 +191,4 @@ class OnionrCrypto:
|
||||||
else:
|
else:
|
||||||
logger.debug("Invalid token, bad proof")
|
logger.debug("Invalid token, bad proof")
|
||||||
|
|
||||||
return retData
|
return retData
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def replayTimestampValidation(timestamp):
|
|
||||||
if epoch.get_epoch() - int(timestamp) > 2419200:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def safeCompare(one, two):
|
|
||||||
# Do encode here to avoid spawning core
|
|
||||||
try:
|
|
||||||
one = one.encode()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
two = two.encode()
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
return hmac.compare_digest(one, two)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def randomShuffle(theList):
|
|
||||||
myList = list(theList)
|
|
||||||
shuffledList = []
|
|
||||||
myListLength = len(myList) + 1
|
|
||||||
while myListLength > 0:
|
|
||||||
removed = secrets.randbelow(myListLength)
|
|
||||||
try:
|
|
||||||
shuffledList.append(myList.pop(removed))
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
myListLength = len(myList)
|
|
||||||
return shuffledList
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
from . import safecompare, replayvalidation, randomshuffle
|
||||||
|
|
||||||
|
replay_validator = replayvalidation.replay_timestamp_validation
|
||||||
|
random_shuffle = randomshuffle.random_shuffle
|
||||||
|
safe_compare = safecompare.safe_compare
|
|
@ -0,0 +1,13 @@
|
||||||
|
import secrets
|
||||||
|
def random_shuffle(theList):
|
||||||
|
myList = list(theList)
|
||||||
|
shuffledList = []
|
||||||
|
myListLength = len(myList) + 1
|
||||||
|
while myListLength > 0:
|
||||||
|
removed = secrets.randbelow(myListLength)
|
||||||
|
try:
|
||||||
|
shuffledList.append(myList.pop(removed))
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
myListLength = len(myList)
|
||||||
|
return shuffledList
|
|
@ -0,0 +1,6 @@
|
||||||
|
import utils # onionr utils epoch, not this utils
|
||||||
|
def replay_timestamp_validation(timestamp):
|
||||||
|
if utils.epoch.get_epoch() - int(timestamp) > 2419200:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
|
@ -0,0 +1,12 @@
|
||||||
|
import hmac
|
||||||
|
def safe_compare(one, two):
|
||||||
|
# Do encode here to avoid spawning core
|
||||||
|
try:
|
||||||
|
one = one.encode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
two = two.encode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
return hmac.compare_digest(one, two)
|
|
@ -0,0 +1,24 @@
|
||||||
|
import nacl.encoding, nacl.public, nacl.signing
|
||||||
|
from .. import getourkeypair
|
||||||
|
pair = getourkeypair.get_keypair()
|
||||||
|
our_pub_key = pair[0]
|
||||||
|
our_priv_key = pair[1]
|
||||||
|
|
||||||
|
def pub_key_decrypt(data, pubkey='', privkey='', encodedData=False):
|
||||||
|
'''pubkey decrypt (Curve25519, taken from Ed25519 pubkey)'''
|
||||||
|
decrypted = False
|
||||||
|
if encodedData:
|
||||||
|
encoding = nacl.encoding.Base64Encoder
|
||||||
|
else:
|
||||||
|
encoding = nacl.encoding.RawEncoder
|
||||||
|
if privkey == '':
|
||||||
|
privkey = our_priv_key
|
||||||
|
ownKey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
||||||
|
|
||||||
|
if stringvalidators.validate_pub_key(privkey):
|
||||||
|
privkey = nacl.signing.SigningKey(seed=privkey, encoder=nacl.encoding.Base32Encoder()).to_curve25519_private_key()
|
||||||
|
anonBox = nacl.public.SealedBox(privkey)
|
||||||
|
else:
|
||||||
|
anonBox = nacl.public.SealedBox(ownKey)
|
||||||
|
decrypted = anonBox.decrypt(data, encoder=encoding)
|
||||||
|
return decrypted
|
|
@ -0,0 +1,17 @@
|
||||||
|
import os
|
||||||
|
import keymanager, config, filepaths
|
||||||
|
from . import generate
|
||||||
|
def get_keypair():
|
||||||
|
key_m = keymanager.KeyManager()
|
||||||
|
if os.path.exists(filepaths.keys_file):
|
||||||
|
if len(config.get('general.public_key', '')) > 0:
|
||||||
|
pubKey = config.get('general.public_key')
|
||||||
|
else:
|
||||||
|
pubKey = key_m.getPubkeyList()[0]
|
||||||
|
privKey = key_m.getPrivkey(pubKey)
|
||||||
|
else:
|
||||||
|
keys = generate.generate_pub_key()
|
||||||
|
pubKey = keys[0]
|
||||||
|
privKey = keys[1]
|
||||||
|
key_m.addKey(pubKey, privKey)
|
||||||
|
return (pubKey, privKey)
|
|
@ -0,0 +1,44 @@
|
||||||
|
import base64, binascii
|
||||||
|
import nacl.encoding, nacl.signing, nacl.exceptions
|
||||||
|
import logger
|
||||||
|
def ed_sign(data, key, encodeResult=False):
|
||||||
|
'''Ed25519 sign data'''
|
||||||
|
try:
|
||||||
|
data = data.encode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
key = nacl.signing.SigningKey(seed=key, encoder=nacl.encoding.Base32Encoder)
|
||||||
|
retData = ''
|
||||||
|
if encodeResult:
|
||||||
|
retData = key.sign(data, encoder=nacl.encoding.Base64Encoder).signature.decode() # .encode() is not the same as nacl.encoding
|
||||||
|
else:
|
||||||
|
retData = key.sign(data).signature
|
||||||
|
return retData
|
||||||
|
|
||||||
|
def ed_verify(data, key, sig, encodedData=True):
|
||||||
|
'''Verify signed data (combined in nacl) to an ed25519 key'''
|
||||||
|
try:
|
||||||
|
key = nacl.signing.VerifyKey(key=key, encoder=nacl.encoding.Base32Encoder)
|
||||||
|
except nacl.exceptions.ValueError:
|
||||||
|
#logger.debug('Signature by unknown key (cannot reverse hash)')
|
||||||
|
return False
|
||||||
|
except binascii.Error:
|
||||||
|
logger.warn('Could not load key for verification, invalid padding')
|
||||||
|
return False
|
||||||
|
retData = False
|
||||||
|
sig = base64.b64decode(sig)
|
||||||
|
try:
|
||||||
|
data = data.encode()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if encodedData:
|
||||||
|
try:
|
||||||
|
retData = key.verify(data, sig) # .encode() is not the same as nacl.encoding
|
||||||
|
except nacl.exceptions.BadSignatureError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
retData = key.verify(data, sig)
|
||||||
|
except nacl.exceptions.BadSignatureError:
|
||||||
|
pass
|
||||||
|
return retData
|
|
@ -18,11 +18,10 @@
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
'''
|
'''
|
||||||
import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, json
|
import multiprocessing, nacl.encoding, nacl.hash, nacl.utils, time, math, threading, binascii, sys, json
|
||||||
import config, logger, onionrblockapi, storagecounter, onionrcrypto
|
import config, logger, onionrblockapi, storagecounter
|
||||||
from onionrutils import bytesconverter
|
from onionrutils import bytesconverter
|
||||||
|
from onionrcrypto import hashers
|
||||||
config.reload()
|
config.reload()
|
||||||
crypto = onionrcrypto.OnionrCrypto()
|
|
||||||
def getDifficultyModifier():
|
def getDifficultyModifier():
|
||||||
'''returns the difficulty modifier for block storage based
|
'''returns the difficulty modifier for block storage based
|
||||||
on a variety of factors, currently only disk use.
|
on a variety of factors, currently only disk use.
|
||||||
|
@ -227,7 +226,7 @@ class POW:
|
||||||
#token = nacl.hash.blake2b(rand + self.data).decode()
|
#token = nacl.hash.blake2b(rand + self.data).decode()
|
||||||
self.metadata['pow'] = nonce
|
self.metadata['pow'] = nonce
|
||||||
payload = json.dumps(self.metadata).encode() + b'\n' + self.data
|
payload = json.dumps(self.metadata).encode() + b'\n' + self.data
|
||||||
token = crypto.sha3Hash(payload)
|
token = hashers.sha3_hash(payload)
|
||||||
try:
|
try:
|
||||||
# on some versions, token is bytes
|
# on some versions, token is bytes
|
||||||
token = token.decode()
|
token = token.decode()
|
||||||
|
|
|
@ -21,7 +21,7 @@ import json
|
||||||
import logger, onionrexceptions
|
import logger, onionrexceptions
|
||||||
from etc import onionrvalues
|
from etc import onionrvalues
|
||||||
from onionrutils import stringvalidators, epoch, bytesconverter
|
from onionrutils import stringvalidators, epoch, bytesconverter
|
||||||
import config, onionrvalues, filepaths, onionrcrypto
|
import config, filepaths, onionrcrypto
|
||||||
def validate_metadata(metadata, blockData):
|
def validate_metadata(metadata, blockData):
|
||||||
'''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string'''
|
'''Validate metadata meets onionr spec (does not validate proof value computation), take in either dictionary or json string'''
|
||||||
# TODO, make this check sane sizes
|
# TODO, make this check sane sizes
|
||||||
|
|
|
@ -49,9 +49,9 @@ class SubprocessPOW:
|
||||||
|
|
||||||
self.data = bytesconverter.str_to_bytes(data)
|
self.data = bytesconverter.str_to_bytes(data)
|
||||||
# Calculate difficulty. Dumb for now, may use good algorithm in the future.
|
# Calculate difficulty. Dumb for now, may use good algorithm in the future.
|
||||||
self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data)
|
self.difficulty = onionrproofs.getDifficultyForNewBlock(bytes(json_metadata + b'\n' + self.data))
|
||||||
|
|
||||||
logger.info('Computing POW (difficulty: %s)...' % self.difficulty)
|
logger.info('Computing POW (difficulty: %s)...' % (self.difficulty,))
|
||||||
|
|
||||||
self.mainHash = '0' * 64
|
self.mainHash = '0' * 64
|
||||||
self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))]
|
self.puzzle = self.mainHash[0:min(self.difficulty, len(self.mainHash))]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
def read_static(file, ret_bin=False):
|
def read_static(file, ret_bin=False):
|
||||||
static_file = os.path.realpath(__file__) + '../static-data/' + file
|
static_file = os.path.dirname(os.path.realpath(__file__)) + '/../static-data/' + file
|
||||||
|
|
||||||
if ret_bin:
|
if ret_bin:
|
||||||
mode = 'rb'
|
mode = 'rb'
|
||||||
|
|
Loading…
Reference in New Issue