├── .gitignore ├── Cakefile ├── Makefile ├── README.md ├── extension └── chromi.coffee └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | *.swo 3 | *.swp 4 | *.crx 5 | *.js 6 | node_modules/* 7 | README.html 8 | -------------------------------------------------------------------------------- /Cakefile: -------------------------------------------------------------------------------- 1 | 2 | # Based on vimium Cakefile: https://github.com/philc/vimium 3 | # 4 | 5 | fs = require "fs" 6 | path = require "path" 7 | child_process = require "child_process" 8 | 9 | spawn = (procName, optArray, silent=false) -> 10 | proc = child_process.spawn procName, optArray 11 | unless silent 12 | proc.stdout.on 'data', (data) -> process.stdout.write data 13 | proc.stderr.on 'data', (data) -> process.stderr.write data 14 | proc 15 | 16 | visitDirectory = (directory, visitor) -> 17 | fs.readdirSync(directory).forEach (filename) -> 18 | filepath = path.join directory, filename 19 | if (fs.statSync filepath).isDirectory() 20 | return visitDirectory filepath, visitor 21 | 22 | return unless (fs.statSync filepath).isFile() 23 | visitor(filepath) 24 | 25 | task "build", "compile all coffeescript files to javascript", -> 26 | coffee = spawn "coffee", ["-c", __dirname] 27 | coffee.on 'exit', (returnCode) -> process.exit returnCode 28 | 29 | task "clean", "removes any js files which were compiled from coffeescript", -> 30 | visitDirectory __dirname, (filepath) -> 31 | return unless (path.extname filepath) == ".js" 32 | 33 | directory = path.dirname filepath 34 | 35 | # Check if there exists a corresponding .coffee file 36 | try 37 | coffeeFile = fs.statSync path.join directory, "#{path.basename filepath, ".js"}.coffee" 38 | catch _ 39 | return 40 | 41 | fs.unlinkSync filepath if coffeeFile.isFile() 42 | 43 | task "autobuild", "continually rebuild coffeescript files using coffee --watch", -> 44 | coffee = spawn "coffee", ["-cw", __dirname] 45 | 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: 3 | cake build 4 | 5 | auto: 6 | cake autobuild 7 | 8 | exclude = 9 | exclude +='*/.git*' 10 | exclude +='*/README*' 11 | exclude +='*/Cakefile' 12 | exclude +='*/Makefile' 13 | exclude +='*.coffee' 14 | 15 | zipfile = ../chromi.zip 16 | 17 | zip: 18 | $(MAKE) build 19 | -rm -v $(zipfile) 20 | cd .. && zip -r chromi chromi -x $(exclude) 21 | unzip -l $(zipfile) 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important 2 | 3 | This project has now been superseded by 4 | [chromix-too](https://github.com/smblott-github/chromix-too). If you're a new 5 | user (or an existing user who's willing to make the switch), then I suggest 6 | that you consider using chromix-too instead. 7 | 8 | Chromi 9 | ====== 10 | 11 | Chromi is a Chrome extension that *facilitates* command-line and scripted 12 | control of Chrome through Chrome's extension 13 | [API](http://developer.chrome.com/extensions/api_index.html). Chromi does not 14 | include a server or a client, so it does very little on its own. A server and 15 | client are available in the 16 | [Chromix](https://github.com/smblott-github/chromix) project. 17 | 18 | Who Might Want to Use Chromi? 19 | ----------------------------- 20 | 21 | ...anyone who wants command-line or scripted access to Chrome's extension 22 | [API](http://developer.chrome.com/extensions/api_index.html) from outside of 23 | Chrome itself. 24 | For example, Chromi allows clients to ask Chrome to load, focus or reload a 25 | tab, remove tabs, or extract Chrome's bookmarks -- all from outside of Chrome 26 | itself. 27 | 28 | Here's an example from [Chromix](https://github.com/smblott-github/chromix): 29 | ``` 30 | chromix with http://www.bbc.co.uk/news/ reload 31 | chromix with http://www.bbc.co.uk/news/ focus 32 | ``` 33 | Reload the BBC News tab, and focus it. 34 | 35 | Only the Chromi extension is included in this project. The client and server are 36 | available from the [Chromix](https://github.com/smblott-github/chromix) project. 37 | 38 | ### Security Warning ... 39 | 40 | Chromi opens a TCP socket to a server on `localhost`. Malicious software with 41 | access to that socket may gain unintended access to Chrome's extension APIs. 42 | 43 | ### New! (21/11/2012) 44 | 45 | The Chromi extension is now available on the [Chrome Web 46 | Store](https://chrome.google.com/webstore/detail/chromi/eeaebnaemaijhbdpnmfbdboenoomadbo). 47 | 48 | ### New! (4/3/2012) 49 | 50 | There's nothing new! This is just a quick note to say... 51 | 52 | Despite there having been no updates recently, this project is very much alive. 53 | The hard work is all done in `chromix`, outside of the extension itself. So 54 | there has been little (well, almost no) need to update the extension itself. 55 | 56 | Details 57 | ------- 58 | 59 | ### Approach 60 | 61 | The Chrome security model limits how extensions interact with 62 | the host operating system, and *vice versa*. This makes it difficult to 63 | control Chrome from the command line or via scripts. 64 | 65 | Chromi overcomes these limitations through the use of a web socket. 66 | Specifically, Chromi uses the following architecture: 67 | 68 | - Client `<-->` Server (`ws://localhost:7441`) `<-->` Chromi (within Chrome) 69 | (where `<-->` indicates a web socket connection). 70 | 71 | The Chromi extension connects to a web socket server on `localhost:7441`. Clients 72 | connecting to that same socket can then send messages to the extension and 73 | receive responses. 74 | 75 | Client's have access to all of the callback-based operations exported by the 76 | Chrome [API](http://developer.chrome.com/extensions/api_index.html). 77 | Event-based callbacks are *not* currently supported. 78 | 79 | ### Messages 80 | 81 | When Chromi receives a suitably-formatted message, it 82 | executes the requested Chrome API function and bounces the response back to the 83 | server (and hence also to the original client). 84 | 85 | The extension accepts text messages with four space-separated fields: 86 | 87 | 1. the literal word `chromi`, 88 | 2. an identifier (which must match the regexp `/^\d+$/`), 89 | 3. the path to a Chrome JavaScript function (such as `chrome.windows.getAll`), and 90 | 4. a URI encoded, JSON stringified list of arguments. 91 | 92 | The extension calls the indicated function with the given arguments and 93 | responds with a message of the form: 94 | 95 | 1. the literal word `Chromi` (note the capital "C", this time), 96 | 2. the identifier provided with the original request, 97 | 3. the literal word `done` (or `error`, in the event of failure), and 98 | 4. a URI encoded, JSON stringified list of results from the function's invocation. 99 | 100 | Chromi is a work in progress: so that's the extent of the documentation for the 101 | moment. Except for the following examples, ... 102 | 103 | ### Examples 104 | 105 | #### Client to Server 106 | 107 | Here's an example of an on-the-wire client request: 108 | ``` 109 | chromi 137294406 chrome.tabs.update %5B86%2C%7B%22selected%22%3Atrue%7D%5D 110 | ``` 111 | which, when URI decoded, reads: 112 | ``` 113 | chromi 137294406 chrome.tabs.update [86,{"selected":true}] 114 | ``` 115 | The client is requesting that Chrome focus tab number `86`. It may have 116 | learned this tab identifier via an earlier call to 117 | `chrome.windows.getAll`. 118 | 119 | Notice that 120 | [`chrome.tabs.update`](http://developer.chrome.com/extensions/tabs.html#method-update) 121 | accepts three arguments: `tabId`, `updateProperties` and `callback`. In this 122 | example, just the first two have been provided. Chromi itself provides the 123 | callback, and that callback arranges to broadcast the response. 124 | 125 | This is the general approach to using Chromi: the caller *must provide all 126 | arguments up to just before the callback*, and Chromi 127 | adds the callback. 128 | 129 | #### Server to Client 130 | 131 | The corresponding response from the extension is: 132 | ``` 133 | Chromi 137294406 done %5B%7B%22active%22%3Atrue%2C%22favIconUrl%22%3A%22http%3A%2F%2Fwww.met.ie%2Ffavicon.ico%22%2C%22highlighted%22%3Atrue%2C%22id%22%3A86%2C%22incognito%22%3Afalse%2C%22index%22%3A2%2C%22pinned%22%3Afalse%2C%22selected%22%3Atrue%2C%22status%22%3A%22complete%22%2C%22title%22%3A%22Rainfall%20Radar%20-%20Met%20%C3%89ireann%20-%20The%20Irish%20Meteorological%20Service%20Online%22%2C%22url%22%3A%22http%3A%2F%2Fwww.met.ie%2Flatest%2Frainfall_radar-old.asp%22%2C%22windowId%22%3A1%7D%5D 134 | 135 | ``` 136 | which, when URI decoded, is: 137 | ``` 138 | Chromi 137294406 done [{"active":true,"favIconUrl":"http://www.met.ie/favicon.ico","highlighted":true,"id":86,"incognito":false,"index":2,"pinned":false,"selected":true,"status":"complete","title":"Rainfall Radar - Met Éireann - The Irish Meteorological Service Online","url":"http://www.met.ie/latest/rainfall_radar-old.asp","windowId":1}] 139 | ``` 140 | Here, the request succeeded and returned a snapshot of the [tab's 141 | state](http://developer.chrome.com/extensions/tabs.html#type-Tab). 142 | This is the data passed by `chrome.tabs.update` to its callback. 143 | 144 | Dependencies and Installation 145 | ----------------------------- 146 | 147 | The Chromi extension is available on the [Chrome Web 148 | Store](https://chrome.google.com/webstore/detail/chromi/eeaebnaemaijhbdpnmfbdboenoomadbo). 149 | 150 | Alternatively, the extension can be 151 | [downloaded](https://github.com/smblott-github/chromi/downloads) and installed 152 | as an unpacked extension directly from the project folder (see "Load unpacked 153 | extension..." on Chrome's "Extensions" page). It may be necessary to enable 154 | "Developer mode" in Chrome. 155 | 156 | The dependencies for building Chromi include, but may not be limited to: 157 | 158 | - [Node.js](http://nodejs.org/) 159 | (Install with your favourite package manager, perhaps something like `sudo apt-get install node`.) 160 | 161 | - [CoffeeScript](http://coffeescript.org/) 162 | (Install with something like `npm install coffee-script`.) 163 | 164 | Run `cake build` in the project's root folder. This compiles the CoffeeScript 165 | source to JavaScript. 166 | 167 | `cake` is installed by `npm` as part of the `coffee-script` package. Depending 168 | on how the install is handled, you may have to search for where `npm` has 169 | installed `cake`. 170 | 171 | Notes 172 | ----- 173 | 174 | If it cannot connect to the server or if a connection fails, then Chromi 175 | attempts to reconnect once every five seconds. 176 | 177 | ### TODO: 178 | 179 | 1. Allow the TCP port number to be configured via an options page. 180 | 2. Is there a reasonable approach to securing communications? 181 | 3. Support callbacks on Chrome events. 182 | -------------------------------------------------------------------------------- /extension/chromi.coffee: -------------------------------------------------------------------------------- 1 | 2 | # ##################################################################### 3 | # Configurables ... 4 | 5 | config = 6 | host: "localhost" # For URI of server. 7 | port: "7441" # For URI of server. 8 | path: "" # For URI of server. 9 | beat: 5000 # Heartbeat frequency in milliseconds. 10 | # Also, recovery interval in event of dropped socket. 11 | 12 | # ##################################################################### 13 | # Constants and utilities. 14 | 15 | # Messages from client to server begin with `chromi`, those from server to client with `chromiCap`. 16 | chromi = "chromi" 17 | chromiCap = "Chromi" 18 | 19 | echo = (msg) -> console.log msg 20 | idRegExp = new RegExp "^\\d+$" 21 | validId = (id) -> id and idRegExp.test id 22 | 23 | # ##################################################################### 24 | # Socket response class. 25 | 26 | class Respond 27 | constructor: (sock) -> @sock = sock 28 | 29 | done: (id, msg) -> @send "done", id, msg 30 | info: (id, msg) -> @send "info", id, msg 31 | error: (id, msg) -> @send "error", id, msg 32 | 33 | send: (type, id, msg) -> 34 | id = "?" unless id 35 | if @send 36 | @sock.send [ chromiCap, id, "#{type}" ].concat(msg).map(encodeURIComponent).join " " 37 | else 38 | echo "#{chromi} error: sending without a socket?" 39 | 40 | # ##################################################################### 41 | # Ping handler. 42 | 43 | window.ping = (callback) -> callback "pong" 44 | 45 | # ##################################################################### 46 | # Request handler. 47 | 48 | requestHandler = (respond, id, msg) -> 49 | # Looks like a valid request? 50 | unless msg.length == 2 and msg[0] and msg[1] 51 | return respond.error id, "invalid request:".split(/\s/).concat msg 52 | 53 | # Parse. 54 | [ method, json ] = msg 55 | 56 | # Locate function: follow path from `window`. 57 | self = func = window 58 | for term in method.split "." 59 | self = func 60 | func = func?[term] if term 61 | 62 | # Do we have a function? 63 | unless func and typeof(func) is "function" 64 | return respond.error id, "could not find function".split(/\s/).concat [method] 65 | 66 | # Parse JSON/argument. 67 | try 68 | args = JSON.parse json 69 | catch error 70 | return respond.error id, "JSON parse".split(/\s/).concat [json] 71 | 72 | # Add callback and call function. 73 | try 74 | # Add callback. 75 | args.push (stuff...) -> respond.done id, [ JSON.stringify stuff ] 76 | func.apply self, args 77 | catch error 78 | error = JSON.stringify error 79 | return respond.error id, "call".split(/\s/).concat [method, json, error] 80 | 81 | # ##################################################################### 82 | # The web socket. 83 | 84 | serverCount = 0 85 | 86 | class WebsocketWrapper 87 | count: 0 88 | 89 | constructor: -> 90 | echo "#{chromiCap} starting #{++serverCount}" 91 | 92 | unless "WebSocket" of window 93 | echo "WebSocket not available: exiting" 94 | return 95 | 96 | unless @sock = new WebSocket "ws://#{config.host}:#{config.port}/" 97 | echo "Could not create WebSocket: exiting" 98 | return 99 | 100 | # Handler: Open. 101 | @sock.onopen = 102 | => 103 | echo " connected" 104 | @respond = new Respond @sock 105 | @respond.info "", [ "connected" ] 106 | @interval = setInterval ( => @respond.info "", [ "heartbeat", ++@count ] ), config.beat 107 | 108 | # Handler: Message. 109 | @sock.onmessage = 110 | (event) => 111 | msg = event.data.split(/\s+/).map decodeURIComponent 112 | [ signal, id ] = msg.splice(0,2) 113 | return requestHandler @respond, id, msg if signal == chromi and validId id 114 | 115 | # Handlers: Errors/close. 116 | @sock.onerror = => @sock.close() 117 | @sock.onclose = => @close() 118 | 119 | # Clean up and, after a brief interval, attempt to reconnect. 120 | close: -> 121 | clearInterval @interval if @interval 122 | [ "interval", "respond", "sock" ].forEach (attribute) => delete @[attribute] 123 | setTimeout ( -> ws = new WebsocketWrapper() ), config.beat 124 | 125 | # ##################################################################### 126 | # Start ... 127 | 128 | ws = new WebsocketWrapper() 129 | 130 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "name": "Chromi", 4 | "version": "1.0.2", 5 | "manifest_version": 2, 6 | "description": "Facilitate command-line and scripted access to chrome's extension API.", 7 | 8 | "permissions": [ 9 | "tabs", 10 | "bookmarks", 11 | "" 12 | ], 13 | 14 | "background": { "scripts": [ "extension/chromi.js" ] } 15 | } 16 | 17 | --------------------------------------------------------------------------------