first
This commit is contained in:
commit
acfb7717fe
11 changed files with 3384 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
/node_modules
|
1413
Cargo.lock
generated
Normal file
1413
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
21
Cargo.toml
Normal file
21
Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "phoninghome"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
axohtml = "0.5.0"
|
||||
ipaddress = "0.1.3"
|
||||
serde = { version = "1.0.199", features = ["serde_derive"] }
|
||||
woothee = "0.13.0"
|
||||
worker = { version = "0.0.18", features = ["d1"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s" # optimize for size in release builds
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
|
86
README.md
Normal file
86
README.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Template: worker-rust
|
||||
|
||||
[![Deploy with Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/workers-sdk/tree/main/templates/experimental/worker-rust)
|
||||
|
||||
A template for kick starting a Cloudflare worker project using [`workers-rs`](https://github.com/cloudflare/workers-rs).
|
||||
|
||||
This template is designed for compiling Rust to WebAssembly and publishing the resulting worker to Cloudflare's [edge infrastructure](https://www.cloudflare.com/network/).
|
||||
|
||||
## Setup
|
||||
|
||||
To create a `my-project` directory using this template, run:
|
||||
|
||||
```sh
|
||||
$ npx wrangler generate my-project https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust
|
||||
# or
|
||||
$ yarn wrangler generate my-project https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust
|
||||
# or
|
||||
$ pnpm wrangler generate my-project https://github.com/cloudflare/workers-sdk/templates/experimental/worker-rust
|
||||
```
|
||||
|
||||
## Wrangler
|
||||
|
||||
Wrangler is used to develop, deploy, and configure your Worker via CLI.
|
||||
|
||||
Further documentation for Wrangler can be found [here](https://developers.cloudflare.com/workers/tooling/wrangler).
|
||||
|
||||
## Usage
|
||||
|
||||
This template starts you off with a `src/lib.rs` file, acting as an entrypoint for requests hitting your Worker. Feel free to add more code in this file, or create Rust modules anywhere else for this project to use.
|
||||
|
||||
With `wrangler`, you can build, test, and deploy your Worker with the following commands:
|
||||
|
||||
```sh
|
||||
# run your Worker in an ideal development workflow (with a local server, file watcher & more)
|
||||
$ npm run dev
|
||||
|
||||
# deploy your Worker globally to the Cloudflare network (update your wrangler.toml file for configuration)
|
||||
$ npm run deploy
|
||||
```
|
||||
|
||||
Read the latest `worker` crate documentation here: https://docs.rs/worker
|
||||
|
||||
## Advanced Example
|
||||
|
||||
As this template comprises only the essential setup, we recommend considering our advanced example to leverage its additional functionalities. The advanced example showcases the creation of multiple routes, logging of requests, retrieval of field data from a form, and other features that may prove useful to your project.
|
||||
The following example has been taken from: [workers-rs](https://github.com/cloudflare/workers-rs). You can learn more about how to use workers with rust by going there.
|
||||
|
||||
```rust
|
||||
use worker::*;
|
||||
|
||||
#[event(fetch)]
|
||||
pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result<Response> {
|
||||
console_log!(
|
||||
"{} {}, located at: {:?}, within: {}",
|
||||
req.method().to_string(),
|
||||
req.path(),
|
||||
req.cf().coordinates().unwrap_or_default(),
|
||||
req.cf().region().unwrap_or("unknown region".into())
|
||||
);
|
||||
|
||||
if !matches!(req.method(), Method::Post) {
|
||||
return Response::error("Method Not Allowed", 405);
|
||||
}
|
||||
|
||||
if let Some(file) = req.form_data().await?.get("file") {
|
||||
return match file {
|
||||
FormEntry::File(buf) => {
|
||||
Response::ok(&format!("size = {}", buf.bytes().await?.len()))
|
||||
}
|
||||
_ => Response::error("`file` part of POST form must be a file", 400),
|
||||
};
|
||||
}
|
||||
|
||||
Response::error("Bad Request", 400)
|
||||
}
|
||||
```
|
||||
|
||||
## WebAssembly
|
||||
|
||||
`workers-rs` (the Rust SDK for Cloudflare Workers used in this template) is meant to be executed as compiled WebAssembly, and as such so **must** all the code you write and depend upon. All crates and modules used in Rust-based Workers projects have to compile to the `wasm32-unknown-unknown` triple.
|
||||
|
||||
Read more about this on the [`workers-rs`](https://github.com/cloudflare/workers-rs) project README.
|
||||
|
||||
## Issues
|
||||
|
||||
If you have any problems with the `worker` crate, please open an issue on the upstream project issue tracker on the [`workers-rs` repository](https://github.com/cloudflare/workers-rs).
|
1280
package-lock.json
generated
Normal file
1280
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
12
package.json
Normal file
12
package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "template-worker-rust",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev --env dev"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.1.2"
|
||||
}
|
||||
}
|
BIN
phoninghome.db
Normal file
BIN
phoninghome.db
Normal file
Binary file not shown.
52
phoninghome.sql
Normal file
52
phoninghome.sql
Normal file
|
@ -0,0 +1,52 @@
|
|||
-- drop table if exists actors;
|
||||
-- drop table if exists asnames;
|
||||
-- -- crashes, downloads, entity_properties, pings, property_keys, vists;
|
||||
-- drop table if exists crashes;
|
||||
-- drop table if exists downloads;
|
||||
-- drop table if exists entity_properties;
|
||||
-- drop table if exists pings;
|
||||
-- drop table if exists property_keys;
|
||||
-- drop table if exists visits;
|
||||
|
||||
|
||||
-- CREATE TABLE IF NOT EXISTS actors (
|
||||
-- id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
-- ip TEXT NOT NULL,
|
||||
-- asn INTEGER NOT NULL,
|
||||
-- colo TEXT NOT NULL,
|
||||
-- continent TEXT NOT NULL,
|
||||
-- country TEXT NOT NULL,
|
||||
-- city TEXT NOT NULL,
|
||||
-- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
-- );
|
||||
|
||||
-- CREATE TABLE IF NOT EXISTS asnames (
|
||||
-- asn INTEGER PRIMARY KEY,
|
||||
-- name TEXT NOT NULL,
|
||||
-- country TEXT NOT NULL
|
||||
-- );
|
||||
|
||||
|
||||
-- Alter table to add browser agent and os to the actors table
|
||||
--ALTER TABLE actors ADD COLUMN browser TEXT;
|
||||
--ALTER TABLE actors ADD COLUMN os TEXT;
|
||||
-- properties.push(("category".to_string(), res.category.to_string()));
|
||||
-- properties.push(("name".to_string(), res.name.to_string()));
|
||||
-- // res.browser_type
|
||||
-- properties.push(("version".to_string(), res.version.to_string()));
|
||||
-- // res.os_version
|
||||
-- properties.push(("vendor".to_string(), res.vendor.to_string()));
|
||||
-- // res.vendor
|
||||
-- properties.push(("os_version".to_string(), res.os_version.to_string()));
|
||||
-- // res.version
|
||||
-- properties.push(("browser_type".to_string(), res.browser_type.to_string()));
|
||||
|
||||
-- ALTER TABLE actors ADD COLUMN category TEXT;
|
||||
-- ALTER TABLE actors ADD COLUMN name TEXT;
|
||||
-- ALTER TABLE actors ADD COLUMN version TEXT;
|
||||
-- ALTER TABLE actors ADD COLUMN vendor TEXT;
|
||||
-- ALTER TABLE actors ADD COLUMN os_version TEXT;
|
||||
-- ALTER TABLE actors ADD COLUMN browser_type TEXT;
|
||||
|
||||
-- user_agent TEXT NOT NULL,
|
||||
--ALTER TABLE actors ADD COLUMN user_agent TEXT;
|
3
rust-toolchain
Normal file
3
rust-toolchain
Normal file
|
@ -0,0 +1,3 @@
|
|||
[toolchain]
|
||||
channel = "1.75"
|
||||
components = [ "rustfmt", "clippy", "llvm-tools-preview", "rust-src" ]
|
500
src/lib.rs
Normal file
500
src/lib.rs
Normal file
|
@ -0,0 +1,500 @@
|
|||
use axohtml::{dom::DOMTree, html, text, types::Metadata};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use worker::*;
|
||||
|
||||
#[event(fetch, respond_with_errors)]
|
||||
async fn main(req: Request, env: Env, ctx: Context) -> Result<Response> {
|
||||
let d1 = env.d1("DB")?;
|
||||
|
||||
// Extract properties from the request
|
||||
let (actor_properties, ip, asn) = extract_actor_properties(&req);
|
||||
|
||||
// Extract properties from the request
|
||||
let (actor_properties, ip, asn) = extract_actor_properties(&req);
|
||||
|
||||
// Create a visit record
|
||||
create_visit(&d1, actor_properties, &ip, asn).await?;
|
||||
/// create_visit(&d1, actor_properties, &ip, asn).await?;
|
||||
get_home(req, ctx, env).await
|
||||
}
|
||||
|
||||
// ping
|
||||
|
||||
// get apple ip ranges
|
||||
// https://mask-api.icloud.com/egress-ip-ranges.csv
|
||||
// formatted as
|
||||
// ```csv
|
||||
// 172.224.224.0/27,GB,GB-EN,London,
|
||||
// 172.224.224.32/31,GB,GB-SC,Aberdeen,
|
||||
// ```
|
||||
// returns a list of ip ranges
|
||||
async fn get_apple_ip_ranges() -> Result<Vec<AppleIpRange>> {
|
||||
let mut text = worker::Fetch::Url(Url::parse("https://mask-api.icloud.com/egress-ip-ranges.csv").unwrap())
|
||||
.send()
|
||||
.await?;
|
||||
let apple_ip_ranges = text
|
||||
.text()
|
||||
.await?
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let mut parts = line.split(',');
|
||||
let range = parts.next()?;
|
||||
let country = parts.next()?;
|
||||
let region = parts.next()?;
|
||||
let city = parts.next()?;
|
||||
let range = ipaddress::IPAddress::parse(range).ok()?;
|
||||
Some(AppleIpRange {
|
||||
range: range,
|
||||
country: country.to_string(),
|
||||
region: region.to_string(),
|
||||
city: city.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<AppleIpRange>>();
|
||||
Ok(apple_ip_ranges)
|
||||
}
|
||||
|
||||
// apple ip range format to cidr
|
||||
// ```csv
|
||||
// 172.224.224.0/27,GB,GB-EN,London,
|
||||
// ```
|
||||
struct AppleIpRange {
|
||||
range: ipaddress::IPAddress,
|
||||
country: String,
|
||||
region: String,
|
||||
city: String,
|
||||
}
|
||||
|
||||
|
||||
fn extract_actor_properties(req: &Request) -> (Vec<(String, String)>, String, u32) {
|
||||
let mut properties = Vec::new();
|
||||
|
||||
// Extract IP address
|
||||
let asn = req.cf().asn();
|
||||
properties.push(("asn".to_string(), asn.to_string()));
|
||||
|
||||
let colo = req.cf().colo();
|
||||
properties.push(("colo".to_string(), colo));
|
||||
|
||||
let ip = req
|
||||
.headers()
|
||||
.get("cf-connecting-ip")
|
||||
.unwrap_or_default()
|
||||
.unwrap();
|
||||
|
||||
|
||||
if let Some(city) = req.cf().city() {
|
||||
properties.push(("city".to_string(), city));
|
||||
}
|
||||
|
||||
if let Some(country) = req.cf().country() {
|
||||
properties.push(("country".to_string(), country));
|
||||
}
|
||||
|
||||
if let Some(continent) = req.cf().continent() {
|
||||
properties.push(("continent".to_string(), continent));
|
||||
}
|
||||
|
||||
// get useragent
|
||||
if let Ok(Some(user_agent)) = req.headers().get("user-agent") {
|
||||
properties.push(("user-agent".to_string(), user_agent.to_string()));
|
||||
if let Some(res) = woothee::parser::Parser::new().parse(&user_agent) {
|
||||
properties.push(("os".to_string(), res.os.to_string()));
|
||||
properties.push(("category".to_string(), res.category.to_string()));
|
||||
properties.push(("name".to_string(), res.name.to_string()));
|
||||
// res.browser_type
|
||||
properties.push(("version".to_string(), res.version.to_string()));
|
||||
// res.os_version
|
||||
properties.push(("vendor".to_string(), res.vendor.to_string()));
|
||||
// res.vendor
|
||||
properties.push(("os_version".to_string(), res.os_version.to_string()));
|
||||
// res.version
|
||||
properties.push(("browser_type".to_string(), res.browser_type.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
(properties, ip, asn)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Actor {
|
||||
id: i32,
|
||||
}
|
||||
|
||||
async fn create_visit(
|
||||
d1: &D1Database,
|
||||
actor_properties: Vec<(String, String)>,
|
||||
ip: &str,
|
||||
asn: u32,
|
||||
) -> Result<()> {
|
||||
let mut properties_map = HashMap::new();
|
||||
for (key, value) in actor_properties {
|
||||
properties_map.insert(key, value);
|
||||
}
|
||||
|
||||
let colo = properties_map
|
||||
.get("colo")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let continent = properties_map
|
||||
.get("continent")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let country = properties_map
|
||||
.get("country")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let city = properties_map
|
||||
.get("city")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let user_agent = properties_map
|
||||
.get("user-agent")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let os = properties_map.get("os").unwrap_or(&"".to_string()).clone();
|
||||
let category = properties_map
|
||||
.get("category")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let name = properties_map
|
||||
.get("name")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let version = properties_map
|
||||
.get("version")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let vendor = properties_map
|
||||
.get("vendor")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let os_version = properties_map
|
||||
.get("os_version")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
let browser_type = properties_map
|
||||
.get("browser_type")
|
||||
.unwrap_or(&"".to_string())
|
||||
.clone();
|
||||
|
||||
let statement = "
|
||||
INSERT INTO actors (
|
||||
ip,
|
||||
asn,
|
||||
colo,
|
||||
continent,
|
||||
country,
|
||||
city,
|
||||
user_agent,
|
||||
os,
|
||||
category,
|
||||
name,
|
||||
version,
|
||||
vendor,
|
||||
os_version,
|
||||
browser_type
|
||||
)
|
||||
VALUES (?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?)
|
||||
";
|
||||
|
||||
let query = d1.prepare(statement).bind(&[
|
||||
ip.into(),
|
||||
asn.into(),
|
||||
colo.into(),
|
||||
continent.into(),
|
||||
country.into(),
|
||||
city.into(),
|
||||
user_agent.into(),
|
||||
os.into(),
|
||||
category.into(),
|
||||
name.into(),
|
||||
version.into(),
|
||||
vendor.into(),
|
||||
os_version.into(),
|
||||
browser_type.into(),
|
||||
])?;
|
||||
query.run().await?;
|
||||
Ok(())
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct FrequentASN {
|
||||
asn: u32,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
async fn get_most_frequent_asns(d1: &D1Database) -> Result<Vec<(String, u32)>> {
|
||||
let statement =
|
||||
"SELECT asn, COUNT(*) as count FROM actors GROUP BY asn ORDER BY count DESC LIMIT 10";
|
||||
let rows = d1
|
||||
.prepare(statement)
|
||||
.all()
|
||||
.await?
|
||||
.results::<FrequentASN>()?;
|
||||
|
||||
let asnames_map = get_asnames().await.unwrap_or_default();
|
||||
|
||||
let unknown_name = "Unknown".to_string();
|
||||
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
let asname = asnames_map
|
||||
.get(&row.asn)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&unknown_name);
|
||||
result.push((asname.to_string(), row.count));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
// response struct
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct RecentVisit {
|
||||
ip: String,
|
||||
asn: u32,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
async fn get_recent_visits(d1: &D1Database) -> Result<Vec<(String, u32, String, String)>> {
|
||||
let statement = "SELECT ip, asn, created_at FROM actors ORDER BY created_at DESC LIMIT 10";
|
||||
let rows = d1
|
||||
.prepare(statement)
|
||||
.all()
|
||||
.await?
|
||||
.results::<RecentVisit>()?;
|
||||
|
||||
let asnames_map = get_asnames().await.unwrap_or_default();
|
||||
let unknown_name = "Unknown".to_string();
|
||||
|
||||
let mut result = Vec::new();
|
||||
for mut row in rows {
|
||||
let asname = asnames_map
|
||||
.get(&row.asn)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&unknown_name);
|
||||
row.ip = obfuscate_ip(&row.ip);
|
||||
result.push((row.ip, row.asn, asname.to_string(), row.created_at));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// get top browsers
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct TopBrowser {
|
||||
name: String,
|
||||
version: String,
|
||||
os: String,
|
||||
count: u32,
|
||||
}
|
||||
|
||||
async fn get_top_browsers(d1: &D1Database) -> Result<Vec<(String, String, String, u32)>> {
|
||||
let statement = "SELECT name, version, os, COUNT(*) as count FROM actors GROUP BY name, version, os ORDER BY count DESC LIMIT 10";
|
||||
let rows = d1.prepare(statement).all().await?.results::<TopBrowser>()?;
|
||||
|
||||
let mut result = Vec::new();
|
||||
for row in rows {
|
||||
result.push((row.name, row.version, row.os, row.count));
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct FirstTimeVisit {
|
||||
ip: String,
|
||||
asn: u32,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
// obfuscate ip
|
||||
fn obfuscate_ip(ip: &str) -> String {
|
||||
let ip = ip.parse::<IpAddr>().unwrap();
|
||||
match ip {
|
||||
IpAddr::V4(ip) => {
|
||||
let octets = ip.octets();
|
||||
format!("{}.{}.{}.xxx", octets[0], octets[1], octets[2])
|
||||
}
|
||||
IpAddr::V6(ip) => {
|
||||
let segments = ip.segments();
|
||||
format!("{:x}:{:x}:{:x}:xxxx", segments[0], segments[1], segments[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_first_time_visits(d1: &D1Database) -> Result<Vec<(String, u32, String, String)>> {
|
||||
let statement = "
|
||||
SELECT ip, asn, MIN(created_at) as created_at
|
||||
FROM actors
|
||||
GROUP BY ip
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
";
|
||||
let rows = d1
|
||||
.prepare(statement)
|
||||
.all()
|
||||
.await?
|
||||
.results::<FirstTimeVisit>()?;
|
||||
|
||||
let asnames_map = get_asnames().await.unwrap_or_default();
|
||||
let unknown_name = "Unknown".to_string();
|
||||
|
||||
let mut result = Vec::new();
|
||||
for mut row in rows {
|
||||
let asname = asnames_map
|
||||
.get(&row.asn)
|
||||
.map(|a| &a.name)
|
||||
.unwrap_or(&unknown_name);
|
||||
row.ip = obfuscate_ip(&row.ip);
|
||||
result.push((row.ip, row.asn, asname.to_string(), row.created_at));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn get_home(req: Request, ctx: Context, env: Env) -> worker::Result<Response> {
|
||||
let d1 = env.d1("DB")?;
|
||||
|
||||
let mut doc: DOMTree<String> = html!(
|
||||
<html>
|
||||
<head>
|
||||
<title>"phoninghome"</title>
|
||||
<meta name=Metadata::Author content="sevki"/>
|
||||
<link rel="stylesheet" href="https://oknotok.computer/notokcss.css"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="icon" type="image/png" href="http://oknotok.computer/phoninghome.png"/>
|
||||
</head>
|
||||
<body class="is-dark" style="width: 90%; margin: 0 auto;">
|
||||
<h1 style="display: flex; justify-content: center; align-items: center;">
|
||||
<img
|
||||
src="http://oknotok.computer/phoninghome.png"
|
||||
class="nes-avata is-large"
|
||||
style="width: 150px; height: 150px;"
|
||||
alt="it's not you, it's we" />
|
||||
"phoninghome"
|
||||
</h1>
|
||||
<p class="official">
|
||||
</p>
|
||||
<div class="nes-container is-dark" style="padding: 5px;">
|
||||
"Most Frequent ASNs"
|
||||
<table class="nes-table is-bordered is-dark">
|
||||
{
|
||||
let asns = get_most_frequent_asns(&d1).await?;
|
||||
asns.into_iter().map(|asn| html!(
|
||||
<tr>
|
||||
<td>{text!("{}", asn.0)}</td>
|
||||
<td>{text!("{}", asn.1)}</td>
|
||||
</tr>
|
||||
)).collect::<Vec<_>>()
|
||||
}
|
||||
</table>
|
||||
"First Time Visits"
|
||||
<table class="nes-table is-bordered is-dark">
|
||||
{
|
||||
let visits = get_first_time_visits(&d1).await?;
|
||||
visits.into_iter().map(|visit| html!(
|
||||
<tr>
|
||||
<td>{text!("{}", visit.0)}</td>
|
||||
<td>{text!("{}", visit.1)}</td>
|
||||
<td>{text!("{}", visit.2)}</td>
|
||||
<td>{text!("{}", visit.3)}</td>
|
||||
</tr>
|
||||
)).collect::<Vec<_>>()
|
||||
|
||||
}
|
||||
</table>
|
||||
// "Top Browsers"
|
||||
// <table class="nes-table is-bordered is-dark">
|
||||
// {
|
||||
// let browsers = get_top_browsers(&d1).await?;
|
||||
// browsers.into_iter().map(|browser| html!(
|
||||
// <tr>
|
||||
// <td>{text!("{}", browser.0)}</td>
|
||||
// <td>{text!("{}", browser.1)}</td>
|
||||
// <td>{text!("{}", browser.2)}</td>
|
||||
// <td>{text!("{}", browser.3)}</td>
|
||||
// </tr>
|
||||
// )).collect::<Vec<_>>()
|
||||
// }
|
||||
// </table>
|
||||
"Recent Visits"
|
||||
<table class="nes-table is-bordered is-dark">
|
||||
{
|
||||
let visits = get_recent_visits(&d1).await?;
|
||||
visits.into_iter().map(|visit| html!(
|
||||
<tr>
|
||||
<td>{text!("{}", visit.0)}</td>
|
||||
<td>{text!("{}", visit.1)}</td>
|
||||
<td>{text!("{}", visit.2)}</td>
|
||||
<td>{text!("{}", visit.3)}</td>
|
||||
</tr>
|
||||
)).collect::<Vec<_>>()
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
<footer>
|
||||
<small>"this is a work in progress, please report any issues to " <a href="https://twitter.com/sevki">{text!("@sevki")}</a></small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
let doc_str = doc.to_string();
|
||||
return Response::from_html(doc_str);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AsName {
|
||||
pub asn: u32,
|
||||
pub name: String,
|
||||
pub country: String,
|
||||
}
|
||||
|
||||
const DATA_URL: &str = "https://ftp.ripe.net/ripe/asnames/asn.txt";
|
||||
|
||||
|
||||
async fn get_asnames() -> Result<HashMap<u32, AsName>> {
|
||||
let mut text = worker::Fetch::Url(Url::parse(DATA_URL).unwrap())
|
||||
.send()
|
||||
.await?;
|
||||
let asnames = text
|
||||
.text()
|
||||
.await?
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let (asn_str, name_country_str) = match line.split_once(' ') {
|
||||
Some((asn, name)) => (asn, name),
|
||||
None => return None,
|
||||
};
|
||||
let (name_str, country_str) = match name_country_str.rsplit_once(", ") {
|
||||
Some((name, country)) => (name, country),
|
||||
None => return None,
|
||||
};
|
||||
Some(AsName {
|
||||
asn: asn_str.parse::<u32>().unwrap(),
|
||||
name: name_str.to_string(),
|
||||
country: country_str.to_string(),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<AsName>>();
|
||||
|
||||
let mut asnames_map = HashMap::new();
|
||||
for asname in asnames {
|
||||
asnames_map.insert(asname.asn, asname);
|
||||
}
|
||||
Ok(asnames_map)
|
||||
}
|
15
wrangler.toml
Normal file
15
wrangler.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name = "phoninghome"
|
||||
main = "build/worker/shim.mjs"
|
||||
compatibility_date = "2023-12-01"
|
||||
|
||||
[build]
|
||||
command = "cargo install -q worker-build && worker-build --release"
|
||||
|
||||
[env.dev]
|
||||
build = { command = "cargo install -q worker-build && worker-build --dev" }
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB" # i.e. available in your Worker on env.DB
|
||||
database_name = "phoninghome"
|
||||
database_id = "9798e254-0d5c-4072-ace4-617f2df4ed05"
|
||||
|
Loading…
Reference in a new issue