untested separation of concerns
This commit is contained in:
parent
8d27e09217
commit
d1375aceb0
135
README.md
135
README.md
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
Free SSL and managed or automatic HTTPS for node.js with Express, Connect, and other middleware systems.
|
Free SSL and managed or automatic HTTPS for node.js with Express, Connect, and other middleware systems.
|
||||||
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -95,7 +94,7 @@ node -e 'require("letsencrypt-express").testing().create( require("express")().u
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Note: using staging server url, remove .testing() for production
|
// Note: using staging server url, remove .testing() for production
|
||||||
var lex = require('letsencrypt-express').testing();
|
var LEX = require('letsencrypt-express').testing();
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
|
@ -103,22 +102,22 @@ app.use('/', function (req, res) {
|
||||||
res.send({ success: true });
|
res.send({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
lex.create({
|
LEX.create({
|
||||||
configDir: './letsencrypt.config' // ~/letsencrypt, /etc/letsencrypt, whatever you want
|
configDir: './letsencrypt.config' // ~/letsencrypt, /etc/letsencrypt, whatever you want
|
||||||
|
|
||||||
, onRequest: app // your express app (or plain node http app)
|
, onRequest: app // your express app (or plain node http app)
|
||||||
|
|
||||||
, letsencrypt: null // you can provide you own instance of letsencrypt
|
, letsencrypt: null // you can provide you own instance of letsencrypt
|
||||||
// if you need to configure it (with an agreeToTerms
|
// if you need to configure it (with an agreeToTerms
|
||||||
// callback, for example)
|
// callback, for example)
|
||||||
|
|
||||||
, approveRegistration: function (hostname, cb) { // PRODUCTION MODE needs this function, but only if you want
|
, approveRegistration: function (hostname, cb) { // PRODUCTION MODE needs this function, but only if you want
|
||||||
// automatic registration (usually not necessary)
|
// automatic registration (usually not necessary)
|
||||||
// renewals for registered domains will still be automatic
|
// renewals for registered domains will still be automatic
|
||||||
cb(null, {
|
cb(null, {
|
||||||
domains: [hostname]
|
domains: [hostname]
|
||||||
, email: 'user@example.com'
|
, email: 'user@example.com'
|
||||||
, agreeTos: true // you
|
, agreeTos: true // you
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).listen([80], [443, 5001], function () {
|
}).listen([80], [443, 5001], function () {
|
||||||
|
@ -164,6 +163,42 @@ console.log(results.plainServers);
|
||||||
console.log(results.tlsServers);
|
console.log(results.tlsServers);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use with raw http / https modules
|
||||||
|
|
||||||
|
Let's say you want to redirect all http to https.
|
||||||
|
|
||||||
|
```
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('https');
|
||||||
|
var LEX = require('letsencrypt-express');
|
||||||
|
var LE = require('letsencrypt');
|
||||||
|
|
||||||
|
var lex = LEX.create({
|
||||||
|
configDir: __dirname + '/letsencrypt.config'
|
||||||
|
, approveRegistration: function (hostname, cb) {
|
||||||
|
cb(null, {
|
||||||
|
domains: [hostname]
|
||||||
|
, email: 'user@example.com'
|
||||||
|
, agreeTos: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
http.createServer(LEX.createAcmeResponder(lex, function redirectHttps(req, res) {
|
||||||
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
||||||
|
res.end('<!-- Hello Mr Developer! Please use HTTPS instead -->');
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
var app = require('express')();
|
||||||
|
|
||||||
|
app.use('/', function (req, res) {
|
||||||
|
res.end('Hello!');
|
||||||
|
});
|
||||||
|
|
||||||
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, app));
|
||||||
|
```
|
||||||
|
|
||||||
### WebSockets with Let's Encrypt
|
### WebSockets with Let's Encrypt
|
||||||
|
|
||||||
Note: you don't need to create websockets for the plain ports.
|
Note: you don't need to create websockets for the plain ports.
|
||||||
|
@ -180,7 +215,7 @@ function onConnection(ws) {
|
||||||
var location = url.parse(ws.upgradeReq.url, true);
|
var location = url.parse(ws.upgradeReq.url, true);
|
||||||
// you might use location.query.access_token to authenticate or share sessions
|
// you might use location.query.access_token to authenticate or share sessions
|
||||||
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312)
|
// or ws.upgradeReq.headers.cookie (see http://stackoverflow.com/a/16395220/151312)
|
||||||
|
|
||||||
ws.on('message', function incoming(message) {
|
ws.on('message', function incoming(message) {
|
||||||
console.log('received: %s', message);
|
console.log('received: %s', message);
|
||||||
});
|
});
|
||||||
|
@ -229,7 +264,20 @@ LEX.createSniCallback(opts) // this will call letsencrypt.renew and letsencr
|
||||||
|
|
||||||
|
|
||||||
// uses `opts.webrootPath` to read from the filesystem
|
// uses `opts.webrootPath` to read from the filesystem
|
||||||
LEX.getChallenge(opts, hostname, key cb)
|
LEX.getChallenge(opts, hostname, key cb)
|
||||||
|
|
||||||
|
LEX.createAcmeResponder(opts, fn) // this will return the necessary request handler for /.well-known/acme-challenges
|
||||||
|
// which then calls `fn` (such as express app) to complete the request
|
||||||
|
//
|
||||||
|
// opts lex instance created with LEX.create(opts)
|
||||||
|
// more generally, any object with a compatible `getChallenge` will work:
|
||||||
|
// `lex.getChallenge(opts, domain, key, function (err, val) {})`
|
||||||
|
//
|
||||||
|
// fn function (req, res) {
|
||||||
|
// console.log(req.method, req.url);
|
||||||
|
//
|
||||||
|
// res.end('Hello!');
|
||||||
|
// }
|
||||||
```
|
```
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
@ -286,6 +334,77 @@ server: url // url use letsencrypt.productionServerUr
|
||||||
// default production
|
// default production
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Fullest Example Ever
|
||||||
|
|
||||||
|
Here's absolutely every option and function exposed
|
||||||
|
|
||||||
|
```
|
||||||
|
var http = require('http');
|
||||||
|
var https = require('https');
|
||||||
|
var LEX = require('letsencrypt-express');
|
||||||
|
var LE = require('letsencrypt');
|
||||||
|
var lex;
|
||||||
|
|
||||||
|
lex = LEX.create({
|
||||||
|
webrootPath: '/tmp/.well-known/acme-challenge'
|
||||||
|
|
||||||
|
, lifetime: 90 * 24 * 60 * 60 * 1000 // expect certificates to last 90 days
|
||||||
|
, failedWait: 5 * 60 * 1000 // if registering fails wait 5 minutes before trying again
|
||||||
|
, renewWithin: 3 * 24 * 60 * 60 * 1000 // renew at least 3 days before expiration
|
||||||
|
, memorizeFor: 1 * 24 * 60 * 60 * 1000 // keep certificates in memory for 1 day
|
||||||
|
|
||||||
|
, approveRegistration: function (hostname, cb) {
|
||||||
|
cb(null, {
|
||||||
|
domains: [hostname]
|
||||||
|
, email: 'user@example.com'
|
||||||
|
, agreeTos: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
, handleRenewFailure: function (err, hostname, certInfo) {
|
||||||
|
console.error("ERROR: Failed to renew domain '", hostname, "':");
|
||||||
|
if (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
}
|
||||||
|
if (certInfo) {
|
||||||
|
console.error(certInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
, letsencrypt: LE.create(
|
||||||
|
// options
|
||||||
|
{ configDir: './letsencrypt.config'
|
||||||
|
|
||||||
|
, server: LE.productionServerUrl
|
||||||
|
, privkeyPath: LE.privkeyPath
|
||||||
|
, fullchainPath: LE.fullchainPath
|
||||||
|
, certPath: LE.certPath
|
||||||
|
, chainPath: LE.chainPath
|
||||||
|
, renewalPath: LE.renewalPath
|
||||||
|
, accountsDir: LE.accountsDir
|
||||||
|
|
||||||
|
, debug: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlers
|
||||||
|
, { setChallenge: LEX.setChallenge
|
||||||
|
, removeChallenge: LEX.removeChallenge
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
, debug: false
|
||||||
|
});
|
||||||
|
|
||||||
|
http.createServer(LEX.createAcmeResponder(lex, function (req, res) {
|
||||||
|
res.setHeader('Location', 'https://' + req.headers.host + req.url);
|
||||||
|
res.end('<!-- Hello Mr Developer! Please use HTTPS instead -->');
|
||||||
|
}));
|
||||||
|
|
||||||
|
https.createServer(lex.httpsOptions, LEX.createAcmeResponder(lex, function (req, res) {
|
||||||
|
res.end('Hello!');
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
## Heroku?
|
## Heroku?
|
||||||
|
|
||||||
This doesn't work on heroku because heroku uses a proxy with built-in https
|
This doesn't work on heroku because heroku uses a proxy with built-in https
|
||||||
|
|
|
@ -151,7 +151,7 @@ cli.main(function(_, options) {
|
||||||
function startServers() {
|
function startServers() {
|
||||||
// Note: using staging server url, remove .testing() for production
|
// Note: using staging server url, remove .testing() for production
|
||||||
var LE = require('letsencrypt');
|
var LE = require('letsencrypt');
|
||||||
var challengeStore = require('../lib/challenge-handlers');
|
var LEX = require('../');
|
||||||
var le = LE.create({
|
var le = LE.create({
|
||||||
configDir: configDir
|
configDir: configDir
|
||||||
, manual: true
|
, manual: true
|
||||||
|
@ -163,10 +163,9 @@ cli.main(function(_, options) {
|
||||||
, renewalPath: LE.renewalPath
|
, renewalPath: LE.renewalPath
|
||||||
, accountsDir: LE.accountsDir
|
, accountsDir: LE.accountsDir
|
||||||
}, {
|
}, {
|
||||||
setChallenge: challengeStore.set
|
setChallenge: LEX.setChallenge
|
||||||
, removeChallenge: challengeStore.remove
|
, removeChallenge: LEX.removeChallenge
|
||||||
});
|
});
|
||||||
var lex = require('../');
|
|
||||||
var app = express();
|
var app = express();
|
||||||
var vhosts = {};
|
var vhosts = {};
|
||||||
|
|
||||||
|
@ -192,7 +191,7 @@ cli.main(function(_, options) {
|
||||||
});
|
});
|
||||||
app.use('/', express.static(path.join(__dirname, '..', 'lib', 'public')));
|
app.use('/', express.static(path.join(__dirname, '..', 'lib', 'public')));
|
||||||
|
|
||||||
lex.create({
|
LEX.create({
|
||||||
onRequest: app
|
onRequest: app
|
||||||
, configDir: configDir
|
, configDir: configDir
|
||||||
, letsencrypt: le
|
, letsencrypt: le
|
||||||
|
|
|
@ -4,12 +4,23 @@ var crypto = require('crypto');
|
||||||
var tls = require('tls');
|
var tls = require('tls');
|
||||||
|
|
||||||
module.exports.create = function (opts) {
|
module.exports.create = function (opts) {
|
||||||
|
var ipc = {}; // in-process cache
|
||||||
|
|
||||||
|
// function (/*err, hostname, certInfo*/) {}
|
||||||
|
function handleRenewFailure(err, hostname, certInfo) {
|
||||||
|
console.error("ERROR: Failed to renew domain '", hostname, "':");
|
||||||
|
if (err) {
|
||||||
|
console.error(err.stack || err);
|
||||||
|
}
|
||||||
|
if (certInfo) {
|
||||||
|
console.error(certInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts) { throw new Error("requires opts to be an object"); }
|
||||||
if (opts.debug) {
|
if (opts.debug) {
|
||||||
console.debug("[LEX] creating sniCallback", JSON.stringify(opts, null, ' '));
|
console.debug("[LEX] creating sniCallback", JSON.stringify(opts, null, ' '));
|
||||||
}
|
}
|
||||||
var ipc = {}; // in-process cache
|
|
||||||
|
|
||||||
if (!opts) { throw new Error("requires opts to be an object"); }
|
|
||||||
if (!opts.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); }
|
if (!opts.letsencrypt) { throw new Error("requires opts.letsencrypt to be a letsencrypt instance"); }
|
||||||
|
|
||||||
if (!opts.lifetime) { opts.lifetime = 90 * 24 * 60 * 60 * 1000; }
|
if (!opts.lifetime) { opts.lifetime = 90 * 24 * 60 * 60 * 1000; }
|
||||||
|
@ -19,7 +30,7 @@ module.exports.create = function (opts) {
|
||||||
|
|
||||||
if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; }
|
if (!opts.approveRegistration) { opts.approveRegistration = function (hostname, cb) { cb(null, null); }; }
|
||||||
//opts.approveRegistration = function (hostname, cb) { cb(null, null); };
|
//opts.approveRegistration = function (hostname, cb) { cb(null, null); };
|
||||||
if (!opts.handleRenewFailure) { opts.handleRenewFailure = function (/*err, hostname, certInfo*/) {}; }
|
if (!opts.handleRenewFailure) { opts.handleRenewFailure = handleRenewFailure; }
|
||||||
|
|
||||||
function assignBestByDates(now, certInfo) {
|
function assignBestByDates(now, certInfo) {
|
||||||
certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 };
|
certInfo = certInfo || { loadedAt: now, expiresAt: 0, issuedAt: 0, lifetime: 0 };
|
||||||
|
|
|
@ -5,6 +5,39 @@ var challengeStore = require('./challenge-handlers');
|
||||||
var createSniCallback = require('./sni-callback').create;
|
var createSniCallback = require('./sni-callback').create;
|
||||||
var LE = require('letsencrypt');
|
var LE = require('letsencrypt');
|
||||||
|
|
||||||
|
function createAcmeResponder(obj, onRequest) {
|
||||||
|
|
||||||
|
function httpAcmeResponder(req, res) {
|
||||||
|
if (LEX.debug) {
|
||||||
|
console.debug('[LEX] ', req.method, req.headers.host, req.url);
|
||||||
|
}
|
||||||
|
var acmeChallengePrefix = '/.well-known/acme-challenge/';
|
||||||
|
|
||||||
|
if (0 !== req.url.indexOf(acmeChallengePrefix)) {
|
||||||
|
onRequest(req, res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = req.url.slice(acmeChallengePrefix.length);
|
||||||
|
|
||||||
|
obj.getChallenge({
|
||||||
|
debug: LEX.debug || obj.debug
|
||||||
|
}, req.headers.host, key, function (err, val) {
|
||||||
|
if (LEX.debug) {
|
||||||
|
console.debug('[LEX] GET challenge, response:');
|
||||||
|
console.debug('challenge:', key);
|
||||||
|
console.debug('response:', val);
|
||||||
|
if (err) {
|
||||||
|
console.debug(err.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res.end(val || '_');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpAcmeResponder;
|
||||||
|
}
|
||||||
|
|
||||||
function lexHelper(obj, app) {
|
function lexHelper(obj, app) {
|
||||||
var defaultPems = require('localhost.daplie.com-certificates');
|
var defaultPems = require('localhost.daplie.com-certificates');
|
||||||
|
|
||||||
|
@ -32,19 +65,14 @@ function lexHelper(obj, app) {
|
||||||
|
|
||||||
if (!obj.getChallenge) {
|
if (!obj.getChallenge) {
|
||||||
if (false !== obj.getChallenge) {
|
if (false !== obj.getChallenge) {
|
||||||
obj.getChallenge = challengeStore.get;
|
obj.getChallenge = LEX.getChallenge;
|
||||||
}
|
}
|
||||||
if (!obj.webrootPath) {
|
if (!obj.webrootPath) {
|
||||||
obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge');
|
obj.webrootPath = path.join(require('os').tmpdir(), 'acme-challenge');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!obj.onRequest && false !== obj.onRequest) {
|
// BEGIN LetsEncrypt options
|
||||||
console.warn("You should either do args.onRequest = app or server.on('request', app),"
|
|
||||||
+ " otherwise only acme-challenge requests will be handled (and the rest will hang)");
|
|
||||||
console.warn("You can silence this warning by setting args.onRequest = false");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!obj.configDir) {
|
if (!obj.configDir) {
|
||||||
obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc');
|
obj.configDir = path.join(require('homedir')(), '/letsencrypt/etc');
|
||||||
}
|
}
|
||||||
|
@ -60,17 +88,21 @@ function lexHelper(obj, app) {
|
||||||
if (!obj.chainPath) {
|
if (!obj.chainPath) {
|
||||||
obj.chainPath = ':config/live/:hostname/chain.pem';
|
obj.chainPath = ':config/live/:hostname/chain.pem';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!obj.server) {
|
if (!obj.server) {
|
||||||
obj.server = LEX.defaultServerUrl;
|
obj.server = LEX.defaultServerUrl;
|
||||||
}
|
}
|
||||||
|
// END LetsEncrypt options
|
||||||
|
|
||||||
|
obj.getChallenge = obj.getChallenge || LEX.getChallenge;
|
||||||
|
obj.setChallenge = obj.setChallenge || LEX.setChallenge;
|
||||||
|
obj.removeChallenge = obj.removeChallenge || LEX.removeChallenge;
|
||||||
|
|
||||||
if (!obj.letsencrypt) {
|
if (!obj.letsencrypt) {
|
||||||
//LE.merge(obj, );
|
//LE.merge(obj, );
|
||||||
// { configDir, webrootPath, server }
|
// { configDir, webrootPath, server }
|
||||||
obj.letsencrypt = LE.create(obj, {
|
obj.letsencrypt = LE.create(obj, {
|
||||||
setChallenge: challengeStore.set
|
setChallenge: obj.setChallenge
|
||||||
, removeChallenge: challengeStore.remove
|
, removeChallenge: obj.removeChallenge
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,39 +164,6 @@ function lexHelper(obj, app) {
|
||||||
httpsOptions.SNICallback = createSniCallback(obj);
|
httpsOptions.SNICallback = createSniCallback(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAcmeResponder(onRequest) {
|
|
||||||
|
|
||||||
function httpAcmeResponder(req, res) {
|
|
||||||
if (LEX.debug) {
|
|
||||||
console.debug('[LEX] ', req.method, req.headers.host, req.url);
|
|
||||||
}
|
|
||||||
var acmeChallengePrefix = '/.well-known/acme-challenge/';
|
|
||||||
|
|
||||||
if (0 !== req.url.indexOf(acmeChallengePrefix)) {
|
|
||||||
onRequest(req, res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = req.url.slice(acmeChallengePrefix.length);
|
|
||||||
|
|
||||||
obj.getChallenge(obj, req.headers.host, key, function (err, val) {
|
|
||||||
if (LEX.debug) {
|
|
||||||
console.debug('[LEX] GET challenge, response:');
|
|
||||||
console.debug('challenge:', key);
|
|
||||||
console.debug('response:', val);
|
|
||||||
if (err) {
|
|
||||||
console.debug(err.stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.end(val || '_');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return httpAcmeResponder;
|
|
||||||
}
|
|
||||||
|
|
||||||
obj.httpAcmeResponder = createAcmeResponder(obj.onHttpRequest||obj.onRequest);
|
|
||||||
obj.httpsAcmeResponder = createAcmeResponder(obj.onHttpsRequest||obj.onRequest);
|
|
||||||
obj.httpsOptions = httpsOptions;
|
obj.httpsOptions = httpsOptions;
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
|
@ -175,6 +174,14 @@ function LEX(obj, app) {
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
|
|
||||||
function listen(plainPorts, tlsPorts, onListening) {
|
function listen(plainPorts, tlsPorts, onListening) {
|
||||||
|
if (!(obj.onRequest || (obj.onHttpRequest && obj.onHttpsRequest)) && false !== obj.onRequest) {
|
||||||
|
console.warn("You should either do args.onRequest = app or server.on('request', app),"
|
||||||
|
+ " otherwise only acme-challenge requests will be handled (and the rest will hang)");
|
||||||
|
console.warn("You can silence this warning by setting args.onRequest = false");
|
||||||
|
}
|
||||||
|
obj.httpAcmeResponder = createAcmeResponder(obj, obj.onHttpRequest || obj.onRequest);
|
||||||
|
obj.httpsAcmeResponder = createAcmeResponder(obj, obj.onHttpsRequest || obj.onRequest);
|
||||||
|
|
||||||
if (plainPorts && (!Array.isArray(plainPorts) || !Array.isArray(tlsPorts))) {
|
if (plainPorts && (!Array.isArray(plainPorts) || !Array.isArray(tlsPorts))) {
|
||||||
throw new Error(".listen() must be used with plain and tls port arrays, like this: `.listen([80], [443, 5001], function () {})`");
|
throw new Error(".listen() must be used with plain and tls port arrays, like this: `.listen([80], [443, 5001], function () {})`");
|
||||||
}
|
}
|
||||||
|
@ -293,6 +300,8 @@ LEX.middleware = function (defaults) {
|
||||||
LEX.stagingServerUrl = LE.stagingServerUrl;
|
LEX.stagingServerUrl = LE.stagingServerUrl;
|
||||||
LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl;
|
LEX.productionServerUrl = LE.productionServerUrl || LE.liveServerUrl;
|
||||||
LEX.defaultServerUrl = LEX.productionServerUrl;
|
LEX.defaultServerUrl = LEX.productionServerUrl;
|
||||||
|
LEX.createAcmeResponder = createAcmeResponder;
|
||||||
|
LEX.normalizeOptions = lexHelper;
|
||||||
LEX.testing = function () {
|
LEX.testing = function () {
|
||||||
LEX.debug = true;
|
LEX.debug = true;
|
||||||
LEX.defaultServerUrl = LEX.stagingServerUrl;
|
LEX.defaultServerUrl = LEX.stagingServerUrl;
|
||||||
|
|
Loading…
Reference in New Issue