In preparation for Chrome’s Not Secure flag, which will update the indicator to show Not Secure when a site is not accessed over https, we wanted people to be able to test whether their site would pass. If you read our previous blog post about the existing misconceptions around using https, and preparing your site, you may have noticed a small fiddle, allowing you to test which sites will be deemed “Secure”. In preparation for the blog post itself, one of our PMs approached me asking for help making this fiddle come to life. It was a simple ask: we need an endpoint which runs logic to see if a given domain will automatically redirect to https.
The logic and requirements turned out to be very simple:
Make a serverless API endpoint
Input: domain (e.g. example.com)
Output: “secure” / “not secure”
Logic:
if http://example.com redirects to https://example.com
Return “secure”
Else
Return “not secure”
One additional requirement here was that we needed to follow redirects all the way; sites often redirect to http://www.example.com first, and only then redirect to https. That is an additional line of code I was prepared to handle.
I’ve done some software engineering in previous jobs, and am now a Solutions Engineer at Cloudflare, but I have no DevOps experience. I’ve set up my fair share of origin servers, installed a few LAMP stacks, and NGINX (thanks to some very detailed guides on the interwebs), but the task of setting up a server to run 10 lines of code on it is daunting. Even with PaaS services such as Heroku, some prerequisite knowledge (and a Github account) is required to get your "Hello, World" app off the ground.
Using Cloudflare Workers, I was able to get an endpoint on the web, soup to nuts, within a few minutes. I spent no time on research, and 99% of the time converting the very simple pseudo code above into real code. The other 1% was spent adding a DNS record. In 10 minutes, I had a demo-ready cURL for the PM to test domains against.
The Worker
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
/**
* Fetch a request and follow redirects
* @param {Request} request
*/
async function handleRequest(request) {
let headers = new Headers({
'Content-Type': 'text/html',
'Access-Control-Allow-Origin': '*'
})
const SECURE_RESPONSE = new Response('secure', {status: 200, headers: headers})
const INSECURE_RESPONSE = new Response('not secure', {status: 200, headers: headers})
const NO_SUCH_SITE = new Response('website not found', {status: 200, headers: headers})
let domain = new URL(request.url).searchParams.get('domain')
if(domain === null) {
return new Response('Please pass in domain via query string', {status: 404})
}
try {
let resp = await fetch(`http://${domain}`, {headers: {'User-Agent': request.headers.get('User-Agent')}})
if(resp.redirected == true && resp.url.startsWith('https')) {
return SECURE_RESPONSE
}
else if(resp.redirected == false && resp.status == 502) {
return NO_SUCH_SITE
}
else {
return INSECURE_RESPONSE
}
}
catch (e) {
return new Response(`Something went wrong ${e}`, {status: 404})
}
}
The Worker itself is fairly simple, but let us walk through what it is doing, as there are a few noteworthy things in here.
Parsing the input
I initially thought about sending the input into the Worker in a POST. However, given the input was fairly simple, and requires no authentication, I decided it would be easier to pass in the domain in the query string.
Here, I instantiate a new URL object, which will handle all the URL parsing for us. Since I’m not using or modifying any other aspects of the URL, I’m performing all the functions on the object in one line. However, if I was looking at other parts such as the hostname, or path, it would have made sense for us to define a separate object.
let domain = new URL(request.url).searchParams.get('domain')
if(domain === null) {
return new Response('Please pass in domain via query string', {status: 404})
}
The URL.searchParams
property returns a URLSearchParams object, which allows us to get
the value of the query string parameter we are looking for directly. In a situation when a parameter is not passed, we return an error response.
Making the subrequest
Next, we will need to make a subrequest to the domain, and validate whether or not it redirects us to https.
On the fetch
, we will also pass in the User-Agent header of the original request, as we have noticed some sites (for example google.com) will vary their responses for different User-Agents.
let resp = await fetch(`http://${domain}`, {headers: {‘User-Agent’: request.headers.get(‘User-Agent’)}})
if(resp.redirected == true && resp.url.startsWith('https')) {
return SECURE_RESPONSE
}
else if(resp.redirected == false && resp.status == 502) {
return NO_SUCH_SITE
}
else {
return INSECURE_RESPONSE
}
Following Redirects
When I initially wrote this Worker, I did more work than I really needed to. In order to cover the use-case of some websites redirecting to their canonical site first (http://example.com -> http://www.example.com), and only then redirecting to https, I was inspecting the redirect URL, and making an additional subrequest to then inspect the outcome. As one of our engineers pointed out, I was doing all that extra work for nothing.
By default, when you make a new fetch
, what actually happens behind the scenes is that the redirect
property is set to follow
. Thus fetch(url)
is the same as fetch(url, {redirect: “follow”})
. So when we are making the subrequest within the Worker, the final resp.url
property we are inspecting will provide us with the final location of the redirect chain.
Somewhat unintuitively, the event.request.redirect
property is by default set to ”manual”
. So if we carried over all the initial request properties in our subrequest, the redirect chain would not have been followed, or we would have had to explicitly override it.
There is a good reason for this default: it allows trivial, pass-through Cloudflare Workers to function correctly in situations where origins assume they are actually redirecting the client itself. One situation is when an HTTP redirect sends browsers to a non-HTTP URL, such as a mailto:
link, which Service Workers have no ability to follow. The intended recipient of the redirect is clearly the browser in this case. Another situation arises when the origin needs the browser to update its navigation bar with a new URL (like when redirecting from HTTP to HTTPS!). If redirects are followed in a Cloudflare Service Worker before returning the resulting response to the browser, the browser will have no way of displaying the correct, redirected URL in the navigation bar.
CORS Headers
let headers = new Headers({
'Content-Type': 'text/html',
'Access-Control-Allow-Origin': '*'
})
As you may have noticed, in the static response, we are always adding a response header called Access-Control-Allow-Origin
. CORS headers are meant to help protect origins from being accessed by other sites. If we tried to run our app directly on the client (from the browser side), the browser would enforce the CORS policies of the domains you were trying to test against, and block those requests. Setting Access-Control-Allow-Origin
to *
will allow this endpoint to be accessed from this blog, or any other sites trying to use it (if you are looking to embed it into your site, you can!). Otherwise, if the javascript on the blog post itself was making browser side calls to various domains, many requests would be blocked by the browser.
Testing our Worker
As I was building the Worker, I was using the preview UI to validate that I was on the right track at every step. The console output is really useful for simple debugging along the way.For example, to make sure the query string was getting parsed properly, I can console.log(domain)
.
It’s hard to get code right on the first try, and it’s not always clear where things went wrong. While the Preview is not reflective of the end to end experience (there are many variables that may change once a request is going over the web), it’s a great developer tool to help validate progress along the way.
Once I got it all working in the preview, it was time for the real test: the cURL.
curl https://secure.ritakozlov.com/?domain=cloudflare.com
secure
curl https://secure.ritakozlov.com/?domain=maxisacutecat.club
not secure
And it works! You may now test your site by running the same cURL from your machine and adjusting the domain parameter, or you can deploy the Worker above on your zone, and have your own testing endpoint.
To sum, Workers are great for building simple, stateless serverless apps (to add flare to your blog posts, and wayyyy beyond!). We are always curious to hear what you are building!
If you have a worker you'd like to share, or want to check out workers from other Cloudflare users, visit the “Recipe Exchange” in the Workers section of the Cloudflare Community Forum.