viewcounter/index.js

132 lines
3.4 KiB
JavaScript

import * as fs from "fs"
import * as http from "http"
import { request } from "https"
// read configuration
let config = readJson("conf.json", {
port: 8001,
path: "/viewcounter",
site: "zvava.org",
countDB: ".count.json",
ratelimitDB: ".ratelimit.json",
saveTimeout: 5000,
})
// read databases
let count = readJson(config.countDB)
let ratelimit = readJson(config.ratelimitDB)
// store the save timeout
let saveTimeout
let server = new http.Server((request, response) => {
if (request.url != config.path)
return close(response, 404)
switch (request.method) {
case "POST":
const chunks = []
request.on("data", chunk => chunks.push(chunk))
request.on("end", async () => {
let page = Buffer.concat(chunks).toString()
// check if page is a valid url
if (/^\/[a-zA-Z0-9/_-]*.html$/.test(page) && await validateUrl(page)) {
// hotfix for caddy
let ip = request.socket.remoteAddress;
let xff = request.rawHeaders.indexOf("X-Forwarded-For");
if (xff != -1) ip = request.rawHeaders[xff + 1];
let message = incrementCount(ip, page)
? "counted page view" : "you have seen this page today"
close(response, 200, { message, views: count[page] })
} else {
close(response, 300, "invalid url provided")
}
})
break
case "GET": close(response, 200, count); break
default: close(response, "invalid method " + request.method)
}
})
server.listen(config.port)
function saveCount() {
clearTimeout(saveTimeout)
saveTimeout = setTimeout(() => {
fs.writeFile(config.countDB, JSON.stringify(count), (err) =>
err && console.error(err))
fs.writeFile(config.ratelimitDB, JSON.stringify(ratelimit), (err) =>
err && console.error(err))
}, config.saveTimeout)
}
// read file, if it exists return it as an object, otherwise return a default object
function readJson(file, defaultJson = {}) {
try { return JSON.parse(fs.readFileSync(file, "utf-8")) } catch {}
return defaultJson
}
// helper function to close an incoming http request
function close(res, code = 300, message = "") {
let data = typeof message == "string"
? { code, message: message || code.toString() }
: { code, ...message }
res.setHeader("Content-type", "application/json")
res.writeHead(code)
res.end(JSON.stringify(data))
}
let validUrlCache = []
let invalidUrlCache = []
// ping the target site and ensure that the url exists
function validateUrl(url) {
return new Promise((resolve) => {
if (validUrlCache.includes(url)) resolve(true)
else if (invalidUrlCache.includes(url)) resolve(false)
else {
request({
method: "HEAD",
host: config.site,
path: url,
}, (response) => {
let v = response.statusCode == 200
if (v) validUrlCache.push(url)
else invalidUrlCache.push(url)
resolve(v)
}).end()
}
})
}
function incrementCount(ip, page) {
if (!ip) return false // in case client disconnected
// calculate date string
let d = new Date(); d = d.getMonth() + "-" + d.getDate()
// will increment count at end
let i = false
// create ratelimit profile for the ip if it doesn't exist already
if (!ratelimit[ip]) ratelimit[ip] = {}
i = !ratelimit[ip][page]
// if last visited date doesn't exist
? true
// else, check if last visited date was today
: ratelimit[ip][page] != d
if (i) {
// update last visited day
ratelimit[ip][page] = d
// increment page count if it exists, otherwise set page count to 1
count[page] = count[page] ? count[page] + 1 : 1
saveCount()
return true
}
}