Mustafa Can Yücel
blog-post-1

A Simple Auto-Updater for Your Software

Auto-updating Your Windows Applications

When using Click-Once publishing to distribute your software, it is very easy to update the installed versions on your clients; if you supply it a website, the Click-Once system automatically checks the newest version and updates the software if necessary. However, if you are using a different method to distribute your software (such as a proper MSI installer), you need to implement your auto-updater or use a third-party library. In this post, I will show you how to implement a very simple reusable auto-updater. Since it is reusable, it can be added to any Windows application with a few lines of code.

Our system will have two components:

  • A reusable class library that checks the version information from a server, downloads the latest installer, and runs it,
  • A server that responds to the class library's requests with the latest version info that includes the version number (semantic) and a download URL. The server holds the version information for applications in separate text files.
We will write the class library in C# (GitHub) and the server (GitHub) in Go. You can find the source code for both in the GitHub repository.

AutoUpdater Class Library

The class library is very minimal; it includes an UpdateEngine class to handle the update operations, and an Indeterminateprogress window under ui namespace that is displayed to the user while downloading the latest installer. The engine has the following fields:

  • m_AppName: The name of the application whose version will be checked
  • m_CurrentVersion: The version of the application that is installed on the user's computer
  • m_UpdateUrl: The URL of the server's endpoint where the latest version information is fetched
  • m_ErrorLogSemaphore: A semaphore to organize possible concurrent writes to the updater's error log file
  • m_HttpClient: The client that will be used for HTTP requests
  • m_IndeterminateProgressWindow: The window that will be displayed during downloading of the latest installer file
  • m_Data: An instance of the custom class VersionData which holds the latest version information

The constructor of the engine class needs three arguments: the name of the application, the current version of the application, and the URL of the server's endpoint to initialize the fields.

The CheckForUpdateAsync method is the first method of the class. It first checks if the server is reachable, and if it is, it fetches the latest version information from the server. If the server is not reachable, it returns false to indicate that the update check failed. If the server is reachable, it parses the version information and compares it with the current version. If the current version is older than the latest version, it downloads the latest installer and runs it. If the current version is the same as the latest version, it returns false to indicate that the application is up-to-date. If the current version is newer than the latest version, it returns false to indicate that the update check failed. If it encounters any error during the update check, it returns false to indicate that the update check failed, and writes this incident to the updater error log file.

The DownloadAndRunUpdate async method performs the following flow of operations:

  • It checks if there is a VersionInfo available (i.e. if the update check is successful). If there is no VersionInfo, it returns false to indicate that the update check failed.
  • It pops up the progress window
  • It downloads the latest installer file into OS temporary folder as a temporary file.
  • Once the download finishes, it updates the extension of the temporary file to ".msi".
  • It runs the installer file using ShellExecute. Otherwise, the installation will fail.
  • It returns true to indicate that the installation has begun.
If it encounters any error during this flow, it returns false to indicate that the operation failed, and writes this incident to the updater error log file.

This class includes a few more private auxiliary methods to perform the operations described above, which are nothing more than string manipulation, file I/O, and parsing operations.

Server

The server is a very simple HTTP server that responds to GET requests with the latest version information. It can be written in any language or framework, but we will use Go for its speed and simplicity.

The version information is stored in text files with the same name as the application (i.e. appname.txt) under data directory as a JSON string with the following structure:

{
    "Version":"2.3.1.0",
    "Url":"https://github.com/someId/someApp/releases/download/v2.3.1.0/Installer.msi"
}
As you can see, the data is very basic; the latest version and the direct download URL. In this case, we are using the releases page of the GitHub repository as the download URL, but it can be any URL that points to the latest installer file.

The server code is in autoupdater.go and quite basic; we will show the code and explain it line by line:

package main
import (
        "bytes"
        "encoding/json"
        "html"
        "html/template"
        "io/ioutil"
        "log"
        "net/http"
        "os"
        "strings"
)

func loadData(appName string) (string, error) {
        fileName := "/home/user/autoupdater/data/" + appName + ".txt"
        body, err := os.ReadFile(fileName)
        if err != nil {
                return "error", err
        }
        return string(body), nil
}

func saveData(appName string, data string) bool {
        fileName := "/home/user/autoupdater/data/" + appName + ".txt"
        err := os.WriteFile(fileName, []byte(data), 0666)
        if err != nil {
                return false
        }
        return true
}

func readImage(url string) ([]byte, error) {
        res, err := http.Get(url)
        if err != nil {
                log.Fatalf("http.Get -> %v", err)
        }
        data, err := ioutil.ReadAll(res.Body)
        if err != nil {
                log.Fatalf("ioutil.Readall -> %v", err)
        }
        res.Body.Close()
        return data, nil
}
func writeResponse(responseCode int, w http.ResponseWriter) {
    var imgData = []byte{}
    var err error
    switch responseCode {
    case http.StatusAccepted:
            imgData, err = readImage("https://httpcats.com/202.jpg")
    case http.StatusBadRequest:
            imgData, err = readImage("https://httpcats.com/400.jpg")
    case http.StatusUnauthorized:
            imgData, err = readImage("https://httpcats.com/401.jpg")
    case http.StatusInternalServerError:
            imgData, err = readImage("https://httpcats.com/500.jpg")
    case http.StatusServiceUnavailable:
            imgData, err = readImage("https://httpcats.com/503.jpg")
    default:
            imgData, err = readImage("https://httpcats.com/500.jpg")
    }
    w.Header().Add("Cache-Control", "no-store")
    w.WriteHeader(responseCode)
    if err == nil {
            w.Header().Set("Content-Type", "image/jpg")
            w.Write(imgData)
    }
}

func main() {
    http.HandleFunc("/update", viewHandler)
    log.Fatal(http.ListenAndServe("127.0.0.1:12345", nil))
}

func viewHandler(w http.ResponseWriter, r *http.Request) {

    switch r.Method {
    case http.MethodGet:
            query := r.URL.Query()
            appName := query.Get("name")
            data, err := loadData(appName)
            if err != nil {
                    writeResponse(http.StatusBadRequest, w)
            } else {
                    w.Header().Add("Cache-Control", "no-store")
                    w.Header().Set("Content-Type", "application/json")
                    w.Write([]byte(data))
            }
    case http.MethodPost:
            err := r.ParseForm()
            if err != nil {
                    writeResponse(http.StatusBadRequest, w)
                    return
            }
            v := r.Form
            appName := v.Get("app_name")
            appVersion := v.Get("app_version")
            downloadUrl := v.Get("download_url")
            password := v.Get("password")
            if password != "somePassword" {
                    writeResponse(http.StatusUnauthorized, w)
                    return
            }
            data := map[string]interface{}{
                    "Url":     downloadUrl,
                    "Version": appVersion,
            }
            jsonData, err := json.Marshal(data)
            if err != nil {
                    writeResponse(http.StatusInternalServerError, w)
                    return
            }
            result := saveData(appName, string(jsonData))

            if result {
                    writeResponse(http.StatusAccepted, w)
                    return
            }

            writeResponse(http.StatusServiceUnavailable, w)

    default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}
func sanitizeHTML(s string) (output string) {
    if !strings.ContainsAny(s, "<>") {
            output = s
    } else {
            s = strings.Replace(s, "\n", "", -1)
            s = strings.Replace(s, "

", "\n", -1) s = strings.Replace(s, "
", "\n", -1) s = strings.Replace(s, "
", "\n", -1) s = strings.Replace(s, "
", "\n", -1) s = strings.Replace(s, "
", "\n", -1) b := bytes.NewBufferString("") inTag := false for _, r := range s { switch r { case '<': inTag = true case '>': inTag = false default: if !inTag { b.WriteRune(r) } } } output = b.String() } output = strings.Replace(output, "‘", "'", -1) output = strings.Replace(output, "’", "'", -1) output = strings.Replace(output, "“", "\"", -1) output = strings.Replace(output, "”", "\"", -1) output = strings.Replace(output, " ", " ", -1) output = strings.Replace(output, """, "\"", -1) output = strings.Replace(output, "'", "'", -1) output = html.UnescapeString(output) output = template.HTMLEscapeString(output) output = strings.Replace(output, """, "\"", -1) output = strings.Replace(output, "'", "'", -1) output = strings.Replace(output, "& ", "& ", -1) output = strings.Replace(output, "&amp; ", "& ", -1) return output }

loadData

This function gets the name of the application, reads the appropriate file, and returns the file content as a string, and nil as error. If any error occurs, it returns the "error" string together with the error instance. Note that the file path should be absolute so that the application can find the file when executed as a systemd service.

saveData

This function gets the name of the application and the content of the file as a string, writes the content to the appropriate file, and returns true as bool. If any error occurs, it returns false as bool. Note that the file path should be absolute so that the application can find the file when executed as a systemd service.

readImage

This is an optional function; it is used to read the images from the internet. We use this to return cat images together with HTTP responses. It gets the URL of the image as a string and returns the content of the image as a byte array and nil as error. If any error occurs, it returns nil as byte array and the error instance.

writeResponse

This function gets the HTTP response code as an integer and the HTTP response writer as an http.ResponseWriter instance. It writes the HTTP response code to the HTTP response writer, together with the appropriate cat image. Note that this function is used only when the response is not 200 OK. If the cat image cannot be loaded, it writes only the status.

sanitizeHTML

This function gets the user input as a string and returns the sanitized HTML content as a string. It is used to prevent XSS attacks. We use user input to update the content of the application version data files. Technically if we are the only ones to update these files, we do not need any security measures, but it is always better to be safe than sorry. This function uses kennygrant's sanitize library for Go.

main

This is the main entry point of the application. It creates a new HTTP server and listens on port 12345 of localhost. It also registers the viewHandler function to the /update path.

viewHandler

This is where the magic happens. This function returns appropriate data or error codes based on the request.

GET requests

It gets the application name from the name query string, reads the appropriate file, and returns the content of the file as a JSON. If any error occurs, it returns HTTP 400. If necessary, the error can be detailed further for file not found or file read errors, but we do not need to do that for this application.

POST requests

POST requests are used for updating the version data of the available applications. If you are planning to update the version data by editing the text files by hand, you do not need this. However, we have a small HTML page that allows authorized people to update version data from a web UI.

This section parses the HTML form-URL-encoded data to get the required parameters such as application name (appName), latest version ("appVersion"), download URL ("downloadURL"), and the password ("password"). If the password is correct, it encodes a JSON string in the correct format, updates the version data file, and returns HTTP 202. If the password is incorrect, it returns HTTP 401. If any other error occurs, it returns HTTP 503.

Other requests

Any other type of request will return HTTP 405.

Building

Once the code is ready, we can build the application with the following command:

go build autoupdater.go
Note that for this to work, you should have installed Go on the server. It is very easy, just follow the instructions on the official Go website. This command will create an executable file named autoupdater in the same folder. You can run this file directly, or you can copy it to a folder in your PATH variable.

HTML Input Form

This is the HTML form that is used to update the version data. It is served directly under our subdomain and uses the /update endpoint with POST request. Aside from the styles, it is a very simple HTML form:

<body>
    <div class="main-block">
        <h1>Add/Update Record</h1>
        <form action="/update" method="post">
            <label id="icon" for="name"><i class="fas fa-envelope"></i></label>
            <input type="text" name="app_name" id="app_name" placeholder="App Name" required />
            <label id="icon" for="version"><i class="fas fa-user"></i></label>
            <input type="text" name="app_version" id="app_version" placeholder="Latest Version" required />
            <label id="icon" for="url"><i class="fas fa-user"></i></label>
            <input type="text" name="download_url" id="download_url" placeholder="Download URL" required />
            <label id="icon" for="password"><i class="fas fa-unlock-alt"></i></label>
            <input type="password" name="password" id="password" placeholder="Password" required />
            <hr />
            <div class="btn-block">
                <button type="submit">Submit</button>
            </div>
        </form>
    </div>
</body>
Now we can update the version data by using the HTML form. We can also use curl to update the version data from the command line.

Server Configuration

The final steps are to configure the server to serve the application and the HTML form, and to configure the DNS records to point to the server. Since our autoupdater executable listens to the localhost, we will use a reverse proxy. This is a sample block for the Caddyfile (remember that it is under the /etc/caddy folder):

domain.example.com {
    handle /update* {
            reverse_proxy http://localhost:12345 {
                    header_up Host {upstream_hostport}
            }
    }
    handle {
            basicauth {
                    username passwordHash
            }
            root * /var/www/domain.example.com
            file_server
    }
}
The above configuration serves the input HTML file on the root domain and redirects the requests to /update to the autoupdater executable. Note that we are using basicauth to protect the / endpoint. The passwordHash is the SHA256 hash of the password. You can use caddy hash-password to generate the hash. We serve the HTML file from the /var/www/domain.example.com directory; you can use any directory you want, provided Caddy has permission to read that folder.

Creating a System Service

Currently, we need to start our server application manually, but we want it to start after the system restarts automatically. We can use systemd for this purpose. We will create a service file under /etc/systemd/system folder. The file should be named autoupdater.service and should contain the following:

[Unit]
Description=AutoUpdater systemd service
After=network.target

[Service]
Type=simple
User='username'
Group='usergroup'
ExecStart=/home/user/autoupdater/autoupdater

[Install]
WantedBy=multi-user.target
Do not forget to change the user and group names and the path to the executable. After creating the service file, we can start the service with sudo systemctl start autoupdater. We can also enable the service to start after the system restarts with sudo systemctl enable autoupdater. If you change any part of this service file, you need to reload it with sudo systemctl daemon-reload.