├── README.md ├── data ├── configs.html ├── configs.js ├── warning.html └── warning.js ├── doc ├── main.md ├── provider_sample.html └── service_commands.md ├── fx_pnh.xpi ├── lib ├── commands.js ├── config.js ├── jsonpath.js ├── main.js ├── mitm.js ├── proxy.js ├── secutils.js ├── servicestub.js └── substitutions.js ├── package.json └── test └── test-main.js /README.md: -------------------------------------------------------------------------------- 1 | FxPnH 2 | ===== 3 | 4 | Introduction 5 | ------------ 6 | 7 | FxPnH is a Firefox addon which makes it possible to use Firefox with 8 | Plug-n-Hack providers. 9 | 10 | Instructions 11 | ------------ 12 | 1. Install the addon. I'll put it up on AMO soon but you'll always be able 13 | to get the latest version 14 | [here](https://github.com/mozmark/ringleader/blob/master/fx_pnh.xpi). 15 | 2. Browse to your tool's configuration page. If you have ZAP with the 16 | Plug-n-Hack extension, you can do that by browsing to 17 | . You can set up any number of configurations 18 | for different tools and switch between them. 19 | 3. Use your PnH provider. 20 | 21 | You can switch between configurations (or clear them completely) using the 22 | *pnh config* command. 23 | 24 | Should you wish to revert your intercepting proxy configuration, you can do 25 | this with the *pnh config clear* command. *png config remove* allows you to 26 | remove a configuration completely. 27 | 28 | Integrating your tools: 29 | ------------------------ 30 | I've designed this to be easy to integrate support in other tools; documentation is in progress [here](https://github.com/mozmark/ringleader/blob/master/doc/main.md). You can also look to see how the Plug-n-Hack ZAP addon works. 31 | -------------------------------------------------------------------------------- /data/configs.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

9 |

Configurations

10 | Configure Plug-n-Hack Configurations 11 |

12 |

13 | Available configs 14 |

15 |
None
16 |
17 |

18 |

19 | 20 |

21 | 22 | 23 | -------------------------------------------------------------------------------- /data/configs.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | var gConfig = ""; 8 | 9 | var cancelClicked = function() { 10 | self.port.emit("cancel","Nope!"); 11 | }; 12 | 13 | var chooseConfig = function(evt) { 14 | gConfig = evt.target.value; 15 | } 16 | 17 | self.port.on("choose", function(evt){ 18 | var names_div = document.getElementById("config_names"); 19 | // names_div.textContent = JSON.stringify(evt); 20 | for (var name of evt.names) { 21 | // TODO: check the item that's the current configuration 22 | var input = document.createElement("input"); 23 | input.type = "radio"; 24 | input.value = name; 25 | input.name = "choices"; 26 | input.addEventListener("click", chooseConfig, false); 27 | var config = document.createElement("div"); 28 | var nameNode = document.createTextNode(name); 29 | config.appendChild(input); 30 | config.appendChild(nameNode); 31 | names_div.appendChild(config); 32 | } 33 | 34 | var noConfig = document.getElementById("noChoice"); 35 | noConfig.addEventListener("click", chooseConfig, false); 36 | 37 | var btn = document.getElementById("apply"); 38 | btn.addEventListener("click",function(evt) { 39 | self.port.emit("select", gConfig); 40 | }, false); 41 | }); 42 | -------------------------------------------------------------------------------- /data/warning.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 |

9 |

Warning

10 | Would you like to enable this site as a Plug-n-Hack provider? 11 |

12 |

13 | If you have existing configurations, you can manage those here: 14 | 15 |

16 |

17 | Plug-n-hack providers (e.g. man-in-the-middle proxies) can intercept and 18 | modify all traffic to and from your browser while activated; Do not 19 | enable unless you completely understand what you are doing. 20 |

21 |

22 |

I understand
23 |

24 |

25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/warning.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | var cancelClicked = function() { 8 | self.port.emit('cancel','Nope!'); 9 | }; 10 | 11 | var confirmClicked = function() { 12 | if (document.getElementById('yup').checked) { 13 | self.port.emit('confirm','ok'); 14 | } 15 | }; 16 | 17 | var checkboxClicked = function(event) { 18 | document.getElementById('confirm').disabled = !event.target.checked; 19 | }; 20 | 21 | var manageClicked = function() { 22 | self.port.emit('manage','manage'); 23 | }; 24 | 25 | document.getElementById('confirm').addEventListener('click', confirmClicked, false); 26 | document.getElementById('cancel').addEventListener('click', cancelClicked, false); 27 | document.getElementById('yup').addEventListener('click', checkboxClicked, false); 28 | document.getElementById('manage').addEventListener('click', manageClicked, false); 29 | -------------------------------------------------------------------------------- /doc/main.md: -------------------------------------------------------------------------------- 1 | Creating a Provider 2 | ------------------- 3 | 4 | Providers must make the following available to allow configuration: 5 | * A web page, similar to the [provider sample](https://github.com/mozmark/Mitm-Tool/blob/master/doc/provider_sample.html), which: 6 | * Tells the user about the tool 7 | * Fires a user-initiated ConfigureSecProxy event (e.g. on a button click) containing the URL of the tool manifest (see below) 8 | * Listens for ConfigureSecProxyStarted, ConfigureSecProxyFailed, ConfigureSecProxyActivated and ConfigureSecProxySucceeded events and notifies the user appropriately. 9 | * A tool manifest, sample to follow, which provides information about the tool (what features are supported, the tool name, etc). 10 | 11 | Supported Features 12 | ------------------ 13 | 14 | Three security tool features are currently supported: 15 | 16 | 1. Proxies 17 | 2. Configuration of a Certificate Authority (e.g. for intercepting proxies) 18 | 3. Command registration - tools can provide descriptors ([see documentation](service_commands.md)) which allow REST APIs to be invoked from the browser 19 | 20 | Using the addon 21 | --------------- 22 | 23 | The addon currently targets Firefox 24; it can be installed and run in earlier versions though some commands may not work properly thanks to some GCLI issues which have been resolved in Fx24. 24 | 25 | Obviously, it's possible to just [download the XPI](https://github.com/mozmark/ringleader/raw/master/fx_pnh.xpi) and run this, though I currently recommend you run this in it's own profile as this isn't (yet) production quality. 26 | 27 | If you're working on integrating a tool, you'll probably find it most useful to run this using the [Add-on SDK](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/index.html) for two reasons: Firstly, addons can write information to the console if invoked using the sdk. Secondly, you can automatically set some prefs that are useful when you're testing with descriptors or content served from a different origin to the API endpoints provided by your tool (by default, PnH expects tools to serve descriptors, etc. from the same origin that's used for the API). 28 | 29 | Your command to run the tool might look like this: 30 | 31 | ``` 32 | cfx run -b /path/to/nightly/firefox --binary-args http://localhost:3000/static/config --static-args="{ \"prefs\": { \"pnh.check.origin\": \"off\" } }" 33 | ``` 34 | -------------------------------------------------------------------------------- /doc/provider_sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | TST Installer UI 4 | 5 | 6 |

Welcome to the Test Proxy (TST)

7 |

8 | Blah blah blah blah blah. Blah blah blah blah blah. Blah blah blah blah blah. 9 | Blah blah blah blah blah. Blah blah blah blah blah. 10 |

11 |

12 | Please be aware that you should only attack applications that you have been 13 | specifically been given permission to test. 14 |

15 |
16 | 20 | 23 | 26 | 29 | 32 |
33 |

34 | 35 |

36 |
37 | 38 | 107 | 108 | -------------------------------------------------------------------------------- /doc/service_commands.md: -------------------------------------------------------------------------------- 1 | Service Commands 2 | ================ 3 | 4 | Introduction 5 | ------------ 6 | Service Commands are a way of creating commands (e.g. GCLI commands) from a web service. 7 | 8 | Commands are defined in a JSON descriptor (which looks a little like a GCLI [command definition](https://github.com/mozilla/gcli/blob/master/docs/writing-commands.md)) 9 | 10 | Restrictions: 11 | ------------- 12 | 13 | PnH will not, by default, allow descriptors (or the services they describe) to reside on different origins. If you need to relax this requirement (e.g. for testing) you can set the pnh.check.origin preference in firefox. Set the pref to 'noport' if you wish to relax the port check or 'off' if you want to turn off origin checks for descriptors and services completely. Please do not require this pref to be set for your service to work; it is for testing only. 14 | 15 | If you want to test with this preference set for you, you can run the addon with cfx from the addon-sdk with the following option: 16 | 17 | ``` 18 | --static-args="{\"prefs\":{\"pnh.check.origin\": \"off\"}}" 19 | ``` 20 | 21 | 22 | The Descriptor: 23 | --------------- 24 | The descriptor is a JSON document containing a list of commands: 25 | ```json 26 | { 27 | "commands":[] 28 | } 29 | ``` 30 | 31 | A command is created for each item in the list. The first command should be empty with the exception of a description: This gives your users information on what your tool does. e.g: 32 | 33 | if you load a command with the prefix 'test' and the following descriptor: 34 | 35 | ```json 36 | { 37 | "commands":[{"description":"this is an example"}] 38 | } 39 | ``` 40 | 41 | then typing 'test' at the command line will give you "this is an example command". 42 | 43 | You probably want commands to be a bit more interesting than this, though. Here's a slightly more interesting example: 44 | 45 | ```json 46 | { 47 | "commands":[ 48 | {"description":"this is an example command for use as an example"}, 49 | { 50 | "name": "command", 51 | "description": "do something", 52 | "returnType": "string", 53 | "execAction": { 54 | "url": "http://localhost:3000/do/something" 55 | } 56 | } 57 | ] 58 | } 59 | ``` 60 | 61 | In this case, we have a sub-command called 'command' that the user can invoke with 'test command'. The command, when executed, results in a (GET) request being made to the url specified in execAction. 62 | 63 | Note: You will need to leave the 'empty' command descriptor (with just the description) at the top; this is to let the command line know what your tool's commands are for. 64 | 65 | This still isn't very interesting, though. What if we want to be able to supply a parameter? And what if we want to actually see something from the response? Let's continue by looking at a real world example; a command to create a new session in the ZAP intercepting proxy: 66 | 67 | ```json 68 | { 69 | "name": "newsession", 70 | "description": "create a new session", 71 | "returnType": "string", 72 | "params": [{ 73 | "name": "name", 74 | "type": "string", 75 | "description": "the name of the new session to create" 76 | }], 77 | "execAction":{ 78 | "url":"http://localhost:8080/JSON/core/action/newSession/?zapapiformat=JSON&name=${$.args.name}", 79 | "expression":"$.Result" 80 | } 81 | } 82 | ``` 83 | 84 | The first thing to notice here is that we are able to specify parameters. Here we have a single parameter called 'name'. String parameters can have any value but it's possible to limit the possible values (and even have default). This will be covered later on. 85 | 86 | The second is that we're using the parameter in the url of the execAction - notice '${$.args.name}' on the end of the URL? This a JSONPath expression which will be evaluated against the command's data, the result of which will be substituted with the value the user enters as a command parameter. All you have to know about this for the time being is that $.args.PARAM gets the value of the 'PARAM' parameter. 87 | 88 | Finally, notice "expression" there in execAction - you can specify a JSONPath expression (the tool supports a safe subset of JSONPath) to extract data from the response to give to the user (as the output for the command). 89 | 90 | execAction also allows you to specify the request method, the request body and the content type for the request body. E.g: 91 | 92 | ```json 93 | { 94 | "name":"someCommand", 95 | "description":"a test command", 96 | "returnType":"string", 97 | "params":[ 98 | { 99 | "name":"p1", 100 | "type": "string", 101 | "description":"parameter one" 102 | } 103 | ], 104 | "execAction":{ 105 | "url":"http://example.com/doSomething", 106 | "method":"POST", 107 | "requestBody":"arg1=${$.args.p1}", 108 | "contentType":"application/x-www-form-urlencoded", 109 | "expression":"$.some.thing.from.response", 110 | "callbackData":{"foo":"bar","wibble":{"type":"expression","expression":"$.response.Result","extract":true}} 111 | } 112 | } 113 | ``` 114 | 115 | In this example we're mimicking a form POST to http://example.com/doSomething 116 | 117 | More on Parameters: 118 | ------------------- 119 | 120 | You can limit the possible values for a parameter by using providing an object (rather than 'string') to as the type. For example: 121 | 122 | ```json 123 | { 124 | "name": "param1", 125 | "type": { 126 | "name": "selection", 127 | "data": ["name1", "name2", "name3"] 128 | }, 129 | "description": "you may only name it name1, name2 or name3", 130 | "defaultValue": "name2" 131 | } 132 | ``` 133 | Here we have a parameter called param1 which can take the values name1, name2 or name3 - if the user does not specify a value it will default to name2. 134 | 135 | It's possible to obtain the list of possible parameter values from JSON data provided by a server: 136 | 137 | ```json 138 | { 139 | "name": "param1", 140 | "type": { 141 | "name": "selection", 142 | "dataAction":{ 143 | "url":"http://example.com/some/url/to/get/params", 144 | "expression":"$.params[*]" 145 | } 146 | }, 147 | "description": "possible values come from the server", 148 | } 149 | ``` 150 | 151 | You can make use of the values in previous parameters in the URL for getting subsequent ones, e.g.: 152 | 153 | ```json 154 | [{ 155 | "name": "param1", 156 | "type": { 157 | "name": "selection", 158 | "dataAction":{ 159 | "url":"http://example.com/some/url/to/get/params", 160 | "expression":"$.params[*]" 161 | } 162 | }, 163 | "description": "possible values come from the server", 164 | }, 165 | { 166 | "name": "param2", 167 | "type": { 168 | "name": "selection", 169 | "dataAction":{ 170 | "url":"http://example.com/some/other/url/to/get/params?param1was=${$.args.param1}", 171 | "expression":"$.params[*]" 172 | } 173 | }, 174 | "description": "more possible values come from the server", 175 | }] 176 | ``` 177 | 178 | Data in Commands: 179 | ----------------- 180 | 181 | In most places where it's possible to extract data for command actions (see Tool Operations) or substitution into an URL (e.g. command execAction URLs) you can provide some kind of expression (see use of $.args.name above). This expression is evaluated against a command object. Available attributes depend on the point at which the expression is evaluated. In particular: 182 | * args will not be available until the command is issued 183 | * response will not be available until the command is issues and execAction has succeeded 184 | 185 | Contextual data in Commands: 186 | ---------------------------- 187 | 188 | Sometimes you might want to make use of certain contextual data in your commands. In these cases, it will be possible to get certain contextual data. For example, the 'tab' attribute of the command data object contains information pertaining to the current tab, with 'tab.key' being an identifier that is unique for a tab. This can be useful when, for instance, you want to identify a specific tab in your service. 189 | 190 | Consider the following command: 191 | 192 | ``` 193 | { 194 | "name":"something", 195 | "description":"set something", 196 | "returnType":"string", 197 | "params":[ 198 | { 199 | "name":"state", 200 | "type": {"name":"selection", "data":["on","off"]}, 201 | "description": "should something be enabled?", 202 | "defaultValue": "on" 203 | } 204 | ], 205 | "execAction":{ 206 | "url":"http://localhost/something?tab=${$.tab.key}&state=${$.args.state}" 207 | } 208 | } 209 | ``` 210 | 211 | Here the tab request parameter will be unique per tab when the command is issued by the user. 212 | 213 | Other contextual data include: 214 | * tab.URL - the URL of the top level document in the current tab represented as a string 215 | * tab.location - the location object pertaining to the current top level document in the tab (useful for getting protocol, port and host information, for example) 216 | 217 | Tool operations: 218 | ---------------- 219 | 220 | The Service Commands functionality used by this tool supports [callback data](https://github.com/mozmark/ServiceTest/blob/master/doc/service_commands.md#callback-data) being sent back to the embedding tool. PNH makes use of this by providing operations that can be invoked from your commands via callbackData. 221 | 222 | This is best illustrated with an example: 223 | 224 | ```json 225 | { 226 | "name": "brk", 227 | "description": "create a new session", 228 | "returnType": "string", 229 | "params": [{ 230 | "name": "state", 231 | "type": { 232 | "name": "selection", 233 | "data": ["on", "off"] 234 | }, 235 | "description": "break on request", 236 | "defaultValue": "on" 237 | }, { 238 | "name": "scope", 239 | "type": { 240 | "name": "selection", 241 | "data": ["tab", "global"] 242 | }, 243 | "description": "local to tab or global", 244 | "defaultValue": "tab" 245 | }], 246 | "execAction": { 247 | "expression": "$.Result", 248 | "callbackData": { 249 | "conditionalCommands": { 250 | "expression": "$.args.state", 251 | "states": { 252 | "on": [{ 253 | "command": "addToHeader", 254 | "params": { 255 | "headerName": "X-Security-Proxy", 256 | "value": "intercept", 257 | "scope": { 258 | "type": "expression", 259 | "expression": "$.args.scope", 260 | "extract": true 261 | } 262 | } 263 | }], 264 | "off": [{ 265 | "command": "removeFromHeader", 266 | "params": { 267 | "headerName": "X-Security-Proxy", 268 | "value": "intercept", 269 | "scope": { 270 | "type": "expression", 271 | "expression": "$.args.scope", 272 | "extract": true 273 | } 274 | } 275 | }] 276 | } 277 | } 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | This is a description for the 'brk' command which causes ZAP (with the mitm-config addon) to break on request / response. callbackData has an attribute called "conditionalCommands" which specifies an expression and a map of states to lists of commands. If the result the expression matches a state, the associated commands will be invoked. 284 | 285 | At present only the 'addToHeader' and 'removeFromHeader' commands are supported. This list will be expanded in time. 286 | 287 | Types: 288 | ------ 289 | 290 | PnH supports the substitution of certain objects with data extracted from another. This can be useful when data is expected in a particular format but a service has already been defined to return something else. 291 | 292 | At present, 2 types are supported; expression types and template types: 293 | 294 | ```json 295 | { 296 | "someThing":{"type":"expression","expression":"$.tab.key","extract":true}, 297 | "someOtherThing":{"type":"template","template":"key=${$.tab.key}"} 298 | } 299 | ``` 300 | 301 | In the above example, someThing is set the the result of evaluating the expression '$.tab.key' expression on the command's data; if the tab key is 4, the resulting value would be 4. someOtherThing is similar, the the result is substituted into the template string; e.g. 'key=4'. 302 | -------------------------------------------------------------------------------- /fx_pnh.xpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozmark/ringleader/1e8171d186341c5cc2bb64d23865925db437a258/fx_pnh.xpi -------------------------------------------------------------------------------- /lib/commands.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const {Cu} = require("chrome"); 8 | 9 | var gcli = undefined; 10 | 11 | try { 12 | Cu.import("resource:///modules/devtools/gcli.jsm"); 13 | } catch (e) { 14 | let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 15 | gcli = devtools["req" + "uire"]("gcli/index"); 16 | } 17 | 18 | const {MITM} = require("./mitm"); 19 | const {Utils} = require("./secutils"); 20 | const {configManager} = require("./config"); 21 | 22 | const EVENT_CONFIG_CHANGED = "PnHConfigChanged"; 23 | 24 | let commands = [ 25 | /** 26 | * 'pnh' command. 27 | */ 28 | { 29 | name: "pnh", 30 | description: 'Commands for interacting with a Plug-n-Hack provider (e.g. OWASP ZAP)' 31 | }, 32 | 33 | /** 34 | * 'pnh config' command 35 | */ 36 | { 37 | name: 'pnh config', 38 | description: 'pnh configuration operations', 39 | }, 40 | 41 | /** 42 | * 'pnh config clear' command 43 | * clear the current config. 44 | */ 45 | { 46 | name: 'pnh config clear', 47 | description: 'clear the current pnh config', 48 | params: [ 49 | { 50 | name: 'config', 51 | type: { name: 'selection', data: configManager.currentConfigs}, 52 | description: 'the config to clear' 53 | } 54 | ], 55 | returnType: 'string', 56 | exec: function(args, context) { 57 | try { 58 | configManager.clear(false, args.config); 59 | return 'ok'; 60 | }catch (e) { 61 | return e.message; 62 | } 63 | } 64 | }, 65 | 66 | /** 67 | * 'pnh config list' command 68 | * list the available configs. 69 | */ 70 | { 71 | name: 'pnh config list', 72 | description: 'list pnh configs', 73 | params: [], 74 | returnType: 'string', 75 | exec: function(args, context) { 76 | return configManager.list().join(', '); 77 | } 78 | }, 79 | 80 | /** 81 | * 'pnh config apply' command 82 | * Apply a pnh config. 83 | */ 84 | { 85 | name: 'pnh config apply', 86 | description: 'apply a pnh config', 87 | params: [ 88 | { 89 | name: 'config', 90 | type: { name: 'selection', data: configManager.list }, 91 | description: 'the config to use' 92 | } 93 | ], 94 | returnType: 'string', 95 | exec: function(args, content) { 96 | try { 97 | configManager.applyConfig(args.config); 98 | return 'ok' 99 | } catch(e) { 100 | // TODO: if it's not a pnh Error give a stack / rethrow 101 | return e.message; 102 | } 103 | } 104 | }, 105 | 106 | /** 107 | * 'pnh config remove' command 108 | * Remove the specified pnh config. 109 | */ 110 | { 111 | name: 'pnh config remove', 112 | description: 'remove a pnh config', 113 | params: [ 114 | { 115 | name: 'config', 116 | type: { name: 'selection', data: configManager.list }, 117 | description: 'the config to remove' 118 | } 119 | ], 120 | returnType: 'string', 121 | exec: function(args, content) { 122 | try { 123 | configManager.deleteConfig(args.config); 124 | return 'ok'; 125 | } catch (e) { 126 | // TODO: if it's not a pnh Error give a stack / rethrow 127 | return e.message; 128 | } 129 | } 130 | }, 131 | 132 | /** 133 | * 'pnh config show' command 134 | * Show the current pnh config. 135 | */ 136 | { 137 | name: 'pnh config show', 138 | description: 'show the current config', 139 | params: [], 140 | returnType: 'string', 141 | exec: function(args, content) { 142 | try { 143 | let configs = configManager.currentConfigs(); 144 | var names = []; 145 | for (let config in configs){ 146 | names.push(configs[config]); 147 | } 148 | if (configs.length > 0) { 149 | return 'current configs are "'+JSON.stringify(names)+'"'; 150 | } 151 | return 'there is no config currently applied'; 152 | } catch (e) { 153 | // TODO: if it's not a pnh Error give a stack / rethrow 154 | return e.message; 155 | } 156 | } 157 | } 158 | ]; 159 | 160 | /** 161 | * Refresh the current commands according to the current config. 162 | * remove, boolean - should conditional commands be removed prior to others 163 | * being added? Mostly useful at initial setup when conditional commands aren't 164 | * already there. 165 | * config - The current configuration. 166 | */ 167 | function refreshCommands(remove, config) { 168 | for(idx in commands) { 169 | let command = commands[idx]; 170 | if (command.conditional) { 171 | if (remove) { 172 | // GCLI api changes some time around FX30. Sucks but we must: 173 | if (gcli.removeCommand) { 174 | gcli.removeCommand(command.name); 175 | } else { 176 | gcli.removeItems([command]); 177 | } 178 | } 179 | if (command.conditional(config)) { 180 | if (gcli.addCommand) { 181 | gcli.addCommand(command); 182 | } else { 183 | gcli.addItems([command]); 184 | } 185 | } 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Install the commands. 192 | */ 193 | function installCommands() { 194 | for(idx in commands) { 195 | if (!commands[idx].conditional) { 196 | if (gcli.addCommand) { 197 | // TODO: we could extract list items and dump into addItems? 198 | gcli.addCommand(commands[idx]); 199 | } else { 200 | gcli.addItems([commands[idx]]); 201 | } 202 | } 203 | } 204 | // TODO: Get a current config in here. 205 | refreshCommands(false); 206 | } 207 | 208 | configManager.on(EVENT_CONFIG_CHANGED, function(config) { 209 | refreshCommands(true,config); 210 | }); 211 | 212 | exports.installCommands = installCommands; 213 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const {Cc, Ci} = require("chrome"); 8 | const base64 = require("sdk/base64"); 9 | const data = require("sdk/self").data; 10 | const events = require("sdk/system/events"); 11 | const promise = require('sdk/core/promise'); 12 | const {readURI} = require("sdk/net/url"); 13 | const {XMLHttpRequest} = require("sdk/net/xhr"); 14 | const storage = require("sdk/simple-storage"); 15 | const { emit } = require("sdk/event/core"); 16 | const { EventTarget } = require("sdk/event/target"); 17 | const { Class } = require("sdk/core/heritage"); 18 | const { merge } = require("sdk/util/object"); 19 | 20 | const nsX509CertDB = "@mozilla.org/security/x509certdb;1"; 21 | const nsIX509Cert = Ci.nsIX509Cert; 22 | const nsIX509CertDB = Ci.nsIX509CertDB; 23 | const certdb = Cc[nsX509CertDB].getService(nsIX509CertDB); 24 | const wMediator = Cc["@mozilla.org/appshell/window-mediator;1"] 25 | .getService(Ci.nsIWindowMediator); 26 | 27 | const {ProxyConfig, ProxyManager, HostPort} = require("./proxy"); 28 | const {ServiceStub} = require("./servicestub"); 29 | const {MITM} = require("./mitm"); 30 | const {Utils} = require("./secutils"); 31 | 32 | const EVENT_ACTIVATED = "ConfigureSecToolActivated"; 33 | const EVENT_STARTED = "ConfigureSecToolStarted"; 34 | const EVENT_SUCCEEDED = "ConfigureSecToolSucceeded"; 35 | const EVENT_FAILED = "ConfigureSecToolFailed"; 36 | const EVENT_CONFIGURE = "ConfigureSecTool"; 37 | const EVENT_CONFIG_CHANGED = "PnHConfigChanged"; 38 | 39 | const ERROR_ALREADY_CONFIGURED = "A provider with this name has already been configured."; 40 | 41 | const SUPPORTED_PROTOCOL = 1.0; 42 | 43 | /** 44 | * ConfigManager - manage security tool configurations. 45 | */ 46 | var ConfigManager = Class({ 47 | extends: EventTarget, 48 | initialize: function initialize(options) { 49 | EventTarget.prototype.initialize.call(this, options); 50 | merge(this, options); 51 | if (!storage.storage.configs) { 52 | storage.storage.configs = {}; 53 | } 54 | if (!storage.storage.currentConfigs) { 55 | storage.storage.currentConfigs = []; 56 | } 57 | }, 58 | 59 | /** 60 | * List the available configurations. 61 | * returns: a list of strings of configuration names. 62 | */ 63 | list : function list() { 64 | return Object.keys(storage.storage.configs); 65 | }, 66 | 67 | /** 68 | * Get the current config names. 69 | * returns: an array of strings of the current config names. 70 | */ 71 | currentConfigs: function currentConfigs() { 72 | return storage.storage.currentConfigs; 73 | }, 74 | 75 | /** 76 | * Clear the applied config: 77 | * suppress - should EVENT_CONFIG_CHANGED be suppressed for this? 78 | * configuration change. 79 | */ 80 | clear : function clear(suppress, configName) { 81 | let configs = storage.storage.currentConfigs; 82 | 83 | names = []; 84 | 85 | if (configName) { 86 | names.push(configName); 87 | } else { 88 | names = names.concat(storage.storage.currentConfigs); 89 | } 90 | 91 | console.log("names are "+JSON.stringify(names)); 92 | 93 | for (name of names) { 94 | if ((name && configs.indexOf(name) >= 0) || !name) { 95 | // TODO: deal with clear with no name 96 | let config = this.fetchConfig(name); 97 | if (config && config.manifest && config.manifest.features['proxy']) { 98 | // get the name of the cert and the saved proxy config 99 | let proxyConfig = JSON.parse(storage.storage.originalConfig); 100 | // remove the cert 101 | if (config.manifest.features.proxy.CACert) { 102 | let cert = certdb.constructX509FromBase64(config.cert.base64); 103 | try { 104 | // TODO: work out how to chek if the cert exists prior to removal 105 | if (certdb.isCertTrusted(cert,nsIX509Cert.CA_CERT,nsIX509CertDB.TRUSTED_SSL)) { 106 | certdb.deleteCertificate(cert); 107 | } 108 | } catch (e) { 109 | console.log(e); 110 | } 111 | } 112 | // apply the original proxy config 113 | ProxyManager.applyConfig(proxyConfig); 114 | if (!suppress) { 115 | emit(this, EVENT_CONFIG_CHANGED, null); 116 | let newConfigs = []; 117 | for (let idx in storage.storage.currentConfigs) { 118 | let configName = storage.storage.currentConfigs[idx]; 119 | if (name!=configName) { 120 | newConfigs.push(configName); 121 | } 122 | } 123 | storage.storage.currentConfigs = newConfigs; 124 | } 125 | } else { 126 | let newConfigs = []; 127 | for (let idx in storage.storage.currentConfigs) { 128 | let configName = storage.storage.currentConfigs[idx]; 129 | if (name!=configName) { 130 | newConfigs.push(configName); 131 | } 132 | } 133 | storage.storage.currentConfigs = newConfigs; 134 | } 135 | } 136 | else { 137 | console.log('there was no config to remove'); 138 | throw new Error('cannot clear: there is no configuration currently applied'); 139 | } 140 | } 141 | }, 142 | 143 | /** 144 | * Save a configuration. 145 | * config - the config to save. 146 | * name - the name to save the config with. 147 | */ 148 | saveConfig : function saveConfig(config, name) { 149 | let existing = this.fetchConfig(name); 150 | if (existing) { 151 | throw new Error(ERROR_ALREADY_CONFIGURED); 152 | } else { 153 | if (storage.storage.currentConfigs.indexOf(name) >= 0) { 154 | throw new Error('cannot modify a currently used config'); 155 | } else { 156 | storage.storage.configs[name] = JSON.stringify(config); 157 | } 158 | } 159 | }, 160 | 161 | /** 162 | * Check a config is compatible with applied configs 163 | */ 164 | ensureCompatible : function(config){ 165 | if (config.manifest.features.proxy) { 166 | for (let idx in this.currentConfigs()) { 167 | let applied = this.fetchConfig(this.currentConfigs()[idx]); 168 | if (applied && applied.manifest.features.proxy) { 169 | throw new Error('You cannot apply two proxies concurrently'); 170 | } 171 | } 172 | } 173 | }, 174 | 175 | /** 176 | * Apply a configuration. 177 | * name - the name of the configuration to apply. 178 | */ 179 | applyConfig : function applyConfig(name) { 180 | //if (storage.storage.current) { 181 | // this.clear(true); 182 | //} 183 | let config = this.fetchConfig(name); 184 | if (config) { 185 | this.ensureCompatible(config); 186 | if (config.manifest && config.manifest.features && config.manifest.features.proxy) { 187 | let pac = config.manifest.features.proxy.PAC; 188 | if (pac && config.url && Utils.CheckOrigin(config.url,pac)) { 189 | // try to fetch the PAC before OKing 190 | readURI(pac).then(function(data) { 191 | // configure from the proxy supplied PAC 192 | storage.storage.originalConfig = JSON.stringify(ProxyManager.get('default')); 193 | ProxyManager.applyAutoConfig(pac); 194 | storage.storage.currentConfigs.push(name); 195 | }, function () { 196 | console.log('unable to read PAC'); 197 | throw new Error("Unable to fetch PAC; refusing the apply config."); 198 | }); 199 | } else if (pac) { 200 | console.log("Proxy PAC is off origin"); 201 | throw new Error('Proxy PAC is off origin - configuration failed'); 202 | } 203 | if (config.cert && config.cert.der && config.cert.base64) { 204 | certdb.addCert(config.cert.der,'Cu,,','NSS ignores nicknames'); 205 | } 206 | } else { 207 | storage.storage.currentConfigs.push(name); 208 | } 209 | emit(this, EVENT_CONFIG_CHANGED, config); 210 | } else { 211 | throw new Error("No manifest found."); 212 | } 213 | }, 214 | 215 | /** 216 | * Delete a configuration. 217 | * name - the name of the configuration to delete. 218 | */ 219 | deleteConfig : function deleteConfig(name) { 220 | if (storage.storage.currentConfigs.indexOf(name) >= 0) { 221 | throw new Error('cannot remove configuration: currently in use'); 222 | } else { 223 | delete storage.storage.configs[name]; 224 | } 225 | }, 226 | 227 | /** 228 | * Fetch a configuration from storage. 229 | * name - the name of the configuration to fetch. 230 | * returns: a Configuration 231 | */ 232 | fetchConfig : function fetchConfig(name) { 233 | let data = storage.storage.configs[name]; 234 | if (data) { 235 | let config = new Configuration(JSON.parse(data)); 236 | return config; 237 | } 238 | return null; 239 | }, 240 | }); 241 | 242 | var configManager = new ConfigManager(); 243 | 244 | var setup = function() { 245 | function dispatchSetupEvent(doc, kind, data){ 246 | try { 247 | let evt = doc.createEvent("CustomEvent"); 248 | evt.initCustomEvent(kind, true, false, data); 249 | doc.dispatchEvent(evt); 250 | } catch (e) { 251 | console.log("Oops!"); 252 | console.log(e); 253 | } 254 | } 255 | 256 | // register the custom event listener to allow providers to be registered: 257 | function handleSetup(event){ 258 | console.log('handling setup'); 259 | // Send EVENT_STARTED event 260 | let doc = event.originalTarget; 261 | dispatchSetupEvent(doc, EVENT_STARTED, {}); 262 | console.log('started event dispatched'); 263 | 264 | let mainWindow = wMediator.getMostRecentWindow("navigator:browser"); 265 | let domWindowUtils = mainWindow.QueryInterface(Ci.nsIInterfaceRequestor) 266 | .getInterface(Ci.nsIDOMWindowUtils); 267 | if (domWindowUtils.isHandlingUserInput) { 268 | Setup.configure(event.detail.url).then( 269 | function(configInfo) { 270 | //TODO: send EVENT_SUCCEEDED 271 | dispatchSetupEvent(doc, EVENT_SUCCEEDED, {"success":"Configuration succeeded."}); 272 | }, 273 | function(errorInfo){ 274 | //TODO: send EVENT_FAILED 275 | dispatchSetupEvent(doc, EVENT_FAILED, JSON.stringify({"failure":errorInfo.message})); 276 | }); 277 | } else { 278 | // do we want to record this somewhere - could be malicious? 279 | } 280 | } 281 | 282 | function recover(){ 283 | for (currentIndex in configManager.currentConfigs()) { 284 | let currentConfigName = configManager.currentConfigs()[currentIndex]; 285 | console.log('current config name '+currentConfigName); 286 | // attempt to load any commands 287 | var currentConfig = configManager.fetchConfig(currentConfigName); 288 | if (currentConfig && currentConfig.manifest && currentConfig.manifest.features) { 289 | var features = currentConfig.manifest.features; 290 | if (features.commands) { 291 | // TODO: Look into providing a tidier interface for a config that 292 | // includes service stub loading, etc. 293 | console.log('adding service stub'); 294 | var stub = new ServiceStub(features.commands.manifest, features.commands.prefix, MITM.callback.bind(MITM)); 295 | console.log('added'); 296 | stub.hook(); 297 | } 298 | } 299 | } 300 | } 301 | 302 | var doSetup = function () { 303 | let succeeded = false; 304 | return function () { 305 | // don't bother if we've already run 306 | if (!succeeded) { 307 | let mainWindow = wMediator.getMostRecentWindow("navigator:browser"); 308 | mainWindow.gBrowser 309 | .addEventListener(EVENT_CONFIGURE, handleSetup, true, true); 310 | // if there are profiles already applied, let's load commands, etc. 311 | recover(); 312 | succeeded = true; 313 | } 314 | }; 315 | }(); 316 | 317 | try { 318 | // try to setup - this may fail if there is no window present yet 319 | doSetup(); 320 | } catch (e) { 321 | // try setup again - once we have a content document global 322 | events.once("content-document-global-created", function(event) { 323 | doSetup(); 324 | }); 325 | } 326 | 327 | // fire an 'activated' event on the current document: 328 | let recentWindow = wMediator.getMostRecentWindow("navigator:browser"); 329 | if (recentWindow && recentWindow.content && recentWindow.content.document) { 330 | dispatchSetupEvent(recentWindow.content.document, EVENT_ACTIVATED, {}); 331 | } 332 | }; 333 | 334 | /** 335 | * A setup helper object 336 | */ 337 | var Setup = { 338 | }; 339 | 340 | Setup.userConfirm = function(url) { 341 | // return a promise with user choice. 342 | let deferred = promise.defer(); 343 | let panel = require("sdk/panel").Panel({ 344 | width: 640, 345 | height: 270, 346 | contentURL: data.url("warning.html"), 347 | contentScriptFile: data.url("warning.js") 348 | }); 349 | panel.on('hide',function(event) {deferred.reject(event);}); 350 | panel.port.on('confirm',function(event) {deferred.resolve(event);panel.hide();}); 351 | panel.port.on('cancel',function(event) {panel.hide();}); 352 | panel.port.on('manage',function(event) {deferred.reject(event);panel.hide();Setup.chooseConfig();}); 353 | // TODO: Send the URL to the UI 354 | 355 | // resolve the promise in a message handler from the script, or something 356 | panel.show(); 357 | return deferred.promise; 358 | }; 359 | 360 | Setup.chooseConfig = function() { 361 | // return a promise with selected config. 362 | let deferred = promise.defer(); 363 | let panel = require("sdk/panel").Panel({ 364 | width: 640, 365 | height: 240, 366 | contentURL: data.url("configs.html"), 367 | contentScriptFile: data.url("configs.js") 368 | }); 369 | 370 | panel.show(); 371 | configs = []; 372 | for (config of configManager.list()) { 373 | configs[configs.length] = config; 374 | } 375 | panel.port.emit("choose", {"names" : configs}); 376 | panel.port.on("select", function(name){ 377 | console.log("I got an event!"); 378 | console.log(name); 379 | // clear any currently applied configs 380 | var configs = configManager.currentConfigs(); 381 | if (configs.length > 0) { 382 | configManager.clear(); 383 | console.log("cleared"); 384 | } 385 | try { 386 | if ('none' != name) { 387 | configManager.applyConfig(name); 388 | } 389 | } catch (e) { 390 | console.log("error applying config: "+e.message); 391 | throw e; 392 | } 393 | panel.hide(); 394 | }); 395 | return deferred.promise; 396 | } 397 | 398 | Setup.configure = function(url) { 399 | return Configuration.fromURL(url).then(function(config) { 400 | if (config && config.manifest) { 401 | let name = config.manifest.toolName; 402 | 403 | if (configManager.currentConfigs().length > 0) { 404 | configManager.clear(); 405 | } 406 | // save and apply the config 407 | try { 408 | configManager.saveConfig(config, name); 409 | configManager.applyConfig(name); 410 | } catch (e) { 411 | console.log("error applying config: "+e.message); 412 | throw e; 413 | } 414 | // TODO: add config data to allow the provider to display details 415 | return 'ok'; 416 | } else { 417 | throw new Error("The manifest is not available for this tool."); 418 | } 419 | }.bind(this)); 420 | }; 421 | 422 | function Configuration(data) { 423 | if (data) { 424 | for (let key in data) { 425 | this[key] = data[key]; 426 | } 427 | } 428 | } 429 | 430 | Configuration.fromURL = function(url) { 431 | let config = new Configuration(); 432 | config.url = url; 433 | return readURI(url).then(function(data) { 434 | let manifest = JSON.parse(data); 435 | config.manifest = manifest; 436 | if (SUPPORTED_PROTOCOL >= config.manifest.protocolVersion){ 437 | // TODO: Send some info on the proxy to userConfirm 438 | return Setup.userConfirm(url).then(function (evt) { 439 | console.log("confirmed with "+JSON.stringify(evt)); 440 | if (manifest.features.commands) { 441 | let commands = manifest.features.commands; 442 | if (commands.prefix && commands.manifest) { 443 | console.log('adding service stub'); 444 | console.log(Utils.CheckOrigin(url, commands.manifest)); 445 | var stub = new ServiceStub(commands.manifest, commands.prefix, MITM.callback.bind(MITM)); 446 | console.log('added'); 447 | stub.hook(); 448 | } 449 | } 450 | if (manifest.features.proxy && manifest.features.proxy.CACert) { 451 | // fetch and install the CA cert 452 | let certURL = manifest.features.proxy.CACert; 453 | if (certURL && Utils.CheckOrigin(url, certURL)) { 454 | // fetch data from the URL. 455 | // TODO: the success of this promise shouldn't depend on an optional 456 | // attribute being read successfully. 457 | return readURI(certURL).then(function(data){ 458 | // split off the header and footer, base64 decode 459 | let b64 = data.split('-----')[2].replace(/\s/g, ''); 460 | let der = base64.decode(b64); 461 | 462 | let cert = certdb.constructX509FromBase64(b64); 463 | 464 | // import the cert with appropriate trust bits. 465 | config.cert = {"der":der,"base64":b64}; 466 | 467 | return config; 468 | }); 469 | } else { 470 | throw new Error('CA Cert is off origin. Configuration failed.'); 471 | } 472 | } else { 473 | // we want a working config even if there's no proxy 474 | return config; 475 | } 476 | }, function() { 477 | throw new Error('Setup cancelled by user.'); 478 | }); 479 | } else { 480 | throw new Error('Version mismatch: fx_pnh supports protocol version '+SUPPORTED_PROTOCOL+' but '+manifest.toolName+ ' requires '+manifest.protocolVersion); 481 | } 482 | }, function() { 483 | throw new Error('Unable to load configuration from provider.'); 484 | }); 485 | }; 486 | 487 | exports.setup = setup; 488 | exports.configManager = configManager; 489 | -------------------------------------------------------------------------------- /lib/jsonpath.js: -------------------------------------------------------------------------------- 1 | /* 2 | * JSONPath with safe mode - based on: 3 | * JSONPath 0.8.0 - XPath for JSON 4 | * 5 | * Copyright (c) 2007 Stefan Goessner (goessner.net) 6 | * Licensed under the MIT (MIT-LICENSE.txt) licence. 7 | */ 8 | function jsonPath(obj, expr, arg, unsafe) { 9 | var P = { 10 | resultType: arg && arg.resultType || "VALUE", 11 | result: [], 12 | normalize: function(expr) { 13 | var subx = []; 14 | return expr.replace(/[\['](\??\(.*?\))[\]']/g, function($0, $1) { 15 | return "[#" + (subx.push($1) - 1) + "]"; 16 | }).replace(/'?\.'?|\['?/g, ";").replace(/;;;|;;/g, ";..;").replace(/;$|'?\]|'$/g, "").replace(/#([0-9]+)/g, function($0, $1) { 17 | return subx[$1]; 18 | }); 19 | }, 20 | asPath: function(path) { 21 | var x = path.split(";"), 22 | p = "$"; 23 | for (var i = 1, n = x.length; i < n; i++) 24 | p += /^[0-9*]+$/.test(x[i]) ? ("[" + x[i] + "]") : ("['" + x[i] + "']"); 25 | return p; 26 | }, 27 | store: function(p, v) { 28 | if (p) P.result[P.result.length] = P.resultType == "PATH" ? P.asPath(p) : v; 29 | return !!p; 30 | }, 31 | trace: function(expr, val, path) { 32 | if (expr) { 33 | var x = expr.split(";"), 34 | loc = x.shift(); 35 | x = x.join(";"); 36 | if (val && val.hasOwnProperty(loc)) P.trace(x, val[loc], path + ";" + loc); 37 | else if (loc === "*") P.walk(loc, x, val, path, function(m, l, x, v, p) { 38 | P.trace(m + ";" + x, v, p); 39 | }); 40 | else if (loc === "..") { 41 | P.trace(x, val, path); 42 | P.walk(loc, x, val, path, function(m, l, x, v, p) { 43 | typeof v[m] === "object" && P.trace("..;" + x, v[m], p + ";" + m); 44 | }); 45 | } else if (/,/.test(loc)) { // [name1,name2,...] 46 | for (var s = loc.split(/'?,'?/), i = 0, n = s.length; i < n; i++) 47 | P.trace(s[i] + ";" + x, val, path); 48 | } else if (/^\(.*?\)$/.test(loc)) // [(expr)] 49 | P.trace(P.eval(loc, val, path.substr(path.lastIndexOf(";") + 1)) + ";" + x, val, path); 50 | else if (/^\?\(.*?\)$/.test(loc)) // [?(expr)] 51 | P.walk(loc, x, val, path, function(m, l, x, v, p) { 52 | if (P.eval(l.replace(/^\?\((.*?)\)$/, "$1"), v[m], m)) P.trace(m + ";" + x, v, p); 53 | }); 54 | else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) // [start:end:step] phyton slice syntax 55 | P.slice(loc, x, val, path); 56 | } else P.store(path, val); 57 | }, 58 | walk: function(loc, expr, val, path, f) { 59 | if (val instanceof Array) { 60 | for (var i = 0, n = val.length; i < n; i++) 61 | if (i in val) f(i, loc, expr, val, path); 62 | } else if (typeof val === "object") { 63 | for (var m in val) 64 | if (val.hasOwnProperty(m)) f(m, loc, expr, val, path); 65 | } 66 | }, 67 | slice: function(loc, expr, val, path) { 68 | if (val instanceof Array) { 69 | var len = val.length, 70 | start = 0, 71 | end = len, 72 | step = 1; 73 | loc.replace(/^(-?[0-9]*):(-?[0-9]*):?(-?[0-9]*)$/g, function($0, $1, $2, $3) { 74 | start = parseInt($1 || start); 75 | end = parseInt($2 || end); 76 | step = parseInt($3 || step); 77 | }); 78 | start = (start < 0) ? Math.max(0, start + len) : Math.min(len, start); 79 | end = (end < 0) ? Math.max(0, end + len) : Math.min(len, end); 80 | for (var i = start; i < end; i += step) 81 | P.trace(i + ";" + expr, val, path); 82 | } 83 | }, 84 | eval: function(x, _v, _vname) { 85 | var message = 'script expressions are not enabled'; 86 | try { 87 | if (unsafe) { 88 | return $ && _v && eval(x.replace(/@/g, "_v")); 89 | } 90 | } catch (e) { 91 | message = e.message; 92 | } 93 | throw new SyntaxError("jsonPath: " + message + ": " + x.replace(/@/g, "_v").replace(/\^/g, "_a")); 94 | } 95 | }; 96 | 97 | var $ = obj; 98 | if (expr && obj && (P.resultType == "VALUE" || P.resultType == "PATH")) { 99 | P.trace(P.normalize(expr).replace(/^\$;/, ""), obj, "$"); 100 | return P.result.length ? P.result : false; 101 | } 102 | } 103 | 104 | exports.jsonPath = jsonPath 105 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | // perform setup 8 | const {Utils} = require("./secutils"); 9 | Utils.setupPrefs(); 10 | require("./config").setup(); 11 | 12 | 13 | // install the commands 14 | const {installCommands} = require("./commands"); 15 | installCommands(); 16 | -------------------------------------------------------------------------------- /lib/mitm.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const {Cc, Ci, Cu} = require("chrome"); 8 | const events = require("sdk/system/events"); 9 | const tabs_tabs = require("sdk/tabs/utils"); 10 | const {jsonPath} = require("./jsonpath"); 11 | const {Utils} = require("./secutils"); 12 | 13 | function HeaderModifier(name) { 14 | this.headerName = name; 15 | this.values = []; 16 | } 17 | 18 | HeaderModifier.prototype.addValue = function(value) { 19 | if (-1 == this.values.indexOf(value)) { 20 | this.values.push(value); 21 | } 22 | }; 23 | 24 | HeaderModifier.prototype.removeValue = function(value) { 25 | let index = this.values.indexOf(value); 26 | if (-1 != value) { 27 | this.values.splice(index,1); 28 | } 29 | }; 30 | 31 | HeaderModifier.prototype.modify = function(aChannel) { 32 | for (let idx in this.values) { 33 | aChannel.setRequestHeader(this.headerName,this.values[idx],true); 34 | } 35 | }; 36 | 37 | function ModifierScopes() { 38 | let scopes = {}; 39 | return { 40 | getScope:function(key) { 41 | if (!key) { 42 | key = 'global'; 43 | } 44 | if (!scopes[key]) { 45 | scopes[key] = {}; 46 | } 47 | return scopes[key]; 48 | } 49 | }; 50 | } 51 | 52 | var MitmProxy = function () { 53 | // we want to map tabs to lists(?) of modifiers so we can run the modifiers 54 | // for any given tab - means we can keep state out of tab expandos 55 | this.modifierScopes = ModifierScopes(); 56 | 57 | //register the modify handler 58 | events.on("http-on-modify-request", this.modify.bind(this), true); 59 | } 60 | 61 | MitmProxy.prototype.modify = function (aEvent) { 62 | let channel = aEvent.subject.QueryInterface(Ci.nsIHttpChannel); 63 | let key = Utils.getKeyFromChannel(channel); 64 | let runModifiers = function(scope) { 65 | for (let type in scope) { 66 | for(let idx in scope[type]) { 67 | let modifier = scope[type][idx]; 68 | modifier.modify(channel); 69 | } 70 | } 71 | }; 72 | // run global modifiers 73 | runModifiers(this.modifierScopes.getScope()); 74 | 75 | //run modifiers for tab 76 | runModifiers(this.modifierScopes.getScope(key)); 77 | } 78 | 79 | MitmProxy.prototype.callback = function (callbackData) { 80 | let commands = []; 81 | let key = callbackData.key; 82 | let addCommands = function(toAdd) { 83 | for (idx in toAdd) { 84 | commands.push(toAdd[idx]); 85 | } 86 | } 87 | if (callbackData && callbackData.commands) { 88 | addCommands(callbackData.commands); 89 | } 90 | if (callbackData && callbackData.conditionalCommands) { 91 | let expression = callbackData.conditionalCommands.expression; 92 | if (expression) { 93 | let result = jsonPath(callbackData, expression); 94 | if (result && result[0] && callbackData.conditionalCommands.states[result[0]]) { 95 | addCommands(callbackData.conditionalCommands.states[result[0]]); 96 | } 97 | } else { 98 | // TODO: log this; it's a problem 99 | } 100 | } 101 | 102 | let scopes = this.modifierScopes; 103 | 104 | /* 105 | * find a header modifier pertaining to a command 106 | */ 107 | let findHeaderModifier = function(command) { 108 | if (command.params && command.params.headerName) { 109 | let headerName = command.params.headerName; 110 | let commandKey = key; 111 | // TODO: if the command specifies global, set commandKey = 'global' 112 | if (command.params.scope && 'global' === command.params.scope) { 113 | commandKey = 'global'; 114 | } else { 115 | commandKey = command.params.tab; 116 | } 117 | let scope = scopes.getScope(commandKey); 118 | if (!scope['HeaderModifiers']) { 119 | scope['HeaderModifiers'] = {}; 120 | } 121 | let headerModifiers = scope['HeaderModifiers']; 122 | if (!headerModifiers[headerName]) { 123 | headerModifiers[headerName] = new HeaderModifier(headerName); 124 | } 125 | return headerModifiers[headerName]; 126 | } 127 | return null; 128 | }; 129 | 130 | // execute the commands in the command list 131 | for(let idx in commands){ 132 | let command = commands[idx]; 133 | if (command.command && command.command === 'addToHeader') { 134 | let modifier = findHeaderModifier(command); 135 | if (modifier) { 136 | modifier.addValue(command.params.value); 137 | } 138 | } 139 | if (command.command && command.command === 'removeFromHeader') { 140 | let modifier = findHeaderModifier(command); 141 | if (modifier) { 142 | modifier.removeValue(command.params.value); 143 | // TODO: if there are no values, maybe remove the modifier? 144 | } 145 | } 146 | } 147 | }; 148 | 149 | exports.MITM = new MitmProxy(); 150 | -------------------------------------------------------------------------------- /lib/proxy.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | var {Cc,Ci} = require("chrome"); 8 | 9 | var prefManager = Cc["@mozilla.org/preferences-service;1"] 10 | .getService(Ci.nsIPrefBranch); 11 | 12 | var PROXY_NONE=0; 13 | var PROXY_MANUAL=1; 14 | var PROXY_AUTO=2; 15 | var PROXY_DETECT=4; 16 | var PROXY_SYSTEM=5; 17 | 18 | /* 19 | * Representation of a host and port; used in ProxyConfig for the proxies 20 | * configured for each protocol. 21 | */ 22 | function HostPort(host, port) { 23 | this.host = host; 24 | this.port = parseInt(port,10); 25 | this.toString = function() { 26 | return this.host+':'+this.port; 27 | }; 28 | } 29 | 30 | HostPort.fromPrefs = function(hostpref, portpref) { 31 | var host = prefManager.getCharPref(hostpref); 32 | var port = prefManager.getIntPref(portpref); 33 | if(host && port) { 34 | if(host.length > 0 && parseInt(port) > 0) { 35 | return new HostPort(host,port); 36 | } 37 | } 38 | return null; 39 | } 40 | 41 | HostPort.fromString = function(endpoint) { 42 | var parts = endpoint.split(':'); 43 | if (2 == parts.length) { 44 | var host = parts[0]; 45 | var port = parseInt(parts[1]); 46 | if (!isNaN(port)) { 47 | return new HostPort(host, port); 48 | } 49 | return null; 50 | } 51 | return null; 52 | } 53 | 54 | /* 55 | * A proxy configuration. Stores the type of proxy configuration (automatic, 56 | * manual, configured from PAC file, etc. as well as the configuration 57 | * information. 58 | */ 59 | function ProxyConfig() { 60 | } 61 | 62 | ProxyConfig.prototype= { 63 | type:PROXY_NONE, 64 | }; 65 | 66 | ProxyConfig.prototype.toString = function() { 67 | return JSON.stringify(this); 68 | } 69 | 70 | /* 71 | * Some built in proxy configs. 72 | */ 73 | var BuiltIn = { 74 | 'system':{"type":5,"socksVersion":5,"proxyExcludes":"localhost, 127.0.0.1"}, 75 | 'none':{"type":0,"socksVersion":5,"proxyExcludes":"localhost, 127.0.0.1"}, 76 | 'detect':{"type":4,"socksVersion":5,"proxyExcludes":"localhost, 127.0.0.1"}, 77 | }; 78 | 79 | /* 80 | * Many proxy configs. 81 | */ 82 | var ProxyManager = { 83 | /** 84 | * Apply a shared config. 85 | * endpoint - the endpoint to configure for shared config (e.g. 'host:port') 86 | * excludes - comma separated string of hosts to exclude 87 | * name - (optional) the name to give this configuration (if the config is to 88 | * be saved). 89 | */ 90 | applySharedConfig:function(endpoint, excludes, name) { 91 | var config = this.sharedProxy(endpoint, excludes); 92 | if (name) { 93 | proxyStore.store(name, config); 94 | } 95 | this.applyConfig(config); 96 | return 'ok'; 97 | }, 98 | 99 | /** 100 | * Apply an auto (PAC) config. 101 | * URL - the URL of the PAC 102 | * name - (optional) the name to give this configuration (if the config is to 103 | * be saved). 104 | */ 105 | applyAutoConfig:function(URL, name) { 106 | var config = ProxyManager.autoProxy(URL); 107 | if (name) { 108 | proxyStore.store(name, config); 109 | } 110 | ProxyManager.applyConfig(config); 111 | return 'ok'; 112 | }, 113 | 114 | /** 115 | * Apply a named config. 116 | * name - the name of the config to apply. 117 | */ 118 | applyNamedConfig:function(name) { 119 | var config = this.get(name); 120 | if (config) { 121 | ProxyManager.applyConfig(config); 122 | return 'ok'; 123 | } 124 | return 'no config with this name'; 125 | }, 126 | 127 | /** 128 | * Apply a manual config. 129 | * http - the endpoint for the HTTP proxy 130 | * ssl - the endpoint for the SSL proxy 131 | * ftp - the endpoint for the FTP proxy 132 | * socks - the endpoint for the socks proxy 133 | * excludes - comma separated string of hosts to exclude 134 | */ 135 | applyManualConfig:function(http, ssl, ftp, socks, excludes, name){ 136 | var config = ProxyManager.manualProxy(http, ssl, ftp, socks, excludes); 137 | if (name) { 138 | proxyStore.store(name, config); 139 | } 140 | ProxyManager.applyConfig(config); 141 | return 'ok'; 142 | }, 143 | 144 | /** 145 | * Add the current configuration. 146 | * name - the name to give this configuration. 147 | */ 148 | addCurrent:function(name) { 149 | var current = ProxyManager.readConfigFromPreferences(); 150 | proxyStore.store(name, current); 151 | return 'ok, I think'; 152 | }, 153 | 154 | /** 155 | * Delete a proxy configuration. 156 | * name - the name of the proxy configuration to delete. 157 | */ 158 | deleteConfig:function(name) { 159 | if (name && 'default' == name) { 160 | return 'you cannot delete the default profile'; 161 | } 162 | proxyStore.remove(name); 163 | return 'deleted'; 164 | }, 165 | 166 | /** 167 | * List the proxy configurations. 168 | * returns an array of proxy config names. 169 | */ 170 | list:function() { 171 | var list = proxyStore.list(); 172 | // TODO: replace with a list of 'special' names 173 | list.push('default'); 174 | for(key in BuiltIn) { 175 | list.push(key); 176 | } 177 | return list; 178 | }, 179 | 180 | /** 181 | * Get a proxy configuration. 182 | * name - the name of the proxy configuration to get. 183 | */ 184 | get:function(name) { 185 | if ('default' == name) { 186 | return ProxyManager.readConfigFromPreferences(); 187 | } 188 | var config = proxyStore.fetch(name); 189 | if (!config) { 190 | config = BuiltIn[name]; 191 | if (config) { 192 | config.__proto__ = ProxyConfig.prototype; 193 | } 194 | } 195 | return config; 196 | }, 197 | 198 | /* 199 | * Read a proxy config from firefox preferences. 200 | */ 201 | readConfigFromPreferences:function() { 202 | var config = new ProxyConfig(); 203 | config.type = prefManager.getIntPref('network.proxy.type'); 204 | var http = HostPort.fromPrefs('network.proxy.http','network.proxy.http_port'); 205 | if (http) { 206 | config.http = http; 207 | } 208 | var ssl = HostPort.fromPrefs('network.proxy.ssl','network.proxy.ssl_port'); 209 | if (ssl) { 210 | config.ssl = ssl; 211 | } 212 | var ftp = HostPort.fromPrefs('network.proxy.ftp','network.proxy.ftp_port'); 213 | if (ftp) { 214 | config.ftp = ftp; 215 | } 216 | var socks = HostPort.fromPrefs('network.proxy.socks','network.proxy.socks_port'); 217 | if (socks) { 218 | config.socks = socks; 219 | } 220 | var shareSettings = prefManager.getBoolPref('network.proxy.share_proxy_settings'); 221 | if (shareSettings) { 222 | config.shareSettings = shareSettings; 223 | } 224 | var socksVersion = prefManager.getIntPref('network.proxy.socks_version'); 225 | if (socksVersion) { 226 | config.socksVersion = socksVersion; 227 | } 228 | var proxyExcludes = prefManager.getCharPref('network.proxy.no_proxies_on'); 229 | if (proxyExcludes) { 230 | config.proxyExcludes = proxyExcludes; 231 | } 232 | var autoConfigURL = prefManager.getCharPref('network.proxy.autoconfig_url'); 233 | if (autoConfigURL) { 234 | config.autoConfigURL = autoConfigURL; 235 | } 236 | return config; 237 | }, 238 | 239 | manualProxy:function(http, ssl, ftp, socks, excludes) { 240 | var config = new ProxyConfig(); 241 | config.http = new HostPort.fromString(http); 242 | config.ssl = new HostPort.fromString(ssl); 243 | config.ftp = new HostPort.fromString(ftp); 244 | config.socks = new HostPort.fromString(socks); 245 | config.excludes = excludes; 246 | return config; 247 | }, 248 | 249 | /* 250 | * Create a shared proxy configuration (e.g. localhost:8080 for all protocols. 251 | */ 252 | sharedProxy:function(endpointString, excludes) { 253 | var config = new ProxyConfig(); 254 | config.type = PROXY_MANUAL; 255 | config.shareSettings = true; 256 | config.http = new HostPort.fromString(endpointString); 257 | config.ssl = new HostPort.fromString(endpointString); 258 | config.ftp = new HostPort.fromString(endpointString); 259 | config.socks = new HostPort.fromString(endpointString); 260 | config.proxyExcludes = excludes; 261 | return config; 262 | }, 263 | 264 | /* 265 | * Create a proxy config for a PAC configuration. 266 | */ 267 | autoProxy:function(pacURL) { 268 | var config = new ProxyConfig(); 269 | config.type = PROXY_AUTO; 270 | config.autoConfigURL = pacURL; 271 | return config; 272 | }, 273 | 274 | /** 275 | * Apply a supplied config to the firefox preferences. 276 | */ 277 | applyConfig:function(config) { 278 | if(!config) { 279 | return; 280 | } 281 | // TODO: Do we need to back up the existing prefs to the 282 | // network.proxy.backup prefs at all? Check FX source for when this happens 283 | // and if we need to imitate this at all. 284 | if (!this.get('original')) { 285 | this.addCurrent('original'); 286 | } 287 | this.addCurrent('undo'); 288 | if (config.type) { 289 | prefManager.setIntPref('network.proxy.type',config.type); 290 | } 291 | if (config.http) { 292 | prefManager.setCharPref('network.proxy.http',config.http.host); 293 | prefManager.setIntPref('network.proxy.http_port',config.http.port); 294 | } 295 | if (config.ftp) { 296 | prefManager.setCharPref('network.proxy.ftp',config.ftp.host); 297 | prefManager.setIntPref('network.proxy.ftp_port',config.ftp.port); 298 | } 299 | if (config.ssl) { 300 | prefManager.setCharPref('network.proxy.ssl',config.ssl.host); 301 | prefManager.setIntPref('network.proxy.ssl_port',config.ssl.port); 302 | } 303 | if (config.socks) { 304 | prefManager.setCharPref('network.proxy.socks',config.socks.host); 305 | prefManager.setIntPref('network.proxy.socks_port',config.socks.port); 306 | } 307 | if (config.shareSettings) { 308 | prefManager.setBoolPref('network.proxy.share_proxy_settings', 309 | config.shareSettings); 310 | } 311 | if (config.socksVersion) { 312 | prefManager.setIntPref('network.proxy.socks_version', 313 | config.socksVersion); 314 | } 315 | if (config.proxyExcludes) { 316 | prefManager.setCharPref('network.proxy.no_proxies_on', 317 | config.proxyExcludes); 318 | } 319 | if (config.autoConfigURL) { 320 | prefManager.setCharPref('network.proxy.autoconfig_url', 321 | config.autoConfigURL); 322 | } 323 | } 324 | }; 325 | 326 | function ProxyStore() { 327 | this.ss = require("sdk/simple-storage"); 328 | if(!this.ss.storage.profiles) { 329 | this.ss.storage.profiles = []; 330 | } 331 | } 332 | 333 | ProxyStore.prototype = { 334 | store:function(name, data) { 335 | this.ss.storage.profiles[name] = JSON.stringify(data); 336 | }, 337 | fetch:function(name) { 338 | var data = this.ss.storage.profiles[name]; 339 | if (data) { 340 | var obj = JSON.parse(data); 341 | obj.__proto__ = ProxyConfig.prototype; 342 | return obj; 343 | } 344 | return null; 345 | }, 346 | remove:function(name) { 347 | delete this.ss.storage.profiles[name]; 348 | }, 349 | list:function() { 350 | var profiles = []; 351 | for(profile in this.ss.storage.profiles) { 352 | profiles[profiles.length] = profile; 353 | } 354 | return profiles; 355 | } 356 | }; 357 | 358 | proxyStore = new ProxyStore(); 359 | 360 | exports.ProxyConfig = ProxyConfig; 361 | exports.ProxyManager = ProxyManager; 362 | -------------------------------------------------------------------------------- /lib/secutils.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const {Ci, Cc} = require("chrome"); 8 | const tabs_tabs = require("sdk/tabs/utils"); 9 | const system = require("sdk/system"); 10 | const {jsonPath} = require("./jsonpath"); 11 | 12 | var prefManager = Cc["@mozilla.org/preferences-service;1"] 13 | .getService(Ci.nsIPrefBranch); 14 | 15 | Utils = {}; 16 | 17 | 18 | Utils.setupPrefs = function() { 19 | if (system.staticArgs && system.staticArgs.prefs) { 20 | for (pref in system.staticArgs.prefs) { 21 | var prefValue = system.staticArgs.prefs[pref]; 22 | // TODO: have some way of checking pref type, call appropriate setPref 23 | prefManager.setCharPref(pref,prefValue); 24 | } 25 | } 26 | }; 27 | 28 | // utility function to get a window from a request 29 | Utils.getRequestWindow = function (aRequest) { 30 | try { 31 | if (aRequest.notificationCallbacks) 32 | return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow; 33 | } catch(e) {} 34 | try { 35 | if (aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) 36 | return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext).associatedWindow; 37 | } catch(e) {} 38 | return null; 39 | }; 40 | 41 | //utility function to get a tab from a channel 42 | Utils.getTabFromChannel = function (aChannel) { 43 | let wnd = Utils.getRequestWindow(aChannel); 44 | return (wnd && wnd.top == wnd) ? tabs_tabs.getTabForContentWindow(wnd.top) : null; 45 | }; 46 | 47 | // utility to get the tab key (e.g. for tabModifiers) from a channel 48 | Utils.getKeyFromChannel = function (aChannel) { 49 | let channelTab = Utils.getTabFromChannel(aChannel); 50 | if (channelTab) { 51 | if (channelTab._tabKey) { 52 | return channelTab._tabKey; 53 | } 54 | } 55 | return null; 56 | }; 57 | 58 | Utils.getNewTabKey = function () { 59 | let current = 0; 60 | return function(){ 61 | return current += 1; 62 | } 63 | }(); 64 | 65 | Utils.getDocumentFromContext = function(aContext) { 66 | let doc = null; 67 | try { 68 | doc = aContext.environment.contentDocument; 69 | if (!doc) { 70 | doc = aContext.environment.document; 71 | } 72 | win = doc.defaultView; 73 | } catch (ex) { 74 | let chromeWindow = aContext.environment.chromeDocument.defaultView; 75 | let tabbrowser = chromeWindow.gBrowser; 76 | let browser = tabbrowser.getBrowserForTab(tabbrowser.selectedTab); 77 | doc = browser.contentDocument; 78 | } 79 | return doc; 80 | }; 81 | 82 | Utils.getKeyFromContext = function (aContext) { 83 | let win = null; 84 | try { 85 | let doc = aContext.environment.contentDocument; 86 | if (!doc) { 87 | doc = aContext.environment.document; 88 | } 89 | win = doc.defaultView; 90 | } catch (ex) { 91 | let chromeWindow = aContext.environment.chromeDocument.defaultView; 92 | let tabbrowser = chromeWindow.gBrowser; 93 | let browser = tabbrowser.getBrowserForTab(tabbrowser.selectedTab); 94 | let document = browser.contentDocument; 95 | win = document.defaultView; 96 | } 97 | 98 | let tab = tabs_tabs.getTabForContentWindow(win); 99 | return Utils.getKeyFromTab(tab); 100 | }; 101 | 102 | Utils.CheckOrigin = function(aURL1, aURL2) { 103 | var ioService = Cc["@mozilla.org/network/io-service;1"] 104 | .getService(Ci.nsIIOService); 105 | var u1 = ioService.newURI(aURL1, null, null); 106 | var u2 = ioService.newURI(aURL2, null, u1); 107 | 108 | var ignorePort = false; 109 | var prefValue = ''; 110 | try { 111 | prefValue = prefManager.getCharPref('pnh.check.origin'); 112 | if (prefValue && prefValue === 'noport') { 113 | console.log('port checks will be ignored'); 114 | ignorePort = true; 115 | } 116 | } catch (e) { 117 | // we don't care if pref check fails; it's most likely not there 118 | } 119 | 120 | if (prefValue && prefValue === 'off') { 121 | console.log('origin checks will be ignored'); 122 | return true; 123 | } 124 | // check scheme 125 | if (!u2.schemeIs(u1.scheme)) { 126 | console.log('origin check failed for '+aURL1+' and '+aURL2+': scheme does not match'); 127 | return false; 128 | } 129 | // check host 130 | if (u2.host!==u1.host) { 131 | console.log('origin check failed for '+aURL1+' and '+aURL2+': host does not match'); 132 | return false; 133 | } 134 | // check port 135 | if (!ignorePort && u2.port!==u1.port) { 136 | console.log('origin check failed for '+aURL1+' and '+aURL2+': port does not match'); 137 | return false; 138 | } 139 | return true; 140 | }; 141 | 142 | Utils.getKeyFromTab = function (aTab) { 143 | if (!aTab._tabKey) { 144 | aTab._tabKey = Utils.getNewTabKey(); 145 | } 146 | return aTab._tabKey; 147 | } 148 | 149 | //utility function to get a tab from a channel 150 | Utils.getWinFromChannel = function (aChannel) { 151 | return Utils.getRequestWindow(aChannel); 152 | }; 153 | 154 | exports.Utils = Utils; 155 | -------------------------------------------------------------------------------- /lib/servicestub.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | const {Cu} = require("chrome"); 8 | 9 | var gcli = undefined; 10 | 11 | try { 12 | Cu.import("resource:///modules/devtools/gcli.jsm"); 13 | } catch (e) { 14 | let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 15 | gcli = devtools["req" + "uire"]("gcli/index"); 16 | } 17 | 18 | const {jsonPath} = require('./jsonpath'); 19 | const {Utils} = require('./secutils'); 20 | const {Substitutions} = require('./substitutions'); 21 | const {readURI} = require("sdk/net/url"); 22 | const {XMLHttpRequest} = require("sdk/net/xhr"); 23 | const promise = require('sdk/core/promise'); 24 | 25 | 26 | 27 | function buildParams(params, aURL) { 28 | let idx; 29 | for (idx in params) { 30 | modifyType(params[idx].type, aURL); 31 | } 32 | return params; 33 | } 34 | 35 | function modifyType(type, aURL) { 36 | if("object" === typeof type) { 37 | // replace selection 'data' with fetched data if 'fetchData' is set 38 | if (type.name && "selection" === type.name && 39 | type.dataAction && 40 | type.dataAction.url && 41 | type.dataAction.expression) { 42 | if(!aURL || aURL && Utils.CheckOrigin(aURL, type.dataAction.url)) { 43 | type.data = function fetchData(context) { 44 | let actionCtx = {args:context.getArgsObject()}; 45 | let generatedURL = Substitutions.parameterize(type.dataAction.url, actionCtx, true); 46 | return readURI(generatedURL) 47 | .then(function(d) { 48 | let obj = JSON.parse(d); 49 | let result = jsonPath(obj, type.dataAction.expression); 50 | return result; 51 | }, function daError(e) { 52 | return e; 53 | }); 54 | }; 55 | } else { 56 | console.log('Could not perform dataAction due to origin restrictions'); 57 | type.data = []; 58 | } 59 | } 60 | } 61 | } 62 | 63 | function getCallbackInfo(callback, data) { 64 | return { 65 | sent:false, 66 | callback:callback, 67 | template:JSON.parse(JSON.stringify(data)), 68 | addArgs:function(args) { 69 | this.template.args = args; 70 | this.checkSend(); 71 | }, 72 | addResponse:function(response) { 73 | this.template.response = response; 74 | this.checkSend(); 75 | }, 76 | /** 77 | * Add context specific information to the command data (tab ID, etc) 78 | */ 79 | addContextInfo:function(context) { 80 | var key = Utils.getKeyFromContext(context); 81 | let location = Utils.getDocumentFromContext(context).location; 82 | this.template.tab = { 83 | key: key, 84 | URL: Utils.getDocumentFromContext(context).URL, 85 | location: { 86 | hash:location.hash, 87 | host:location.host, 88 | hostname:location.hostname, 89 | href:location.href, 90 | pathname:location.pathname, 91 | port:location.port, 92 | protocol:location.protocol, 93 | search:location.search 94 | } 95 | }; 96 | }, 97 | checkSend:function() { 98 | // substitute values in template 99 | if (!this.sent && 100 | Substitutions.modifyData(this.template, this.template)) { 101 | // TODO: check callback is actually a function. Also, we should 102 | // probably check if callback is OK before bothering with adding 103 | // args and response 104 | 105 | if (this.callback) { 106 | this.callback(this.template); 107 | } 108 | this.sent = true; 109 | } else { 110 | // warn if both this.args and this.response are present 111 | // and modify fails 112 | if (this.args && this.response && this.callback) { 113 | console.log("modification failed even with args and response"); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Create command proxies from a descriptor; give the resulting commands the 122 | * specified prefix. 123 | */ 124 | var ServiceStub = function (url, prefix, callback) { 125 | this.url = url; 126 | this.prefix = prefix; 127 | this.callback = callback; 128 | this.manifest = {}; 129 | }; 130 | 131 | /** 132 | * Take a command object and augment. 133 | */ 134 | ServiceStub.prototype.modCommand = function(command) { 135 | let descriptorURL = this.url; 136 | try { 137 | 138 | command.item = "command"; 139 | let callbackData = {}; 140 | if (command.execAction && command.execAction.callbackData) { 141 | callbackData = command.execAction.callbackData; 142 | } 143 | if (command.name) { 144 | command.name = this.prefix+' '+command.name; 145 | } else { 146 | command.name = this.prefix; 147 | } 148 | if (command.params) { 149 | command.params = buildParams(command.params, this.url); 150 | } 151 | if (command.execAction) { 152 | let callback = this.callback; 153 | command.exec = function ServiceStub_exec(args, context) { 154 | let callbackInfo = getCallbackInfo(callback, callbackData); 155 | callbackInfo.addContextInfo(context); 156 | callbackInfo.addArgs(args); 157 | 158 | if (command.execAction.url) { 159 | let generatedURL = command.execAction.url; 160 | // This is dumb and messy, sort it out 161 | // TODO: tidy the below when issue #42 is resolved 162 | if ("object" === typeof generatedURL) { 163 | Substitutions.modifyData(command.execAction, callbackInfo); 164 | generatedURL = command.execAction.url; 165 | } else { 166 | generatedURL = Substitutions.parameterize(command.execAction.url,callbackInfo.template,true); 167 | } 168 | if(Utils.CheckOrigin(descriptorURL, generatedURL)) { 169 | let deferred = promise.defer(); 170 | 171 | let method = "GET"; 172 | if (command.execAction.method) { 173 | method = command.execAction.method; 174 | } 175 | 176 | let requestBody = null; 177 | if (command.execAction.requestBody) { 178 | requestBody = Substitutions.parameterize(command.execAction.requestBody,callbackInfo.template,true); 179 | } 180 | 181 | let contentType= "application/json"; 182 | if (command.execAction.contentType) { 183 | contentType = command.execAction.contentType; 184 | } 185 | 186 | xhr = new XMLHttpRequest(); 187 | xhr.open(method, generatedURL, true); 188 | xhr.setRequestHeader("content-type",contentType); 189 | xhr.onload = function() { 190 | let result = 'OK'; 191 | if(command.execAction.expression) { 192 | console.log('response text is '+this.responseText); 193 | let obj = JSON.parse(this.responseText); 194 | result = jsonPath(obj,command.execAction.expression); 195 | callbackInfo.addResponse(obj); 196 | console.log('obj is '+JSON.stringify(obj)); 197 | console.log('response is '+result); 198 | } 199 | deferred.resolve(result); 200 | } 201 | xhr.onerror = function(error) { 202 | console.log('there was problem reading the command URI'); 203 | deferred.reject(error); 204 | }; 205 | 206 | xhr.send(requestBody); 207 | return deferred.promise; 208 | 209 | } else { 210 | console.log('origin checks for execAction failed'); 211 | throw new Error('help!'); 212 | } 213 | } else { 214 | let deferred = promise.defer(); 215 | deferred.resolve("OK"); 216 | return deferred.promise; 217 | } 218 | }; 219 | } 220 | } catch (e) { 221 | console.log(e); 222 | } 223 | }; 224 | 225 | /** 226 | * Fetches the available command descriptions from the descriptor, adds the 227 | * GCLI commands for each. 228 | */ 229 | ServiceStub.prototype.hook = function () { 230 | readURI(this.url).then(function(data) { 231 | try { 232 | this.manifest = JSON.parse(data); 233 | let key; 234 | let commands = this.manifest.commands; 235 | let prefix = this.manifest.prefix; 236 | 237 | for(key in commands) { 238 | let command = commands[key]; 239 | // replace JSON descriptor info with actual parameter objects / functions 240 | // (where applicable) 241 | this.modCommand(command); 242 | if (gcli.addCommand) { 243 | gcli.addCommand(command); 244 | } else { 245 | gcli.addItems([command]); 246 | } 247 | } 248 | } catch (e) { 249 | console.log("Error: unable to parse descriptor "+e); 250 | } 251 | }.bind(this), 252 | function(error) { 253 | console.log(error); 254 | }); 255 | }; 256 | 257 | exports.ServiceStub = ServiceStub; 258 | -------------------------------------------------------------------------------- /lib/substitutions.js: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 | /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 | /* This Source Code Form is subject to the terms of the Mozilla Public 4 | * License, v. 2.0. If a copy of the MPL was not distributed with this 5 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 | 7 | jsonpath = require("./jsonpath"); 8 | 9 | var Substitutions = { 10 | parameterize : function(aStr, aParams, URLEncode) { 11 | var re_outer = /(\$\{[^\}]+\})/; 12 | var re_inner = /\$\{([^\}]+)\}/; 13 | 14 | var substitute = function(tok) { 15 | var match = tok.match(re_inner); 16 | if (match && match[1]) { 17 | var inner = match[1]; 18 | var result; 19 | if(-1 != inner.indexOf('$')) { 20 | result = jsonpath.jsonPath(aParams, inner); 21 | } else if(aParams[match[1]]){ 22 | result = aParams[match[1]]; 23 | } 24 | if (URLEncode) { 25 | return encodeURIComponent(result); 26 | } else { 27 | return result; 28 | } 29 | } 30 | return tok; 31 | }; 32 | var substituted = []; 33 | var toSub = aStr.split(re_outer); 34 | for(tok in toSub) { 35 | var toPush = substitute(toSub[tok]); 36 | substituted.push(toPush); 37 | } 38 | return substituted.join(''); 39 | }, 40 | modifyExpressions: function(obj, root){ 41 | var success = true; 42 | if("object" === typeof obj) { 43 | for(attr in obj){ 44 | if("object" === typeof obj[attr]) { 45 | if (obj[attr].type && "expression" === obj[attr].type 46 | && obj[attr].expression) { 47 | result = jsonpath.jsonPath(root, obj[attr].expression); 48 | if (result && obj[attr].extract) { 49 | obj[attr] = result[0]; 50 | } else { 51 | obj[attr] = result; 52 | if (!result) { 53 | success = false; 54 | } 55 | } 56 | } else { 57 | if (!this.modifyExpressions(obj[attr], root)) { 58 | success = false; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | return success; 65 | }, 66 | modifyTemplates: function(obj, root){ 67 | var success = true; 68 | if("object" === typeof obj) { 69 | for(attr in obj){ 70 | if("object" === typeof obj[attr]) { 71 | if (obj[attr].type && "template" === obj[attr].type 72 | && obj[attr].template) { 73 | result = this.parameterize(obj[attr].template, root); 74 | console.log('result is '+result); 75 | obj[attr] = result; 76 | } else { 77 | if (!this.modifyTemplates(obj[attr], root)) { 78 | success = false; 79 | } 80 | } 81 | } 82 | } 83 | } 84 | return success; 85 | }, 86 | modifyData: function(obj,root) { 87 | return this.modifyExpressions(obj, root) && this.modifyTemplates(obj, root); 88 | } 89 | }; 90 | 91 | exports.Substitutions = Substitutions; 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fx_pnh", 3 | "license": "MPL 2.0", 4 | "author": "Mark Goodwin", 5 | "version": "0.4", 6 | "fullName": "Firefox Plug-n-Hack", 7 | "id": "jid1-CZ1BeoFM9Mmlzg@jetpack", 8 | "description": "A Firefox addon for configuring and using security tools", 9 | "main": "lib/main.js" 10 | } 11 | -------------------------------------------------------------------------------- /test/test-main.js: -------------------------------------------------------------------------------- 1 | var main = require("main"); 2 | 3 | exports["test main"] = function(assert) { 4 | assert.pass("Unit test running!"); 5 | }; 6 | 7 | exports["test main async"] = function(assert, done) { 8 | assert.pass("async Unit test running!"); 9 | done(); 10 | }; 11 | 12 | require("test").run(exports); 13 | --------------------------------------------------------------------------------