Автоматическое соединение HTTPS / перенаправление с помощью node.js / express


182

Я пытался настроить HTTPS с проектом node.js, над которым я работаю. Я, по сути, следовал документации для node.js для этого примера:

// curl -k https://localhost:8000/
var https = require('https');
var fs = require('fs');

var options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(8000);

Теперь, когда я делаю

curl -k https://localhost:8000/

я получил

hello world

как и ожидалось. Но если я сделаю

curl -k http://localhost:8000/

я получил

curl: (52) Empty reply from server

В ретроспективе это кажется очевидным, что это будет работать таким образом, но в то же время люди, которые в конечном итоге посещают мой проект, не собираются вводить https : // yadayada, и я хочу, чтобы весь трафик был https с момента их попадания сайт.

Как я могу получить узел (и Express, поскольку это среда, которую я использую), чтобы передать весь входящий трафик на https, независимо от того, был ли он указан? Я не смог найти никаких документов, которые касались этого. Или просто предполагается, что в производственной среде узел имеет что-то, что находится перед ним (например, nginx), который обрабатывает такой тип перенаправления?

Это мой первый опыт веб-разработки, поэтому, пожалуйста, прости мое невежество, если это что-то очевидно.



3
для тех, кто РАЗЛОЖИТСЯ В HEROKU, ответы на этот вопрос не помогут вам (вы получите «слишком много перенаправлений»), но этот ответ на другой вопрос поможет
Rico Kahler

Ответы:


179

Райан, спасибо, что указал мне правильное направление. Я немного уточнил ваш ответ (2-й абзац) с помощью некоторого кода, и он работает. В этом сценарии эти фрагменты кода помещаются в мое экспресс-приложение:

// set up plain http server
var http = express.createServer();

// set up a route to redirect http to https
http.get('*', function(req, res) {  
    res.redirect('https://' + req.headers.host + req.url);

    // Or, if you don't want to automatically detect the domain name from the request header, you can hard code it:
    // res.redirect('https://example.com' + req.url);
})

// have it listen on 8080
http.listen(8080);

Экспресс-сервер https прослушивает ATM на 3000. Я установил эти правила iptables, чтобы узел не запускался от имени пользователя root:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 3000

Все вместе, это работает именно так, как я хотел.


15
Действительно важный вопрос (в отношении безопасности). Перед тем, как на самом деле произойдет перенаправление, сможет ли злоумышленник вынюхать и украсть куки (идентификатор сессии)?
Коста

4
Как бы я исправить этот результирующий симптом? Error 310 (net::ERR_TOO_MANY_REDIRECTS): There were too many redirects
bodine

16
На самом деле, это кажется лучше , ... просто оберните редирект сif(!req.secure){}
bodine

6
Я просто хочу указать на ответ на важный вопрос @ Косты о безопасности, который приведен в другом посте - stackoverflow.com/questions/8605720/…
ThisClark

2
может захотеть обернуть if(req.protocol==='http')заявление
Макс

120

Если вы используете обычные порты, так как HTTP пытается использовать порт 80 по умолчанию, а HTTPS пытается использовать порт 443 по умолчанию, вы можете просто иметь два сервера на одной машине: Вот код:

var https = require('https');

var fs = require('fs');
var options = {
    key: fs.readFileSync('./key.pem'),
    cert: fs.readFileSync('./cert.pem')
};

https.createServer(options, function (req, res) {
    res.end('secure!');
}).listen(443);

// Redirect from http port 80 to https
var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
    res.end();
}).listen(80);

Тест с https:

$ curl https://127.0.0.1 -k
secure!

С http:

$ curl http://127.0.0.1 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Date: Sun, 01 Jun 2014 06:15:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Более подробная информация: Nodejs HTTP и HTTPS через один и тот же порт


4
res.writeHead(301, etc.)будет правильно работать только для вызовов GET, поскольку 301не говорит клиенту использовать тот же метод. Если вы хотите сохранить используемый метод (и все остальные параметры), вы должны использовать res.writeHead(307, etc.). И если это все еще не работает, вам может понадобиться прокси. Источник: http://stackoverflow.com/a/17612942/1876359
ocramot

113

Спасибо этому парню: https://www.tonyerwin.com/2014/09/redirecting-http-to-https-with-nodejs.html

app.use (function (req, res, next) {
        if (req.secure) {
                // request was via https, so do no special handling
                next();
        } else {
                // request was via http, so redirect to https
                res.redirect('https://' + req.headers.host + req.url);
        }
});

11
Это лучший ответ!
Штеффан

3
Договорились, должен быть первый ответ.
1984

12
Следующая строка помогла этому решению работать для меня:app.enable('trust proxy');
arbel03

2
Как указано выше ^^^^: «доверенный прокси» требуется, если вы находитесь за прокси-сервером, брандмауэром или балансировщиком нагрузки любого типа, который перенаправляет трафик: например, балансировка нагрузки AWS, которая отправляет все запросы http и https на один и тот же некорневой порт (например, 3000) на вашем веб-сервере VPC.
alanwaring

1
для AWS ELB использовать app.get('X-Forwarded-Proto') != 'http'вместо req.secure aws.amazon.com/premiumsupport/knowledge-center/...
Шак

25

С Nginx вы можете воспользоваться заголовком «x-forwarded-proto»:

function ensureSec(req, res, next){
    if (req.headers["x-forwarded-proto"] === "https"){
       return next();
    }
    res.redirect("https://" + req.headers.host + req.url);  
}

5
Я обнаружил, что req.headers ["x-forwarded-proto"] === "https") не является надежным, однако req.secure работает!
Zugwalt

Я нашел то же самое, это кажется очень ненадежным.
Дакопенхаген

2
req.secure - правильный путь, однако он некорректен, если вы находитесь за прокси-сервером, потому что req.secure эквивалентен proto == "https", однако за прокси-экспрессом может быть сказано, что ваш proto - https, http
dacopenhagen

7
Вам необходимо app.enable('trust proxy');: «Указывает, что приложение находится за фронтальным прокси-сервером и использовать заголовки X-Forwarded- * для определения соединения и IP-адреса клиента». expressjs.com/en/4x/api.html#app.set
dskrvk

Не рассматривайте URL как строки, вместо этого используйте модуль url.
arboreal84

12

Начиная с 0.4.12, у нас нет реального чистого способа прослушивания HTTP и HTTPS на одном и том же порту с использованием HTTP / HTTPS-серверов Node.

Некоторые люди решили эту проблему с помощью HTTPS-сервера Node (который также работает с Express.js), который прослушивает 443 (или какой-либо другой порт), а также имеет небольшой http-сервер, привязанный к 80 и перенаправляющий пользователей на защищенный порт.

Если вам абсолютно необходимо иметь возможность обрабатывать оба протокола на одном порту, вам нужно установить на этот порт nginx, lighttpd, apache или какой-либо другой веб-сервер и действовать как обратный прокси-сервер для Node.


Спасибо Райан. Под «... иметь небольшой http-сервер, привязать к 80 и перенаправить ...» Я предполагаю, что вы имеете в виду http-сервер другого узла . Я думаю, у меня есть представление о том, как это может работать, но можете ли вы указать на пример кода? Кроме того, знаете ли вы, есть ли «более чистое» решение этой проблемы где-либо в дорожной карте узла?
Джейк

Ваше приложение Node.js может иметь несколько http (s) серверов. Я проверил проблемы Node.js ( github.com/joyent/node/issues ) и список рассылки ( groups.google.com/group/nodejs ) и не увидел сообщений о проблемах, но увидел пару сообщений о проблеме в списке рассылки. Насколько я могу сказать, это не в трубе. Я бы порекомендовал сообщить об этом на github и посмотреть, какие отзывы вы получите.
Райан Олдс

Действительно важный вопрос (в отношении безопасности). Перед тем, как на самом деле произойдет перенаправление, сможет ли «злоумышленник» прослушать и украсть куки (идентификатор сеанса)?
Коста

Да, код состояния 3xx и, возможно, заголовок Location отправляются обратно агенту, после чего агент запрашивает URL-адрес, указанный в заголовке Location.
Райан Олдс

Это 2015 год, это все еще так?
Эколог

10

Вы можете использовать модуль express-force-https :

npm install --save express-force-https

var express = require('express');
var secure = require('express-force-https');

var app = express();
app.use(secure);

1
Используйте это с осторожностью, потому что пакет не обновлялся годами. Я столкнулся с проблемами в AWS, где кодирование было плохим.
Мартавис П.

1
родственный пакет: npmjs.com/package/express-to-https - но я понятия не имею, работает ли он / isbetter / etc
quetzalcoatl

Обратите внимание, что если вы используете промежуточное программное обеспечение для обслуживания статических файлов (включая одностраничные приложения), любое промежуточное программное обеспечение перенаправления должно быть присоединено в appпервую очередь. (см. этот ответ )
Мэтью Р.

9

Я использую решение, предложенное Basarat, но мне также нужно перезаписать порт, потому что у меня было 2 разных порта для протоколов HTTP и HTTPS.

res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(http_port,https_port) + req.url });

Я предпочитаю также использовать нестандартный порт, так что запускать nodejs без прав root. Мне нравятся 8080 и 8443, потому что я много лет программировал на tomcat.

Мой полный файл стал

var fs = require('fs');
var http = require('http');
var http_port    =   process.env.PORT || 8080; 
var app = require('express')();

// HTTPS definitions
var https = require('https');
var https_port    =   process.env.PORT_HTTPS || 8443; 
var options = {
   key  : fs.readFileSync('server.key'),
   cert : fs.readFileSync('server.crt')
};

app.get('/', function (req, res) {
   res.send('Hello World!');
});

https.createServer(options, app).listen(https_port, function () {
   console.log('Magic happens on port ' + https_port); 
});

// Redirect from http port to https
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(http_port,https_port) + req.url });
    console.log("http request, will go to >> ");
    console.log("https://" + req.headers['host'].replace(http_port,https_port) + req.url );
    res.end();
}).listen(http_port);

Затем я использую iptable для переписывания трафика 80 и 443 через порты HTTP и HTTPS.

sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8443

Спасибо Лоренцо, это сработало для меня. Вместо использования опций я использовал letsencrypt. Я удалил варианты вар. Я добавил параметры letsencrypt (privateKey, сертификат, CA и учетные данные), и я изменил параметры для учетных данных: https.createServer (учетные данные, приложение)
Nhon Ha

8

Этот ответ необходимо обновить для работы с Express 4.0. Вот как я получил отдельный http-сервер для работы:

var express = require('express');
var http = require('http');
var https = require('https');

// Primary https app
var app = express()
var port = process.env.PORT || 3000;
app.set('env', 'development');
app.set('port', port);
var router = express.Router();
app.use('/', router);
// ... other routes here
var certOpts = {
    key: '/path/to/key.pem',
    cert: '/path/to/cert.pem'
};
var server = https.createServer(certOpts, app);
server.listen(port, function(){
    console.log('Express server listening to port '+port);
});


// Secondary http app
var httpApp = express();
var httpRouter = express.Router();
httpApp.use('*', httpRouter);
httpRouter.get('*', function(req, res){
    var host = req.get('Host');
    // replace the port in the host
    host = host.replace(/:\d+$/, ":"+app.get('port'));
    // determine the redirect destination
    var destination = ['https://', host, req.url].join('');
    return res.redirect(destination);
});
var httpServer = http.createServer(httpApp);
httpServer.listen(8080);

1
Я заставил это работать, изменив httpApp.use ('*', httpRouter); на httpApp.use ('/', httpRouter); и переместить его в строку перед созданием сервера hhtp.
KungWaz

8

Если ваше приложение находится за доверенным прокси-сервером (например, AWS ELB или правильно настроенным nginx), этот код должен работать:

app.enable('trust proxy');
app.use(function(req, res, next) {
    if (req.secure){
        return next();
    }
    res.redirect("https://" + req.headers.host + req.url);
});

Ноты:

  • Это предполагает, что вы размещаете свой сайт на 80 и 443, если нет, вам нужно будет изменить порт при перенаправлении
  • Это также предполагает, что вы прекращаете SSL на прокси. Если вы делаете SSL сквозной, используйте ответ от @basarat выше. Сквозной конец SSL - лучшее решение.
  • app.enable («доверенный прокси») позволяет Express проверять заголовок X-Forwarded-Proto

7

Я считаю, что req.protocol работает, когда я использую экспресс (не тестировал без, но я подозреваю, что он работает). используя текущий узел 0.10.22 с экспрессом 3.4.3

app.use(function(req,res,next) {
  if (!/https/.test(req.protocol)){
     res.redirect("https://" + req.headers.host + req.url);
  } else {
     return next();
  } 
});

5

Большинство ответов здесь предлагают использовать заголовок req.headers.host.

Заголовок Host требуется HTTP 1.1, но на самом деле он необязателен, поскольку заголовок может фактически не отправляться HTTP-клиентом, и узел / экспресс примет этот запрос.

Вы можете спросить: какой HTTP-клиент (например, браузер) может отправить запрос без этого заголовка? Протокол HTTP очень тривиален. Вы можете создать HTTP-запрос в несколько строк кода, чтобы не отправлять заголовок узла, и если каждый раз, когда вы получаете искаженный запрос, вы генерируете исключение, и в зависимости от того, как вы обрабатываете такие исключения, это может привести к отключению вашего сервера.

Поэтому всегда проверяйте все входные данные . Это не паранойя, я получил запросы без заголовка хоста в моем сервисе.

Кроме того, никогда не рассматривайте URL как строки . Используйте модуль url узла, чтобы изменить определенные части строки. Обработка URL-адресов как строк может быть использована многими способами. Не делай этого.


Ваш ответ был бы идеальным, если бы вы дали пример.
SerG

Звучит законно, но некоторые ссылки / ссылки были бы великолепны.
19

3
var express = require('express');
var app = express();

app.get('*',function (req, res) {
    res.redirect('https://<domain>' + req.url);
});

app.listen(80);

Это то, что мы используем, и это прекрасно работает!


1
Не рассматривайте URL как строки, вместо этого используйте модуль url.
arboreal84

4
Приведите примеры с чувством ответственности. Переполнение стека - телефонная игра.
arboreal84

1
это не вызовет бесконечный цикл?
Мухаммед Умер

Нет, потому что он прослушивает только порт 80 и отправляет на порт 443. Единственный способ, которым может произойти бесконечный цикл, - это если какой-нибудь слушатель на 443 перенаправляет обратно на 80, чего нет в моем коде. Я надеюсь, что в этом есть смысл. Спасибо!
Ник Котенберг

3

Вы можете использовать модуль "net" для прослушивания HTTP и HTTPS на одном и том же порту

var https = require('https');
var http = require('http');
var fs = require('fs');

var net=require('net');
var handle=net.createServer().listen(8000)

var options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(handle);

http.createServer(function(req,res){
  res.writeHead(200);
  res.end("hello world\n");
}).listen(handle)

5
Когда я запускаю это на узле 0.8, кажется, что отвечает только последний сервер, который вызывает .listen. В этом случае HTTP работает, но не HTTPS. Если я изменяю порядок .createServer, то HTTP работает, но не HTTPS. :(
Джо

1
Это не работает, как описано. Я могу подтвердить проблему, которую видел Джо.
Степан Мазуров

2

Это сработало для меня:

app.use(function(req,res,next) {
    if(req.headers["x-forwarded-proto"] == "http") {
        res.redirect("https://[your url goes here]" + req.url, next);
    } else {
        return next();
    } 
});

Это может быть правильно, но вы должны уточнить, как это отвечает на вопрос задающего.
Джо С

1

Вы можете создать 2 сервера Node.js - один для HTTP и HTTPS

Вы также можете определить функцию установки, которую будут выполнять оба сервера, чтобы вам не пришлось писать многократно дублированный код.

Вот как я это сделал: (используя restify.js, но должен работать для express.js или для самого узла)

http://qugstart.com/blog/node-js/node-js-restify-server-with-both-http-and-https/


0

Это сработало для меня:

/* Headers */
require('./security/Headers/HeadersOptions').Headers(app);

/* Server */
const ssl = {
    key: fs.readFileSync('security/ssl/cert.key'),
    cert: fs.readFileSync('security/ssl/cert.pem')
};
//https server
https.createServer(ssl, app).listen(443, '192.168.1.2' && 443, '127.0.0.1');
//http server
app.listen(80, '192.168.1.2' && 80, '127.0.0.1');
app.use(function(req, res, next) {
    if(req.secure){
        next();
    }else{
        res.redirect('https://' + req.headers.host + req.url);
    }
});

Рекомендуем добавить заголовки перед перенаправлением на https

Теперь, когда вы делаете:

curl http://127.0.0.1 --include

Ты получаешь:

HTTP/1.1 302 Found
//
Location: https://127.0.0.1/
Vary: Accept
Content-Type: text/plain; charset=utf-8
Content-Length: 40
Date: Thu, 04 Jul 2019 09:57:34 GMT
Connection: keep-alive

Found. Redirecting to https://127.0.0.1/

Я использую экспресс 4.17.1

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.