Some styling updates and created a separate front page.
This commit is contained in:
		
							parent
							
								
									5db9c2010f
								
							
						
					
					
						commit
						7af2671993
					
				
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,2 +1,2 @@ | ||||
| js/pkijs.org | ||||
| js/browser-csr | ||||
| app/js/pkijs.org | ||||
| app/js/browser-csr | ||||
|  | ||||
							
								
								
									
										237
									
								
								app/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								app/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,237 @@ | ||||
| <html> | ||||
|   <head> | ||||
|     <title>Greenlock™</title> | ||||
|     <meta property="og:image" content="https://greenlock.ppl.family/img/greenlock-mark-400x400.png" /> | ||||
|     <link href="style/main.css" rel="stylesheet"> | ||||
|   </head> | ||||
|   <body hidden> | ||||
|     <div class="column-container wide"> | ||||
|       <div class="column-row"> | ||||
|         <img src="img/greenlock-146.png"> | ||||
|       </div> | ||||
|       <div class="column-row"> | ||||
|           <h1>Get the green lock for your website</h1> | ||||
| 
 | ||||
|         <!-- Step 1 Choose Domain(s) --> | ||||
|         <form class="js-acme-form js-acme-form-domains"> | ||||
|           <h1><label>What's your domain?</label></h1> | ||||
|           <h4>Certificates are valid for 90 days. Renewal is free :)</h4> | ||||
|           <input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required> | ||||
|           <br> | ||||
|           <button type="submit">Next</button> | ||||
| 
 | ||||
|           <br> | ||||
|           <br> | ||||
|           <br> | ||||
|           <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required> | ||||
|             Production</label> | ||||
|           <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required> | ||||
|             Testing</label> | ||||
|           <br> | ||||
|           <input class="js-acme-directory-url" type="url" placeholder="ACME directory url"> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 2 Create Account --> | ||||
|         <form class="js-acme-form js-acme-form-account"> | ||||
|           <h1><label>What's your email?</label></h1> | ||||
|           <input class="js-acme-account-email" type="email" placeholder="john@doe.family" required> | ||||
|           <br> | ||||
|           <br> | ||||
|           <label><input class="js-acme-account-tos" type="checkbox" required> | ||||
|             Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt™ Terms of Service</a>?</label> | ||||
|           <br> | ||||
|           <br> | ||||
|           <label><input class="js-greenlock-account-tos" type="checkbox" required> | ||||
|             Agree to <a class="js-gl-tos" target="_blank" href="./legal.html">Greenlock™ Terms of Service</a>?</label> | ||||
|           <br> | ||||
|           <br> | ||||
|           <!-- | ||||
|           <a href="#">advanced (use existing account)</a> | ||||
|           <br> | ||||
|           <br> | ||||
|           --> | ||||
|           <button type="submit">Next</button> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 3 Set Challanges --> | ||||
|         <form class="js-acme-form js-acme-form-challenges"> | ||||
| 
 | ||||
|           <h1>How will you validate your domain?</h1> | ||||
|           <br> | ||||
|           <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="http-01" checked required> | ||||
|             File Upload to HTTP Web Server</label> | ||||
|           <br> | ||||
|           <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="dns-01" required> | ||||
|             TXT Records on DNS Name Server</label> | ||||
|           <br> | ||||
| 
 | ||||
|           <div class="js-acme-challenges"> | ||||
| 
 | ||||
|           <h2>Verify Domains & Sub-Domains</h2> | ||||
| 
 | ||||
|           <table class="js-acme-table-http-01"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Hostname</th> | ||||
|                 <th>File Location</th> | ||||
|                 <th>File Contents</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td>example.com</td> | ||||
|                 <td>.well-known/acme-challenge/xxx</td> | ||||
|                 <td>sec.ret</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
| 
 | ||||
|           <table class="js-acme-table-dns-01"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Hostname</th> | ||||
|                 <th>TXT Host</th> | ||||
|                 <th>TXT Value</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td>example.com</td> | ||||
|                 <td>_acme-challenge.example.com</td> | ||||
|                 <td>4A54</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="js-acme-wildcard"> | ||||
|             <h2>Verify Wildcard Domains</h2> | ||||
| 
 | ||||
|             <table class="js-acme-table-wildcard"> | ||||
|               <thead> | ||||
|                 <tr> | ||||
|                   <th>Hostname</th> | ||||
|                   <th>TXT Host</th> | ||||
|                   <th>TXT Value</th> | ||||
|                 </tr> | ||||
|               </thead> | ||||
|               <tbody> | ||||
|                 <tr> | ||||
|                   <td>example.com</td> | ||||
|                   <td>_acme-challenge.example.com</td> | ||||
|                   <td>4A54</td> | ||||
|                 </tr> | ||||
|               </tbody> | ||||
|             </table> | ||||
|           </div> | ||||
| 
 | ||||
|           <button type="submit">Next</button> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 4 Process Challanges --> | ||||
|         <form class="js-acme-form js-acme-form-poll"> | ||||
|           Verifying Domains... (give us 5 seconds or so...) | ||||
| 
 | ||||
|           <!-- | ||||
|           <table class="js-acme-table-verifying"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Hostname</th> | ||||
|                 <th>Type</th> | ||||
|                 <th>Pass</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td>example.com</td> | ||||
|                 <td>http-01</td> | ||||
|                 <td>-</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
| 
 | ||||
|           <a href="#">advanced (use existing keypair for domain)</a> | ||||
| 
 | ||||
|           <button type="submit">Next</button> | ||||
|           --> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 5 Get Certs --> | ||||
|         <form class="js-acme-form js-acme-form-download"> | ||||
|           <div> | ||||
|           <h2><label>privkey.pem</label></h2> | ||||
|           <textarea cols="80" rows="10" class="js-privkey">-</textarea> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|           <h2><label>fullchain.pem</label></h2> | ||||
|           <textarea cols="80" rows="60" class="js-fullchain">-</textarea> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|           <h3>node.js https server example</h3> | ||||
|           <pre><code>'use strict'; | ||||
| 
 | ||||
|     var https = require('https'); | ||||
|     var server = https.createServer({ | ||||
|       key: require('fs').readFileSync('./privkey.pem') | ||||
|     , cert: require('fs').readFileSync('./fullchain.pem') | ||||
|     }, function (req, res) { | ||||
|       res.end("Hello, World!"); | ||||
|     }).listen(443, function () { | ||||
|       console.log('Listening on', this.address()); | ||||
|     }) | ||||
|     </code></pre> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- | ||||
|             TODO | ||||
|           <label>cert.pem</label> | ||||
|           <textarea class="js-cert">-</textarea> | ||||
| 
 | ||||
|           <label>chain.pem</label> | ||||
|           <textarea class="js-chain">-</textarea> | ||||
| 
 | ||||
|           <button type="button">Download SSL Certificates</button> | ||||
|           <br> | ||||
|           <a href="#">Advanced (copy and paste)</a> | ||||
|           <br> | ||||
|           <button type="submit">Start Over</button> | ||||
|           --> | ||||
|         </form> | ||||
| 
 | ||||
|           <br> | ||||
|           <br> | ||||
|           <br> | ||||
|           <div><small> | ||||
|           <h3></h3> | ||||
|           <a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git) | ||||
|           <!-- or | ||||
|           <pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre> | ||||
|           Or view the live site code (same as live-site branch): | ||||
|           <pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre> | ||||
|           --> | ||||
|           </small></div> | ||||
| 
 | ||||
|         <script src="./js/bacme.js"></script> | ||||
|         <script src="./js/app.js"></script> | ||||
| 
 | ||||
|         <script src="./js/pkijs.org/v1.3.33/common.js"></script> | ||||
|         <script src="./js/pkijs.org/v1.3.33/asn1.js"></script> | ||||
|         <script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script> | ||||
|         <script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script> | ||||
|         <script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script> | ||||
| 
 | ||||
|         <!-- Global site tag (gtag.js) - Google Analytics --> | ||||
|         <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script> | ||||
|         <script> | ||||
|           window.dataLayer = window.dataLayer || []; | ||||
|           function gtag(){dataLayer.push(arguments);} | ||||
|           gtag('js', new Date()); | ||||
| 
 | ||||
|           gtag('config', 'UA-118745161-2'); | ||||
|         </script> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										514
									
								
								app/js/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										514
									
								
								app/js/app.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,514 @@ | ||||
| (function () { | ||||
| 'use strict'; | ||||
| 
 | ||||
|   var $qs = function (s) { return window.document.querySelector(s); }; | ||||
|   var $qsa = function (s) { return window.document.querySelectorAll(s); }; | ||||
|   var info = {}; | ||||
|   var steps = {}; | ||||
|   var nonce; | ||||
|   var kid; | ||||
|   var i = 1; | ||||
| 
 | ||||
|   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; | ||||
|   function updateApiType() { | ||||
|     var input = this || Array.prototype.filter.call( | ||||
|       $qsa('.js-acme-api-type'), function ($el) { return $el.checked; } | ||||
|     )[0]; | ||||
|     console.log('ACME api type radio:', input.value); | ||||
|     $qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value); | ||||
|   } | ||||
|   $qsa('.js-acme-api-type').forEach(function ($el) { | ||||
|     $el.addEventListener('change', updateApiType); | ||||
|   }); | ||||
|   updateApiType(); | ||||
| 
 | ||||
|   function hideForms() { | ||||
|     $qsa('.js-acme-form').forEach(function (el) { | ||||
|       el.hidden = true; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   function submitForm(ev) { | ||||
|     var j = i; | ||||
|     i += 1; | ||||
|     steps[j].submit(ev); | ||||
|   } | ||||
|   $qsa('.js-acme-form').forEach(function ($el) { | ||||
|     $el.addEventListener('submit', function (ev) { | ||||
|       ev.preventDefault(); | ||||
|       submitForm(ev); | ||||
|     }); | ||||
|   }); | ||||
|   function updateChallengeType() { | ||||
|     var input = this || Array.prototype.filter.call( | ||||
|       $qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; } | ||||
|     )[0]; | ||||
|     console.log('ch type radio:', input.value); | ||||
|     $qs('.js-acme-table-wildcard').hidden = true; | ||||
|     $qs('.js-acme-table-http-01').hidden = true; | ||||
|     $qs('.js-acme-table-dns-01').hidden = true; | ||||
|     if (info.challenges.wildcard) { | ||||
|       $qs('.js-acme-table-wildcard').hidden = false; | ||||
|     } | ||||
|     if (info.challenges[input.value]) { | ||||
|       $qs('.js-acme-table-' + input.value).hidden = false; | ||||
|     } | ||||
|   } | ||||
|   $qsa('.js-acme-challenge-type').forEach(function ($el) { | ||||
|     $el.addEventListener('change', updateChallengeType); | ||||
|   }); | ||||
| 
 | ||||
|   function saveContact(email, domains) { | ||||
|     // to be used for good, not evil
 | ||||
|     return window.fetch('https://api.ppl.family/api/ppl.family/public/list', { | ||||
|       method: 'POST' | ||||
|     , cors: true | ||||
|     , headers: new Headers({ 'Content-Type': 'application/json' }) | ||||
|     , body: JSON.stringify({ address: email, comment: 'greenlock sub for ' + domains.join(',') }) | ||||
|     }).then(function (resp) { | ||||
|       return resp.json().then(function (data) { | ||||
|         /* | ||||
|         if (data.error) { | ||||
|           window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead."); | ||||
|           return; | ||||
|         } | ||||
|         */ | ||||
|       }); | ||||
|     }, function () { | ||||
|       /* | ||||
|       window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead."); | ||||
|       */ | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   steps[1] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-domains').hidden = false; | ||||
|   }; | ||||
|   steps[1].submit = function () { | ||||
|     info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) { | ||||
|       return { type: 'dns', value: hostname.toLowerCase().trim() }; | ||||
|     }); | ||||
|     info.identifiers.sort(function (a, b) { | ||||
|       if (a === b) { return 0; } | ||||
|       if (a < b) { return 1; } | ||||
|       if (a > b) { return -1; } | ||||
|     }); | ||||
| 
 | ||||
|     return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) { | ||||
|       $qs('.js-acme-tos-url').href = directory.meta.termsOfService; | ||||
|       return BACME.nonce().then(function (_nonce) { | ||||
|         nonce = _nonce; | ||||
| 
 | ||||
|         console.log("MAGIC STEP NUMBER in 1 is:", i); | ||||
|         steps[i](); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   steps[2] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-account').hidden = false; | ||||
|   }; | ||||
|   steps[2].submit = function () { | ||||
|     var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); | ||||
| 
 | ||||
| 
 | ||||
|     info.contact = [ 'mailto:' + email ]; | ||||
|     info.agree = $qs('.js-acme-account-tos').checked; | ||||
|     info.greenlockAgree = $qs('.js-gl-tos').checked; | ||||
|     // TODO
 | ||||
|     // options for
 | ||||
|     // * regenerate key
 | ||||
|     // * ECDSA / RSA / bitlength
 | ||||
| 
 | ||||
|     // TODO ping with version and account creation
 | ||||
|     setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; })); | ||||
| 
 | ||||
|     var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null'); | ||||
|     var p; | ||||
| 
 | ||||
|     function createKeypair() { | ||||
|       return BACME.accounts.generateKeypair({ | ||||
|         type: 'ECDSA' | ||||
|       , bitlength: '256' | ||||
|       }).then(function (jwk) { | ||||
|         localStorage.setItem('account:' + email, JSON.stringify(jwk)); | ||||
|         return jwk; | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     if (jwk) { | ||||
|       p = Promise.resolve(jwk); | ||||
|     } else { | ||||
|       p = createKeypair(); | ||||
|     } | ||||
| 
 | ||||
|     function createAccount(jwk) { | ||||
|       console.log('account jwk:'); | ||||
|       console.log(jwk); | ||||
|       delete jwk.key_ops; | ||||
|       info.jwk = jwk; | ||||
|       return BACME.accounts.sign({ | ||||
|         jwk: jwk | ||||
|       , contacts: [ 'mailto:' + email ] | ||||
|       , agree: info.agree | ||||
|       , nonce: nonce | ||||
|       , kid: kid | ||||
|       }).then(function (signedAccount) { | ||||
|         return BACME.accounts.set({ | ||||
|           signedAccount: signedAccount | ||||
|         }).then(function (account) { | ||||
|           console.log('account:'); | ||||
|           console.log(account); | ||||
|           kid = account.kid; | ||||
|           return kid; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function (_jwk) { | ||||
|       jwk = _jwk; | ||||
|       kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null'); | ||||
|       var p2 | ||||
| 
 | ||||
|       // TODO save account id rather than always retrieving it
 | ||||
|       if (kid) { | ||||
|         p2 = Promise.resolve(kid); | ||||
|       } else { | ||||
|         p2 = createAccount(jwk); | ||||
|       } | ||||
| 
 | ||||
|       return p2.then(function (_kid) { | ||||
|         kid = _kid; | ||||
|         info.kid = kid; | ||||
|         return BACME.orders.sign({ | ||||
|           jwk: jwk | ||||
|         , identifiers: info.identifiers | ||||
|         , kid: kid | ||||
|         }).then(function (signedOrder) { | ||||
|           return BACME.orders.create({ | ||||
|             signedOrder: signedOrder | ||||
|           }).then(function (order) { | ||||
|             info.finalizeUrl = order.finalize; | ||||
|             info.orderUrl = order.url; // from header Location ???
 | ||||
|             return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) { | ||||
|               return BACME.challenges.all().then(function (claims) { | ||||
|                 console.log('claims:'); | ||||
|                 console.log(claims); | ||||
|                 var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; | ||||
|                 var map = { | ||||
|                   'http-01': '.js-acme-table-http-01' | ||||
|                 , 'dns-01': '.js-acme-table-dns-01' | ||||
|                 , 'wildcard': '.js-acme-table-wildcard' | ||||
|                 } | ||||
|                 var tpls = {}; | ||||
|                 info.challenges = obj; | ||||
|                 Object.keys(map).forEach(function (k) { | ||||
|                   var sel = map[k] + ' tbody'; | ||||
|                   console.log(sel); | ||||
|                   tpls[k] = $qs(sel).innerHTML; | ||||
|                   $qs(map[k] + ' tbody').innerHTML = ''; | ||||
|                 }); | ||||
| 
 | ||||
|                 // TODO make Promise-friendly
 | ||||
|                 return Promise.all(claims.map(function (claim) { | ||||
|                   var hostname = claim.identifier.value; | ||||
|                   return Promise.all(claim.challenges.map(function (c) { | ||||
|                     var keyAuth = BACME.challenges['http-01']({ | ||||
|                       token: c.token | ||||
|                     , thumbprint: thumbprint | ||||
|                     , challengeDomain: hostname | ||||
|                     }); | ||||
|                     return BACME.challenges['dns-01']({ | ||||
|                       keyAuth: keyAuth.value | ||||
|                     , challengeDomain: hostname | ||||
|                     }).then(function (dnsAuth) { | ||||
|                       var data = { | ||||
|                         type: c.type | ||||
|                       , hostname: hostname | ||||
|                       , url: c.url | ||||
|                       , token: c.token | ||||
|                       , keyAuthorization: keyAuth | ||||
|                       , httpPath: keyAuth.path | ||||
|                       , httpAuth: keyAuth.value | ||||
|                       , dnsType: dnsAuth.type | ||||
|                       , dnsHost: dnsAuth.host | ||||
|                       , dnsAnswer: dnsAuth.answer | ||||
|                       }; | ||||
| 
 | ||||
|                       console.log(''); | ||||
|                       console.log('CHALLENGE'); | ||||
|                       console.log(claim); | ||||
|                       console.log(c); | ||||
|                       console.log(data); | ||||
|                       console.log(''); | ||||
| 
 | ||||
|                       if (claim.wildcard) { | ||||
|                         obj.wildcard.push(data); | ||||
|                         $qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>'; | ||||
|                       } else if(obj[data.type]) { | ||||
| 
 | ||||
|                         obj[data.type].push(data); | ||||
|                         if ('dns-01' === data.type) { | ||||
|                           $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>'; | ||||
|                         } else if ('http-01' === data.type) { | ||||
|                           $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.httpPath + '</td><td>' + data.httpAuth + '</td></tr>'; | ||||
|                         } | ||||
|                       } | ||||
| 
 | ||||
|                     }); | ||||
| 
 | ||||
|                   })); | ||||
|                 })).then(function () { | ||||
| 
 | ||||
|                   // hide wildcard if no wildcard
 | ||||
|                   // hide http-01 and dns-01 if only wildcard
 | ||||
|                   if (!obj.wildcard.length) { | ||||
|                     $qs('.js-acme-wildcard').hidden = true; | ||||
|                   } | ||||
|                   if (!obj['http-01'].length) { | ||||
|                     $qs('.js-acme-challenges').hidden = true; | ||||
|                   } | ||||
| 
 | ||||
|                   updateChallengeType(); | ||||
| 
 | ||||
|                   console.log("MAGIC STEP NUMBER in 2 is:", i); | ||||
|                   steps[i](); | ||||
|                 }); | ||||
| 
 | ||||
|               }); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }).catch(function (err) { | ||||
|       console.error('Step \'' + i + '\' Error:'); | ||||
|       console.error(err); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   steps[3] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-challenges').hidden = false; | ||||
|   }; | ||||
|   steps[3].submit = function () { | ||||
|     var chType; | ||||
|     Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) { | ||||
|       if ($el.checked) { | ||||
|         chType = $el.value; | ||||
|         return true; | ||||
|       } | ||||
|     }); | ||||
|     console.log('chType is:', chType); | ||||
|     var chs = []; | ||||
| 
 | ||||
|     // do each wildcard, if any
 | ||||
|     // do each challenge, by selected type only
 | ||||
|     [ 'wildcard', chType].forEach(function (typ) { | ||||
|       info.challenges[typ].forEach(function (ch) { | ||||
|         // { jwk, challengeUrl, accountId (kid) }
 | ||||
|         chs.push({ | ||||
|           jwk: info.jwk | ||||
|         , challengeUrl: ch.url | ||||
|         , accountId: info.kid | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|     console.log("INFO.challenges !!!!!", info.challenges); | ||||
| 
 | ||||
|     var results = []; | ||||
|     function nextChallenge() { | ||||
|       var ch = chs.pop(); | ||||
|       if (!ch) { return results; } | ||||
|       return BACME.challenges.accept(ch).then(function (result) { | ||||
|         results.push(result); | ||||
|         return nextChallenge(); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // for now just show the next page immediately (its a spinner)
 | ||||
|     steps[i](); | ||||
|     return nextChallenge().then(function (results) { | ||||
|       console.log('challenge status:', results); | ||||
|       var polls = results.slice(0); | ||||
|       var allsWell = true; | ||||
| 
 | ||||
|       function checkPolls() { | ||||
|         return new Promise(function (resolve) { | ||||
|           setTimeout(resolve, 1000); | ||||
|         }).then(function () { | ||||
|           return Promise.all(polls.map(function (poll) { | ||||
|             return BACME.challenges.check({ challengePollUrl: poll.url }); | ||||
|           })).then(function (polls) { | ||||
|             console.log(polls); | ||||
| 
 | ||||
|             polls = polls.filter(function (poll) { | ||||
|               //return 'valid' !== poll.status && 'invalid' !== poll.status;
 | ||||
|               if ('pending' === poll.status) { | ||||
|                 return true; | ||||
|               } | ||||
|               if ('valid' !== poll.status) { | ||||
|                 allsWell = false; | ||||
|                 console.warn('BAD POLL STATUS', poll); | ||||
|               } | ||||
|               // TODO show status in HTML
 | ||||
|             }); | ||||
| 
 | ||||
|             if (polls.length) { | ||||
|               return checkPolls(); | ||||
|             } | ||||
|             return true; | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       return checkPolls().then(function () { | ||||
|         if (allsWell) { | ||||
|           return submitForm(); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   // spinner
 | ||||
|   steps[4] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-poll').hidden = false; | ||||
|   } | ||||
|   steps[4].submit = function () { | ||||
|     console.log('Congrats! Auto advancing...'); | ||||
| 
 | ||||
|     var key = info.identifiers.map(function (ident) { return ident.value; }).join(','); | ||||
|     var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null'); | ||||
|     var p; | ||||
| 
 | ||||
|     function createKeypair() { | ||||
|       return BACME.accounts.generateKeypair({ | ||||
|         type: 'ECDSA' | ||||
|       , bitlength: '256' | ||||
|       }).then(function (serverJwk) { | ||||
|         localStorage.setItem('server:' + key, JSON.stringify(serverJwk)); | ||||
|         return serverJwk; | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     if (serverJwk) { | ||||
|       p = Promise.resolve(serverJwk); | ||||
|     } else { | ||||
|       p = createKeypair(); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function (_serverJwk) { | ||||
|       serverJwk = _serverJwk; | ||||
|       info.serverJwk = serverJwk; | ||||
|       // { serverJwk, domains }
 | ||||
|       return BACME.orders.generateCsr({ | ||||
|         serverJwk: serverJwk | ||||
|       , domains: info.identifiers.map(function (ident) { | ||||
|           return ident.value; | ||||
|         }) | ||||
|       }).then(function (csrweb64) { | ||||
|         return BACME.orders.finalize({ | ||||
|           csr: csrweb64 | ||||
|         , jwk: info.jwk | ||||
|         , finalizeUrl: info.finalizeUrl | ||||
|         , accountId: info.kid | ||||
|         }); | ||||
|       }).then(function () { | ||||
|         function checkCert() { | ||||
|           return new Promise(function (resolve) { | ||||
|             setTimeout(resolve, 1000); | ||||
|           }).then(function () { | ||||
|             return BACME.orders.check({ orderUrl: info.orderUrl }); | ||||
|           }).then(function (reply) { | ||||
|             if ('processing' === reply) { | ||||
|               return checkCert(); | ||||
|             } | ||||
|             return reply; | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         return checkCert(); | ||||
|       }).then(function (reply) { | ||||
|         return BACME.orders.receive({ certificateUrl: reply.certificate }); | ||||
|       }).then(function (certs) { | ||||
|         console.log('WINNING!'); | ||||
|         console.log(certs); | ||||
|         $qs('.js-fullchain').value = certs; | ||||
| 
 | ||||
|         // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
 | ||||
| 				function spkiToPEM(keydata){ | ||||
| 						var keydataS = arrayBufferToString(keydata); | ||||
| 						var keydataB64 = window.btoa(keydataS); | ||||
| 						var keydataB64Pem = formatAsPem(keydataB64); | ||||
| 						return keydataB64Pem; | ||||
| 				} | ||||
| 
 | ||||
| 				function arrayBufferToString( buffer ) { | ||||
| 						var binary = ''; | ||||
| 						var bytes = new Uint8Array( buffer ); | ||||
| 						var len = bytes.byteLength; | ||||
| 						for (var i = 0; i < len; i++) { | ||||
| 								binary += String.fromCharCode( bytes[ i ] ); | ||||
| 						} | ||||
| 						return binary; | ||||
| 				} | ||||
| 
 | ||||
| 
 | ||||
| 				function formatAsPem(str) { | ||||
| 						var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n'; | ||||
| 
 | ||||
| 						while(str.length > 0) { | ||||
| 								finalString += str.substring(0, 64) + '\n'; | ||||
| 								str = str.substring(64); | ||||
| 						} | ||||
| 
 | ||||
| 						finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----'; | ||||
| 
 | ||||
| 						return finalString; | ||||
| 				} | ||||
| 
 | ||||
|         var wcOpts; | ||||
|         var pemName; | ||||
|         if (/^R/.test(info.serverJwk.kty)) { | ||||
|           pemName = 'RSA'; | ||||
|           wcOpts = { | ||||
|             name: "RSASSA-PKCS1-v1_5" | ||||
|           , hash: { name: "SHA-256" } | ||||
|           }; | ||||
|         } else { | ||||
|           pemName = 'EC'; | ||||
|           wcOpts = { | ||||
|             name: "ECDSA" | ||||
|           , namedCurve: "P-256" | ||||
|           } | ||||
|         } | ||||
| 				return crypto.subtle.importKey( | ||||
|           "jwk" | ||||
|         , info.serverJwk | ||||
|         , wcOpts | ||||
|         , true | ||||
|         , ["sign"] | ||||
| 				).then(function (privateKey) { | ||||
| 				  return window.crypto.subtle.exportKey("pkcs8", privateKey); | ||||
| 				}).then (function (keydata) { | ||||
| 					var pem = spkiToPEM(keydata); | ||||
| 					$qs('.js-privkey').value = pem; | ||||
|           steps[i](); | ||||
| 				}).catch(function(err){ | ||||
| 					console.error(err); | ||||
| 				}); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   steps[5] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-download').hidden = false; | ||||
|   } | ||||
| 
 | ||||
|   steps[1](); | ||||
| 
 | ||||
|   $qs('body').hidden = false; | ||||
| }()); | ||||
							
								
								
									
										
											BIN
										
									
								
								fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										303
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								index.html
									
									
									
									
									
								
							| @ -2,236 +2,93 @@ | ||||
|   <head> | ||||
|     <title>Greenlock™</title> | ||||
|     <meta property="og:image" content="https://greenlock.ppl.family/img/greenlock-mark-400x400.png" /> | ||||
|     <link href="style/main.css" rel="stylesheet"> | ||||
|     <link href="styles/main.css" rel="stylesheet"> | ||||
|     <style> | ||||
|       @font-face { | ||||
|         font-family: 'Source Sans Pro'; | ||||
|         font-style: normal; | ||||
|         font-display: block; | ||||
|         font-weight: 400; | ||||
|         src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'), url(./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2) format('woff2'); | ||||
|         unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
|       } | ||||
|       @font-face { | ||||
|         font-family: 'Source Sans Pro'; | ||||
|         font-style: normal; | ||||
|         font-weight: 700; | ||||
|         font-display: block; | ||||
|         src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), url(./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2) format('woff2'); | ||||
|         unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; | ||||
|       } | ||||
|     </style> | ||||
|     <link rel="preload" href="./fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous"> | ||||
|     <link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous"> | ||||
| 
 | ||||
|     <link rel="prefetch" href="./app/js/app.js"> | ||||
|     <link rel="prefetch" href="./app/js/bacme.js"> | ||||
|     <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/common.js"> | ||||
|     <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/asn1.js"> | ||||
|     <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_schema.js"> | ||||
|     <link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_simpl.js"> | ||||
|     <link rel="prefetch" href="./app/js/browser-csr/v1.0.0-alpha/csr.js"> | ||||
|   </head> | ||||
|   <body hidden> | ||||
|   <body class="js-app-ready"> | ||||
|     <script> | ||||
|       document.querySelector('body').classList.remove("js-app-ready"); | ||||
|     </script> | ||||
|     <div class="column-container wide"> | ||||
| 
 | ||||
|       <div class="column-row"> | ||||
|         <img src="img/greenlock-146.png"> | ||||
|       </div> | ||||
|       <div class="column-row"> | ||||
|           <h1>Get the green lock for your website</h1> | ||||
| 
 | ||||
|         <!-- Step 1 Choose Domain(s) --> | ||||
|         <form class="js-acme-form js-acme-form-domains"> | ||||
|           <h1><label>What's your domain?</label></h1> | ||||
|           <h4>Certificates are valid for 90 days. Renewal is free :)</h4> | ||||
|           <input class="js-acme-domains" type="text" placeholder="example.com,*.example.com" required> | ||||
|           <br> | ||||
|           <button type="submit">Next</button> | ||||
| 
 | ||||
|           <br> | ||||
|           <br> | ||||
|           <br> | ||||
|           <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="v02" checked required> | ||||
|             Production</label> | ||||
|           <label><input class="js-acme-api-type" name="acme-api-type" type="radio" value="staging-v02" required> | ||||
|             Testing</label> | ||||
|           <br> | ||||
|           <input class="js-acme-directory-url" type="url" placeholder="ACME directory url"> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 2 Create Account --> | ||||
|         <form class="js-acme-form js-acme-form-account"> | ||||
|           <h1><label>What's your email?</label></h1> | ||||
|           <input class="js-acme-account-email" type="email" placeholder="john@doe.family" required> | ||||
|           <br> | ||||
|           <br> | ||||
|           <label><input class="js-acme-account-tos" type="checkbox" required> | ||||
|             Agree to <a class="js-acme-tos-url" target="acme-tos">Let's Encrypt™ Terms of Service</a>?</label> | ||||
|           <br> | ||||
|           <br> | ||||
|           <label><input class="js-greenlock-account-tos" type="checkbox" required> | ||||
|             Agree to <a class="js-gl-tos" target="_blank" href="./legal.html">Greenlock™ Terms of Service</a>?</label> | ||||
|           <br> | ||||
|           <br> | ||||
|           <!-- | ||||
|           <a href="#">advanced (use existing account)</a> | ||||
|           <br> | ||||
|           <br> | ||||
|           --> | ||||
|           <button type="submit">Next</button> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 3 Set Challanges --> | ||||
|         <form class="js-acme-form js-acme-form-challenges"> | ||||
| 
 | ||||
|           <h1>How will you validate your domain?</h1> | ||||
|           <br> | ||||
|           <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="http-01" checked required> | ||||
|             File Upload to HTTP Web Server</label> | ||||
|           <br> | ||||
|           <label><input class="js-acme-challenge-type" name="acme-challenge-type" type="radio" value="dns-01" required> | ||||
|             TXT Records on DNS Name Server</label> | ||||
|           <br> | ||||
| 
 | ||||
|           <div class="js-acme-challenges"> | ||||
| 
 | ||||
|           <h2>Verify Domains & Sub-Domains</h2> | ||||
| 
 | ||||
|           <table class="js-acme-table-http-01"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Hostname</th> | ||||
|                 <th>File Location</th> | ||||
|                 <th>File Contents</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td>example.com</td> | ||||
|                 <td>.well-known/acme-challenge/xxx</td> | ||||
|                 <td>sec.ret</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
| 
 | ||||
|           <table class="js-acme-table-dns-01"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Hostname</th> | ||||
|                 <th>TXT Host</th> | ||||
|                 <th>TXT Value</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td>example.com</td> | ||||
|                 <td>_acme-challenge.example.com</td> | ||||
|                 <td>4A54</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="js-acme-wildcard"> | ||||
|             <h2>Verify Wildcard Domains</h2> | ||||
| 
 | ||||
|             <table class="js-acme-table-wildcard"> | ||||
|               <thead> | ||||
|                 <tr> | ||||
|                   <th>Hostname</th> | ||||
|                   <th>TXT Host</th> | ||||
|                   <th>TXT Value</th> | ||||
|                 </tr> | ||||
|               </thead> | ||||
|               <tbody> | ||||
|                 <tr> | ||||
|                   <td>example.com</td> | ||||
|                   <td>_acme-challenge.example.com</td> | ||||
|                   <td>4A54</td> | ||||
|                 </tr> | ||||
|               </tbody> | ||||
|             </table> | ||||
|           </div> | ||||
| 
 | ||||
|           <button type="submit">Next</button> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 4 Process Challanges --> | ||||
|         <form class="js-acme-form js-acme-form-poll"> | ||||
|           Verifying Domains... (give us 5 seconds or so...) | ||||
| 
 | ||||
|           <!-- | ||||
|           <table class="js-acme-table-verifying"> | ||||
|             <thead> | ||||
|               <tr> | ||||
|                 <th>Hostname</th> | ||||
|                 <th>Type</th> | ||||
|                 <th>Pass</th> | ||||
|               </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td>example.com</td> | ||||
|                 <td>http-01</td> | ||||
|                 <td>-</td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
| 
 | ||||
|           <a href="#">advanced (use existing keypair for domain)</a> | ||||
| 
 | ||||
|           <button type="submit">Next</button> | ||||
|           --> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Step 5 Get Certs --> | ||||
|         <form class="js-acme-form js-acme-form-download"> | ||||
|           <div> | ||||
|           <h2><label>privkey.pem</label></h2> | ||||
|           <textarea cols="80" rows="10" class="js-privkey">-</textarea> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|           <h2><label>fullchain.pem</label></h2> | ||||
|           <textarea cols="80" rows="60" class="js-fullchain">-</textarea> | ||||
|           </div> | ||||
| 
 | ||||
|           <div> | ||||
|           <h3>node.js https server example</h3> | ||||
|           <pre><code>'use strict'; | ||||
| 
 | ||||
|     var https = require('https'); | ||||
|     var server = https.createServer({ | ||||
|       key: require('fs').readFileSync('./privkey.pem') | ||||
|     , cert: require('fs').readFileSync('./fullchain.pem') | ||||
|     }, function (req, res) { | ||||
|       res.end("Hello, World!"); | ||||
|     }).listen(443, function () { | ||||
|       console.log('Listening on', this.address()); | ||||
|     }) | ||||
|     </code></pre> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- | ||||
|             TODO | ||||
|           <label>cert.pem</label> | ||||
|           <textarea class="js-cert">-</textarea> | ||||
| 
 | ||||
|           <label>chain.pem</label> | ||||
|           <textarea class="js-chain">-</textarea> | ||||
| 
 | ||||
|           <button type="button">Download SSL Certificates</button> | ||||
|           <br> | ||||
|           <a href="#">Advanced (copy and paste)</a> | ||||
|           <br> | ||||
|           <button type="submit">Start Over</button> | ||||
|           --> | ||||
|         </form> | ||||
| 
 | ||||
|           <br> | ||||
|           <br> | ||||
|           <br> | ||||
|           <div><small> | ||||
|           <h3></h3> | ||||
|           <a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git) | ||||
|           <!-- or | ||||
|           <pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre> | ||||
|           Or view the live site code (same as live-site branch): | ||||
|           <pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre> | ||||
|           --> | ||||
|           </small></div> | ||||
| 
 | ||||
|         <script src="./js/bacme.js"></script> | ||||
|         <script src="./js/app.js"></script> | ||||
| 
 | ||||
|         <script src="./js/pkijs.org/v1.3.33/common.js"></script> | ||||
|         <script src="./js/pkijs.org/v1.3.33/asn1.js"></script> | ||||
|         <script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script> | ||||
|         <script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script> | ||||
|         <script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script> | ||||
| 
 | ||||
|         <!-- Global site tag (gtag.js) - Google Analytics --> | ||||
|         <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script> | ||||
|         <script> | ||||
|           window.dataLayer = window.dataLayer || []; | ||||
|           function gtag(){dataLayer.push(arguments);} | ||||
|           gtag('js', new Date()); | ||||
| 
 | ||||
|           gtag('config', 'UA-118745161-2'); | ||||
|         </script> | ||||
|         <h1>Get the green lock for your website</h1> | ||||
|       </div> | ||||
|       <div class="column-row"> | ||||
|         <!-- Step 1 Choose Domain(s) --> | ||||
|         <form id="js-acme-form" action="./app/" method=> | ||||
|           <div class="domain-psuedo-input"> | ||||
|             <span class="secure-green">Secure</span> | <span class="secure-green">https:</span>//<input aria-label="domains to secure" id="acme-domains" type="text" name="acme-domains" placeholder="Your domain name" required> | ||||
|           </div> | ||||
|           <button type="submit">Go</button> | ||||
|           <div class="domain-subtext">Domain, subdomain, or wildcard domain</div> | ||||
|            | ||||
|           <div class="acme-advanced-fields"> | ||||
|             <label><input name="acme-api-type" type="radio" value="v02" checked required> | ||||
|               Production | ||||
|             </label> | ||||
|             <label><input name="acme-api-type" type="radio" value="staging-v02" required> | ||||
|               Testing</label> | ||||
|             <input id="js-acme-api-url" name="acme-api-url" type="url" placeholder="ACME directory url"> | ||||
|             <div> | ||||
|               <a href="https://git.coolaj86.com/coolaj86/greenlock.html">View Source</a> (git) | ||||
|             </div> | ||||
|           </div> | ||||
|         </form> | ||||
|       </div> | ||||
|       <div class="column-row"> | ||||
|         <div class="why-you-need"> | ||||
|           <h2>Why you need SSL certificates</h2> | ||||
|           If your website doesn't have the green lock from an SSL Certificate, Google Chrome will soon label your website as not secure. | ||||
|         </div> | ||||
|       </div> | ||||
|       <!-- or | ||||
|       <pre><code>git clone https://git.coolaj86.com/coolaj86/greenlock.html.git</code></pre> | ||||
|       Or view the live site code (same as live-site branch): | ||||
|       <pre><code>wget https://greenlock.ppl.family --mirror --convert-links --adjust-extension --page-requisites --no-parent</code></pre> | ||||
|       --> | ||||
| 
 | ||||
|       <script src="./js/app.js"></script> | ||||
| 
 | ||||
|       <!-- Global site tag (gtag.js) - Google Analytics --> | ||||
|       <script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script> | ||||
|       <script> | ||||
|         window.dataLayer = window.dataLayer || []; | ||||
|         function gtag(){dataLayer.push(arguments);} | ||||
|         gtag('js', new Date()); | ||||
| 
 | ||||
|         gtag('config', 'UA-118745161-2'); | ||||
|       </script> | ||||
|     </div> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| @ -1,14 +1,14 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| mkdir -p js/pkijs.org/v1.3.33/ | ||||
| pushd js/pkijs.org/v1.3.33/ | ||||
| mkdir -p app/js/pkijs.org/v1.3.33/ | ||||
| pushd app/js/pkijs.org/v1.3.33/ | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_schema.js | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.js | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js | ||||
| popd | ||||
| 
 | ||||
| mkdir -p js/browser-csr/v1.0.0-alpha/ | ||||
| pushd js/browser-csr/v1.0.0-alpha/ | ||||
| mkdir -p app/js/browser-csr/v1.0.0-alpha/ | ||||
| pushd app/js/browser-csr/v1.0.0-alpha/ | ||||
|   wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/01cdc0e91b5bf03f12e1b25b4129e3cde927987c/csr.js | ||||
| popd | ||||
|  | ||||
							
								
								
									
										516
									
								
								js/app.js
									
									
									
									
									
								
							
							
						
						
									
										516
									
								
								js/app.js
									
									
									
									
									
								
							| @ -3,512 +3,26 @@ | ||||
| 
 | ||||
|   var $qs = function (s) { return window.document.querySelector(s); }; | ||||
|   var $qsa = function (s) { return window.document.querySelectorAll(s); }; | ||||
|   var info = {}; | ||||
|   var steps = {}; | ||||
|   var nonce; | ||||
|   var kid; | ||||
|   var i = 1; | ||||
| 
 | ||||
|   var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; | ||||
|   function updateApiType() { | ||||
|     var input = this || Array.prototype.filter.call( | ||||
|       $qsa('.js-acme-api-type'), function ($el) { return $el.checked; } | ||||
|     )[0]; | ||||
|     console.log('ACME api type radio:', input.value); | ||||
|     $qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value); | ||||
|     var formData = new FormData($qs("#js-acme-form")); | ||||
| 
 | ||||
|     console.log('ACME api type radio:'); | ||||
| 
 | ||||
|     var value = formData.get("acme-api-type"); | ||||
|     $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value); | ||||
|   } | ||||
|   $qsa('.js-acme-api-type').forEach(function ($el) { | ||||
|     $el.addEventListener('change', updateApiType); | ||||
|   }); | ||||
|   $qs('#js-acme-form').addEventListener('change', updateApiType); | ||||
| 
 | ||||
|   updateApiType(); | ||||
| 
 | ||||
|   function hideForms() { | ||||
|     $qsa('.js-acme-form').forEach(function (el) { | ||||
|       el.hidden = true; | ||||
|   try { | ||||
|     document.fonts.load().then(function() { | ||||
|       $qs('body').classList.add("js-app-ready"); | ||||
|     }).catch(function(error) { | ||||
|       $qs('body').classList.add("js-app-ready"); | ||||
|     }); | ||||
|   } catch(e) { | ||||
|     setTimeout(function() {$qs('body').classList.add("js-app-ready");}, 200); | ||||
|   } | ||||
| 
 | ||||
|   function submitForm(ev) { | ||||
|     var j = i; | ||||
|     i += 1; | ||||
|     steps[j].submit(ev); | ||||
|   } | ||||
|   $qsa('.js-acme-form').forEach(function ($el) { | ||||
|     $el.addEventListener('submit', function (ev) { | ||||
|       ev.preventDefault(); | ||||
|       submitForm(ev); | ||||
|     }); | ||||
|   }); | ||||
|   function updateChallengeType() { | ||||
|     var input = this || Array.prototype.filter.call( | ||||
|       $qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; } | ||||
|     )[0]; | ||||
|     console.log('ch type radio:', input.value); | ||||
|     $qs('.js-acme-table-wildcard').hidden = true; | ||||
|     $qs('.js-acme-table-http-01').hidden = true; | ||||
|     $qs('.js-acme-table-dns-01').hidden = true; | ||||
|     if (info.challenges.wildcard) { | ||||
|       $qs('.js-acme-table-wildcard').hidden = false; | ||||
|     } | ||||
|     if (info.challenges[input.value]) { | ||||
|       $qs('.js-acme-table-' + input.value).hidden = false; | ||||
|     } | ||||
|   } | ||||
|   $qsa('.js-acme-challenge-type').forEach(function ($el) { | ||||
|     $el.addEventListener('change', updateChallengeType); | ||||
|   }); | ||||
| 
 | ||||
|   function saveContact(email, domains) { | ||||
|     // to be used for good, not evil
 | ||||
|     return window.fetch('https://api.ppl.family/api/ppl.family/public/list', { | ||||
|       method: 'POST' | ||||
|     , cors: true | ||||
|     , headers: new Headers({ 'Content-Type': 'application/json' }) | ||||
|     , body: JSON.stringify({ address: email, comment: 'greenlock sub for ' + domains.join(',') }) | ||||
|     }).then(function (resp) { | ||||
|       return resp.json().then(function (data) { | ||||
|         /* | ||||
|         if (data.error) { | ||||
|           window.alert("Couldn't save your contact. Email coolaj86@gmail.com instead."); | ||||
|           return; | ||||
|         } | ||||
|         */ | ||||
|       }); | ||||
|     }, function () { | ||||
|       /* | ||||
|       window.alert("Didn't get your contact. Bad network connection? Email coolaj86@gmail.com instead."); | ||||
|       */ | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   steps[1] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-domains').hidden = false; | ||||
|   }; | ||||
|   steps[1].submit = function () { | ||||
|     info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) { | ||||
|       return { type: 'dns', value: hostname.toLowerCase().trim() }; | ||||
|     }); | ||||
|     info.identifiers.sort(function (a, b) { | ||||
|       if (a === b) { return 0; } | ||||
|       if (a < b) { return 1; } | ||||
|       if (a > b) { return -1; } | ||||
|     }); | ||||
| 
 | ||||
|     return BACME.directory({ directoryUrl: $qs('.js-acme-directory-url').value }).then(function (directory) { | ||||
|       $qs('.js-acme-tos-url').href = directory.meta.termsOfService; | ||||
|       return BACME.nonce().then(function (_nonce) { | ||||
|         nonce = _nonce; | ||||
| 
 | ||||
|         console.log("MAGIC STEP NUMBER in 1 is:", i); | ||||
|         steps[i](); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   steps[2] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-account').hidden = false; | ||||
|   }; | ||||
|   steps[2].submit = function () { | ||||
|     var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); | ||||
| 
 | ||||
| 
 | ||||
|     info.contact = [ 'mailto:' + email ]; | ||||
|     info.agree = $qs('.js-acme-account-tos').checked; | ||||
|     info.greenlockAgree = $qs('.js-gl-tos').checked; | ||||
|     // TODO
 | ||||
|     // options for
 | ||||
|     // * regenerate key
 | ||||
|     // * ECDSA / RSA / bitlength
 | ||||
| 
 | ||||
|     // TODO ping with version and account creation
 | ||||
|     setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; })); | ||||
| 
 | ||||
|     var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null'); | ||||
|     var p; | ||||
| 
 | ||||
|     function createKeypair() { | ||||
|       return BACME.accounts.generateKeypair({ | ||||
|         type: 'ECDSA' | ||||
|       , bitlength: '256' | ||||
|       }).then(function (jwk) { | ||||
|         localStorage.setItem('account:' + email, JSON.stringify(jwk)); | ||||
|         return jwk; | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     if (jwk) { | ||||
|       p = Promise.resolve(jwk); | ||||
|     } else { | ||||
|       p = createKeypair(); | ||||
|     } | ||||
| 
 | ||||
|     function createAccount(jwk) { | ||||
|       console.log('account jwk:'); | ||||
|       console.log(jwk); | ||||
|       delete jwk.key_ops; | ||||
|       info.jwk = jwk; | ||||
|       return BACME.accounts.sign({ | ||||
|         jwk: jwk | ||||
|       , contacts: [ 'mailto:' + email ] | ||||
|       , agree: info.agree | ||||
|       , nonce: nonce | ||||
|       , kid: kid | ||||
|       }).then(function (signedAccount) { | ||||
|         return BACME.accounts.set({ | ||||
|           signedAccount: signedAccount | ||||
|         }).then(function (account) { | ||||
|           console.log('account:'); | ||||
|           console.log(account); | ||||
|           kid = account.kid; | ||||
|           return kid; | ||||
|         }); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function (_jwk) { | ||||
|       jwk = _jwk; | ||||
|       kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null'); | ||||
|       var p2 | ||||
| 
 | ||||
|       // TODO save account id rather than always retrieving it
 | ||||
|       if (kid) { | ||||
|         p2 = Promise.resolve(kid); | ||||
|       } else { | ||||
|         p2 = createAccount(jwk); | ||||
|       } | ||||
| 
 | ||||
|       return p2.then(function (_kid) { | ||||
|         kid = _kid; | ||||
|         info.kid = kid; | ||||
|         return BACME.orders.sign({ | ||||
|           jwk: jwk | ||||
|         , identifiers: info.identifiers | ||||
|         , kid: kid | ||||
|         }).then(function (signedOrder) { | ||||
|           return BACME.orders.create({ | ||||
|             signedOrder: signedOrder | ||||
|           }).then(function (order) { | ||||
|             info.finalizeUrl = order.finalize; | ||||
|             info.orderUrl = order.url; // from header Location ???
 | ||||
|             return BACME.thumbprint({ jwk: jwk }).then(function (thumbprint) { | ||||
|               return BACME.challenges.all().then(function (claims) { | ||||
|                 console.log('claims:'); | ||||
|                 console.log(claims); | ||||
|                 var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; | ||||
|                 var map = { | ||||
|                   'http-01': '.js-acme-table-http-01' | ||||
|                 , 'dns-01': '.js-acme-table-dns-01' | ||||
|                 , 'wildcard': '.js-acme-table-wildcard' | ||||
|                 } | ||||
|                 var tpls = {}; | ||||
|                 info.challenges = obj; | ||||
|                 Object.keys(map).forEach(function (k) { | ||||
|                   var sel = map[k] + ' tbody'; | ||||
|                   console.log(sel); | ||||
|                   tpls[k] = $qs(sel).innerHTML; | ||||
|                   $qs(map[k] + ' tbody').innerHTML = ''; | ||||
|                 }); | ||||
| 
 | ||||
|                 // TODO make Promise-friendly
 | ||||
|                 return Promise.all(claims.map(function (claim) { | ||||
|                   var hostname = claim.identifier.value; | ||||
|                   return Promise.all(claim.challenges.map(function (c) { | ||||
|                     var keyAuth = BACME.challenges['http-01']({ | ||||
|                       token: c.token | ||||
|                     , thumbprint: thumbprint | ||||
|                     , challengeDomain: hostname | ||||
|                     }); | ||||
|                     return BACME.challenges['dns-01']({ | ||||
|                       keyAuth: keyAuth.value | ||||
|                     , challengeDomain: hostname | ||||
|                     }).then(function (dnsAuth) { | ||||
|                       var data = { | ||||
|                         type: c.type | ||||
|                       , hostname: hostname | ||||
|                       , url: c.url | ||||
|                       , token: c.token | ||||
|                       , keyAuthorization: keyAuth | ||||
|                       , httpPath: keyAuth.path | ||||
|                       , httpAuth: keyAuth.value | ||||
|                       , dnsType: dnsAuth.type | ||||
|                       , dnsHost: dnsAuth.host | ||||
|                       , dnsAnswer: dnsAuth.answer | ||||
|                       }; | ||||
| 
 | ||||
|                       console.log(''); | ||||
|                       console.log('CHALLENGE'); | ||||
|                       console.log(claim); | ||||
|                       console.log(c); | ||||
|                       console.log(data); | ||||
|                       console.log(''); | ||||
| 
 | ||||
|                       if (claim.wildcard) { | ||||
|                         obj.wildcard.push(data); | ||||
|                         $qs(map.wildcard).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>'; | ||||
|                       } else if(obj[data.type]) { | ||||
| 
 | ||||
|                         obj[data.type].push(data); | ||||
|                         if ('dns-01' === data.type) { | ||||
|                           $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.dnsHost + '</td><td>' + data.dnsAnswer + '</td></tr>'; | ||||
|                         } else if ('http-01' === data.type) { | ||||
|                           $qs(map[data.type]).innerHTML += '<tr><td>' + data.hostname + '</td><td>' + data.httpPath + '</td><td>' + data.httpAuth + '</td></tr>'; | ||||
|                         } | ||||
|                       } | ||||
| 
 | ||||
|                     }); | ||||
| 
 | ||||
|                   })); | ||||
|                 })).then(function () { | ||||
| 
 | ||||
|                   // hide wildcard if no wildcard
 | ||||
|                   // hide http-01 and dns-01 if only wildcard
 | ||||
|                   if (!obj.wildcard.length) { | ||||
|                     $qs('.js-acme-wildcard').hidden = true; | ||||
|                   } | ||||
|                   if (!obj['http-01'].length) { | ||||
|                     $qs('.js-acme-challenges').hidden = true; | ||||
|                   } | ||||
| 
 | ||||
|                   updateChallengeType(); | ||||
| 
 | ||||
|                   console.log("MAGIC STEP NUMBER in 2 is:", i); | ||||
|                   steps[i](); | ||||
|                 }); | ||||
| 
 | ||||
|               }); | ||||
|             }); | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }).catch(function (err) { | ||||
|       console.error('Step \'' + i + '\' Error:'); | ||||
|       console.error(err); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   steps[3] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-challenges').hidden = false; | ||||
|   }; | ||||
|   steps[3].submit = function () { | ||||
|     var chType; | ||||
|     Array.prototype.some.call($qsa('.js-acme-challenge-type'), function ($el) { | ||||
|       if ($el.checked) { | ||||
|         chType = $el.value; | ||||
|         return true; | ||||
|       } | ||||
|     }); | ||||
|     console.log('chType is:', chType); | ||||
|     var chs = []; | ||||
| 
 | ||||
|     // do each wildcard, if any
 | ||||
|     // do each challenge, by selected type only
 | ||||
|     [ 'wildcard', chType].forEach(function (typ) { | ||||
|       info.challenges[typ].forEach(function (ch) { | ||||
|         // { jwk, challengeUrl, accountId (kid) }
 | ||||
|         chs.push({ | ||||
|           jwk: info.jwk | ||||
|         , challengeUrl: ch.url | ||||
|         , accountId: info.kid | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|     console.log("INFO.challenges !!!!!", info.challenges); | ||||
| 
 | ||||
|     var results = []; | ||||
|     function nextChallenge() { | ||||
|       var ch = chs.pop(); | ||||
|       if (!ch) { return results; } | ||||
|       return BACME.challenges.accept(ch).then(function (result) { | ||||
|         results.push(result); | ||||
|         return nextChallenge(); | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     // for now just show the next page immediately (its a spinner)
 | ||||
|     steps[i](); | ||||
|     return nextChallenge().then(function (results) { | ||||
|       console.log('challenge status:', results); | ||||
|       var polls = results.slice(0); | ||||
|       var allsWell = true; | ||||
| 
 | ||||
|       function checkPolls() { | ||||
|         return new Promise(function (resolve) { | ||||
|           setTimeout(resolve, 1000); | ||||
|         }).then(function () { | ||||
|           return Promise.all(polls.map(function (poll) { | ||||
|             return BACME.challenges.check({ challengePollUrl: poll.url }); | ||||
|           })).then(function (polls) { | ||||
|             console.log(polls); | ||||
| 
 | ||||
|             polls = polls.filter(function (poll) { | ||||
|               //return 'valid' !== poll.status && 'invalid' !== poll.status;
 | ||||
|               if ('pending' === poll.status) { | ||||
|                 return true; | ||||
|               } | ||||
|               if ('valid' !== poll.status) { | ||||
|                 allsWell = false; | ||||
|                 console.warn('BAD POLL STATUS', poll); | ||||
|               } | ||||
|               // TODO show status in HTML
 | ||||
|             }); | ||||
| 
 | ||||
|             if (polls.length) { | ||||
|               return checkPolls(); | ||||
|             } | ||||
|             return true; | ||||
|           }); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       return checkPolls().then(function () { | ||||
|         if (allsWell) { | ||||
|           return submitForm(); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   // spinner
 | ||||
|   steps[4] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-poll').hidden = false; | ||||
|   } | ||||
|   steps[4].submit = function () { | ||||
|     console.log('Congrats! Auto advancing...'); | ||||
| 
 | ||||
|     var key = info.identifiers.map(function (ident) { return ident.value; }).join(','); | ||||
|     var serverJwk = JSON.parse(localStorage.getItem('server:' + key) || 'null'); | ||||
|     var p; | ||||
| 
 | ||||
|     function createKeypair() { | ||||
|       return BACME.accounts.generateKeypair({ | ||||
|         type: 'ECDSA' | ||||
|       , bitlength: '256' | ||||
|       }).then(function (serverJwk) { | ||||
|         localStorage.setItem('server:' + key, JSON.stringify(serverJwk)); | ||||
|         return serverJwk; | ||||
|       }) | ||||
|     } | ||||
| 
 | ||||
|     if (serverJwk) { | ||||
|       p = Promise.resolve(serverJwk); | ||||
|     } else { | ||||
|       p = createKeypair(); | ||||
|     } | ||||
| 
 | ||||
|     return p.then(function (_serverJwk) { | ||||
|       serverJwk = _serverJwk; | ||||
|       info.serverJwk = serverJwk; | ||||
|       // { serverJwk, domains }
 | ||||
|       return BACME.orders.generateCsr({ | ||||
|         serverJwk: serverJwk | ||||
|       , domains: info.identifiers.map(function (ident) { | ||||
|           return ident.value; | ||||
|         }) | ||||
|       }).then(function (csrweb64) { | ||||
|         return BACME.orders.finalize({ | ||||
|           csr: csrweb64 | ||||
|         , jwk: info.jwk | ||||
|         , finalizeUrl: info.finalizeUrl | ||||
|         , accountId: info.kid | ||||
|         }); | ||||
|       }).then(function () { | ||||
|         function checkCert() { | ||||
|           return new Promise(function (resolve) { | ||||
|             setTimeout(resolve, 1000); | ||||
|           }).then(function () { | ||||
|             return BACME.orders.check({ orderUrl: info.orderUrl }); | ||||
|           }).then(function (reply) { | ||||
|             if ('processing' === reply) { | ||||
|               return checkCert(); | ||||
|             } | ||||
|             return reply; | ||||
|           }); | ||||
|         } | ||||
| 
 | ||||
|         return checkCert(); | ||||
|       }).then(function (reply) { | ||||
|         return BACME.orders.receive({ certificateUrl: reply.certificate }); | ||||
|       }).then(function (certs) { | ||||
|         console.log('WINNING!'); | ||||
|         console.log(certs); | ||||
|         $qs('.js-fullchain').value = certs; | ||||
| 
 | ||||
|         // https://stackoverflow.com/questions/40314257/export-webcrypto-key-to-pem-format
 | ||||
| 				function spkiToPEM(keydata){ | ||||
| 						var keydataS = arrayBufferToString(keydata); | ||||
| 						var keydataB64 = window.btoa(keydataS); | ||||
| 						var keydataB64Pem = formatAsPem(keydataB64); | ||||
| 						return keydataB64Pem; | ||||
| 				} | ||||
| 
 | ||||
| 				function arrayBufferToString( buffer ) { | ||||
| 						var binary = ''; | ||||
| 						var bytes = new Uint8Array( buffer ); | ||||
| 						var len = bytes.byteLength; | ||||
| 						for (var i = 0; i < len; i++) { | ||||
| 								binary += String.fromCharCode( bytes[ i ] ); | ||||
| 						} | ||||
| 						return binary; | ||||
| 				} | ||||
| 
 | ||||
| 
 | ||||
| 				function formatAsPem(str) { | ||||
| 						var finalString = '-----BEGIN ' + pemName + ' PRIVATE KEY-----\n'; | ||||
| 
 | ||||
| 						while(str.length > 0) { | ||||
| 								finalString += str.substring(0, 64) + '\n'; | ||||
| 								str = str.substring(64); | ||||
| 						} | ||||
| 
 | ||||
| 						finalString = finalString + '-----END ' + pemName + ' PRIVATE KEY-----'; | ||||
| 
 | ||||
| 						return finalString; | ||||
| 				} | ||||
| 
 | ||||
|         var wcOpts; | ||||
|         var pemName; | ||||
|         if (/^R/.test(info.serverJwk.kty)) { | ||||
|           pemName = 'RSA'; | ||||
|           wcOpts = { | ||||
|             name: "RSASSA-PKCS1-v1_5" | ||||
|           , hash: { name: "SHA-256" } | ||||
|           }; | ||||
|         } else { | ||||
|           pemName = 'EC'; | ||||
|           wcOpts = { | ||||
|             name: "ECDSA" | ||||
|           , namedCurve: "P-256" | ||||
|           } | ||||
|         } | ||||
| 				return crypto.subtle.importKey( | ||||
|           "jwk" | ||||
|         , info.serverJwk | ||||
|         , wcOpts | ||||
|         , true | ||||
|         , ["sign"] | ||||
| 				).then(function (privateKey) { | ||||
| 				  return window.crypto.subtle.exportKey("pkcs8", privateKey); | ||||
| 				}).then (function (keydata) { | ||||
| 					var pem = spkiToPEM(keydata); | ||||
| 					$qs('.js-privkey').value = pem; | ||||
|           steps[i](); | ||||
| 				}).catch(function(err){ | ||||
| 					console.error(err); | ||||
| 				}); | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   steps[5] = function () { | ||||
|     hideForms(); | ||||
|     $qs('.js-acme-form-download').hidden = false; | ||||
|   } | ||||
| 
 | ||||
|   steps[1](); | ||||
| 
 | ||||
|   $qs('body').hidden = false; | ||||
| }()); | ||||
|  | ||||
							
								
								
									
										100
									
								
								styles/main.css
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								styles/main.css
									
									
									
									
									
								
							| @ -1,5 +1,105 @@ | ||||
| .column-row { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   text-align: center; | ||||
|   align-items: center; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|   position: relative; | ||||
|   margin-top: 5.777777778em; | ||||
|   min-height: 36em; | ||||
|   font-size: 18px; | ||||
|   font-family: 'Source Sans Pro', sans-serif; | ||||
|   font-stretch: normal; | ||||
|   line-height: 1.33; | ||||
|   letter-spacing:  -0.4px; | ||||
|   color: #1a1a1a; | ||||
|   opacity: 0; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|   font-size: 2.666666667em; | ||||
|   max-width: 8em; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| input { | ||||
|   font-size: 1em; | ||||
|   padding: 0.444444444em; | ||||
|   border: solid #d9d9d9 1px; | ||||
|   border-radius: 2px; | ||||
|   font-family: inherit; | ||||
| } | ||||
| 
 | ||||
| button { | ||||
|   padding: 0.444444444em 1.2em; | ||||
|   font-size: 1em; | ||||
|   background-color: #5bc17f; | ||||
|   border: solid 1px #5bc17f; | ||||
|   border-radius: 2px; | ||||
|   font-weight: normal; | ||||
|   font-stretch: normal; | ||||
|   letter-spacing: -0.4px; | ||||
|   font-family: inherit; | ||||
|   text-align: center; | ||||
|   color: white; | ||||
|   height: 40px; | ||||
|   line-height: 1.13; | ||||
| } | ||||
| 
 | ||||
| .acme-advanced-fields { | ||||
|   position: absolute; | ||||
|   bottom: 0; | ||||
|   padding: 1em; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .domain-subtext { | ||||
|   font-size: 0.833333333em; | ||||
|   color: #666; | ||||
|   text-align: center; | ||||
|   margin: 0.5em; | ||||
| } | ||||
| 
 | ||||
| input#acme-domains:before { | ||||
|   content: "Secure | https://"; | ||||
| } | ||||
| 
 | ||||
| .domain-psuedo-input { | ||||
|   display: inline-block; | ||||
|   margin-right: .6666667em; | ||||
|   border: solid #d9d9d9 1px; | ||||
|   border-radius: 2px; | ||||
|   padding: 0.44444444em; | ||||
|   color: #d9d9d9; | ||||
| } | ||||
| 
 | ||||
| input#acme-domains { | ||||
|   border: none; | ||||
|   padding: 0; | ||||
|   padding-right: 0; | ||||
|   width: 17.2222222em; | ||||
|   color: #222; | ||||
| } | ||||
| 
 | ||||
| input#acme-domains:focus { | ||||
|   outline: none; | ||||
| } | ||||
| 
 | ||||
| span.secure-green { | ||||
|   color: #5bc17f; | ||||
| } | ||||
| 
 | ||||
| .why-you-need { | ||||
|   width: 26.555556em; | ||||
| } | ||||
| 
 | ||||
| body.js-app-ready { | ||||
|   transition: opacity 0.2s; | ||||
|   opacity: 1; | ||||
| } | ||||
| 
 | ||||
| .acme-advanced-fields > * { | ||||
|   margin: 0 0.5em; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user