Skip to content

Commit

Permalink
Merge pull request #1667 from jc21/develop
Browse files Browse the repository at this point in the history
v2.9.13
  • Loading branch information
jc21 committed Dec 22, 2021
2 parents 9ab5333 + 6392df3 commit 84bc33d
Show file tree
Hide file tree
Showing 18 changed files with 353 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.9.12
2.9.13
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ pipeline {
-v "$(pwd)/global:/app/global" \\
-w /app \\
node:latest \\
sh -c "yarn install && yarn eslint . && rm -rf node_modules"
sh -c "ln -s /usr/bin/python3 /usr/bin/python && yarn install && yarn eslint . && rm -rf node_modules"
'''

echo 'Docker Build ...'
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.9.12-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.9.13-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>
Expand Down
90 changes: 90 additions & 0 deletions backend/internal/certificate.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const _ = require('lodash');
const fs = require('fs');
const https = require('https');
const tempWrite = require('temp-write');
const moment = require('moment');
const logger = require('../logger').ssl;
Expand All @@ -15,6 +16,7 @@ const letsencryptConfig = '/etc/letsencrypt.ini';
const certbotCommand = 'certbot';
const archiver = require('archiver');
const path = require('path');
const { isArray } = require('lodash');

function omissions() {
return ['is_deleted'];
Expand Down Expand Up @@ -1124,6 +1126,94 @@ const internalCertificate = {
} else {
return Promise.resolve();
}
},

testHttpsChallenge: async (access, domains) => {
await access.can('certificates:list');

if (!isArray(domains)) {
throw new error.InternalValidationError('Domains must be an array of strings');
}
if (domains.length === 0) {
throw new error.InternalValidationError('No domains provided');
}

// Create a test challenge file
const testChallengeDir = '/data/letsencrypt-acme-challenge/.well-known/acme-challenge';
const testChallengeFile = testChallengeDir + '/test-challenge';
fs.mkdirSync(testChallengeDir, {recursive: true});
fs.writeFileSync(testChallengeFile, 'Success', {encoding: 'utf8'});

async function performTestForDomain (domain) {
logger.info('Testing http challenge for ' + domain);
const url = `http://${domain}/.well-known/acme-challenge/test-challenge`;
const formBody = `method=G&url=${encodeURI(url)}&bodytype=T&requestbody=&headername=User-Agent&headervalue=None&locationid=1&ch=false&cc=false`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(formBody)
}
};

const result = await new Promise((resolve) => {

const req = https.request('https://www.site24x7.com/tools/restapi-tester', options, function (res) {
let responseBody = '';

res.on('data', (chunk) => responseBody = responseBody + chunk);
res.on('end', function () {
const parsedBody = JSON.parse(responseBody + '');
if (res.statusCode !== 200) {
logger.warn(`Failed to test HTTP challenge for domain ${domain}`, res);
resolve(undefined);
}
resolve(parsedBody);
});
});

// Make sure to write the request body.
req.write(formBody);
req.end();
req.on('error', function (e) { logger.warn(`Failed to test HTTP challenge for domain ${domain}`, e);
resolve(undefined); });
});

if (!result) {
// Some error occurred while trying to get the data
return 'failed';
} else if (`${result.responsecode}` === '200' && result.htmlresponse === 'Success') {
// Server exists and has responded with the correct data
return 'ok';
} else if (`${result.responsecode}` === '200') {
// Server exists but has responded with wrong data
logger.info(`HTTP challenge test failed for domain ${domain} because of invalid returned data:`, result.htmlresponse);
return 'wrong-data';
} else if (`${result.responsecode}` === '404') {
// Server exists but responded with a 404
logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`);
return '404';
} else if (`${result.responsecode}` === '0' || (typeof result.reason === 'string' && result.reason.toLowerCase() === 'host unavailable')) {
// Server does not exist at domain
logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`);
return 'no-host';
} else {
// Other errors
logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`);
return `other:${result.responsecode}`;
}
}

const results = {};

for (const domain of domains){
results[domain] = await performTestForDomain(domain);
}

// Remove the test challenge file
fs.unlinkSync(testChallengeFile);

return results;
}
};

Expand Down
50 changes: 50 additions & 0 deletions backend/migrations/20211108145214_regenerate_default_host.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const migrate_name = 'stream_domain';
const logger = require('../logger').migrate;
const internalNginx = require('../internal/nginx');

async function regenerateDefaultHost(knex) {
const row = await knex('setting').select('*').where('id', 'default-site').first();

if (!row) {
return Promise.resolve();
}

return internalNginx.deleteConfig('default')
.then(() => {
return internalNginx.generateConfig('default', row);
})
.then(() => {
return internalNginx.test();
})
.then(() => {
return internalNginx.reload();
});
}

/**
* Migrate
*
* @see http://knexjs.org/#Schema
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.up = function (knex) {
logger.info('[' + migrate_name + '] Migrating Up...');

return regenerateDefaultHost(knex);
};

/**
* Undo Migrate
*
* @param {Object} knex
* @param {Promise} Promise
* @returns {Promise}
*/
exports.down = function (knex) {
logger.info('[' + migrate_name + '] Migrating Down...');

return regenerateDefaultHost(knex);
};
27 changes: 26 additions & 1 deletion backend/routes/api/nginx/certificates.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ router
.catch(next);
});

/**
* Test HTTP challenge for domains
*
* /api/nginx/certificates/test-http
*/
router
.route('/test-http')
.options((req, res) => {
res.sendStatus(204);
})
.all(jwtdecode())

/**
* GET /api/nginx/certificates/test-http
*
* Test HTTP challenge for domains
*/
.get((req, res, next) => {
internalCertificate.testHttpsChallenge(res.locals.access, JSON.parse(req.query.domains))
.then((result) => {
res.status(200)
.send(result);
})
.catch(next);
});

/**
* Specific certificate
*
Expand Down Expand Up @@ -209,7 +235,6 @@ router
.catch(next);
});


/**
* Download LE Certs
*
Expand Down
11 changes: 11 additions & 0 deletions backend/schema/endpoints/certificates.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@
"targetSchema": {
"type": "boolean"
}
},
{
"title": "Test HTTP Challenge",
"description": "Tests whether the HTTP challenge should work",
"href": "/nginx/certificates/{definitions.identity.example}/test-http",
"access": "private",
"method": "GET",
"rel": "info",
"http_header": {
"$ref": "../examples.json#/definitions/auth_header"
}
}
]
}
8 changes: 1 addition & 7 deletions backend/templates/_location.conf
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
location {{ path }} {
set $targetUri {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};
{% unless path contains "~" and path contains "(" and path contains ")" %}
if ($request_uri != /){
set $targetUri $targetUri$request_uri;
}
{% endunless %}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass $targetUri;
proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path }};

{% if access_list_id > 0 %}
{% if access_list.items.length > 0 %}
Expand Down
3 changes: 1 addition & 2 deletions docker/rootfs/etc/nginx/conf.d/include/proxy.conf
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
set $targetUri $forward_scheme://$server:$port$request_uri;
add_header X-Served-By $host;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass $targetUri;
proxy_pass $forward_scheme://$server:$port$request_uri;

10 changes: 10 additions & 0 deletions frontend/js/app/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,16 @@ module.exports = {
return fetch('post', 'nginx/certificates/' + id + '/renew', undefined, {timeout});
},

/**
* @param {Number} id
* @returns {Promise}
*/
testHttpChallenge: function (domains) {
return fetch('get', 'nginx/certificates/test-http?' + new URLSearchParams({
domains: JSON.stringify(domains),
}));
},

/**
* @param {Number} id
* @returns {Promise}
Expand Down
13 changes: 13 additions & 0 deletions frontend/js/app/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,19 @@ module.exports = {
}
},

/**
* Certificate Test Reachability
*
* @param model
*/
showNginxCertificateTestReachability: function (model) {
if (Cache.User.isAdmin() || Cache.User.canManage('certificates')) {
require(['./main', './nginx/certificates/test'], function (App, View) {
App.UI.showModalDialog(new View({model: model}));
});
}
},

/**
* Audit Log
*/
Expand Down
8 changes: 8 additions & 0 deletions frontend/js/app/nginx/certificates/form.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
<input type="text" name="domain_names" class="form-control" id="input-domains" value="<%- domain_names.join(',') %>" required>
<div class="text-blue"><i class="fe fe-alert-triangle"></i> <%- i18n('ssl', 'hosts-warning') %></div>
</div>
<div class="mb-3 test-domains-container">
<button type="button" class="btn btn-secondary test-domains col-sm-12"><%- i18n('certificates', 'test-reachability') %></button>
<div class="text-secondary small">
<i class="fe fe-info"></i>
<%- i18n('certificates', 'reachability-info') %>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12">
<div class="form-group">
Expand Down
27 changes: 26 additions & 1 deletion frontend/js/app/nginx/certificates/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ module.exports = Mn.View.extend({
non_loader_content: '.non-loader-content',
le_error_info: '#le-error-info',
domain_names: 'input[name="domain_names"]',
test_domains_container: '.test-domains-container',
test_domains_button: '.test-domains',
buttons: '.modal-footer button',
cancel: 'button.cancel',
save: 'button.save',
Expand Down Expand Up @@ -56,10 +58,12 @@ module.exports = Mn.View.extend({
this.ui.dns_provider_credentials.prop('required', 'required');
}
this.ui.dns_challenge_content.show();
this.ui.test_domains_container.hide();
} else {
this.ui.dns_provider.prop('required', false);
this.ui.dns_provider_credentials.prop('required', false);
this.ui.dns_challenge_content.hide();
this.ui.dns_challenge_content.hide();
this.ui.test_domains_container.show();
}
},

Expand Down Expand Up @@ -205,6 +209,23 @@ module.exports = Mn.View.extend({
this.ui.non_loader_content.show();
});
},
'click @ui.test_domains_button': function (e) {
e.preventDefault();
const domainNames = this.ui.domain_names[0].value.split(',');
if (domainNames && domainNames.length > 0) {
this.model.set('domain_names', domainNames);
this.model.set('back_to_add', true);
App.Controller.showNginxCertificateTestReachability(this.model);
}
},
'change @ui.domain_names': function(e){
const domainNames = e.target.value.split(',');
if (domainNames && domainNames.length > 0) {
this.ui.test_domains_button.prop('disabled', false);
} else {
this.ui.test_domains_button.prop('disabled', true);
}
},
'change @ui.other_certificate_key': function(e){
this.setFileName("other_certificate_key_label", e)
},
Expand Down Expand Up @@ -257,6 +278,10 @@ module.exports = Mn.View.extend({
this.ui.credentials_file_content.hide();
this.ui.loader_content.hide();
this.ui.le_error_info.hide();
const domainNames = this.ui.domain_names[0].value.split(',');
if (!domainNames || domainNames.length === 0 || (domainNames.length === 1 && domainNames[0] === "")) {
this.ui.test_domains_button.prop('disabled', true);
}
},

initialize: function (options) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/js/app/nginx/certificates/list/item.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<% if (provider === 'letsencrypt') { %>
<a href="#" class="renew dropdown-item"><i class="dropdown-icon fe fe-refresh-cw"></i> <%- i18n('certificates', 'force-renew') %></a>
<a href="#" class="download dropdown-item"><i class="dropdown-icon fe fe-download"></i> <%- i18n('certificates', 'download') %></a>
<% if (meta.dns_challenge === false) { %>
<a href="#" class="test dropdown-item"><i class="dropdown-icon fe fe-globe"></i> <%- i18n('certificates', 'test-reachability') %></a>
<% } %>
<div class="dropdown-divider"></div>
<% } %>
<a href="#" class="delete dropdown-item"><i class="dropdown-icon fe fe-trash-2"></i> <%- i18n('str', 'delete') %></a>
Expand Down

0 comments on commit 84bc33d

Please sign in to comment.