Ghost

A while ago, Revue, the newsletter service we were using, announced that it was shutting down.

Revue has announced that it is closing

We had to look for other alternatives. I tried using Wordpress, but it’s true that this ancient system hasn’t evolved particularly much over the years, and then my editorial partner at Newsletter suggested Ghost, which looked really good.

Ghost

The Ghost project was started by John O'Nolan, the former head of the Wordpress UI team, after he left the project, and in 2012 he said on the Blog that started the project:

WordPress is so much more than just a blogging platform

All I can say is that I couldn’t agree more.

Ghost as a platform is very easy to use, the design sense is great, of course the official host price is also very pleasing. The good thing is that Ghost is an open source project, and those who are willing to do the tossing can also drum up.

When I tossed Ghost over the weekend, I found that it would infinitely 301 redirect loop after setting up the https certificate, eventually the problem was solved but I wanted to figure out what was going on.

1. I suspect that Nginx has opened an infinite loop

Let’s start with the Nginx configuration. A complete Nginx Conf instance can refer to the official website here, theoretically we configure multiple different servers when listen on port 80 ( http) and port 443 (https), and then do a host judgment on the 80 server configuration and forward it all directly to https.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    listen [::]:80;
    server_name example.com;

    return 404; # managed by Certbot
}

Now there is certbot which is easy to configure. I didn’t find any misconfiguration of Nginx after looking at it for half a day.

Last I looked this article mentioned the need to add this configuration:

1
proxy_set_header X-Forwarded-Proto https;

The article also mentions that Cloudflare’s SSL rules may also lead to unlimited 301 but I had already transferred the domain name from Cloudflare DNS at that time, so it has nothing to do with Cloudflare.

Ghost is written in NodeJS and runs on port 2368 by default. We must forward the requests after Nginx access, but the official Ghost-CLI does not have this article after setup:

1
2
3
4
5
6
7
location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme; #Self-added
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $http_host;
    proxy_pass http://127.0.0.1:2368;
}

Referring to the official Nginx documentation, it is equivalent to redefining the HTTP HEADER for inbound requests, where X-Forwarded-Proto is also a “de facto standard” Header, see the Mozilla documentation. The documentation says that this Header is mainly used by the client to tell whether the protocol used to connect to the proxy is HTTP or HTTPS; the real standard is detailed in the Forwarded field.

2. Why must I bring X-Forwarded-Proto Header?

Since the 301 redirect is not caused by Nginx, it is Ghost?

Let’s go through Ghost’s code to see. It turns out that Ghost uses ExpressJS as the Server, which ghost/core/core/shared/express.js code has this paragraph:

1
2
3
// Make sure 'req.secure' is valid for proxied requests
// (X-Forwarded-Proto header will be checked, if present)
app.enable('trust proxy');

Let’s look at express’s code and see what this 'trust proxy' has done.

In lib/application.js, the method app.set writes the configuration for the trust proxy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 switch (setting) {
    case 'etag':
      this.set('etag fn', compileETag(val));
      break;
    case 'query parser':
      this.set('query parser fn', compileQueryParser(val));
      break;
    case 'trust proxy':
      this.set('trust proxy fn', compileTrust(val));

      // trust proxy inherit back-compat
      Object.defineProperty(this.settings, trustProxyDefaultSymbol, {
        configurable: true,
        value: false
      });

      break;
  }

Which is again done inside the compileTrust() method by proxy-addr, item in here.

This step is to translate the IP address description, e.g. :

1
2
3
4
app.set('trust proxy', 'loopback') // A subnet description
app.set('trust proxy', 'loopback, 123.123.123.123') // One subnet and one IP
app.set('trust proxy', 'loopback, linklocal, uniquelocal') // Multiple subnets
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']) // Multiple subnets

The latter part of the argument is then processed by the compile() function.

1
2
3
app.enable('trust proxy');
// Equivalent to the following code
app.set('trust proxy', true);

So the next parameter is empty, which means trust everything, because the express is proxied, so we have to rely on proxyaddr to resolve the real IP of the client request.

So not only do you need X-Forwarded-Proto, but in fact the Ghost-Cli Nginx configuration template is complete with several key fields:

1
2
3
4
5
6
7
8
location  {
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $http_host;
    proxy_pass http://127.0.0.1:;
    proxy_redirect off;
}

3. How does Ghost forward http requests to https?

Ghost’s configuration file is managed using nconf, when we configure the URL of http protocol in Ghost directory there will be no problem of infinite redirect, but https will.

That is, if you configure the website url to https://example.com, but visit http://example.com, Ghost will automatically redirect to https for you.

The specific implementation is in ghost/core/core/server/web/shared/middleware/url-redirects.js , and the core function is this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Takes care of
 *
 * 1. required SSL redirects
 */
_private.getFrontendRedirectUrl = ({requestedHost, requestedUrl, queryParameters, secure}) => {
    const siteUrl = urlUtils.urlFor('home', true);

    debug('getFrontendRedirectUrl', requestedHost, requestedUrl, siteUrl);

    // CASE: configured canonical url is HTTPS, but request is HTTP, redirect to requested host + SSL
    if (urlUtils.isSSL(siteUrl) && !secure) {
        debug('redirect because protocol does not match');

        return _private.redirectUrl({
            redirectTo: https://${requestedHost},
            pathname: requestedUrl,
            query: queryParameters
        });
    }
};

Ghost has a front-end page for users (FrontendApp) and a back-end page for administrators (AdminApp). The front-end page is also started with an Express Server, and then uses the above mentioned middleware to check if each request is secure. This secure or not comes from req.secure, and this req.secure will take the X-Forwarded-Proto in the Header if the trust proxy is started. The code is in the lib/request.js file of the Express project, in defineGettter about the protocol and secure functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
defineGetter(req, 'protocol', function protocol(){
  var proto = this.connection.encrypted
    ? 'https'
    : 'http';
  var trust = this.app.get('trust proxy fn');

  if (!trust(this.connection.remoteAddress, 0)) {
    return proto;
  }

  // Note: X-Forwarded-Proto is normally only ever a
  //       single value, but this is to be safe.
  var header = this.get('X-Forwarded-Proto') || proto
  var index = header.indexOf(',')

  return index !== -1
    ? header.substring(0, index).trim()
    : header.trim()
});

As you can see from the above code, express checks the this.connection.encrypted property first. I guess Nginx forwarding is not encrypted, because the local express server just opens an http service, and Nginx forwarding to it really doesn’t need to be encrypted either. So it jumps to the step of taking the Header. So if our Nginx configuration doesn’t come with X-Forwarded-Proto, express will think that the real client request is not https, and redirect to https, resulting in an infinite loop.

4. What’s next?

Although Ghost is a simple problem to solve, “just change the configuration”. However, it is interesting to trace the process of looking at the code of each project through the layers. In this article, the author mentioned that x-forwarded-proto is checked by Ghost, but since the whole service involves Nginx, Ghost, and Express, I wanted to find out what the problem was.

Through this tracing also a first glimpse of the general structure of the Ghost project, the process is still quite interesting. But it seems that Ghost is not particularly open, after all, the platform hosting is the most profitable part of the project.

Wordpress for so many years, has now become a giant with historical baggage, these years a variety of “alternative open source CMS” also emerged, but also the ups and downs, disappeared a lot. I feel great to see that Ghost is now making money, and the only way to keep going is to keep making money.

Ghost’s design sense is really in the current open source CMS without its right, hope Ghost can always be evergreen.