| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  | // TODO handle www and no-www together somehow?
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  | var PromiseA = require('bluebird'); | 
					
						
							| 
									
										
										
										
											2015-12-17 04:59:47 +00:00
										 |  |  | var leCore = require('letiny-core'); | 
					
						
							| 
									
										
										
										
											2015-12-17 08:46:40 +00:00
										 |  |  | var merge = require('./lib/common').merge; | 
					
						
							| 
									
										
										
										
											2015-12-20 00:27:48 +00:00
										 |  |  | var tplCopy = require('./lib/common').tplCopy; | 
					
						
							| 
									
										
										
										
											2016-08-11 10:49:36 -06:00
										 |  |  | var isValidDomain = require('./lib/common').isValidDomain; | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  | var LE = module.exports; | 
					
						
							| 
									
										
										
										
											2015-12-16 01:11:31 -08:00
										 |  |  | LE.productionServerUrl = leCore.productionServerUrl; | 
					
						
							| 
									
										
										
										
											2015-12-17 04:59:47 +00:00
										 |  |  | LE.stagingServerUrl = leCore.stagingServerUrl; | 
					
						
							| 
									
										
										
										
											2015-12-16 01:11:31 -08:00
										 |  |  | LE.configDir = leCore.configDir; | 
					
						
							| 
									
										
										
										
											2015-12-16 10:07:00 +00:00
										 |  |  | LE.logsDir = leCore.logsDir; | 
					
						
							|  |  |  | LE.workDir = leCore.workDir; | 
					
						
							| 
									
										
										
										
											2015-12-16 01:11:31 -08:00
										 |  |  | LE.acmeChallengPrefix = leCore.acmeChallengPrefix; | 
					
						
							|  |  |  | LE.knownEndpoints = leCore.knownEndpoints; | 
					
						
							| 
									
										
										
										
											2016-08-11 10:49:36 -06:00
										 |  |  | LE.isValidDomain = isValidDomain; | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  | LE.privkeyPath = ':config/live/:hostname/privkey.pem'; | 
					
						
							|  |  |  | LE.fullchainPath = ':config/live/:hostname/fullchain.pem'; | 
					
						
							|  |  |  | LE.certPath = ':config/live/:hostname/cert.pem'; | 
					
						
							|  |  |  | LE.chainPath = ':config/live/:hostname/chain.pem'; | 
					
						
							| 
									
										
										
										
											2015-12-21 10:27:57 -07:00
										 |  |  | LE.renewalPath = ':config/renewal/:hostname.conf'; | 
					
						
							|  |  |  | LE.accountsDir = ':config/accounts/:server'; | 
					
						
							| 
									
										
										
										
											2016-02-12 21:33:50 -05:00
										 |  |  | LE.defaults = { | 
					
						
							|  |  |  |   privkeyPath: LE.privkeyPath | 
					
						
							|  |  |  | , fullchainPath: LE.fullchainPath | 
					
						
							|  |  |  | , certPath: LE.certPath | 
					
						
							|  |  |  | , chainPath: LE.chainPath | 
					
						
							|  |  |  | , renewalPath: LE.renewalPath | 
					
						
							|  |  |  | , accountsDir: LE.accountsDir | 
					
						
							|  |  |  | , server: LE.productionServerUrl | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 01:11:31 -08:00
										 |  |  | // backwards compat
 | 
					
						
							| 
									
										
										
										
											2015-12-17 04:59:47 +00:00
										 |  |  | LE.stagingServer = leCore.stagingServerUrl; | 
					
						
							| 
									
										
										
										
											2015-12-16 01:11:31 -08:00
										 |  |  | LE.liveServer = leCore.productionServerUrl; | 
					
						
							|  |  |  | LE.knownUrls = leCore.knownEndpoints; | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-17 08:46:40 +00:00
										 |  |  | LE.merge = require('./lib/common').merge; | 
					
						
							|  |  |  | LE.tplConfigDir = require('./lib/common').tplConfigDir; | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-16 01:11:31 -08:00
										 |  |  |                     // backend, defaults, handlers
 | 
					
						
							|  |  |  | LE.create = function (defaults, handlers, backend) { | 
					
						
							| 
									
										
										
										
											2015-12-19 02:13:10 -08:00
										 |  |  |   if (!backend) { backend = require('./lib/core'); } | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  |   if (!handlers) { handlers = {}; } | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  |   if (!handlers.lifetime) { handlers.lifetime = 90 * 24 * 60 * 60 * 1000; } | 
					
						
							|  |  |  |   if (!handlers.renewWithin) { handlers.renewWithin = 3 * 24 * 60 * 60 * 1000; } | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  |   if (!handlers.memorizeFor) { handlers.memorizeFor = 1 * 24 * 60 * 60 * 1000; } | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  |   if (!handlers.sniRegisterCallback) { | 
					
						
							|  |  |  |     handlers.sniRegisterCallback = function (args, cache, cb) { | 
					
						
							|  |  |  |       // TODO when we have ECDSA, just do this automatically
 | 
					
						
							|  |  |  |       cb(null, null); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-12-15 15:40:44 +00:00
										 |  |  |   if (!handlers.getChallenge) { | 
					
						
							| 
									
										
										
										
											2015-12-16 12:57:53 +00:00
										 |  |  |     if (!defaults.manual && !defaults.webrootPath) { | 
					
						
							| 
									
										
										
										
											2015-12-15 15:40:44 +00:00
										 |  |  |       // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
 | 
					
						
							|  |  |  |       throw new Error("handlers.getChallenge or defaults.webrootPath must be set"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     handlers.getChallenge = function (hostname, key, done) { | 
					
						
							|  |  |  |       // TODO associate by hostname?
 | 
					
						
							|  |  |  |       // hmm... I don't think there's a direct way to associate this with
 | 
					
						
							|  |  |  |       // the request it came from... it's kinda stateless in that way
 | 
					
						
							|  |  |  |       // but realistically there only needs to be one handler and one
 | 
					
						
							|  |  |  |       // "directory" for this. It's not that big of a deal.
 | 
					
						
							|  |  |  |       var defaultos = LE.merge(defaults, {}); | 
					
						
							| 
									
										
										
										
											2015-12-17 04:44:28 +00:00
										 |  |  |       var getChallenge = require('./lib/default-handlers').getChallenge; | 
					
						
							| 
									
										
										
										
											2015-12-17 08:46:40 +00:00
										 |  |  |       var copy = merge(defaults, { domains: [hostname] }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-20 00:27:48 +00:00
										 |  |  |       tplCopy(copy); | 
					
						
							| 
									
										
										
										
											2015-12-15 15:40:44 +00:00
										 |  |  |       defaultos.domains = [hostname]; | 
					
						
							| 
									
										
										
										
											2015-12-17 08:46:40 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-17 04:44:28 +00:00
										 |  |  |       if (3 === getChallenge.length) { | 
					
						
							|  |  |  |         getChallenge(defaultos, key, done); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       else if (4 === getChallenge.length) { | 
					
						
							|  |  |  |         getChallenge(defaultos, hostname, key, done); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       else { | 
					
						
							|  |  |  |         done(new Error("handlers.getChallenge [1] receives the wrong number of arguments")); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-15 15:40:44 +00:00
										 |  |  |     }; | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-12-15 03:38:21 -08:00
										 |  |  |   if (!handlers.setChallenge) { | 
					
						
							|  |  |  |     if (!defaults.webrootPath) { | 
					
						
							|  |  |  |       // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
 | 
					
						
							|  |  |  |       throw new Error("handlers.setChallenge or defaults.webrootPath must be set"); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-12-15 12:01:05 +00:00
										 |  |  |     handlers.setChallenge = require('./lib/default-handlers').setChallenge; | 
					
						
							| 
									
										
										
										
											2015-12-15 03:38:21 -08:00
										 |  |  |   } | 
					
						
							|  |  |  |   if (!handlers.removeChallenge) { | 
					
						
							|  |  |  |     if (!defaults.webrootPath) { | 
					
						
							|  |  |  |       // GET /.well-known/acme-challenge/{{challengeKey}} should return {{tokenValue}}
 | 
					
						
							| 
									
										
										
										
											2015-12-15 15:40:44 +00:00
										 |  |  |       throw new Error("handlers.removeChallenge or defaults.webrootPath must be set"); | 
					
						
							| 
									
										
										
										
											2015-12-15 03:38:21 -08:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-12-15 12:01:46 +00:00
										 |  |  |     handlers.removeChallenge = require('./lib/default-handlers').removeChallenge; | 
					
						
							| 
									
										
										
										
											2015-12-15 03:38:21 -08:00
										 |  |  |   } | 
					
						
							|  |  |  |   if (!handlers.agreeToTerms) { | 
					
						
							|  |  |  |     if (defaults.agreeTos) { | 
					
						
							|  |  |  |       console.warn("[WARN] Agreeing to terms by default is risky business..."); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-12-15 13:12:16 +00:00
										 |  |  |     handlers.agreeToTerms = require('./lib/default-handlers').agreeToTerms; | 
					
						
							| 
									
										
										
										
											2015-12-15 03:38:21 -08:00
										 |  |  |   } | 
					
						
							|  |  |  |   if ('function' === typeof backend.create) { | 
					
						
							|  |  |  |     backend = backend.create(defaults, handlers); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   else { | 
					
						
							|  |  |  |     // ignore
 | 
					
						
							|  |  |  |     // this backend was created the v1.0.0 way
 | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2015-12-17 08:46:40 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // replaces strings of workDir, certPath, etc
 | 
					
						
							|  |  |  |   // if they have :config/etc/live or :conf/etc/archive
 | 
					
						
							|  |  |  |   // to instead have the path of the configDir
 | 
					
						
							|  |  |  |   LE.tplConfigDir(defaults.configDir, defaults); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  |   backend = PromiseA.promisifyAll(backend); | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-19 02:18:32 -08:00
										 |  |  |   var utils = require('./lib/common'); | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |   //var attempts = {};  // should exist in master process only
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   var le; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 15:05:45 +00:00
										 |  |  |   // TODO check certs on initial load
 | 
					
						
							|  |  |  |   // TODO expect that certs expire every 90 days
 | 
					
						
							|  |  |  |   // TODO check certs with setInterval?
 | 
					
						
							|  |  |  |   //options.cacheContextsFor = options.cacheContextsFor || (1 * 60 * 60 * 1000);
 | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   le = { | 
					
						
							| 
									
										
										
										
											2015-12-15 12:12:15 +00:00
										 |  |  |     backend: backend | 
					
						
							| 
									
										
										
										
											2016-08-11 10:49:36 -06:00
										 |  |  |   , isValidDomain: isValidDomain | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |   , pyToJson: function (pyobj) { | 
					
						
							|  |  |  |       if (!pyobj) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var jsobj = {}; | 
					
						
							|  |  |  |       Object.keys(pyobj).forEach(function (key) { | 
					
						
							|  |  |  |         jsobj[key] = pyobj[key]; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       jsobj.__lines = undefined; | 
					
						
							|  |  |  |       jsobj.__keys = undefined; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return jsobj; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |   , register: function (args, cb) { | 
					
						
							|  |  |  |       if (defaults.debug || args.debug) { | 
					
						
							|  |  |  |         console.log('[LE] register'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (!Array.isArray(args.domains)) { | 
					
						
							|  |  |  |         cb(new Error('args.domains should be an array of domains')); | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       var copy = LE.merge(defaults, args); | 
					
						
							|  |  |  |       var err; | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |       if (!utils.isValidDomain(args.domains[0])) { | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |         err = new Error("invalid domain name: '" + args.domains + "'"); | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  |         err.code = "INVALID_DOMAIN"; | 
					
						
							|  |  |  |         cb(err); | 
					
						
							|  |  |  |         return; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |       if ((!args.domains.length && args.domains.every(le.isValidDomain))) { | 
					
						
							|  |  |  |         // NOTE: this library can't assume to handle the http loopback
 | 
					
						
							|  |  |  |         // (or dns-01 validation may be used)
 | 
					
						
							|  |  |  |         // so we do not check dns records or attempt a loopback here
 | 
					
						
							|  |  |  |         cb(new Error("node-letsencrypt: invalid hostnames: " + args.domains.join(','))); | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-13 01:04:12 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |       if (defaults.debug || args.debug) { | 
					
						
							|  |  |  |         console.log("[NLE]: begin registration"); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return backend.registerAsync(copy).then(function (pems) { | 
					
						
							| 
									
										
										
										
											2016-02-10 15:41:15 -05:00
										 |  |  |         if (defaults.debug || args.debug) { | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |           console.log("[NLE]: end registration"); | 
					
						
							| 
									
										
										
										
											2015-12-20 00:27:48 +00:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |         cb(null, pems); | 
					
						
							|  |  |  |         //return le.fetch(args, cb);
 | 
					
						
							|  |  |  |       }, cb); | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2016-08-04 14:26:49 -04:00
										 |  |  |   , fetch: function (args, cb) { | 
					
						
							|  |  |  |       if (defaults.debug || args.debug) { | 
					
						
							|  |  |  |         console.log('[LE] fetch'); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  |       return backend.fetchAsync(args).then(function (certInfo) { | 
					
						
							| 
									
										
										
										
											2015-12-17 05:44:41 +00:00
										 |  |  |         if (args.debug) { | 
					
						
							| 
									
										
										
										
											2015-12-17 08:46:40 +00:00
										 |  |  |           console.log('[LE] raw fetch certs', certInfo && Object.keys(certInfo)); | 
					
						
							| 
									
										
										
										
											2015-12-17 05:44:41 +00:00
										 |  |  |         } | 
					
						
							|  |  |  |         if (!certInfo) { cb(null, null); return; } | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         // key, cert, issuedAt, lifetime, expiresAt
 | 
					
						
							|  |  |  |         if (!certInfo.expiresAt) { | 
					
						
							|  |  |  |           certInfo.expiresAt = certInfo.issuedAt + (certInfo.lifetime || handlers.lifetime); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (!certInfo.lifetime) { | 
					
						
							|  |  |  |           certInfo.lifetime = (certInfo.lifetime || handlers.lifetime); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         // a pretty good hard buffer
 | 
					
						
							|  |  |  |         certInfo.expiresAt -= (1 * 24 * 60 * 60 * 100); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-17 05:44:41 +00:00
										 |  |  |         cb(null, certInfo); | 
					
						
							| 
									
										
										
										
											2015-12-13 05:03:48 +00:00
										 |  |  |       }, cb); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |   , getConfig: function (args, cb) { | 
					
						
							| 
									
										
										
										
											2015-12-21 10:27:57 -07:00
										 |  |  |       if (defaults.debug || args.debug) { | 
					
						
							|  |  |  |         console.log('[LE] getConfig'); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |       backend.getConfigAsync(args).then(function (pyobj) { | 
					
						
							|  |  |  |         cb(null, le.pyToJson(pyobj)); | 
					
						
							|  |  |  |       }, function (err) { | 
					
						
							| 
									
										
										
										
											2016-02-10 15:41:15 -05:00
										 |  |  |         console.error("[letsencrypt/index.js] getConfig"); | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |         console.error(err.stack); | 
					
						
							|  |  |  |         return cb(null, []); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , getConfigs: function (args, cb) { | 
					
						
							| 
									
										
										
										
											2015-12-21 10:27:57 -07:00
										 |  |  |       if (defaults.debug || args.debug) { | 
					
						
							|  |  |  |         console.log('[LE] getConfigs'); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |       backend.getConfigsAsync(args).then(function (configs) { | 
					
						
							|  |  |  |         cb(null, configs.map(le.pyToJson)); | 
					
						
							|  |  |  |       }, function (err) { | 
					
						
							|  |  |  |         if ('ENOENT' === err.code) { | 
					
						
							|  |  |  |           cb(null, []); | 
					
						
							|  |  |  |         } else { | 
					
						
							| 
									
										
										
										
											2016-02-10 15:41:15 -05:00
										 |  |  |           console.error("[letsencrypt/index.js] getConfigs"); | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |           console.error(err.stack); | 
					
						
							|  |  |  |           cb(err); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   , setConfig: function (args, cb) { | 
					
						
							| 
									
										
										
										
											2015-12-21 10:27:57 -07:00
										 |  |  |       if (defaults.debug || args.debug) { | 
					
						
							|  |  |  |         console.log('[LE] setConfig'); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2015-12-20 02:41:17 -08:00
										 |  |  |       backend.configureAsync(args).then(function (pyobj) { | 
					
						
							|  |  |  |         cb(null, le.pyToJson(pyobj)); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   }; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2015-12-12 14:20:12 +00:00
										 |  |  |   return le; | 
					
						
							| 
									
										
										
										
											2015-12-11 06:22:46 -08:00
										 |  |  | }; |