PaperCut Blog

Tech & DevCoding

A Go program to modify PaperCut MF/NG Advanced Config Settings

A Go program to modify PaperCut MF/NG Advanced Config Settings

Gopher Logo

TL;DR — Alec loves programming in Go and someone asked for a utility to modify the value of auth.webservices.auth-token config value when installing a third-party integration package.

Why does Alec like Go?

  1. Go is a powerful system programming language, but at the same time is highly productive when compared to languages like Java and C++
  2. The quality of the type system and compiler means that, often, if my programs compile then they work (but I do write small programs). This has made programming a much more enjoyable experience
  3. The Go build system has been greatly simplified over the last several releases

Introduction

As mentioned in a previous blog post, the PaperCut MF/NG web services API has lots of great functionality to help you integrate PaperCut MF/NG with other systems or automate application management tasks.

But in order to make the connection secure, each API call needs to provide an auth token as the 1st parameter, and this auth token needs to be configured in PaperCut MF/NG. You configure the auth token value by editing the value of the advanced configuration key auth.webservices.auth-token. This can be done via the web admin interface

Config Editor Set Auth-Token

or by running server-command (this snippet is adapted from the manual and uses PowerShell on Windows)

&'C:\Program Files\PaperCut MF\server\bin\win\server-command.exe' set-config auth.webservices.auth-token `
 '{\"payments\":\"Zuj0hiazoo5hahwa\","\userUpdate\":\"heitieGuacoh8zo6\"}'

Note that when running the server-command utility you need to be on the PaperCut MF/NG application server and use an account that has elevated privilege.

Recently someone asked how they can automate the update of the auth token as part of a third party install process.

In response, I created a standalone Go program that sets the auth.webservices.auth-token config key using the server-command binary and I’ve released the source code in case it’s useful for other integrators or larger PaperCut sites.

Why would you care?

Consider someone who develops an integration with PaperCut NG or PaperCut MF using the public web services AP. In order to make it work they need to install their authentication token in PaperCut — but typing JSON strings can be a bit fiddly and it’s easy to make a mistake. It would be simpler to add the new token during the install of the new integration with no human intervention.

That’s what this program does. It can be added to an installer process that runs on the PaperCut MF/NG Application Server. Alternatively (if the integration is installed on another machine or in the cloud) a small installer can be created to run on the PaperCut Application Server that just adds the auth token.

Why use Go?

  1. Go is trivial to cross-compile across the three major operating systems PaperCut MF/NG supports (Windows, Linux, and macOS)
  2. Go provides standalone binaries with no need to provide other software (e.g. libraries or runtime systems c.f. Python).
  3. Java was also an option (PaperCut MF/NG ships with a JRE) but for me at least Go is easier to use and more productive

How does it work?

Note: All the source code for this example is here, I am not an accomplished Go programmer, so there is much that can be improved (hint: Pull requests are welcome).**

The 1st complication is that PaperCut MF/NG can store the value of auth.webservices.auth-token in four different formats and the new value will depend on the previously stored format. i.e.

Previously Stored format for auth.webservices.auth-token New storage format
1. Blank (not set) Simple JSON object with single key-value pair
2. JSON array An array with an additional value appended
(simple list of strings) (if value is already present then don’t bother updating)
3. JSON object (key-value pairs) A JSON object with an additional key-value pair
This is the recommended format
4. A simple string A JSON object with an entry for the existing string
(key is “default”) and a second key-value pair

How can you do this in Go? Let’s look at all four cases:

  1. Blank

This is the simplest case. Just create (make()) a new map and add the supplied key-value pair.

        if len(result) == 0 {

                auth := make(map[string]string)
                auth[tokenName] = securityToken

                update(auth)

                return
        }
}

We’ve hidden the complexity of updating the config key in a helper function.

func update(jsonData interface{}) {
        if value, err := json.Marshal(jsonData); err != nil {
                log.Fatalf("could not marshal %v", jsonData)
        } else {
                if output, err := execServerCommand("set-config", "auth.webservices.auth-token", string(value)); err != nil {
                        log.Fatalf("Failed to update auth.webservices.auth-token: %v", output)
                }
                log.Printf("Updated auth.webservices.auth-token with new token value %v", value)
        }
}

The update() function will take any type (interface{}) and validate to make sure it’s valid JSON. It then calls another helper function, execServerCommand() that will actually execute the server-command binary and pass in the corresponding command and arguments.

func execServerCommand(args ...string) (value []byte, err error) {

        var out bytes.Buffer

        cmd := exec.Command(serverCommandBin, args...)
        cmd.Stdout = &out
        log.Printf("Running command %v and waiting for it to finish...", append([]string{serverCommandBin}, args...))

        err = cmd.Run()
        value = []byte(strings.TrimSpace(out.String()))

        return
}

Now we have all the basic pieces and we just need to process the other possible data formats.

  1. Array:
        var tokensAsArray []string

        // Note: if the config key is not an array of strings this parse will fail
        if notAnArray := json.Unmarshal(result, &tokensAsArray); notAnArray == nil {
                log.Printf("auth.webservices.auth-token is a json array %v", tokensAsArray)

                for _, i := range tokensAsArray {
                        if i == securityToken {
                                log.Printf("Security token %v already installed", i)
                                return
                        }
                }

                update(append(tokensAsArray, securityToken))
                return
        }

Before we can unmarshall the JSON string (into an array) we need to create an array slice to hold the result

Notice the cheeky use of the error return value from Unmarshal() to let us know if we have an array. If we get something that is not nil we can ignore the error and try the next format.

If the authentication token is already present in the array then we don’t need to do anything beyond logging an informative message.

  1. Object
        var tokensAsObject map[string]string


        // Note: if the config key is not a map of strings indexed by strings this parse will fail
        if notAnObject := json.Unmarshal(result, &tokensAsObject); notAnObject == nil {
                log.Printf("auth.webservices.auth-token is a json object %v", tokensAsObject)


                tokensAsObject[tokenName] = securityToken


                update(tokensAsObject)


                return
        }

Now we just apply the previous pattern, but try and parse the JSON string into a map. If we are successful then it’s a simple matter to add the key-value pair (overwriting any existing key of the same name) and calling update()

  1. Simple string

If the preceding tests fail then we must have a simple string (for example “old-token”) and we just need to construct a new map that will save “old-token” under the key “default” and add our new key-value pair

        tokensAsObject = make(map[string]string)
        tokensAsObject["default"] = string(result) // Save the old auth key as well
        tokensAsObject[tokenName] = securityToken


        update(tokensAsObject)

The value of the authentication token, and the key under which it is stored, are passed to the Go program as command line arguments at the start of main()

Finding the PaperCut MF/NG server-command binary

This actually turns out to be a bit fiddly because each platform has a unique install location and, on macOS and Windows, MF and NG are also different from each other. I’ve not tested this across all platforms as much as I would like, so if you find a problem please raise an issue on GitHub.

We can discover the runtime operating system by looking at the value of runtime.GOOS. This is the platform the Go binary was compiled for, so for instance it will not give us any clues about if we are running on Windows Server 10 or Windows 8.

On Windows another complication is that PaperCut may not be installed in C:\Program Files and so the program looks for the value of the environment value PROGRAMFILES. Note that this may not be enough and you might need to extend the code and, for example, provide the Windows install directory as a third argument.

The init() function is basically a net of nested if statements that looks at the most sensible options for each platform. I won’t post the whole function in here, but you can find it in the source code here

Another Example

This is not the first time I’ve used Go with PaperCut MF/NG. When moving from macOS to Windows 10 I had to reimplement my Custom User Sync Integration module in Go (Python did not seem to work well on Windows).

You can find the source code here and I’m not going to go through the code in detail, but it’s worth mentioning a couple of cool Go features used:

Struct tags

For example

type userAttributesT struct {
        Fullname        string `json:"fullname"`
        Email           string `json:"email"`
        Dept            string `json:"dept"`
        Office          string `json:"office"`
        PrimaryCardno   string `json:"primarycardno"`
        OtherEmail      string `json:otheremail`
        SecondaryCardno string `json:secondarycardno`
        UserAlias       string `json:useralias`
        HomeDirectory   string `json:homedirectory`
        PIN             string `json:pin`
        Password        string `json:"password"`
}

More information about struct tags and JSON here

Initialization of unnamed fields

The code also made use of initialization of unnamed fields, which I wrote about in my personal blog.

And finally

I hope these brief examples will encourage you to try Go, it’s a lot of fun and not as hard as it looks. Here are some videos that might offer further encouragement.

  • justforfunc: The Magic Gate

  • The Go Team write a server

  • Generating Go code

(I should do more of this)

Comments