Conexión / redirección automática HTTPS con node.js / express

181

He estado tratando de configurar HTTPS con un proyecto node.js en el que estoy trabajando. Básicamente, he seguido la documentación de node.js para este ejemplo:

// 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);

Ahora cuando lo hago

curl -k https://localhost:8000/

yo obtengo

hello world

como se esperaba. Pero si lo hago

curl -k http://localhost:8000/

yo obtengo

curl: (52) Empty reply from server

En retrospectiva, esto parece obvio que funcionaría de esta manera, pero al mismo tiempo, las personas que eventualmente visiten mi proyecto no escribirán https : // yadayada, y quiero que todo el tráfico sea https desde el momento en que llegan. el sitio.

¿Cómo puedo obtener el nodo (y Express, ya que es el marco que estoy usando) para transferir todo el tráfico entrante a https, independientemente de si se especificó o no? No he podido encontrar ninguna documentación que aborde esto. ¿O simplemente se supone que en un entorno de producción, el nodo tiene algo que se encuentra frente a él (por ejemplo, nginx) que maneja este tipo de redirección?

Esta es mi primera incursión en el desarrollo web, así que perdona mi ignorancia si esto es algo obvio.

Jake
fuente
He respondido esto sucintamente aquí: stackoverflow.com/a/23894573/1882064
arcseldon
3
para cualquiera que DESPLIEGUE A HEROKU, las respuestas en esta pregunta no lo ayudarán (obtendrá "demasiados redireccionamientos") pero esta respuesta de otra pregunta sí lo hará
Rico Kahler

Respuestas:

179

Ryan, gracias por señalarme en la dirección correcta. Desarrollé su respuesta (segundo párrafo) un poco con algo de código y funciona. En este escenario, estos fragmentos de código se colocan en mi aplicación express:

// 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);

El servidor https express escucha ATM en 3000. Configuré estas reglas de iptables para que el nodo no tenga que ejecutarse como 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

En conjunto, esto funciona exactamente como yo quería.

Jake
fuente
15
Pregunta realmente importante (con respecto a la seguridad). Antes de que ocurra esa redirección, ¿es posible que un atacante detecte y robe una cookie (ID de sesión)?
Costa
44
¿Cómo solucionaría este síntoma resultante? Error 310 (net::ERR_TOO_MANY_REDIRECTS): There were too many redirects
bodine
16
En realidad, esto parece mejor , ... simplemente envuelva la redirección conif(!req.secure){}
bodine
66
Solo quiero señalar la respuesta a la importante pregunta de @ Costa sobre seguridad dada en otra publicación: stackoverflow.com/questions/8605720/…
ThisClark
2
podría querer envolver una if(req.protocol==='http')declaración
Max
120

Si sigue los puertos convencionales ya que HTTP intenta el puerto 80 de forma predeterminada y HTTPS prueba el puerto 443 de manera predeterminada, simplemente puede tener dos servidores en la misma máquina: Aquí está el código:

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);

Prueba con https:

$ curl https://127.0.0.1 -k
secure!

Con 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

Más detalles: Nodejs HTTP y HTTPS sobre el mismo puerto

basarat
fuente
44
res.writeHead(301, etc.)solo funcionará correctamente para llamadas GET, ya 301que no le dice al cliente que use el mismo método. Si desea mantener el método utilizado (y todos los demás parámetros), debe utilizarlo res.writeHead(307, etc.). Y si todavía no funciona, podría tener que hacer un proxy. Fuente: http://stackoverflow.com/a/17612942/1876359
ocramot
113

Gracias a este chico: 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);
        }
});
Jeremias Binder
fuente
11
¡Esta es la mejor respuesta!
Steffan
3
De acuerdo, debería ser la primera respuesta.
1984
12
La siguiente línea ayudó a que esta solución funcionara para mí:app.enable('trust proxy');
arbel03
2
Como se indicó anteriormente ^^^^: se requiere 'proxy de confianza' si está detrás de algún tipo de proxy, firewall o equilibrador de carga que redirige el tráfico: por ejemplo, el equilibrio de carga de AWS que envía todas las solicitudes http y https al mismo puerto no root (por ejemplo, 3000) en el servidor web de su VPC.
alanwaring
1
para uso de AWS ELB en app.get('X-Forwarded-Proto') != 'http'lugar de req.secure aws.amazon.com/premiumsupport/knowledge-center/…
shak
25

Con Nginx puede aprovechar el encabezado "x-forwards-proto":

function ensureSec(req, res, next){
    if (req.headers["x-forwarded-proto"] === "https"){
       return next();
    }
    res.redirect("https://" + req.headers.host + req.url);  
}
ccyrille
fuente
55
Encontré que req.headers ["x-forwards-proto"] === "https") no es confiable, sin embargo, req.secure funciona.
Zugwalt
Encontré lo mismo, esto parece muy poco confiable.
dacopenhagen
2
req.secure es la forma correcta, sin embargo, esto es defectuoso si está detrás de un proxy porque req.secure es equivalente a proto == "https", sin embargo, detrás de un proxy express puede decir que su proto es https, http
dacopenhagen
77
Debe app.enable('trust proxy');: "Indica que la aplicación está detrás de un proxy frontal y usar los encabezados X-Forward- * para determinar la conexión y la dirección IP del cliente". expressjs.com/en/4x/api.html#app.set
dskrvk
No trate las URL como cadenas, use el módulo de URL en su lugar.
arboreal84
12

A partir de 0.4.12 no tenemos una forma limpia de escuchar HTTP y HTTPS en el mismo puerto utilizando los servidores HTTP / HTTPS de Node.

Algunas personas han resuelto este problema haciendo que el servidor HTTPS de Node (esto también funciona con Express.js) escuche 443 (o algún otro puerto) y también tenga un pequeño servidor http vinculado a 80 y redirija a los usuarios al puerto seguro.

Si absolutamente tiene que ser capaz de manejar ambos protocolos en un solo puerto, entonces necesita poner nginx, lighttpd, apache o algún otro servidor web en ese puerto y actuar como un proxy inverso para Node.

Ryan Olds
fuente
Gracias Ryan Por "... haga que un pequeño servidor http se una a 80 y redirija ..." Supongo que se refiere a otro servidor http de nodo . Creo que tengo una idea de cómo podría funcionar esto, pero ¿puedes señalar algún código de ejemplo? Además, ¿sabe si hay una solución "más limpia" para este problema en algún lugar de la hoja de ruta de nodos?
Jake
Su aplicación Node.js puede tener múltiples servidores http (s). Revisé los problemas de Node.js ( github.com/joyent/node/issues ) y la lista de correo ( groups.google.com/group/nodejs ) y no vi ningún problema reportado, pero vi algunas publicaciones sobre el problema en la lista de correo. Por lo que puedo decir, esto no está en la tubería. Recomendaría informarlo en github y ver qué comentarios recibes.
Ryan Olds
Pregunta realmente importante (con respecto a la seguridad). Antes de que ocurra esa redirección, ¿es posible que un "atacante" detecte y robe una cookie (ID de sesión)?
Costa
Sí, un código de estado 3xx y posiblemente un encabezado de ubicación se envían de vuelta al agente, momento en el cual el agente solicita la URL especificada por el encabezado de ubicación.
Ryan Olds
Es 2015, ¿sigue siendo así?
TheEnvironmentalist
10

Puede usar el módulo 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);
Mateus Ferreira
fuente
1
Use esto con precaución porque el paquete no se ha actualizado en años. Me encontré con problemas en AWS donde la codificación era mala.
Martavis P.
1
paquete hermano: npmjs.com/package/express-to-https - pero no tengo idea si funciona / es mejor / etc
quetzalcoatl
Tenga en cuenta que si usa middleware para servir archivos estáticos (incluidas las aplicaciones de una sola página), appprimero debe adjuntar cualquier middleware de redireccionamiento . (vea esta respuesta )
Matthew R.
9

Uso la solución propuesta por Basarat pero también necesito sobrescribir el puerto porque solía tener 2 puertos diferentes para los protocolos HTTP y HTTPS.

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

Prefiero también usar un puerto no estándar para iniciar nodejs sin privilegios de root. Me gustan 8080 y 8443 porque vengo de muchos años de programación en tomcat.

Mi archivo completo se convierte

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);

Luego uso iptable para redactar tráfico 80 y 443 en mis puertos HTTP y 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
Lorenzo Eccher
fuente
Gracias Lorenzo, esto funcionó para mí. En lugar de usar opciones, usé letsencrypt. Eliminé varias opciones. Agregué los parámetros de letsencrypt (privateKey, certificado, ca y credenciales), y cambié las opciones a credenciales: https.createServer (credenciales, aplicación)
Nhon Ha
8

Esta respuesta debe actualizarse para funcionar con Express 4.0. Así es como conseguí que el servidor http por separado funcionara:

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);
Pan comido
fuente
1
Conseguí que esto funcionara cambiando httpApp.use ('*', httpRouter); a httpApp.use ('/', httpRouter); y moverlo a la línea antes de crear el servidor hhtp.
KungWaz
8

Si su aplicación está detrás de un proxy de confianza (por ejemplo, un AWS ELB o un nginx configurado correctamente), este código debería funcionar:

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

Notas:

  • Esto supone que está alojando su sitio en 80 y 443, de lo contrario, deberá cambiar el puerto cuando redirija
  • Esto también supone que está finalizando el SSL en el proxy. Si está haciendo SSL de extremo a extremo, use la respuesta de @basarat anterior. SSL de extremo a extremo es la mejor solución.
  • app.enable ('proxy de confianza') permite que express compruebe el encabezado X-Fordered-Proto
Jamie McCrindle
fuente
6

Encuentro que req.protocol funciona cuando estoy usando express (no lo he probado sin, pero sospecho que funciona). utilizando el nodo actual 0.10.22 con express 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();
  } 
});
Catalizador
fuente
5

La mayoría de las respuestas aquí sugieren usar el encabezado req.headers.host.

HTTP 1.1 requiere el encabezado Host, pero en realidad es opcional, ya que el encabezado podría no ser enviado por un cliente HTTP, y node / express aceptará esta solicitud.

Puede preguntar: ¿qué cliente HTTP (por ejemplo: navegador) puede enviar una solicitud que falta ese encabezado? El protocolo HTTP es muy trivial. Puede crear una solicitud HTTP en pocas líneas de código, para no enviar un encabezado de host, y si cada vez que recibe una solicitud con formato incorrecto, arroja una excepción, y dependiendo de cómo maneje dichas excepciones, esto puede hacer que su servidor caiga.

Por lo tanto, siempre valide todas las entradas . Esto no es paranoia, he recibido solicitudes que carecen del encabezado del host en mi servicio.

Además, nunca trate las URL como cadenas . Use el módulo URL del nodo para modificar partes específicas de una cadena. Tratar las URL como cadenas se puede explotar de muchas, muchas maneras. No lo hagas

arboreal84
fuente
Su respuesta sería perfecta si hubiera dado un ejemplo.
SerG
Suena legítimo pero algunas referencias / enlaces serían geniales.
Giwan
3
var express = require('express');
var app = express();

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

app.listen(80);

¡Esto es lo que usamos y funciona muy bien!

Nick Kotenberg
fuente
1
No trate las URL como cadenas, use el módulo de URL en su lugar.
arboreal84
44
Dar ejemplos con sentido de responsabilidad. El desbordamiento de pila es el juego telefónico.
arboreal84
1
esto no causará un bucle infinito?
Muhammad Umer
No, porque solo escucha en el puerto 80 y envía al puerto 443. La única forma en que sucedería un bucle infinito es si algún oyente en 443 redirige de nuevo a 80, que no está en mi código. Espero que tenga sentido. ¡Gracias!
Nick Kotenberg
3

puede usar el módulo "net" para escuchar HTTP y HTTPS en el mismo puerto

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)
luofei614
fuente
55
Cuando ejecuto esto en el Nodo 0.8, solo el último servidor que llama .listen parece responder. En este caso, HTTP funciona, pero no HTTPS. Si invierto el orden de .createServer, entonces HTTP funciona pero no HTTPS. :(
Joe
1
Esto no funciona como se describe. Puedo confirmar el problema que vio Joe.
Stepan Mazurov
2

Esto funcionó para mí:

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();
    } 
});
Shummy1991
fuente
Esto puede ser correcto, pero debe explicar cómo responde esto a la pregunta del autor de la pregunta.
Joe C
0

Esto funcionó para mí:

/* 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);
    }
});

Recomendamos agregar los encabezados antes de redirigir a https

Ahora, cuando lo haces:

curl http://127.0.0.1 --include

Usted obtiene:

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/

Yo uso express 4.17.1

χρηστος απατσιδης
fuente