2018-04-17 18:28:11 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2018-05-24 14:30:53 +10:00
|
|
|
import json
|
2018-04-17 18:28:11 +01:00
|
|
|
import os
|
2018-05-24 14:30:53 +10:00
|
|
|
import pathlib
|
2018-05-11 10:20:22 -04:00
|
|
|
import re
|
2018-05-23 18:07:32 +10:00
|
|
|
import shutil
|
2018-04-17 18:28:11 +01:00
|
|
|
import struct
|
2018-05-24 12:56:06 +10:00
|
|
|
import subprocess
|
2018-05-24 14:30:53 +10:00
|
|
|
import sys
|
|
|
|
import tempfile
|
2018-05-11 10:20:22 -04:00
|
|
|
import unicodedata
|
2018-04-17 18:28:11 +01:00
|
|
|
|
2018-05-24 12:56:06 +10:00
|
|
|
VERSION = "0.1.6"
|
2018-04-17 18:28:11 +01:00
|
|
|
|
2018-05-11 10:20:22 -04:00
|
|
|
|
2018-04-17 18:28:11 +01:00
|
|
|
class NoConnectionError(Exception):
|
|
|
|
""" Exception thrown when stdin cannot be read """
|
|
|
|
|
2018-05-24 12:56:06 +10:00
|
|
|
|
2018-05-23 18:07:32 +10:00
|
|
|
def is_command_on_path(command):
|
|
|
|
""" Returns 'True' if the if the specified command is found on
|
|
|
|
user's $PATH.
|
|
|
|
"""
|
|
|
|
if shutil.which(command):
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
return False
|
2018-04-17 18:28:11 +01:00
|
|
|
|
2018-05-24 12:56:06 +10:00
|
|
|
|
2018-04-17 18:28:11 +01:00
|
|
|
def eprint(*args, **kwargs):
|
2018-05-24 12:56:06 +10:00
|
|
|
""" Print to stderr, which gets echoed in the browser console
|
|
|
|
when run by Firefox
|
2018-04-17 18:28:11 +01:00
|
|
|
"""
|
|
|
|
print(*args, file=sys.stderr, flush=True, **kwargs)
|
|
|
|
|
|
|
|
|
2018-05-09 22:09:10 +02:00
|
|
|
def getenv(variable, default):
|
2018-04-17 18:28:11 +01:00
|
|
|
""" Get an environment variable value, or use the default provided """
|
|
|
|
return os.environ.get(variable) or default
|
|
|
|
|
2018-05-11 10:20:22 -04:00
|
|
|
|
2018-04-17 18:28:11 +01:00
|
|
|
def getMessage():
|
2018-04-18 21:49:33 +01:00
|
|
|
"""Read a message from stdin and decode it.
|
|
|
|
|
|
|
|
"Each message is serialized using JSON, UTF-8 encoded and is preceded with
|
|
|
|
a 32-bit value containing the message length in native byte order."
|
|
|
|
|
|
|
|
https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Native_messaging#App_side
|
|
|
|
|
|
|
|
"""
|
2018-04-17 18:28:11 +01:00
|
|
|
rawLength = sys.stdin.buffer.read(4)
|
|
|
|
if len(rawLength) == 0:
|
|
|
|
sys.exit(0)
|
|
|
|
messageLength = struct.unpack('@I', rawLength)[0]
|
|
|
|
message = sys.stdin.buffer.read(messageLength).decode('utf-8')
|
|
|
|
return json.loads(message)
|
|
|
|
|
|
|
|
|
|
|
|
# Encode a message for transmission,
|
|
|
|
# given its content.
|
|
|
|
def encodeMessage(messageContent):
|
|
|
|
""" Encode a message for transmission, given its content."""
|
|
|
|
encodedContent = json.dumps(messageContent).encode('utf-8')
|
|
|
|
encodedLength = struct.pack('@I', len(encodedContent))
|
|
|
|
return {'length': encodedLength, 'content': encodedContent}
|
|
|
|
|
|
|
|
|
|
|
|
# Send an encoded message to stdout
|
|
|
|
def sendMessage(encodedMessage):
|
|
|
|
""" Send an encoded message to stdout."""
|
|
|
|
sys.stdout.buffer.write(encodedMessage['length'])
|
|
|
|
sys.stdout.buffer.write(encodedMessage['content'])
|
2018-04-26 12:33:01 +01:00
|
|
|
try:
|
|
|
|
sys.stdout.buffer.write(encodedMessage['code'])
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2018-04-17 18:28:11 +01:00
|
|
|
sys.stdout.buffer.flush()
|
|
|
|
|
|
|
|
|
2018-05-10 21:22:14 +01:00
|
|
|
def findUserConfigFile():
|
|
|
|
""" Find a user config file, if it exists. Return the file path, or None
|
|
|
|
if not found
|
|
|
|
"""
|
|
|
|
home = os.path.expanduser('~')
|
|
|
|
config_dir = getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
|
|
|
|
|
|
|
|
# Will search for files in this order
|
|
|
|
candidate_files = [
|
|
|
|
os.path.join(config_dir, "tridactyl", "tridactylrc"),
|
|
|
|
os.path.join(home, '.tridactylrc')
|
|
|
|
]
|
|
|
|
|
|
|
|
config_path = None
|
|
|
|
|
|
|
|
# find the first path in the list that exists
|
|
|
|
for path in candidate_files:
|
|
|
|
if os.path.isfile(path):
|
|
|
|
config_path = path
|
|
|
|
break
|
|
|
|
|
|
|
|
return config_path
|
|
|
|
|
|
|
|
|
|
|
|
def getUserConfig():
|
|
|
|
# look it up freshly each time - the user could have moved or killed it
|
|
|
|
cfg_file = findUserConfigFile()
|
|
|
|
|
|
|
|
# no file, return
|
|
|
|
if not cfg_file:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# for now, this is a simple file read, but if the files can
|
|
|
|
# include other files, that will need more work
|
|
|
|
return open(cfg_file, 'r').read()
|
|
|
|
|
|
|
|
|
2018-05-11 10:20:22 -04:00
|
|
|
def sanitizeFilename(fn):
|
|
|
|
""" Transform a string to make it suitable for use as a filename.
|
|
|
|
|
|
|
|
From https://stackoverflow.com/a/295466/147356"""
|
|
|
|
|
|
|
|
fn = unicodedata.normalize('NFKD', fn).encode(
|
|
|
|
'ascii', 'ignore').decode('ascii')
|
|
|
|
fn = re.sub('[^\w\s/.-]', '', fn).strip().lower()
|
|
|
|
fn = re.sub('\.\.+', '', fn)
|
|
|
|
fn = re.sub('[-/\s]+', '-', fn)
|
|
|
|
return fn
|
|
|
|
|
2018-05-24 14:30:53 +10:00
|
|
|
|
2018-05-24 12:56:06 +10:00
|
|
|
def is_valid_firefox_profile(profile_dir):
|
|
|
|
is_valid = False
|
|
|
|
validity_indicator = "times.json"
|
|
|
|
|
|
|
|
if pathlib.WindowsPath(profile_dir).is_dir():
|
|
|
|
test_path = "%s\\%s" % (profile_dir, validity_indicator)
|
|
|
|
|
|
|
|
if pathlib.WindowsPath(test_path).is_file():
|
|
|
|
is_valid = True
|
|
|
|
|
|
|
|
return is_valid
|
|
|
|
|
|
|
|
|
|
|
|
def win_firefox_restart(message):
|
|
|
|
"""Handle 'win_firefox_restart' message."""
|
|
|
|
reply = {}
|
|
|
|
profile_dir = message["profiledir"].strip()
|
|
|
|
|
|
|
|
ff_bin_name = "firefox.exe"
|
|
|
|
ff_lock_name = "parent.lock"
|
|
|
|
|
|
|
|
if profile_dir != "auto" \
|
|
|
|
and not is_valid_firefox_profile(profile_dir):
|
|
|
|
reply = {
|
|
|
|
"cmd": "error",
|
|
|
|
"error": "%s %s %s" % (
|
|
|
|
"Invalid profile directory specified.",
|
|
|
|
"Vaild profile directory path(s) can be found by",
|
|
|
|
"navigating to 'about:support'."
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
elif not is_command_on_path(ff_bin_name):
|
|
|
|
reply = {
|
|
|
|
"cmd": "error",
|
|
|
|
"error": "firefox.exe is not found on %PATH%."
|
|
|
|
}
|
|
|
|
|
|
|
|
else:
|
|
|
|
# {{{
|
|
|
|
# Native messenger can't seem to create detached process on
|
|
|
|
# Windows while Firefox is quitting, which is essential to
|
|
|
|
# trigger restarting Firefox. So, below we are resorting to
|
|
|
|
# create a scheduled task with the task start-time set in
|
|
|
|
# the near future.
|
|
|
|
#
|
|
|
|
|
|
|
|
#
|
2018-05-24 14:30:53 +10:00
|
|
|
# subprocess.Popen(
|
2018-05-24 12:56:06 +10:00
|
|
|
# [ff_bin_path, "-profile", profile_dir],
|
|
|
|
# shell=False,
|
|
|
|
# creationflags=0x208 \
|
|
|
|
# | subprocess.CREATE_NEW_PROCESS_GROUP)
|
|
|
|
#
|
|
|
|
|
|
|
|
#
|
|
|
|
# 'schtasks.exe' is limited as in it doesn't support
|
|
|
|
# task-time with granularity in seconds. So, falling back
|
|
|
|
# to PowerShell as the last resort.
|
|
|
|
#
|
|
|
|
|
|
|
|
# out_str = ""
|
|
|
|
# task_time = time.strftime("%H:%M",
|
|
|
|
# time.localtime(
|
|
|
|
# time.time() + 60))
|
|
|
|
#
|
|
|
|
# out_str = subprocess.check_output(
|
|
|
|
# ["schtasks.exe",
|
|
|
|
# "/Create",
|
|
|
|
# "/F",
|
|
|
|
# "/SC",
|
|
|
|
# "ONCE",
|
|
|
|
# "/TN",
|
|
|
|
# "tridactyl",
|
|
|
|
# "/TR",
|
|
|
|
# "calc",
|
|
|
|
# "/IT",
|
|
|
|
# "/ST",
|
|
|
|
# task_time],
|
|
|
|
# shell=True)
|
|
|
|
# }}}
|
|
|
|
|
|
|
|
ff_bin_path = "\"%s\"" % shutil.which(ff_bin_name)
|
2018-05-25 14:06:29 +10:00
|
|
|
new_tab_url = "https://www.mozilla.org/en-US/firefox/"
|
2018-05-24 12:56:06 +10:00
|
|
|
|
|
|
|
if profile_dir == "auto":
|
|
|
|
ff_lock_path = ff_bin_path
|
2018-05-25 14:06:29 +10:00
|
|
|
ff_args = "\"%s\" \"%s\" \"%s\" \"%s\"" % \
|
|
|
|
("-foreground",
|
|
|
|
"-ProfileManager",
|
|
|
|
"-new-tab",
|
|
|
|
new_tab_url)
|
2018-05-24 12:56:06 +10:00
|
|
|
else:
|
2018-05-24 14:30:53 +10:00
|
|
|
ff_lock_path = "\"%s/%s\"" % (profile_dir,
|
|
|
|
ff_lock_name)
|
2018-05-25 14:06:29 +10:00
|
|
|
ff_args = "\"%s\" \"%s\" \"%s\" \"%s\" \"%s\"" % \
|
|
|
|
("-foreground",
|
|
|
|
"-profile",
|
|
|
|
profile_dir,
|
|
|
|
"-new-tab",
|
|
|
|
new_tab_url)
|
2018-05-24 12:56:06 +10:00
|
|
|
|
|
|
|
try:
|
|
|
|
restart_ps1_content = '''$profileDir = "%s"
|
|
|
|
if ($profileDir -ne "auto") {
|
|
|
|
$lockFilePath = %s
|
|
|
|
$locked = $true
|
|
|
|
$num_try = 10
|
|
|
|
} else {
|
|
|
|
$locked = $false
|
|
|
|
}
|
|
|
|
while (($locked -eq $true) -and ($num_try -gt 0)) {
|
|
|
|
try {
|
|
|
|
[IO.File]::OpenWrite($lockFilePath).close()
|
|
|
|
$locked=$fals
|
|
|
|
} catch{
|
|
|
|
$num_try-=1
|
|
|
|
Write-Host "[+] Trial: $num_try [lock == true]"
|
|
|
|
Start-Sleep -Seconds 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($locked -eq $true) {
|
|
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
|
|
[System.Windows.MessageBox]::Show(
|
|
|
|
"Restarting Firefox failed. Please manually restart.",
|
|
|
|
"Tridactyl")
|
|
|
|
} else {
|
|
|
|
Write-Host "[+] Restarting Firefox ..."
|
|
|
|
& %s %s
|
|
|
|
}
|
|
|
|
''' % (profile_dir, ff_lock_path, ff_bin_path, ff_args)
|
|
|
|
|
|
|
|
delay_sec = 1
|
|
|
|
task_cmd = "powershell"
|
|
|
|
task_name = "firefox-restart"
|
|
|
|
native_messenger_dirname = ".tridactyl"
|
|
|
|
|
|
|
|
restart_ps1_path = "%s\\%s\\%s" % (
|
|
|
|
os.path.expanduser('~'),
|
|
|
|
native_messenger_dirname,
|
|
|
|
"win_firefox_restart.ps1")
|
|
|
|
|
|
|
|
task_arg = "\"%s\"" % restart_ps1_path
|
|
|
|
|
|
|
|
open(restart_ps1_path, "w+").write(restart_ps1_content)
|
|
|
|
|
|
|
|
startupinfo = subprocess.STARTUPINFO()
|
|
|
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
|
|
|
|
|
|
subprocess.check_output(
|
|
|
|
["powershell",
|
|
|
|
"-NonInteractive",
|
|
|
|
"-NoProfile",
|
|
|
|
"-WindowStyle",
|
|
|
|
"Minimized",
|
|
|
|
"-InputFormat",
|
|
|
|
"None",
|
|
|
|
"-ExecutionPolicy",
|
|
|
|
"Bypass",
|
|
|
|
"-Command",
|
|
|
|
"Register-ScheduledTask \
|
|
|
|
-TaskName '%s' \
|
|
|
|
-Force \
|
|
|
|
-Action (New-ScheduledTaskAction \
|
|
|
|
-Execute '%s' \
|
|
|
|
-Argument '%s') \
|
|
|
|
-Trigger (New-ScheduledTaskTrigger \
|
|
|
|
-Once \
|
|
|
|
-At \
|
|
|
|
(Get-Date).AddSeconds(%d).ToString(\
|
|
|
|
'HH:mm:ss'))" % (task_name,
|
|
|
|
task_cmd,
|
|
|
|
task_arg,
|
|
|
|
delay_sec)
|
|
|
|
], shell=False, startupinfo=startupinfo)
|
|
|
|
|
|
|
|
reply = {
|
|
|
|
"code": 0,
|
|
|
|
"content": "Restarting in %d seconds..." % delay_sec
|
|
|
|
}
|
|
|
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
reply = {
|
|
|
|
"code": -1,
|
|
|
|
"cmd": "error",
|
|
|
|
"error": "Error creating restart task."
|
|
|
|
}
|
|
|
|
|
|
|
|
return reply
|
|
|
|
|
2018-05-11 10:20:22 -04:00
|
|
|
|
2018-04-17 18:28:11 +01:00
|
|
|
def handleMessage(message):
|
|
|
|
""" Generate reply from incoming message. """
|
|
|
|
cmd = message["cmd"]
|
|
|
|
reply = {'cmd': cmd}
|
|
|
|
|
|
|
|
if cmd == 'version':
|
|
|
|
reply = {'version': VERSION}
|
|
|
|
|
2018-05-10 21:22:14 +01:00
|
|
|
elif cmd == 'getconfig':
|
|
|
|
file_content = getUserConfig()
|
|
|
|
if file_content:
|
|
|
|
reply['content'] = file_content
|
|
|
|
else:
|
|
|
|
reply['code'] = 'File not found'
|
|
|
|
|
2018-04-17 18:45:54 +01:00
|
|
|
elif cmd == 'run':
|
2018-04-26 12:33:01 +01:00
|
|
|
commands = message["command"]
|
|
|
|
|
|
|
|
try:
|
|
|
|
p = subprocess.check_output(commands, shell=True)
|
|
|
|
reply["content"] = p.decode("utf-8")
|
|
|
|
reply["code"] = 0
|
|
|
|
|
|
|
|
except subprocess.CalledProcessError as process:
|
|
|
|
reply["code"] = process.returncode
|
|
|
|
reply["content"] = process.output.decode("utf-8")
|
2018-04-17 18:45:54 +01:00
|
|
|
|
2018-04-18 21:49:33 +01:00
|
|
|
elif cmd == 'eval':
|
|
|
|
output = eval(message["command"])
|
|
|
|
reply['content'] = output
|
|
|
|
|
2018-04-17 18:45:54 +01:00
|
|
|
elif cmd == 'read':
|
2018-04-28 00:08:39 +01:00
|
|
|
try:
|
2018-05-10 21:37:13 +01:00
|
|
|
with open(os.path.expandvars(os.path.expanduser(message["file"])), "r") as file:
|
2018-04-28 00:08:39 +01:00
|
|
|
reply['content'] = file.read()
|
|
|
|
reply['code'] = 0
|
|
|
|
except FileNotFoundError:
|
|
|
|
reply['content'] = ""
|
|
|
|
reply['code'] = 2
|
|
|
|
|
|
|
|
elif cmd == 'mkdir':
|
|
|
|
os.makedirs(
|
|
|
|
os.path.relpath(message["dir"]), exist_ok=message["exist_ok"]
|
|
|
|
)
|
|
|
|
reply['content'] = ""
|
|
|
|
reply['code'] = 0
|
2018-04-17 18:45:54 +01:00
|
|
|
|
|
|
|
elif cmd == 'write':
|
2018-04-26 12:33:01 +01:00
|
|
|
with open(message["file"], "w") as file:
|
2018-04-17 18:45:54 +01:00
|
|
|
file.write(message["content"])
|
|
|
|
|
2018-04-24 22:36:38 +01:00
|
|
|
elif cmd == 'temp':
|
2018-05-11 10:20:22 -04:00
|
|
|
prefix = message.get('prefix')
|
2018-05-17 17:53:33 +02:00
|
|
|
if prefix is None:
|
|
|
|
prefix = ''
|
|
|
|
prefix = 'tmp_{}_'.format(sanitizeFilename(prefix))
|
2018-05-11 10:20:22 -04:00
|
|
|
|
|
|
|
(handle, filepath) = tempfile.mkstemp(prefix=prefix)
|
2018-05-11 09:49:40 -04:00
|
|
|
with os.fdopen(handle, "w") as file:
|
2018-04-24 22:36:38 +01:00
|
|
|
file.write(message["content"])
|
|
|
|
reply['content'] = filepath
|
|
|
|
|
2018-05-09 22:09:10 +02:00
|
|
|
elif cmd == 'env':
|
|
|
|
reply['content'] = getenv(message["var"], "")
|
|
|
|
|
2018-05-24 12:56:06 +10:00
|
|
|
elif cmd == "win_firefox_restart":
|
|
|
|
reply = win_firefox_restart(message)
|
2018-05-23 18:07:32 +10:00
|
|
|
|
2018-04-17 18:28:11 +01:00
|
|
|
else:
|
|
|
|
reply = {'cmd': 'error', 'error': 'Unhandled message'}
|
|
|
|
eprint('Unhandled message: {}'.format(message))
|
|
|
|
|
|
|
|
return reply
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
message = getMessage()
|
|
|
|
reply = handleMessage(message)
|
2018-04-17 18:45:54 +01:00
|
|
|
sendMessage(encodeMessage(reply))
|