Add editing
This commit is contained in:
parent
fe7715abb5
commit
c1303a1a66
51
README.md
51
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:
|
||||
<https://pypi.org/project/coloredlogs/>
|
||||
|
||||
For the ```select``` and ```nmap``` commands, the ```jq``` utility is
|
||||
required. It's available in most distros, or <https://stedolan.github.io/jq/>
|
||||
|
||||
For the ```nmap``` commands, the ```nmap``` utility is
|
||||
required. It's available in most distros, or <https://nmap.org/>
|
||||
|
||||
## 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:
|
||||
<https://git.plastiras.org/emdee/tox_profile/wiki/MultiDevice-Announcements-POC>
|
||||
|
||||
### 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
|
||||
|
||||
|
@ -20,7 +20,7 @@ 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
|
||||
@ -90,11 +90,15 @@ except ImportError as e:
|
||||
|
||||
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:
|
||||
@ -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("<I", state, index)[0]
|
||||
data_type = struct.unpack_from("<H", state, index + 4)[0]
|
||||
@ -409,16 +417,14 @@ def process_chunk(index, state):
|
||||
result = state[index + 8:index + 8 + length]
|
||||
|
||||
label = dSTATE_TYPE[data_type]
|
||||
if oArgs.command == 'edit' and oArgs.edit:
|
||||
section,num,key,val = oArgs.edit.split(',',3)
|
||||
|
||||
diff = index - len(bOUT)
|
||||
if diff:
|
||||
LOG.debug(f"PROCESS_CHUNK {label} index={index} bOUT={len(bOUT)} delta={diff} length={length}")
|
||||
# plan on repacking as we read - this is just a starting point
|
||||
# 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.
|
||||
bOUT += struct.pack("<I", length) + \
|
||||
struct.pack("<H", data_type) + \
|
||||
struct.pack("<H", check) + \
|
||||
result
|
||||
if diff > 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])
|
||||
@ -443,16 +449,28 @@ 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)
|
||||
@ -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:
|
||||
@ -495,20 +519,31 @@ 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...
|
||||
# drop through
|
||||
|
||||
# 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("<I", length) + \
|
||||
struct.pack("<H", data_type) + \
|
||||
struct.pack("<H", check) + \
|
||||
result
|
||||
|
||||
if data_type == MESSENGER_STATE_TYPE_END or \
|
||||
index + 8 >= 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
|
||||
|
||||
# failsafe
|
||||
if index + 8 >= len(state): return
|
||||
process_chunk(new_index, state)
|
||||
process_chunk(new_index, state, oArgs)
|
||||
|
||||
def bAreWeConnected():
|
||||
# FixMe: Linux
|
||||
@ -631,12 +666,14 @@ def oMainArgparser(_=None):
|
||||
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()
|
||||
@ -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}")
|
||||
|
||||
@ -748,53 +805,68 @@ if __name__ == '__main__':
|
||||
elif iRet == 0:
|
||||
LOG.info(f"{oArgs.nodes} iRet={iRet} to {oArgs.output}")
|
||||
|
||||
elif oArgs.command in ['save', 'info']:
|
||||
if oArgs.command == 'save':
|
||||
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"
|
||||
|
||||
mark = b'\x00\x00\x00\x00\x1f\x1b\xed\x15'
|
||||
bOUT = mark
|
||||
# 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()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user