132 lines
3.4 KiB
JavaScript
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
|
|
}
|
|
}
|