├── .gitignore ├── README.md ├── electron_inject ├── __init__.py └── __main__.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💉 electron-inject 2 | 3 | You find yourself locked out of closed source electron applications with no easy way to enable developer tools? ↷ *electron-inject* is here to help 👲 4 | 5 | 6 | *electron-inject* is an application wrapper that utilizes the remote debug console to inject javascript code into electron based applications. For example, this can be pretty handy to enable otherwise unavailable features like the built-in developer console. 7 | 8 | ![slack](https://cloud.githubusercontent.com/assets/2865694/24376228/70b2c2b0-133b-11e7-893c-c7a0ad262343.gif) 9 | 10 | 11 | # install 12 | 13 | $ pip install electron-inject 14 | 15 | or 16 | 17 | $ python setup.py install 18 | 19 | # usage 20 | 21 | $ python -m electron_inject --help 22 | Usage: 23 | usage: 24 | electron_inject [options] - 25 | 26 | example: 27 | electron_inject --enable-devtools-hotkeys - /path/to/electron/powered/application [--app-params app-args] 28 | 29 | 30 | Options: 31 | -h, --help show this help message and exit 32 | -d, --enable-devtools-hotkeys 33 | Enable Hotkeys F12 (Toggle Developer Tools) and F5 34 | (Refresh) [default: False] 35 | -b, --browser Launch Devtools in default browser. [default: False] 36 | -t TIMEOUT, --timeout=TIMEOUT 37 | Try hard to inject for the time specified [default: 38 | none] 39 | -r RENDER_SCRIPTS, --render-script=RENDER_SCRIPTS 40 | Add a script to be injected into each window (render 41 | thread) 42 | 43 | # Showcase 44 | 45 | Inject hotkeys *F12:toggle devconsole* and *F5:reload* into closed source apps with devconsole disabled. 46 | 47 | `--enable-devtools-hotkeys` .. enable developer hotkeys 48 | `--timeout=xx` .. patch all known remote webContent/windows in a timeframe of `xx` seconds. set this to an arbitrary high value to make sure we're patching all future windows. 49 | 50 | ## whatsapp 51 | 52 | `$ python -m electron_inject -d -t 60 - \\PATH\TO\Local\WhatsApp\app-0.2.2244\WhatsApp.exe` 53 | 54 | ![whatsapp gif](https://cloud.githubusercontent.com/assets/2865694/24376256/81d44e88-133b-11e7-961f-060e7b8201ed.gif) 55 | 56 | If this gives you an error try launching it with the alternative browser method: 57 | 58 | `$ python -m electron_inject --browser - \PATH\TO\Local\WhatsApp\app-0.2.2244\WhatsApp.exe` 59 | 60 | ## slack 61 | 62 | `$ python -m electron_inject -d -t 60 - \\PATH\TO\Local\slack\app-2.5.2\slack.exe` 63 | 64 | ![slack](https://cloud.githubusercontent.com/assets/2865694/24376228/70b2c2b0-133b-11e7-893c-c7a0ad262343.gif) 65 | 66 | # Render Scripts 67 | 68 | Passing the -r file parameter allows to pass a list of scripts to be injected into the render thread. It does not follow imports, just evaluate the text 69 | 70 | `python -m electron_inject -r ./test.js -r ~/test2.js -r /usr/bin/test3.js - /opt/electron-api-demos/Electron\ API\ Demos` 71 | 72 | # Acknowledgments 73 | 74 | - [NathanPB](https://github.com/NathanPB) - #7 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /electron_inject/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @author: github.com/tintinweb 4 | 5 | import requests 6 | import time 7 | import websocket 8 | import json 9 | import socket 10 | import subprocess 11 | import os 12 | import sys 13 | import logging 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | SCRIPT_HOTKEYS_F12_DEVTOOLS_F5_REFRESH = """document.addEventListener("keydown", function (e) { 18 | if (e.which === 123) { 19 | //F12 20 | require("electron").remote.BrowserWindow.getFocusedWindow().webContents.toggleDevTools(); 21 | } else if (e.which === 116) { 22 | //F5 23 | location.reload(); 24 | } 25 | });""" 26 | 27 | 28 | class LazyWebsocket(object): 29 | def __init__(self, url): 30 | self.url = url 31 | self.ws = None 32 | 33 | def _connect(self): 34 | if not self.ws: 35 | self.ws = websocket.create_connection(self.url) 36 | return self.ws 37 | 38 | def send(self, *args, **kwargs): 39 | return self._connect().send(*args, **kwargs) 40 | 41 | def recv(self, *args, **kwargs): 42 | return self.ws.recv(*args, **kwargs) 43 | 44 | def sendrcv(self, msg): 45 | self.send(msg) 46 | return self.recv() 47 | 48 | def close(self): 49 | self.ws.close() 50 | 51 | 52 | class ElectronRemoteDebugger(object): 53 | def __init__(self, host, port): 54 | self.params = {'host': host, 'port': port} 55 | 56 | def windows(self): 57 | params = self.params.copy() 58 | params.update({'ts': int(time.time())}) 59 | 60 | ret = [] 61 | for w in self.requests_get("http://%(host)s:%(port)s/json/list?t=%(ts)d" % params).json(): 62 | url = w.get("webSocketDebuggerUrl") 63 | if not url: 64 | continue 65 | w['ws'] = LazyWebsocket(url) 66 | ret.append(w) 67 | return ret 68 | 69 | def requests_get(self, url, tries=5, delay=1): 70 | last_exception = Exception("failed to request after %d tries."%tries) 71 | for _ in range(tries): 72 | try: 73 | return requests.get(url) 74 | except requests.exceptions.ConnectionError as ce: 75 | # ignore it 76 | last_exception = ce 77 | time.sleep(delay) 78 | raise last_exception 79 | 80 | 81 | def sendrcv(self, w, msg): 82 | return w['ws'].sendrcv(msg) 83 | 84 | def eval(self, w, expression): 85 | 86 | data = {'id': 1, 87 | 'method': "Runtime.evaluate", 88 | 'params': {'contextId': 1, 89 | 'doNotPauseOnExceptionsAndMuteConsole': False, 90 | 'expression': expression, 91 | 'generatePreview': False, 92 | 'includeCommandLineAPI': True, 93 | 'objectGroup': 'console', 94 | 'returnByValue': False, 95 | 'userGesture': True}} 96 | 97 | ret = json.loads(w['ws'].sendrcv(json.dumps(data))) 98 | if "result" not in ret: 99 | return ret 100 | if ret['result'].get('wasThrown'): 101 | raise Exception(ret['result']['result']) 102 | return ret['result'] 103 | 104 | @classmethod 105 | def execute(cls, path, port=None): 106 | if port is None: 107 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 108 | sock.bind(('', 0)) 109 | port = sock.getsockname()[1] 110 | sock.close() 111 | 112 | cmd = "%s %s" % (path, "--remote-debugging-port=%d" % port) 113 | print (cmd) 114 | p = subprocess.Popen(cmd, shell=True) 115 | time.sleep(0.5) 116 | if p.poll() is not None: 117 | raise Exception("Could not execute cmd (not found or already running?): %r"%cmd) 118 | 119 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 120 | for _ in range(30): 121 | result = sock.connect_ex(('localhost', port)) 122 | if result > 0: 123 | break 124 | time.sleep(1) 125 | return cls("localhost", port=port) 126 | 127 | 128 | def launch_url(url): 129 | #https://stackoverflow.com/questions/4216985/call-to-operating-system-to-open-url 130 | if sys.platform == 'win32': 131 | os.startfile(url) 132 | elif sys.platform == 'darwin': 133 | subprocess.Popen(['open', url]) 134 | else: 135 | try: 136 | subprocess.Popen(['xdg-open', url]) 137 | except OSError: 138 | logger.info ('Please open a browser on: ' + url) 139 | 140 | 141 | def inject(target, devtools=False, browser=False, timeout=None, scripts=None, port=None): 142 | timeout = time.time() + int(timeout) if timeout else 5 143 | scripts = dict.fromkeys(scripts or []) 144 | 145 | for name in scripts: 146 | with open(name, "r") as file: 147 | scripts[name] = file.read() 148 | 149 | erb = ElectronRemoteDebugger.execute(target, port) 150 | 151 | windows_visited = set() 152 | while True: 153 | for w in (_ for _ in erb.windows() if _.get('id') not in windows_visited): 154 | try: 155 | if devtools: 156 | logger.info("injecting hotkeys script into %s" % w.get('id')) 157 | logger.debug(erb.eval(w, SCRIPT_HOTKEYS_F12_DEVTOOLS_F5_REFRESH)) 158 | 159 | for name, content in scripts.items(): 160 | logger.info("injecting %s into %s" % (name, w.get('id'))) 161 | logger.debug(erb.eval(w, content)) 162 | 163 | except Exception as e: 164 | logger.exception(e) 165 | finally: 166 | # patch windows only once 167 | windows_visited.add(w.get('id')) 168 | 169 | if time.time() > timeout or all(w.get('id') in windows_visited for w in erb.windows()): 170 | break 171 | logger.debug("timeout not hit.") 172 | time.sleep(1) 173 | 174 | # launch browser? 175 | if browser: 176 | launch_url("http://%(host)s:%(port)s/" % erb.params) 177 | 178 | 179 | if __name__ == "__main__": 180 | erb = ElectronRemoteDebugger("localhost", 8888) 181 | for w in erb.windows(): 182 | print (erb.eval(w, SCRIPT_HOTKEYS_F12_DEVTOOLS_F5_REFRESH)) 183 | -------------------------------------------------------------------------------- /electron_inject/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @author: github.com/tintinweb 4 | 5 | import sys 6 | from optparse import OptionParser 7 | from electron_inject import inject 8 | import logging 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | if __name__ == '__main__': 14 | logging.basicConfig(format='[%(filename)s - %(funcName)20s() ][%(levelname)8s] %(message)s', 15 | level=logging.INFO) 16 | logger.setLevel(logging.DEBUG) 17 | 18 | usage = """ 19 | usage: 20 | electron_inject [options] - 21 | 22 | example: 23 | electron_inject --enable-devtools-hotkeys - /path/to/electron/powered/application [--app-params app-args] 24 | """ 25 | parser = OptionParser(usage=usage) 26 | parser.add_option("-d", "--enable-devtools-hotkeys", 27 | action="store_true", dest="enable_devtools_hotkeys", default=False, 28 | help="Enable Hotkeys F12 (Toggle Developer Tools) and F5 (Refresh) [default: %default]") 29 | parser.add_option("-b", "--browser", 30 | action="store_true", dest="browser", default=False, 31 | help="Launch Devtools in default browser. [default: %default]") 32 | parser.add_option("-s", "--silent", 33 | action="store_true", dest="silent", default=False, 34 | help="Stay silent. Do not ask any questions. [default: %default]") 35 | parser.add_option("-t", "--timeout", 36 | default=None, 37 | help="Try hard to inject for the time specified [default: %default]") 38 | parser.add_option('-r', "--render-script", 39 | action="append", 40 | dest="render_scripts", 41 | default=[], 42 | type="string", 43 | help="Add a script to be injected into each window (render thread)") 44 | 45 | if "--help" in sys.argv: 46 | parser.print_help() 47 | sys.exit(1) 48 | if "-" not in sys.argv: 49 | parser.error("mandatory delimiter '-' missing. see usage or --help") 50 | 51 | argidx = sys.argv.index("-") 52 | target = sys.argv[argidx + 1] 53 | if " " in target: 54 | target = '"%s"' % target 55 | target = ' '.join([target] + sys.argv[argidx + 2:]).strip() 56 | 57 | # parse args 58 | (options, args) = parser.parse_args(sys.argv[:argidx]) 59 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)-8s - %(message)s') 60 | 61 | if not len(target): 62 | logger.error("mandatory argument missing! see usage.") 63 | sys.exit(1) 64 | 65 | if not options.silent and not options.browser and not len(options.render_scripts): # if non-silent standard execution 66 | # ask user if they want to open devtools in browser 67 | if(input("Do you want to open the Developer Console in your Browser? [y/N]").strip().lower().startswith("y")): 68 | options.browser = True 69 | 70 | inject( 71 | target, 72 | devtools=options.enable_devtools_hotkeys, 73 | browser=options.browser, 74 | timeout=options.timeout, 75 | scripts=options.render_scripts, 76 | ) 77 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | from setuptools import setup 6 | 7 | def read(fname): 8 | try: 9 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 10 | except Exception: 11 | return "Not available" 12 | setup( 13 | name="electron-inject", 14 | version="0.7", 15 | packages=["electron_inject"], 16 | author="tintinweb", 17 | author_email="tintinweb@oststrom.com", 18 | description=( 19 | "An electron application wrapper that utilizes the remote debug console to inject code into electron applications to enable developer tools"), 20 | license="GPLv3", 21 | keywords=["electron", "inject", "devtools", "developer tools"], 22 | url="https://github.com/tintinweb/electron-inject/", 23 | download_url="https://github.com/tintinweb/electron-inject/tarball/v0.6", 24 | #python setup.py register -r https://testpypi.python.org/pypi 25 | long_description=read("README.md"), 26 | long_description_content_type='text/markdown', 27 | install_requires=['websocket-client','requests'], 28 | package_data = { 29 | 'electron_inject': ['electron_inject'], 30 | }, 31 | ) 32 | --------------------------------------------------------------------------------