8 Commits

10 changed files with 149 additions and 59 deletions
+12
View File
@@ -195,6 +195,18 @@ Then run the program with the required flags enabled:
access_log --swd --device-country
```
## ASN database
If you want to keep track of ASN for each visitor, for instance for
grouping possible attacks or IA crawls, create a database based on
<https://iptoasn.com/>:
```bash
./contrib/asn_database.sh
```
And start the server with the `--asn-database=` flag.
## Crawler user agents
Download the [crawler user agents
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
set -e
db="${1:-$PWD/asn.sqlite3}"
sqlite3 "$db" "CREATE TABLE IF NOT EXISTS asn (range_start INTEGER, range_end INTEGER, id INTEGER, country STRING, description TEXT);"
sqlite3 "$db" "DELETE from asn;"
wget https://iptoasn.com/data/ip2asn-v4-u32.tsv.gz -O - | gunzip | sqlite3 -tabs "$db" ".import '|cat -' asn"
echo "ASN database created/updated at $db"
+3 -1
View File
@@ -53,7 +53,9 @@ CREATE TABLE IF NOT EXISTS "access_logs" (
"network_co2" float DEFAULT NULL,
"consumer_device_co2" float DEFAULT NULL,
"production_co2" float DEFAULT NULL,
"total_co2" float DEFAULT NULL
"total_co2" float DEFAULT NULL,
"node" varchar DEFAULT NULL,
"asn" integer DEFAULT NULL
);
CREATE INDEX IF NOT EXISTS "index_access_logs_on_host" ON "access_logs" ("host");
+1 -1
View File
@@ -1,5 +1,5 @@
name: "access_log"
version: "0.5.12"
version: "0.6.0"
authors:
- "f <f@sutty.nl>"
targets:
+5 -5
View File
@@ -111,31 +111,31 @@ describe SWD do
describe "#consumer_device_co2" do
it "should extract the consumer device co2 emissions" do
swd.consumer_device_co2.should(eq 0.6343432152938272)
swd.consumer_device_co2.should(eq 0.6854924454444445)
end
end
describe "#production_co2" do
it "should extract the production co2 emissions" do
swd.production_co2.should(eq 0.2317792517419753)
swd.production_co2.should(eq 0.2504683935277778)
end
end
describe "#network_co2" do
it "should extract the network co2 emissions" do
swd.network_co2.should(eq 0.1707847118098766)
swd.network_co2.should(eq 0.1845556583888889)
end
end
describe "#datacenter_co2" do
it "should extract the datacenter co2 emissions" do
swd.datacenter_co2.should(eq 0.1829836197962963)
swd.datacenter_co2.should(eq 0.19773820541666665)
end
end
describe "#total_co2" do
it "should extract the total co2 emissions" do
swd.total_co2.should(eq 1.2198907986419754)
swd.total_co2.should(eq 1.3182547027777778)
end
end
end
+36 -11
View File
@@ -10,8 +10,9 @@ require "system"
require "./models/access_log"
require "./models/crawler"
require "./swd"
require "./asn"
VERSION = "0.5.8"
VERSION = "0.6.0"
Log.setup_from_env
@@ -19,13 +20,15 @@ Log.setup_from_env
socket = "/tmp/access_log.socket"
# The default database URI
database = "sqlite3://./development.sqlite3"
# ASN database
asn_database = ""
# Detect web crawlers
crawler = false
# Parse the crawlers repository
crawlers = [] of Crawler
# Fields in database
# TODO: Obtain them from AccessLog
fields = %w[id remote_user host msec server_protocol request_method request_completion uri request_uri query_string status sent_http_content_type sent_http_content_encoding sent_http_etag sent_http_last_modified http_accept http_accept_encoding http_accept_language http_pragma http_cache_control http_if_none_match http_dnt http_user_agent http_origin http_referer request_time bytes_sent body_bytes_sent request_length http_connection pipe connection_requests geoip2_data_country_name geoip2_data_city_name ssl_server_name ssl_protocol ssl_early_data ssl_session_reused ssl_curves ssl_ciphers ssl_cipher sent_http_x_xss_protection sent_http_x_frame_options sent_http_x_content_type_options sent_http_strict_transport_security nginx_version pid crawler datacenter_co2 network_co2 consumer_device_co2 production_co2 total_co2 node]
fields = %w[id remote_user host msec server_protocol request_method request_completion uri request_uri query_string status sent_http_content_type sent_http_content_encoding sent_http_etag sent_http_last_modified http_accept http_accept_encoding http_accept_language http_pragma http_cache_control http_if_none_match http_dnt http_user_agent http_origin http_referer request_time bytes_sent body_bytes_sent request_length http_connection pipe connection_requests geoip2_data_country_name geoip2_data_city_name ssl_server_name ssl_protocol ssl_early_data ssl_session_reused ssl_curves ssl_ciphers ssl_cipher sent_http_x_xss_protection sent_http_x_frame_options sent_http_x_content_type_options sent_http_strict_transport_security nginx_version pid crawler datacenter_co2 network_co2 consumer_device_co2 production_co2 total_co2 node asn]
# Params for the query
params = [] of String
# SWD
@@ -56,6 +59,10 @@ OptionParser.parse do |p|
database = d
end
p.on "-I DATABASE", "--asn-database=DATABASE", "ASN Database URI" do |d|
asn_database = d
end
p.on "-c crawler-user-agents.json", "--crawlers crawler-user-agents.json", "Crawlers repository" do |c|
crawler = true
crawlers = Array(Crawler).from_json File.read(c)
@@ -110,26 +117,41 @@ if crawler
crawler_re = Regex.union(crawlers.map { |c| c.pattern })
end
def remove_socket!(socket)
FileUtils.rm(socket) if File.exists? socket
end
remove_socket!(socket)
asn = ASN.new(asn_database) unless asn_database.empty?
server = Socket.unix(Socket::Type::DGRAM)
server.bind Socket::UNIXAddress.new(socket)
def close_server!(server, asn)
server.try(&.close)
asn.try(&.close)
end
Signal::INT.trap do
server.close
FileUtils.rm(socket) if File.exists? socket
remove_socket!(socket)
close_server!(server, asn)
exit
end
Signal::KILL.trap do
server.close
FileUtils.rm(socket) if File.exists? socket
remove_socket!(socket)
close_server!(server, asn)
exit
end
MAX_SIZE = 1024 * 64 # 64K
# Open the database and wait for JSONL input.
db = DB.open database do |db|
DB.open database do |db|
while true
begin
msg, _ = server.receive(1024 * 64) # 64K
msg, _ = server.receive(MAX_SIZE)
# TODO: is this the best method?
_, json = msg.split("{", 2)
json = "{#{json}"
@@ -137,6 +159,8 @@ db = DB.open database do |db|
access_log = AccessLog.from_json(json || "{}")
d = device_country ? access_log.geoip2_data_country_iso_code : nil
s = SWD.new(access_log.bytes_sent, renewable, datacenter, d, intensity) if swd
i = access_log.remote_addr
a = asn.try(&.query(i)) if i
# Execute query, detect crawler if enabled.
# TODO: Does Crystal support splats? It does but we need to cast
@@ -195,14 +219,15 @@ db = DB.open database do |db|
(swd ? s.try(&.consumer_device_co2) : nil),
(swd ? s.try(&.production_co2) : nil),
(swd ? s.try(&.total_co2) : nil),
(node ? System.hostname : nil)
(node ? System.hostname : nil),
a.try(&.id)
# Ignore parsing errors
rescue e : JSON::ParseException
Log.warn &.emit("Parse exception", error: e.message)
rescue IO::Error
server.close
FileUtils.rm(socket) if File.exists? socket
remove_socket!(socket)
close_server!(server, asn)
exit
end
end
+41
View File
@@ -0,0 +1,41 @@
require "db"
require "sqlite3"
class AsnResult
include DB::Serializable
property id : Int64?
property country : String?
property description : String?
end
class ASN
QUERY = "select id, country, description from asn where ? between range_start and range_end limit 1;"
property db : DB::Database
def initialize(database : String)
@db = DB.open(database)
@cache = Hash(UInt32, AsnResult).new
end
# Binary representation of an IPv4 address
def to_binary(address : String) : UInt32
(address.split(".").map(&.to_u32).reduce(0.to_u32) { |t, v| (t << 8) + v })
end
# Lookups an address and returns the ASN for it
def query(address : String)
query(to_binary(address))
end
def query(binary_address : UInt32) : AsnResult?
@cache[binary_address] ||= @db.query_one(QUERY, binary_address.to_i64, &.read(AsnResult))
rescue DB::NoResultsError
nil
end
def close
@db.close
end
end
+1
View File
@@ -58,4 +58,5 @@ class AccessLog
property sent_http_strict_transport_security : String
property nginx_version : String
property pid : String
property remote_addr : String?
end
+1 -1
View File
@@ -27,7 +27,7 @@ class SWD
NETWORK_ENERGY = 0.14
DATACENTER_ENERGY = 0.15
PRODUCTION_ENERGY = 0.19
GLOBAL_GRID_INTENSITY = 437.66
GLOBAL_GRID_INTENSITY = 472.95
RENEWABLES_GRID_INTENSITY = 50.0
@transfered_bytes_to_gb : Float32? | Float64? = nil
+40 -40
View File
@@ -2,19 +2,19 @@ class SWD
module IntensityData
# @see {co2.js/data/output/average-intensities.json}
AVERAGE_INTENSITY_BY_ISO_CODE = {
"AE": 467.51,
"AF": 123.71,
"AE": 492.7,
"AG": 611.11,
"AL": 24.42,
"AM": 244.34,
"AM": 243.52,
"AO": 167.22,
"AR": 358.95,
"AR": 344.83,
"AS": 647.06,
"AT": 102.62,
"AU": 551.59,
"AU": 553.76,
"AW": 550.0,
"AZ": 633.07,
"BA": 637.76,
"AZ": 632.89,
"BA": 638.05,
"BB": 600.0,
"BD": 694.63,
"BE": 117.58,
@@ -25,37 +25,37 @@ class SWD
"BJ": 590.0,
"BN": 892.67,
"BO": 468.02,
"BR": 103.21,
"BR": 106.06,
"BS": 653.66,
"BT": 24.19,
"BW": 849.42,
"BY": 313.62,
"BY": 323.63,
"BZ": 155.56,
"CA": 174.81,
"CA": 184.99,
"CD": 27.04,
"CF": 0.0,
"CG": 713.73,
"CH": 36.6,
"CH": 36.72,
"CI": 393.53,
"CK": 250.0,
"CL": 265.52,
"CM": 285.71,
"CN": 559.55,
"CN": 557.5,
"CO": 285.8,
"CR": 63.01,
"CU": 638.98,
"CV": 480.0,
"CY": 512.24,
"CZ": 413.86,
"DE": 344.14,
"DE": 342.06,
"DJ": 450.0,
"DK": 143.3,
"DM": 600.0,
"DO": 566.05,
"DO": 565.97,
"DZ": 633.65,
"EC": 209.7,
"EE": 341.02,
"EG": 571.92,
"EG": 574.5,
"ER": 590.91,
"ES": 146.15,
"ET": 23.55,
@@ -65,7 +65,7 @@ class SWD
"FO": 354.17,
"FR": 44.18,
"GA": 429.47,
"GB": 210.89,
"GB": 215.79,
"GD": 666.67,
"GE": 143.06,
"GF": 204.08,
@@ -86,27 +86,27 @@ class SWD
"HT": 534.65,
"HU": 182.82,
"ID": 682.43,
"IE": 279.7,
"IE": 279.79,
"IL": 567.26,
"IN": 708.32,
"IN": 708.96,
"IQ": 689.4,
"IR": 641.94,
"IR": 648.68,
"IS": 28.33,
"IT": 287.53,
"IT": 287.75,
"JM": 561.25,
"JO": 539.21,
"JP": 482.32,
"KE": 88.79,
"KG": 162.71,
"JP": 483.73,
"KE": 84.83,
"KG": 152.65,
"KH": 497.46,
"KI": 500.0,
"KM": 642.86,
"KN": 636.36,
"KP": 344.26,
"KR": 414.27,
"KR": 415.65,
"KW": 637.24,
"KY": 642.86,
"KZ": 801.95,
"KZ": 801.79,
"LA": 232.12,
"LB": 369.47,
"LC": 650.0,
@@ -118,12 +118,12 @@ class SWD
"LV": 136.22,
"LY": 830.53,
"MA": 577.65,
"MD": 631.68,
"MD": 629.56,
"ME": 413.51,
"MG": 477.27,
"MK": 568.97,
"ML": 394.5,
"MM": 578.82,
"MM": 569.69,
"MN": 784.01,
"MO": 448.98,
"MQ": 516.78,
@@ -133,20 +133,20 @@ class SWD
"MU": 633.03,
"MV": 611.77,
"MW": 54.65,
"MX": 484.83,
"MY": 609.85,
"MX": 483.14,
"MY": 604.43,
"MZ": 127.81,
"NA": 47.62,
"NC": 585.76,
"NE": 687.5,
"NG": 507.85,
"NI": 288.33,
"NL": 253.31,
"NL": 252.7,
"NO": 30.75,
"NP": 23.36,
"NR": 750.0,
"NZ": 120.11,
"OM": 545.25,
"OM": 545.33,
"PA": 258.74,
"PE": 263.27,
"PF": 436.62,
@@ -162,10 +162,10 @@ class SWD
"QA": 602.83,
"RE": 525.22,
"RO": 245.55,
"RS": 673.16,
"RU": 449.2,
"RS": 670.8,
"RU": 446.17,
"RW": 301.89,
"SA": 696.31,
"SA": 691.95,
"SB": 636.36,
"SC": 571.43,
"SD": 214.33,
@@ -179,22 +179,22 @@ class SWD
"SR": 383.18,
"SS": 610.17,
"ST": 555.56,
"SV": 103.13,
"SV": 99.29,
"SY": 682.27,
"SZ": 142.86,
"TC": 653.85,
"TD": 615.39,
"TG": 478.26,
"TH": 554.5,
"TJ": 112.79,
"TH": 555.43,
"TJ": 98.7,
"TM": 1306.3,
"TN": 560.25,
"TO": 571.43,
"TR": 469.7,
"TT": 682.11,
"TW": 635.65,
"TW": 635.15,
"TZ": 371.59,
"UA": 256.21,
"UA": 250.47,
"UG": 57.39,
"US": 383.55,
"UY": 96.7,
@@ -203,12 +203,12 @@ class SWD
"VE": 180.25,
"VG": 647.06,
"VI": 641.79,
"VN": 471.16,
"VN": 486.13,
"VU": 500.0,
"WS": 400.0,
"XK": 958.72,
"YE": 586.32,
"ZA": 708.88,
"ZA": 713.48,
"ZM": 111.0,
"ZW": 298.44
}