How complex can an API be that responds to a simple /ping service?. That is the idea of this series, add more and more tools to convert a simple project into a complex idea

This first part is starts with the basics, create a simple code, upload to a server and share in internet.

The code used to create the API

The API will be built in Golang and for now will use the native router of Golang.

The route is simple as that:

s := http.NewServeMux()
s.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("pong"))
		fmt.Print(".")
})

After that, it is necessary to make the server listen for requests with:

if err := http.ListenAndServe(":8080", s); err != nil {
  fmt.Println(err)
}

With that, we can start the server with:

go run main.go

Try a request with:

curl 127.0.0.1:8080/ping

(curl is a command-line tool )

And obtain a 200 OK “pong”. We have a simple ping API!

(In the terminal you will see a “Running!” and a “.” for every request. This will be important later)

You can reach the complete code at this point here: https://github.com/sanrinconr/simple-ping/blob/part-1/ping-in-local/main.go

Exposing the API to internet

To expose the service to the world is necessary:

  • A domain where the people can access
  • Server that will receive the requests (a personal computer is valid but the idea is to have the API up 24/7)

The two things can be bought, in my case i buy a domain in cloudfare

And for the server i buy some cheap vps (virtual private server) that i find in a post in lowendbox

Now is moment to login in the server over ssh (your provider must give you the access password). To keep the things simple, execute all of this as root user (this is a huge security hole but for now i want to mantain the things simple, in other post this will be fixed using docker).

Once you are in the machine, download the project with:

git clone https://github.com/sanrinconr/simple-ping.git
cd simple-ping
git checkout part-1/ping-in-local

And run with

go run main.go

The API will be up and listening connections. Where does it listen?. To the ip of the server!

If you do something like:

curl {public_ip_of_server}:8080/ping

Where {public_ip_of_server} is the ip given by your provider to the server

You will get a “pong” as response!

But using an IP to access the server is ugly, it is better to using a domain.

Configure a domain (DNS)

This part depends of the provider that you choose but in general the idea is:

  • Enter in the dashboard to manage your domains.
  • Click in the domain that you buy
  • At some point, a configuration called “Update DNS configurations” or something similar should appear. Click in it
  • Create a type “A” registry that pair your domain with the ip of your server

(an A register can answer the question “if somebody calls this domain, what ip must be used to route the request?”)

After some hours (or minutes) you can access the domain with

curl http://{your_domain}:8080/ping

And receive a pong!

But… the “:8080” is awful, isn’t it?

To correct that, allow in the code the use of custom ports with

port := "8080"
if customPort := os.Getenv("PORT"); customPort != "" {
  port = customPort
}

fmt.Println("Running!")
if err := http.ListenAndServe(":"+port, s); err != nil {
  fmt.Println(err)
}

The code at this point can be found here: https://github.com/sanrinconr/simple-ping/blob/part-1/add-port-environment-variable/main.go

With that (remember to do git checkout part-1/add-port-environment-variable in the server) if you run PORT=80 go run main.go The API will run in the port 80!

Now, you can access to the server without writing a port (because 80 is the default for http) with:

curl http://{your_domain}/ping

(you can run git checkout part-1/add-port-environment-variable in your machine and execute this version)

Sadly, if you try to access http://{your_domain}/ping in a normal browser like firefox or chrome you will get an error or directly an “Unable to connect”. Why does that happen if the connection appears to work using curl?. Can be a variety of reasons but this problem can be solved implementing HTTPs in the API (you can learn more here) which is the minimal industry standard for security

Configure HTTPs

The “easy” way is use the method “http.ListenAndServeTLS” but to do that we need a two new params, a certificate and a private key.

To make a first test you can create a certificate and private key with this openssl command:

openssl req -x509 -newkey rsa:2048 -keyout server.key -out server.crt -days 10 -nodes -subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"

(In human words the command means, create a certificate x509 using a new key, this key will be saved in “server.key” and the certificate will be saved in “server.crt”. The certificate will be valid for 10 days and the private key will not be encrypted with a password. Finally with the -subj some metadata about the issuer is requested)

Now, exists two new files, server.crt and server.key and code just needs to read the files. Like the environment PORT is used to set a custom port, CERT_FILE_LOCATION and KEY_FILE_LOCATION variables will do the same with the locations of the files, the code will look like that:

locationCertificate := os.Getenv("CERT_FILE_LOCATION")
locationKey := os.Getenv("KEY_FILE_LOCATION")

if err := http.ListenAndServeTLS(":"+port, locationCertificate, locationKey, s); err != nil {
  fmt.Println(err)
}

The complete code at this point can be found here: https://github.com/sanrinconr/simple-ping/blob/part-1/support-https/main.go

Remember execute git checkout part-1/support-https in your server if you want to execute this version. To run the API execute: CERT_FILE_LOCATION={api_location}/server.crt KEY_FILE_LOCATION={api_location}/server.key PORT=443 go run main.go

Now if you try in your browser the url https://{your_domain}/ping you will get a security warning. Why happens this?, because the certificate used isn’t created by a trusted entity.

To solve this problem is possible get a certificate from a trusted entity. In this case let’s encrypt will be used. To generate the certificate you must install snap (a package manager) and certbot (the program to communicate with let’s encrypt and get the certificate). To do that, execute:

apt update
apt install snapd
snap install --classic certbot
certbot certonly

(commands apply for ubuntu only, to other systems check: https://certbot.eff.org/instructions?ws=other&os=osx <- example for macOS)

Select the option 1 (Runs an HTTP server locally which serves the necessary validation files under the /.well-known/acme-challenge/….) and next type your domain.

Finally check the output you will see where the cert and the key are located (typically the route is /etc/letsencrypt/live/{your_domain})

Now you only need to run the server using the new generated key and cert. To do that, run:

CERT_FILE_LOCATION=/etc/letsencrypt/live/{your_domain}/fullchain.pem KEY_FILE_LOCATION=/etc/letsencrypt/live/{your_domain}/privkey.pem PORT=443 go run main.go

And now if you go to https://{your_domain}/ping you will see a “pong” without any warning!

There is only one problem left to solve. If you log out of the ssh terminal of your server the API will also be turned off, how mantain up 24/7?

The most simple way (but not the ideal) is use crontab (a tool that allows you to tell the machine “do this every x time”, in our case will be “execute this executable every reboot”)

To do that, run

crontab -e

And an editor will open. Add a line like:

@reboot CERT_FILE_LOCATION=/etc/letsencrypt/live/{your_domain}/fullchain.pem KEY_FILE_LOCATION=/etc/letsencrypt/live/{your_domain}/privkey.pem PORT=443 go run /root/projects/simple-ping/main.go >> /root/projects/simple-ping/log.txt

That means

  • @reboot: do the task every reboot
  • CERT_FILE_LOCATION=/etc/letsencrypt/live/{your_domain}/fullchain.pem KEY_FILE_LOCATION=/etc/letsencrypt/live/{your_domain}/privkey.pem PORT=443 go run /root/projects/simple-ping/main.go: Executable to run like you do in terminal
  • >> {api_location}/log.txt: Append the output of the executable to the file located in “{api_location}/log.txt”

Remember the “running!” and the “.” that appears when a requests is done?, we save this outputs to a file to know how much the API is used. This is a super basic way to observe the API, but is enough at the moment (and is better than be blind)

Reboot the server and check the API is running typing in browser:

https://{your_domain}/ping

And that is! we have a ping server. If you want to try without installing nothing my ping API is located in:

https://health.santiagorincon.dev/ping

Things to impove in next parts

  • Why download code in the host machine?. It only need to know about a binary
  • Don’t execute the API as the root user (which allows some ‘magic’ such as running on port 443, reading certificates in privileged locations, which isn’t a good practice).
  • Improve the use of crontab, is a long command
  • The character “.” in the log.txt file imply a request, is a bad way to observe the API

The api is super simple at the moment, but is the first step to start doing more complex things, see you in the next post!.