Skip to content

v2.9.13 #1667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 22, 2021
Merged

v2.9.13 #1667

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading