2

I'm running a static site on GAE and using a custom domain (let's call it example.com) with SSL certificates enabled. I'd like to canonicalize URLs to https://www.example.com/. That means catching any requests to myproject.appspot.com, plain HTTP, and/or the naked domain, and redirecting to www over HTTPS.

I understand that it's not possible to put redirect logic in app.yaml, but ideally I'd like to keep the static file serving logic there, and only have app code for the redirect. (As opposed to doing the static serving in app code as well.)

Here's what I have so far:

Contents of the file app.yaml:

runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /
  static_files: www/index.html
  upload: www/index.html

- url: /(.*)
  static_files: www/\1
  upload: www/(.*)

Contents of the file dispatch.yaml:

dispatch:
- url: "myproject.appspot.com/*"
  module: canonicalizer

Contents of the file canonicalizer.yaml:

module: canonicalizer
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
  script: canonicalizer.app

Contents of the file canonicalizer.py:

import webapp2

def get_redirect_uri(handler, *args, **kwargs):
    return 'https://www.example.com/' + kwargs.get('path')

app = webapp2.WSGIApplication([
    webapp2.Route('/<path:.*>',
    webapp2.RedirectHandler,
    defaults={'_uri': get_redirect_uri, '_code': 302}),
], debug=True)

As you can see, I've only attempted to implement redirecting myproject.appspot.com so far. I haven't been able to get it working; myproject.appspot.com still serves content rather than redirecting to the custom domain.

I saw a similar SO question and used it as a basis for my code above. I followed it fairly closely, so I'm not sure if it's outdated or missing details.

I'm not very familiar with webapp2. Also open to solutions in a different framework or even different programming language.

user2752467
  • 864
  • 5
  • 16

2 Answers2

4

As sllopis said in their answer, an HTTP to HTTPS redirect can be implemented via a secure: always element.

The rest of what I wanted to do needed to be done in app code. The code in my answer was on the right track, but I had some confusion about how services work in GAE and about dispatch.yaml. Here's my final code:

<application root>/app.yaml

runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /
  static_files: www/index.html
  upload: www/index.html
  secure: always
  redirect_http_response_code: 301

- url: /(.*)
  static_files: www/\1
  upload: www/(.*)
  secure: always
  redirect_http_response_code: 301

<application root>/dispatch.yaml

dispatch:
- url: "*.appspot.com/*"
  service: canonicalizer

- url: "example.com/*"
  service: canonicalizer

<application root>/canonicalizer/app.yaml

service: canonicalizer
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /.*
  script: canonicalizer.app

<application root>/canonicalizer/canonicalizer.py

import webapp2

def get_redirect_uri(handler, *args, **kwargs):
    return 'https://www.justinforcentral.com/' + kwargs.get('path')

app = webapp2.WSGIApplication([
    webapp2.Route('/<path:.*>',
    webapp2.RedirectHandler,
    defaults={'_uri': get_redirect_uri, '_code': 301}),
], debug=False)

This allows all the redirects to be done while still maintaining the ability to route the static site via static_files handlers.

As an aside, I also didn't realize that simply doing gcloud app deploy . from the application root only deploys the default service. To deploy this whole thing I had to run gcloud app deploy . dispatch.yaml canonicalizer.

user2752467
  • 864
  • 5
  • 16
  • Thank you @JustinLardinois for your answer. I'm sure this will help other community users who run into the same issue. – sllopis Jan 17 '20 at 08:14
  • 1
    The final note is essential - you couldn't see how close you were without the canonicalizer service deployed... – Dan Cornilescu Jan 17 '20 at 12:15
  • I'm now working on a different app in the Python 3 runtime. Here's roughly a Flask equivalent of the webapp2 canonicalizer: https://amir.rachum.com/blog/2016/08/27/flask-redirect/ – user2752467 Jul 22 '20 at 09:45
1

Mapping Custom Domains to GAE

App Engine allows applications to be served via a custom domain, such as example.com, instead of the default appspot.com address. You can create a domain mapping for your App Engine app so that it uses a custom domain.

You will need to do the following:

  1. Verify that you are the owner of your domain through Webmaster Central
  2. Ensure that your domain has been verified.
  3. Delegate the ownership of your domain to other users or service accounts, if needed.
  4. Map your domain to your App Engine app.
  5. Fill out the form with the listed resource records, including their type and canonical name (CNAME).
  6. Add this information to the DNS configuration of your domain registrar.

Securing Custom Domains with SSL

By default, when you map your custom domain to your app, App Engine issues a managed certificate for SSL for HTTPS connections. Securing your custom domains with SSL offers more information about this.


Handling URL requests that do not use HTTPS

Any URL handler can use the secure setting, including script handlers and static file handlers. If secure is set to always, requests for a URL that match this handler that do not use HTTPS are automatically redirected to the HTTPS URL with the same path. Query parameters are preserved for the redirect.

Example in the app.yaml file:

handlers:
- url: /youraccount/.*
  secure: always
  script: auto

Conclusion

As a result, after following these steps, you should have a custom domain properly mapped to your App Engine site that uses SSL certificates to secure the custom domain.

Moreover, by adding the secure:always handler in your app.yaml file, any URL requests made against your App Engine site will be automatically redirected to the HTTPS URL with the same path.


Update - Redirect all URLs with Google App Engine

Credits to How to redirect all URLs with Google App Engine:

app.yaml

handlers:
- url: /.*
  script: main.py

main.py

import webapp2

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.redirect("https://example.com", True)

app = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

Then, you can adjust this code to your needs.

sllopis
  • 2,292
  • 1
  • 8
  • 13
  • +1 because this does work for redirecting to HTTPS, but it doesn't cover redirecting to www or from appspot.com. I already had a custom domain and SSL set up so feel free to edit those instructions out of the answer. – user2752467 Jan 16 '20 at 07:56
  • You can also check [Block requests from *.appspot.com and force custom domain in GAE](https://stackoverflow.com/questions/1364733/block-requests-from-appspot-com-and-force-custom-domain-in-google-app-engine/1364875#1364875) and find one of the answers that suits you best. – sllopis Jan 16 '20 at 10:19
  • your answer does solve the HTTPS problem. Correct me if I'm wrong but I don't think your edit or your comment are relevant to the remainder of my question. I'm not trying to redirect traffic away from my app to an external site; I'm trying redirect requests to `example.com` or to `appspot.com` to `www.example.com`. And I'm serving a static site, so I can't simply check `os.environ['HTTP_HOST']` on every request. My question is asking if there's a way to do that canonicalization while still handling the static content via `app.yaml`. – user2752467 Jan 16 '20 at 21:50