diff --git a/README.md b/README.md
index 22cc220..3bd0348 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@ or YAML, and then extended to accept JSON or YAML to write a profile.
## Usage
Reads a tox profile and prints out information on what's in there to stderr.
-Call it with one argument, the filename of the profile for the decrypt or info
-commands, or the filename of the nodes file for the nodes command.
+Call it with one argument, the filename of the profile for the decrypt, edit
+or info commands, or the filename of the nodes file for the nodes command.
3 commands are supported:
1. ```--command decrypt``` decrypts the profile and writes to the result
@@ -52,13 +52,24 @@ Optional arguments:
```info``` will output the profile on stdout, or to a file with ```--output```
-Choose one of ```{info,repr,yaml,json,pprint}```
+Choose one of ```{info,repr,yaml,json,pprint,save}```
for the format for info command.
Choose one of ```{nmap_udp,nmap_tcp}```
to run tests using ```nmap``` for the ```DHT``` and ```TCP_RELAY```
sections of the profile. Reguires ```nmap``` and uses ```sudo```.
+#### Saving a copy
+
+The code now can generate a saved copy of the profile as it parses the profile.
+Use the command ```--command info --info save``` with ```--output```
+and a filename, to process the file with info to stderr, and it will
+save an copy of the file to the ```--output``` (unencrypted).
+
+It may be shorter than the original profile by up to 512 bytes, as the
+original toxic profile is padded at the end with nulls (or maybe in the
+decryption).
+
### --command nodes
Takes a DHTnodes.json file as an argument.
@@ -74,6 +85,22 @@ nodes. Reguires ```nmap``` and uses ```sudo```.
Decrypt a profile.
+### --command edit
+
+The code now can generate an edited copy of the profile.
+Use the command ```--command edit --edit section,key,val``` with
+```--output``` and a filename, to process the file with info to stderr,
+and it will save an copy of the edited file to the
+```--output``` file (unencrypted). There's not much editing yet; give
+```--command edit --edit help``` to get a list of what Available Sections,
+and Supported Quads (section,num,key,type) that can be edited.
+Currently it is:
+```
+NAME,0,Nick_name,str
+STATUSMESSAGE,0,Status_message,str
+STATUS,0,Online_status,int
+```
+
## Requirements
If you want to read encrypted profiles, you need to download
@@ -93,22 +120,20 @@ If you want to write in YAML, you need Python yaml:
If you have coloredlogs installed it will make use of it:
+For the ```select``` and ```nmap``` commands, the ```jq``` utility is
+required. It's available in most distros, or
+
+For the ```nmap``` commands, the ```nmap``` utility is
+required. It's available in most distros, or
+
## Future Directions
+This has not been tested on Windwoes, but is should be simple to fix.
+
Because it's written in Python it is easy to extend to, for example,
rekeying a profile when copying a profile to a new device:
-### Editing - save
-
-The code now can generate a saved copy of the profile as it parses the profile.
-Use the command ```--command save``` with ```--output``` and a filename,
-to process the file with info to stderr, and it will save an copy of the file
-to the ```--output``` (unencrypted).
-
-It may be shorter than the original profile by up to 512 bytes, as the
-original toxic profile is padded at the end with nulls. So this code
-can be extended to edit the profile before saving it.
## Specification
diff --git a/logging_tox_savefile.py b/logging_tox_savefile.py
index 9eaa82e..6d24ed8 100644
--- a/logging_tox_savefile.py
+++ b/logging_tox_savefile.py
@@ -7,7 +7,7 @@ Call it with one argument, the filename of the profile for the decrypt or info
commands, or the filename of the nodes file for the nodes command.
3 commands are supported:
---command decrypt
+--command decrypt
decrypts the profile and writes to the result to stdout
--command info
@@ -20,16 +20,16 @@ commands, or the filename of the nodes file for the nodes command.
"""
--output Destination for info/decrypt - defaults to stdout
--info default='info',
- choices=['info', 'repr', 'yaml','json', 'pprint']
+ choices=['info', 'save', 'repr', 'yaml','json', 'pprint']
with --info=info prints info about the profile to stderr
nmap_udp - test DHT nodes with nmap
nmap_tcp - test TCP_RELAY nodes with nmap
nmap_onion - test PATH_NODE nodes with nmap
- indents the output as: 'yaml','json', 'pprint'
+ indents the output as: 'yaml','json', 'pprint'
--indent for pprint/yaml/json default=2
--output Destination for the command - required
- --nodes
+ --nodes
choices=['select_tcp', 'select_udp', 'nmap_tcp', 'select_version', 'nmap_udp']
select_udp - select udp nodes
select_tcp - select tcp nodes
@@ -87,14 +87,18 @@ except ImportError as e:
print("Link all 3 from libtoxcore.so if you have only libtoxcore.so")
ToxEncryptSave = None
download_url = None
-
+
LOG = logging.getLogger('TSF')
-bHAVE_NMAP = shutil.which('nmap')
+# Fix for Windows
sDIR = os.environ.get('TMPDIR', '/tmp')
-# nodes
sTOX_VERSION = "1000002018"
+bHAVE_NMAP = shutil.which('nmap')
bHAVE_JQ = shutil.which('jq')
+bMARK = b'\x00\x00\x00\x00\x1f\x1b\xed\x15'
+bDEBUG = 'DEBUG' in os.environ and os.environ['DEBUG'] != 0
+def trace(s): LOG.log(LOG.level, '+ ' +s)
+LOG.trace = trace
#messenger.c
MESSENGER_STATE_TYPE_NOSPAMKEYS = 1
@@ -172,6 +176,7 @@ Length Contents
8 uint64_t Last seen time
"""
+ global sENC
dStatus = { # Status Meaning
0: 'Not a friend',
1: 'Friend added',
@@ -191,12 +196,12 @@ Length Contents
o = delta+1+32+1024+1+2+128; l = 2
nsize = struct.unpack_from(">H", result, o)[0]
o = delta+1+32+1024+1+2; l = 128
- name = str(result[o:o+nsize], 'utf-8')
+ name = str(result[o:o+nsize], sENC)
o = delta+1+32+1024+1+2+128+2+1007; l = 2
msize = struct.unpack_from(">H", result, o)[0]
o = delta+1+32+1024+1+2+128+2; l = 1007
- mame = str(result[o:o+msize], 'utf-8')
+ mame = str(result[o:o+msize], sENC)
LOG.info(f"Friend #{i} {dStatus[status]} {name} {pk}")
lIN += [{"Status": dStatus[status],
"Name": name,
@@ -207,6 +212,7 @@ def lProcessGroups(state, index, length, result, label="GROUPS"):
"""
No GROUPS description in spec.html
"""
+ global sENC
lIN = []
i = 0
if not msgpack:
@@ -218,7 +224,7 @@ def lProcessGroups(state, index, length, result, label="GROUPS"):
for group in groups:
assert len(group) == 7, group
i += 1
-
+
state_values, \
state_bin, \
topic_info, \
@@ -237,7 +243,8 @@ def lProcessGroups(state, index, length, result, label="GROUPS"):
topic_lock, \
voice_state = state_values
LOG.info(f"lProcessGroups #{i} version={version}")
- dBINS = {"Version": version}
+ dBINS = {"Version": version,
+ "Privacy_state": privacy_state}
lIN += [{"State_values": dBINS}]
assert len(state_bin) == 5, state_bin
@@ -251,7 +258,7 @@ def lProcessGroups(state, index, length, result, label="GROUPS"):
lIN += [{"State_bin": dBINS}]
assert len(topic_info) == 6, topic_info
- topic_info_topic = str(topic_info[3], 'utf-8')
+ topic_info_topic = str(topic_info[3], sENC)
LOG.info(f"lProcessGroups #{i} topic_info_topic={topic_info_topic}")
dBINS = {"topic_info_topic": topic_info_topic}
lIN += [{"Topic_info": dBINS}]
@@ -290,7 +297,7 @@ def lProcessGroups(state, index, length, result, label="GROUPS"):
assert len(self_info) == 4, self_info
self_nick_len, self_role, self_status, self_nick = self_info
- self_nick = str(self_nick, 'utf-8')
+ self_nick = str(self_nick, sENC)
LOG.info(f"lProcessGroups #{i} self_nick={self_nick}")
dBINS = {"Self_nick": self_nick}
lIN += [{"Self_info": dBINS}]
@@ -398,8 +405,9 @@ def lProcessDHTnodes(state, index, length, result, label="DHTnode"):
relay += 1
return lIN
-def process_chunk(index, state):
+def process_chunk(index, state, oArgs=None):
global lOUT, bOUT, aOUT
+ global sENC
length = struct.unpack_from(" 0:
+ LOG.warn(f"PROCESS_CHUNK {label} index={index} bOUT={len(bOUT)} delta={diff} length={length}")
+ elif bDEBUG:
+ LOG.trace(f"PROCESS_CHUNK {label} index={index} bOUT={len(bOUT)} delta={diff} length={length}")
+
if data_type == MESSENGER_STATE_TYPE_NOSPAMKEYS:
nospam = bin_to_hex(result[0:4])
public_key = bin_to_hex(result[4:36])
@@ -443,17 +449,29 @@ def process_chunk(index, state):
lOUT += [{label: lIN}]; aOUT.update({label: lIN})
elif data_type == MESSENGER_STATE_TYPE_NAME:
- name = str(state[index + 8:index + 8 + length], 'utf-8')
+ name = str(result, sENC)
LOG.info(f"{label} Nick_name = " +name)
aIN = {"Nick_name": name}
lOUT += [{label: aIN}]; aOUT.update({label: aIN})
-
+ if oArgs.command == 'edit' and section == label:
+ ## NAME,0,Nick_name,str
+ if key == "Nick_name":
+ result = bytes(val, sENC)
+ length = len(result)
+ LOG.info(f"{label} {key} EDITED to {val}")
+
elif data_type == MESSENGER_STATE_TYPE_STATUSMESSAGE:
- mess = str(state[index + 8:index + 8 + length], 'utf-8')
+ mess = str(result, sENC)
LOG.info(f"{label} StatusMessage = " +mess)
aIN = {"Status_message": mess}
lOUT += [{label: aIN}]; aOUT.update({label: aIN})
-
+ if oArgs.command == 'edit' and section == label:
+ ## STATUSMESSAGE,0,Status_message,str
+ if key == "Status_message":
+ result = bytes(val, sENC)
+ length = len(result)
+ LOG.info(f"{label} {key} EDITED to {val}")
+
elif data_type == MESSENGER_STATE_TYPE_STATUS:
# 1 uint8_t status (0 = online, 1 = away, 2 = busy)
dStatus = {0: 'online', 1: 'away', 2: 'busy'}
@@ -462,6 +480,12 @@ def process_chunk(index, state):
LOG.info(f"{label} = " +status)
aIN = {f"Online_status": status}
lOUT += [{"STATUS": aIN}]; aOUT.update({"STATUS": aIN})
+ if oArgs.command == 'edit' and section == label:
+ ## STATUS,0,Online_status,int
+ if key == "Online_status":
+ result = struct.pack(">b", int(val))
+ length = len(result)
+ LOG.info(f"{label} {key} EDITED to {val}")
elif data_type == MESSENGER_STATE_TYPE_GROUPS:
if length > 0:
@@ -485,7 +509,7 @@ def process_chunk(index, state):
LOG.debug(f"process_chunk {label} bytes={length}")
lIN = lProcessNodeInfo(state, index, length, result, "PATHnode")
lOUT += [{label: lIN}]; aOUT.update({label: lIN})
-
+
elif data_type == MESSENGER_STATE_TYPE_CONFERENCES:
lIN = []
if length > 0:
@@ -495,22 +519,33 @@ def process_chunk(index, state):
lOUT += [{label: []}]; aOUT.update({label: []})
elif data_type != MESSENGER_STATE_TYPE_END:
- LOG.warn("UNRECOGNIZED datatype={datatype}")
+ LOG.error("UNRECOGNIZED datatype={datatype}")
+ sys.exit(1)
else:
- diff = len(bSAVE) - len(bOUT)
- if diff:
- # if short repacking as we read - tox_profile is padded with nulls
- LOG.debug(f"PROCESS_CHUNK bSAVE={len(bSAVE)} bOUT={len(bOUT)} delta={diff}")
-
LOG.info("END") # That's all folks...
- return
-
- # failsafe
- if index + 8 >= len(state): return
- process_chunk(new_index, state)
+ # drop through
-def bAreWeConnected():
+ # We repack as we read: or edit as we parse; simply edit result and length.
+ # We'll add the results back to bOUT to see if we get what we started with.
+ # Then will will be able to selectively null sections or selectively edit.
+ assert length == len(result), length
+ bOUT += struct.pack("= len(state):
+ diff = len(bSAVE) - len(bOUT)
+ if oArgs.command != 'edit' and diff > 0:
+ # if short repacking as we read - tox_profile is padded with nulls
+ LOG.warn(f"PROCESS_CHUNK bSAVE={len(bSAVE)} bOUT={len(bOUT)} delta={diff}")
+ return
+
+ process_chunk(new_index, state, oArgs)
+
+def bAreWeConnected():
# FixMe: Linux
sFile = f"/proc/{os.getpid()}/net/route"
if not os.path.isfile(sFile): return None
@@ -616,7 +651,7 @@ def vSetupLogging(loglevel=logging.DEBUG):
logging._defaultFormatter = logging.Formatter(datefmt='%m-%d %H:%M:%S')
logging._defaultFormatter.default_time_format = '%m-%d %H:%M:%S'
logging._defaultFormatter.default_msec_format = ''
-
+
def oMainArgparser(_=None):
if not os.path.exists('/proc/sys/net/ipv6'):
bIpV6 = 'False'
@@ -627,16 +662,18 @@ def oMainArgparser(_=None):
parser = argparse.ArgumentParser(epilog=__doc__)
# list(dSTATE_TYPE.values())
# ['nospamkeys', 'dht', 'friends', 'name', 'statusmessage', 'status', 'groups', 'tcp_relay', 'path_node', 'conferences']
-
+
parser.add_argument('--output', type=str, default='',
help='Destination for info/decrypt - defaults to stderr')
parser.add_argument('--command', type=str, default='info',
- choices=['info', 'decrypt', 'nodes', 'save'],
- # required=True,
+ choices=['info', 'decrypt', 'nodes', 'edit'],
+ required=True,
help='Action command - default: info')
+ parser.add_argument('--edit', type=str, default='',
+ help='comma seperated SECTION,key,value - unfinished')
parser.add_argument('--indent', type=int, default=2,
help='Indent for yaml/json/pprint')
- choices=['info', 'repr', 'yaml','json', 'pprint']
+ choices=['info', 'save', 'repr', 'yaml','json', 'pprint']
if bHAVE_NMAP: choices += ['nmap_tcp', 'nmap_udp', 'nmap_onion']
parser.add_argument('--info', type=str, default='info',
choices=choices,
@@ -652,18 +689,38 @@ def oMainArgparser(_=None):
help='Action for nodes command (requires jq)')
parser.add_argument('--download_nodes_url', type=str,
default='https://nodes.tox.chat/json')
+ parser.add_argument('--encoding', type=str, default=sENC)
parser.add_argument('profile', type=str, nargs='?', default=None,
help='tox profile file - may be encrypted')
return parser
+# grep '#''#' logging_tox_savefile.py|sed -e 's/.* //'
+sEDIT_HELP = """
+NAME,0,Nick_name,str
+STATUSMESSAGE,0,Status_message,str
+STATUS,0,Online_status,int
+"""
+
+global lOUT, bOUT, aOUT, sENC
+lOUT = []
+aOUT = {}
+bOUT = b''
+sENC = 'utf-8'
if __name__ == '__main__':
lArgv = sys.argv[1:]
parser = oMainArgparser()
oArgs = parser.parse_args(lArgv)
+ if oArgs.command in ['edit'] and oArgs.edit == 'help':
+ l = list(dSTATE_TYPE.values())
+ l.remove('END')
+ print('Available Sections: ' +repr(l))
+ print('Supported Quads: section,num,key,type ' +sEDIT_HELP)
+ sys.exit(0)
sFile = oArgs.profile
assert os.path.isfile(sFile), sFile
+ sENC = oArgs.encoding
vSetupLogging()
bSAVE = open(sFile, 'rb').read()
@@ -712,7 +769,7 @@ if __name__ == '__main__':
else:
cmd = vBashFileNmapTcp()
iRet = os.system(f"bash {cmd} < '{sFile}'" +f" >'{oArgs.output}'")
-
+
elif oArgs.nodes == 'nmap_udp' and bHAVE_NMAP:
assert bHAVE_JQ, "jq is required for this command"
assert oArgs.output, "--output required for this command"
@@ -722,7 +779,7 @@ if __name__ == '__main__':
else:
cmd = vBashFileNmapUdp()
iRet = os.system(f"bash {cmd} < '{sFile}'" +f" >'{oArgs.output}'")
-
+
elif oArgs.nodes == 'download' and download_url:
if not bAreWeConnected():
LOG.error(f"{oArgs.nodes} not connected")
@@ -739,7 +796,7 @@ if __name__ == '__main__':
oStream.write(bSAVE)
else:
oStream = sys.stdout
- oStream.write(str(bSAVE, 'utf-8'))
+ oStream.write(str(bSAVE, sENC))
iRet = -1
LOG.info(f"downloaded list of nodes saved to {oStream}")
@@ -747,54 +804,69 @@ if __name__ == '__main__':
LOG.warn(f"{oArgs.nodes} iRet={iRet} to {oArgs.output}")
elif iRet == 0:
LOG.info(f"{oArgs.nodes} iRet={iRet} to {oArgs.output}")
-
- elif oArgs.command in ['save', 'info']:
- if oArgs.command == 'save':
- assert oArgs.output, "--output required for this command"
- mark = b'\x00\x00\x00\x00\x1f\x1b\xed\x15'
- bOUT = mark
+ elif oArgs.command in ['info', 'edit']:
+ if oArgs.command in ['edit']:
+ assert oArgs.output, "--output required for this command"
+ assert oArgs.edit != '', "--edit required for this command"
+ elif oArgs.command == 'info':
+ # assert oArgs.info != '', "--info required for this command"
+ if oArgs.info in ['save', 'yaml', 'json', 'repr', 'pprint']:
+ assert oArgs.output, "--output required for this command"
+
# toxEsave
- assert bSAVE[:8] == bOUT, "Not a Tox profile"
+ assert bSAVE[:8] == bMARK, "Not a Tox profile"
+ bOUT = bMARK
iErrs = 0
- lOUT = []; aOUT = {}
- process_chunk(len(bOUT), bSAVE)
- if aOUT:
- if oArgs.output:
- oStream = open(oArgs.output, 'wb')
- else:
- oStream = sys.stdout
+ process_chunk(len(bOUT), bSAVE, oArgs)
+ if not bOUT:
+ LOG.error(f"{oArgs.command} NO bOUT results")
+ else:
+ oStream = None
+ LOG.debug(f"command={oArgs.command} len bOUT={len(bOUT)} results")
- if oArgs.command == 'save':
- oStream.write(bOUT)
+ if oArgs.command in ['edit'] or oArgs.info in ['save']:
+ LOG.debug(f"{oArgs.command} saving to {oArgs.output}")
+ oStream = open(oArgs.output, 'wb', encoding=None)
+ if oStream.write(bOUT) > 0: iRet = 0
+ LOG.info(f"{oArgs.info}ed iRet={iRet} to {oArgs.output}")
elif oArgs.info == 'info':
pass
elif oArgs.info == 'yaml' and yaml:
+ LOG.debug(f"{oArgs.command} saving to {oArgs.output}")
+ oStream = open(oArgs.output, 'wt', encoding=sENC)
yaml.dump(aOUT, stream=oStream, indent=oArgs.indent)
- oStream.write('\n')
+ if oStream.write('\n') > 0: iRet = 0
+ LOG.info(f"{oArgs.info}ing iRet={iRet} to {oArgs.output}")
elif oArgs.info == 'json' and json:
+ LOG.debug(f"{oArgs.command} saving to {oArgs.output}")
+ oStream = open(oArgs.output, 'wb', encoding=None)
json.dump(aOUT, oStream, indent=oArgs.indent)
- oStream.write('\n')
+ if oStream.write('\n') > 0: iRet = 0
+ LOG.info(f"{oArgs.info}ing iRet={iRet} to {oArgs.output}")
elif oArgs.info == 'repr':
- oStream.write(repr(aOUT))
- oStream.write('\n')
+ LOG.debug(f"{oArgs.command} saving to {oArgs.output}")
+ oStream = open(oArgs.output, 'wt', encoding=sENC)
+ if oStream.write(repr(bOUT)) > 0: iRet = 0
+ if oStream.write('\n') > 0: iRet = 0
+ LOG.info(f"{oArgs.info}ing iRet={iRet} to {oArgs.output}")
elif oArgs.info == 'pprint':
+ LOG.debug(f"{oArgs.command} saving to {oArgs.output}")
+ oStream = open(oArgs.output, 'wt', encoding=sENC)
pprint(aOUT, stream=oStream, indent=oArgs.indent, width=80)
+ iRet = 0
+ LOG.info(f"{oArgs.info}ing iRet={iRet} to {oArgs.output}")
elif oArgs.info == 'nmap_tcp' and bHAVE_NMAP:
assert oArgs.output, "--output required for this command"
- oStream.close()
vOsSystemNmapTcp(aOUT["TCP_RELAY"], oArgs)
elif oArgs.info == 'nmap_udp' and bHAVE_NMAP:
assert oArgs.output, "--output required for this command"
- oStream.close()
vOsSystemNmapUdp(aOUT["DHT"], oArgs)
elif oArgs.info == 'nmap_onion' and bHAVE_NMAP:
assert oArgs.output, "--output required for this command"
- oStream.close()
vOsSystemNmapUdp(aOUT["PATH_NODE"], oArgs)
-
if oStream and oStream != sys.stdout and oStream != sys.stderr:
oStream.close()