Skip to content

Instantly share code, notes, and snippets.

@jahands
Last active May 14, 2020 18:04
Show Gist options
  • Save jahands/0e71f764f83ef15af381997d751bee3e to your computer and use it in GitHub Desktop.
Save jahands/0e71f764f83ef15af381997d751bee3e to your computer and use it in GitHub Desktop.
Count requests for every IP to CountAPI.xyz using Cloudflare Workers

I created this in order to count how many requests each IP is using, without sending personal data to CountAPI.xyz To accomplish this, I used Workers KV to map the IP address to a random UUID, which is used as the identifier on CountAPI.

Here's an example of what's stored in workers KV, with the user's IP as the key:

{
    "userID": "5e64db68-d628-4938-9c01-0968a2f76861",
    "userIP": "1.2.3.4",
    "firstSeenDate": "2020-05-11T02:12:16.377Z",
    "counterURL": "https://api.countapi.xyz/info/sldkfjsoiefsdf_users/5e64db68-d628-4938-9c01-0968a2f76861",
    "meta": {
        "cf": {
            "first_country": "US",
            "first_colo": "IAD",
            "first_asn": 32780
        }
    }
}

Example api result calling api.countapi.xyz/info:

{
    "namespace": "sldkfjsoiefsdf_users",
    "key": "5e64db68-d628-4938-9c01-0968a2f76861",
    "ttl": 15769870400,
    "created": 1589163136907,
    "update_lowerbound": -1,
    "update_upperbound": 10000,
    "enable_reset": false,
    "value": 3630
}

"value" is the total number of requests that user has made.

Then, to view all of the data, you can use the Cloudflare API to list all of the keys, and then call the CountAPI using each user's UUID.

import { handleRequest } from './handler'
import { RequestCounter } from './requestCounter'
// COUNTAPI_NAMESPACE is any string, I usually just generate random characters.
// KV_NAMESPACE is a KV namespace mapped to an environment variable.
let counter = new RequestCounter(COUNTAPI_NAMESPACE, KV_NAMESPACE)
addEventListener('fetch', event => {
event.waitUntil(counter.countRequest(request))
event.respondWith(handleRequest(event.request))
})
import { v4 as uuidv4 } from 'uuid'
export class RequestCounter {
public requests: number = 0
public persistence: number = 0
private countAPINamespace: string
private KV_NAMESPACE: any
constructor(countAPINamespace: string, KV_NAMESPACE: any) {
this.countAPINamespace = countAPINamespace
this.KV_NAMESPACE = KV_NAMESPACE
}
public async countRequest(request: Request) {
this.requests++
this.persistence++
if (this.persistence < 10) {
// Assume it isn't persistant
await this.sendCountRequest(request, this.requests)
this.requests = 0
} else if (this.persistence < 100) {
if (this.requests >= 10) {
// Assume we can safely log every 10 requests
await this.sendCountRequest(request, this.requests)
this.requests = 0
}
} else {
if (this.requests >= 25) {
// Assume we can safely log every 10 requests
await this.sendCountRequest(request, this.requests)
this.requests = 0
}
}
}
private async sendCountRequest(request: Request, requestCount: number) {
let userIP = request.headers.get('cf-connecting-ip')
let user = JSON.parse(await this.KV_NAMESPACE.get(userIP))
if (user) {
let resPromise = fetch(this.getCountUpdateUrl(user.userID, requestCount))
let res = await resPromise
if (!res.ok) {
// try one more time
await fetch(this.getCountUpdateUrl(user.userID, requestCount))
}
} else {
let userID = uuidv4()
await this.KV_NAMESPACE.put(
userIP,
JSON.stringify({
userID: userID,
userIP: userIP,
firstSeenDate: new Date(),
counterURL: this.getCountHitUrl(userID).replace('/hit/', '/info/'),
meta: {
cf: {
first_country: request.cf.country,
first_colo: request.cf.colo,
first_asn: request.cf.asn,
},
},
}),
)
await fetch(
'https://api.countapi.xyz/create?key=' +
userID +
'&namespace=' +
this.countAPINamespace +
'_users' +
'&update_upperbound=10000' +
'&value=' +
requestCount,
)
}
}
private getCountUpdateUrl(id: string, amount: number): string {
return (
'https://api.countapi.xyz/update/' +
this.countAPINamespace +
'_users/' +
id.split('/').join('__') +
'?amount=' +
amount
)
}
private getCountHitUrl(id: string): string {
return (
'https://api.countapi.xyz/hit/' +
this.countAPINamespace +
'_users/' +
id.split('/').join('__')
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment