Skip to content
Newer
Older
100644 593 lines (536 sloc) 25.2 KB
d0d2bdb First commit
Mashery authored Mar 6, 2011
1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3
4 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5 <head>
6 <title>JavaScript OAuth 1.0 Call Testing Tool</title>
7 <script src="./jquery.min.js" type="text/javascript">
8 </script>
9 <script type="text/javascript">
10 //<![CDATA[
11 /* OAuthSimple
12 * A simpler version of OAuth
13 * NOTE: this page uses a modified version of the OAuthSimple library due to constraints with how this page is served. Please see
14 * http://github.com/jrconlin/oauthsimple for the proper library.
15 *
16 * author: jr conlin
17 * mail: src thatWeirdACirclyThing anticipatr dot com
18 * copyright: unitedHeroes.net
19 * version: 0.2 (Not Ready For Prime Time)
20 * url: http://unitedHeroes.net/OAuthSimple
21 *
22 * Copyright (c) 2008, unitedHeroes.net
23 * All rights reserved.
24 *
25 * Redistribution and use in source and binary forms, with or without
26 * modification, are permitted provided that the following conditions are met:
27 * * Redistributions of source code must retain the above copyright
28 * notice, this list of conditions and the following disclaimer.
29 * * Redistributions in binary form must reproduce the above copyright
30 * notice, this list of conditions and the following disclaimer in the
31 * documentation and/or other materials provided with the distribution.
32 * * Neither the name of the unitedHeroes.net nor the
33 * names of its contributors may be used to endorse or promote products
34 * derived from this software without specific prior written permission.
35 *
36 * THIS SOFTWARE IS PROVIDED BY UNITEDHEROES.NET ''AS IS'' AND ANY
37 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
38 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
39 * DISCLAIMED. IN NO EVENT SHALL UNITEDHEROES.NET BE LIABLE FOR ANY
40 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
41 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
45 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46 */
47 var OAuthSimple;
48 var testURL;
49
50 if (OAuthSimple == null)
51 {
52 /* Simple OAuth
53 *
54 * This class only builds the OAuth elements, it does not do the actual
55 * transmission or reception of the tokens. It does not validate elements
56 * of the token. It is for client use only.
57 *
58 * api_key is the API key, also known as the OAuth consumer key
59 * shared_secret is the shared secret (duh).
60 *
61 * Both the api_key and shared_secret are generally provided by the site
62 * offering OAuth services. You need to specify them at object creation
63 * because nobody <explative>ing uses OAuth without that minimal set of
64 * signatures.
65 *
66 * If you want to use the higher order security that comes from the
67 * OAuth token (sorry, I don't provide the functions to fetch that because
68 * sites aren't horribly consistent about how they offer that), you need to
69 * pass those in either with .setTokensAndSecrets() or as an argument to the
70 * .sign() or .getHeaderString() functions.
71 *
72 * Example:
73 <code>
74 var oauthObject = OAuthSimple().sign({path:'http://example.com/rest/',
75 parameters: 'foo=bar&gorp=banana',
76 signatures:{
77 api_key:'12345abcd',
78 shared_secret:'xyz-5309'
79 }});
80 document.getElementById('someLink').href=oauthObject.signed_url;
81 <\/code>
82 *
83 * that will sign as a "GET" using "SHA1-MAC" the url. If you need more than
84 * that, read on, McDuff.
85 */
86
87 /** OAuthSimple creator
88 *
89 * Create an instance of OAuthSimple
90 *
91 * @param api_key {string} The API Key (sometimes referred to as the consumer key) This value is usually supplied by the site you wish to use.
92 * @param shared_secret (string) The shared secret. This value is also usually provided by the site you wish to use.
93 */
94 OAuthSimple = function (consumer_key,shared_secret)
95 {
96 this._secrets={};
97
98
99 // General configuration options.
100 if (consumer_key != null)
101 this._secrets['consumer_key'] = consumer_key;
102 if (shared_secret != null)
103 this._secrets['shared_secret'] = shared_secret;
104 this._default_signature_method= "HMAC-SHA1";
105 this._action = "GET";
106 this._nonce_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
107
108 /** set the parameters either from a hash or a string
109 *
110 * @param {string,object} List of parameters for the call, this can either be a URI string (e.g. "foo=bar&gorp=banana" or an object/hash)
111 */
112 this.setParameters = function (parameters) {
113 if (parameters == null)
114 parameters = {};
115 if (typeof(parameters) == 'string')
116 parameters=this._parseParameterString(parameters);
117 this._parameters = parameters;
118 if (this._parameters['oauth_nonce'] == null)
119 this._getNonce();
120 if (this._parameters['oauth_timestamp'] == null)
121 this._getTimestamp();
122 if (this._parameters['oauth_method'] == null)
123 this.setSignatureMethod();
124 if (this._parameters['oauth_consumer_key'] == null)
125 this._getApiKey();
126 if(this._parameters['oauth_token'] == null)
127 this._getAccessToken();
128
129 return this;
130 };
131
132 /** convienence method for setParameters
133 *
134 * @param parameters {string,object} See .setParameters
135 */
136 this.setQueryString = function (parameters) {
137 return this.setParameters(parameters);
138 };
139
140 /** Set the target URL (does not include the parameters)
141 *
142 * @param path {string} the fully qualified URI (excluding query arguments) (e.g "http://example.org/foo")
143 */
144 this.setURL = function (path) {
145 if (path == '')
146 throw ('No path specified for OAuthSimple.setURL');
147 if (path.indexOf(' ') != -1) {
148 $("#path").select();
149 throw ('Space detected in request path/URL');
150 };
151 this._path = path;
152 return this;
153 };
154
155 /** convienence method for setURL
156 *
157 * @param path {string} see .setURL
158 */
159 this.setPath = function(path){
160 return this.setURL(path);
161 };
162
163 /** set the "action" for the url, (e.g. GET,POST, DELETE, etc.)
164 *
165 * @param action {string} HTTP Action word.
166 */
167 this.setAction = function(action) {
168 if (action == null)
169 action="GET";
170 action = action.toUpperCase();
171 if (action.match('[^A-Z]'))
172 throw ('Invalid action specified for OAuthSimple.setAction');
173 this._action = action;
174 return this;
175 };
176
177 /** set the signatures (as well as validate the ones you have)
178 *
179 * @param signatures {object} object/hash of the token/signature pairs {api_key:, shared_secret:, oauth_token: oauth_secret:}
180 */
181 this.setTokensAndSecrets = function(signatures) {
182 if (signatures)
183 for (var i in signatures)
184 this._secrets[i] = signatures[i];
185 // Aliases
186 if (this._secrets['api_key'])
187 this._secrets.consumer_key = this._secrets.api_key;
188 if (this._secrets['access_token'])
189 this._secrets.oauth_token = this._secrets.access_token;
190 if (this._secrets['access_secret'])
191 this._secrets.oauth_secret = this._secrets.access_secret;
192 // Gauntlet
193 if (this._secrets.consumer_key == null)
194 throw('Missing required consumer_key in OAuthSimple.setTokensAndSecrets');
195 if (this._secrets.shared_secret == null)
196 throw('Missing required shared_secret in OAuthSimple.setTokensAndSecrets');
197 if ((this._secrets.oauth_token!=null) && (this._secrets.oauth_secret == null))
198 throw('Missing oauth_secret for supplied oauth_token in OAuthSimple.setTokensAndSecrets');
199 return this;
200 };
201
202 /** set the signature method (currently only Plaintext or SHA-MAC1)
203 *
204 * @param method {string} Method of signing the transaction (only PLAINTEXT and SHA-MAC1 allowed for now)
205 */
206 this.setSignatureMethod = function(method) {
207 if (method == null)
208 method = this._default_signature_method;
209 //TODO: accept things other than PlainText or SHA-MAC1
210 if (method.toUpperCase().match(/(PLAINTEXT|HMAC-SHA1)/) == null)
211 throw ('Unknown signing method specified for OAuthSimple.setSignatureMethod');
212 this._parameters['oauth_signature_method']= method.toUpperCase();
213 return this;
214 };
215
216 /** sign the request
217 *
218 * note: all arguments are optional, provided you've set them using the
219 * other helper functions.
220 *
221 * @param args {object} hash of arguments for the call
222 * {action:, path:, parameters:, method:, signatures:}
223 * all arguments are optional.
224 */
225 this.sign = function (args) {
226 if (args == null)
227 args = {};
228 // Set any given parameters
229 if(args['action'] != null)
230 this.setAction(args['action']);
231 if (args['path'] != null)
232 this.setPath(args['path']);
233 if (args['method'] != null)
234 this.setSignatureMethod(args['method']);
235 this.setTokensAndSecrets(args['signatures']);
236 if (args['parameters'] != null)
237 this.setParameters(args['parameters']);
238 // check the parameters
239 var normParams = this._normalizedParameters();
240 var sig = this._generateSignature(normParams);
241 this._parameters['oauth_signature']=sig.signature;
242 return {
243 parameters: this._parameters,
244 sig_string: sig.sig_string,
245 signature: this._oauthEscape(this._parameters['oauth_signature']),
246 signed_url: this._path + '?' + this._normalizedParameters(),
247 header: this.getHeaderString()
248 };
249 };
250
251 /** Return a formatted "header" string
252 *
253 * NOTE: This doesn't set the "Authorization: " prefix, which is required.
254 * I don't set it because various set header functions prefer different
255 * ways to do that.
256 *
257 * @param args {object} see .sign
258 */
259 this.getHeaderString = function(args) {
260 if (this._parameters['oauth_signature'] == null)
261 this.sign(args);
262
263 var result = 'OAuth ';
264 for (var pName in this._parameters)
265 {
266 if (pName.match(/^oauth/) == null)
267 continue;
268 if ((this._parameters[pName]) instanceof Array)
269 {
270 var pLength = this._parameters[pName].length;
271 for (var j=0;j<pLength;j++)
272 {
273 result += pName +'="'+this._oauthEscape(this._parameters[pName][j])+'" ';
274 }
275 }
276 else
277 {
278 result += pName + '="'+this._oauthEscape(this._parameters[pName])+'" ';
279 }
280 }
281 return result;
282 };
283
284 // Start Private Methods.
285
286 /** convert the parameter string into a hash of objects.
287 *
288 */
289 this._parseParameterString = function(paramString){
290 var elements = paramString.split('&');
291 var result={};
292 for(var element=elements.shift();element;element=elements.shift())
293 {
294 var keyToken=element.split('=');
295 var value;
296 if (keyToken[1])
297 value = decodeURIComponent(keyToken[1]);
298 if(result[keyToken[0]]){
299 if (!(result[keyToken[0]] instanceof Array))
300 {
301 result[keyToken[0]] = Array(result[keyToken[0]],value);
302 }
303 else
304 {
305 result[keyToken[0]].push(value);
306 }
307 }
308 else
309 {
310 result[keyToken[0]]=value;
311 }
312 }
313 return result;
314 };
315
316 this._oauthEscape = function(string) {
317 if (string == null)
318 return "";
319 if (string instanceof Array)
320 {
321 throw('Array passed to _oauthEscape');
322 }
323 return encodeURIComponent(string).replace(/\!/g, "%21").
324 replace(/\*/g, "%2A").
325 replace(/'/g, "%27").
326 replace(/\(/g, "%28").
327 replace(/\)/g, "%29");
328 };
329
330 this._getNonce = function (length) {
331 if (length == null)
332 length=5;
333 var result = "";
334 var cLength = this._nonce_chars.length;
335 for (var i = 0; i < length;i++) {
336 var rnum = Math.floor(Math.random() *cLength);
337 result += this._nonce_chars.substring(rnum,rnum+1);
338 }
339 this._parameters['oauth_nonce']=result;
340 return result;
341 };
342
343 this._getApiKey = function() {
344 if (this._secrets.consumer_key == null)
345 throw('No consumer_key set for OAuthSimple.');
346 this._parameters['oauth_consumer_key']=this._secrets.consumer_key;
347 return this._parameters.oauth_consumer_key;
348 };
349
350 this._getAccessToken = function() {
351 if (this._secrets['oauth_secret'] == null)
352 return '';
353 if (this._secrets['oauth_token'] == null)
354 throw('No oauth_token (access_token) set for OAuthSimple.');
355 this._parameters['oauth_token'] = this._secrets.oauth_token;
356 return this._parameters.oauth_token;
357 };
358
359 this._getTimestamp = function() {
360 var d = new Date();
361 var ts = Math.floor(d.getTime()/1000);
362 this._parameters['oauth_timestamp'] = ts;
363 return ts;
364 };
365
366 this._normalizedParameters = function() {
367 var elements = new Array();
368 var paramNames = [];
369 var ra =0;
370 for (var paramName in this._parameters)
371 {
372 if (ra++ > 1000)
373 throw('runaway 1');
374 paramNames.unshift(paramName);
375 }
376 paramNames = paramNames.sort();
377 pLen = paramNames.length;
378 for (var i=0;i<pLen; i++)
379 {
380 paramName=paramNames[i];
381 //skip secrets.
382 if (paramName.match(/\w+_secret/))
383 continue;
384 if (this._parameters[paramName] instanceof Array)
385 {
386 var sorted = this._parameters[paramName].sort();
387 var spLen = sorted.length;
388 for (var j = 0;j<spLen;j++){
389 if (ra++ > 1000)
390 throw('runaway 1');
391 elements.push(this._oauthEscape(paramName) + '=' +
392 this._oauthEscape(sorted[j]));
393 }
394 continue;
395 }
396 elements.push(this._oauthEscape(paramName) + '=' +
397 this._oauthEscape(this._parameters[paramName]));
398 }
399 return elements.join('&');
400 };
401
402 this.b64_hmac_sha1 = function(k,d,_p,_z){
403 // heavily optimized and compressed version of http://pajhome.org.uk/crypt/md5/sha1.js
404 // _p = b64pad, _z = character size; not used here but I left them available just in case
405 if(!_p){_p='=';}if(!_z){_z=8;}function _f(t,b,c,d){if(t<20){return(b&c)|((~b)&d);}if(t<40){return b^c^d;}if(t<60){return(b&c)|(b&d)|(c&d);}return b^c^d;}function _k(t){return(t<20)?1518500249:(t<40)?1859775393:(t<60)?-1894007588:-899497514;}function _s(x,y){var l=(x&0xFFFF)+(y&0xFFFF),m=(x>>16)+(y>>16)+(l>>16);return(m<<16)|(l&0xFFFF);}function _r(n,c){return(n<<c)|(n>>>(32-c));}function _c(x,l){x[l>>5]|=0x80<<(24-l%32);x[((l+64>>9)<<4)+15]=l;var w=[80],a=1732584193,b=-271733879,c=-1732584194,d=271733878,e=-1009589776;for(var i=0;i<x.length;i+=16){var o=a,p=b,q=c,r=d,s=e;for(var j=0;j<80;j++){if(j<16){w[j]=x[i+j];}else{w[j]=_r(w[j-3]^w[j-8]^w[j-14]^w[j-16],1);}var t=_s(_s(_r(a,5),_f(j,b,c,d)),_s(_s(e,w[j]),_k(j)));e=d;d=c;c=_r(b,30);b=a;a=t;}a=_s(a,o);b=_s(b,p);c=_s(c,q);d=_s(d,r);e=_s(e,s);}return[a,b,c,d,e];}function _b(s){var b=[],m=(1<<_z)-1;for(var i=0;i<s.length*_z;i+=_z){b[i>>5]|=(s.charCodeAt(i/8)&m)<<(32-_z-i%32);}return b;}function _h(k,d){var b=_b(k);if(b.length>16){b=_c(b,k.length*_z);}var p=[16],o=[16];for(var i=0;i<16;i++){p[i]=b[i]^0x36363636;o[i]=b[i]^0x5C5C5C5C;}var h=_c(p.concat(_b(d)),512+d.length*_z);return _c(o.concat(h),512+160);}function _n(b){var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",s='';for(var i=0;i<b.length*4;i+=3){var r=(((b[i>>2]>>8*(3-i%4))&0xFF)<<16)|(((b[i+1>>2]>>8*(3-(i+1)%4))&0xFF)<<8)|((b[i+2>>2]>>8*(3-(i+2)%4))&0xFF);for(var j=0;j<4;j++){if(i*8+j*6>b.length*32){s+=_p;}else{s+=t.charAt((r>>6*(3-j))&0x3F);}}}return s;}function _x(k,d){return _n(_h(k,d));}return _x(k,d);
406 };
407
408 this._generateSignature = function() {
409
410 var secretKey = this._oauthEscape(this._secrets.shared_secret)+'&'+
411 this._oauthEscape(this._secrets.oauth_secret);
412 if (this._parameters['oauth_signature_method'] == 'PLAINTEXT')
413 {
414 return {sig_string:null,signature:secretKey};
415 }
416 if (this._parameters['oauth_signature_method'] == 'HMAC-SHA1')
417 {
418 var sigString = this._oauthEscape(this._action)+'&'+this._oauthEscape(this._path)+'&'+this._oauthEscape(this._normalizedParameters());
419 return {'sig_string':sigString,'signature':this.b64_hmac_sha1(secretKey,sigString)};
420 }
421 return null;
422 };
423
424 return this;
425 }
426 }
427
428 function showAdvanced() {
429 document.getElementById('advanced').style.display="block";
430 document.getElementById('action_p').style.display="block";
431 }
432
433 function populate() {
434 var d = new Date();
435 document.getElementById('timestamp').value = Math.floor(d.getTime()/1000);
436 if (!document.getElementById('nonce').value)
437 document.getElementById('nonce').value = '1234';
438 jQuery('#advswitch').click(showAdvanced);
439 return;
440 }
441
442 function generate() {
443 var args=document.getElementById('args').value;
444 if (args)
445 args += '&';
446 /* Note: normally, you don't need to glom the parameters like this, but since we're allowing the user to overwrite them... */
447 var oauth = OAuthSimple();
448 oauth.setAction(document.getElementById('action').value);
449 try {
450 var o = oauth.sign({path:document.getElementById('path').value,
451 parameters: args+'oauth_nonce='+document.getElementById('nonce').value+'&oauth_timestamp='+document.getElementById('timestamp').value+'&oauth_version='+document.getElementById('version').value,
452 signatures:{
453 api_key:document.getElementById('key').value,
454 shared_secret:document.getElementById('secret').value,
455 access_token:document.getElementById('accessToken').value,
456 access_secret:document.getElementById('accessSecret').value
457 }
458 });
459
460 $("#testLink").show();
461 $("#result").show();
462 document.getElementById('result').innerHTML=o.signed_url;
463 testURL = o.signed_url;
464 document.getElementById('advanced').innerHTML = '<i>Signature String:<\/i> '+o.sig_string+'<br />';
465 }catch (e)
466 { alert (e); }
467 return false;
468 }
469
470 function testLink() {
471 $("#divTestOutput").show();
472 $("#testOutput").attr('src',testURL);
473 $("#resetLink").show();
474 }
475
476 function resetLink() {
477 $("#testOutput").attr('src','about:blank');
478 $("#divTestOutput").hide();
479 $("#resetLink").hide();
480 }
481 $(document).ready(function() {populate();jQuery('#refresh').click(function(){populate();});$('#testLink').click(function(){testLink();});$("#resetLink").click(function(){resetLink();});});
482 //]]>
483 </script>
484 <style type="text/css" media="screen">
485 /*<![CDATA[*/
486 <!--
487 body {font-family:Helvetica,Arial;font-size:9pt;}
488 #oauth_test p {margin:.5em 0 0 0;padding:0;width:900px;}
489 #oauth_test input {border:1px solid #DDD;background-color:#EEE;width:400px;}
490 #oauth_test label {width:100px;float:left;text-align:right; margin-right:.5em;}
491 #oauth_test button {margin-left:100px;}
492 #oauth_test hr {width:512px;margin-left: 0px;}
493 #oauth_test #result {width:500px;background-color:#EEF;margin:1em;padding:.5em;font-family:"courier new";font-size:10px;}
494 #oauth_test .optional {color:#888;}
495 #oauth_test .sample {margin:0 0 0 110px;padding:0;color:#AAA;font-style:italic;}
496 #oauth_test #advswitch {text-align:right; color:#888;}
497 #oauth_test #refresh {text-decoration:underline;}
498 #oauth_test #advanced {font-family:"courier new";background-color:#EEF; overflow:scroll;white-space:nowrap;}
499 #oauth_test #action_p {display:block;}
500 -->
501 /*]]>*/
502 </style>
503 </head>
504
505 <body>
506 <h2 class="first">JavaScript OAuth 1.0 Testing Tool</h2>
507
508 <div id="container" style="border: px coral solid;width:100%;">
509 <div id="oauth_test" style="float:left;width:550px;">
510 <form id="form" onsubmit="generate();return false;" name="form">
511 <p><label for="key" title=
512 "Also known as Consumer Key - the value provided when you registered your key or application.">
513 API Key:</label><input id="key" name="key" title=
514 "Also known as Consumer Key - the value provided when you registered your key or application." /></p>
515
516 <p><label for="secret">Shared Secret:</label><input id="secret" name="secret"
517 title="Shared secret that is associated with your consumer key." /></p>
518
519 <p><label class="optional" for="secret" title=
520 "Also known as User Token - a value provided after a user grants permission to your app.">
521 Access Token:</label><input id="accessToken" name="token" title=
522 "Also known as User Token -- a value provided after a user grants permission to your app." />
523 <span style="color:#888;font-size:12px;">(optional)</span></p>
524
525 <p><label class="optional" for="secret">Access Secret:</label><input id=
526 "accessSecret" name="tokensecret" /> <span style=
527 "color:#888;font-size:12px;">(optional)</span></p>
528 <hr />
529
530 <p id="action_p"><label for="action">Action:</label><select id="action" name=
531 "action">
532 <option value="GET">
533 GET
534 </option>
535
536 <option value="POST">
537 POST
538 </option>
539
540 <option value="PUT">
541 PUT
542 </option>
543
544 <option value="DELETE">
545 DELETE
546 </option>
547 </select></p>
548
549 <p><label for="path">Path:</label><input type="url" id="path" name="path" /></p>
550
551 <div class="sample">
552 Netflix example: http://api.netflix.com/catalog/titles
553 </div>
554
555 <p><label for="args">Args:</label><input id="args" name="args" /></p>
556
557 <div class="sample">
558 Netflix example: term=heavenly%20creatures&amp;output=json
559 </div>
560
561 <p><label for="nonce">Nonce:</label><input id="nonce" name="nonce" /></p>
562 <hr />
563
564 <p><label for="timestamp">Timestamp:</label><input id="timestamp" name=
565 "timestamp" style="width:100px;" /><span style=
566 "margin-left: 20px;font-size:12px;color:#888;" id="refresh">Refresh</span></p>
567
568 <p><label for="version">Version:</label><input id="version" name="version" value=
569 "1.0" style="width:100px;" /></p>
570 </form>
571
572 <p><button id="doit" onclick="generate();return false;">Generate Signed
573 Call</button></p>
574
575 <p id="result" style="display:none;">&nbsp;</p>
576
577 <p><button id="testLink" style="display:none;">Execute Live Call</button>
578 <button id="resetLink" style="display:none;">Clear API Response</button></p><br />
579
580 <p id="advanced" style="display:none;">&nbsp;</p>
581 </div><!-- oauth -->
582
583 <div id="right" style="position:absolute;left:590px;top:10px;width:500px;">
584 <div id="divTestOutput" style="display:none;">
585 <iframe src="about:blank" id="testOutput" style="width:100%;height:450px;" name=
586 "testOutput"></iframe>
587 </div>
588 </div>
589 </div><!-- container -->
590 </body>
591 </html>
592
Something went wrong with that request. Please try again.