48 Commits

Author SHA1 Message Date
fauno 2bf97c1f71 v0.6.5 2025-11-16 12:15:16 -03:00
fauno 74928b740a doc: avoid repetition 2025-11-16 12:14:45 -03:00
fauno 9c5257b590 feat: limit_conn_status and limit_req_status 2025-11-16 12:14:35 -03:00
fauno b4e6a0100a v0.6.4 2025-11-07 11:40:57 -03:00
fauno b86f0318a3 feat: set empty values as nil 2025-11-07 11:40:57 -03:00
fauno 9fdf239422 feat: presence 2025-11-07 11:40:57 -03:00
fauno 01833e22ca v0.6.3 2025-11-07 10:30:16 -03:00
fauno 8a5f6cc260 fix: forgot the actual data 2025-11-07 10:30:04 -03:00
fauno 9452c5fde0 v0.6.2 2025-11-06 18:07:56 -03:00
fauno c72839287d fix: null responses 2025-11-06 18:07:32 -03:00
fauno 30f672d29c fix: don't fail if status tables are empty 2025-11-06 18:01:16 -03:00
fauno 31a5c1e796 feat: fetch metadata headers 2025-11-06 17:42:42 -03:00
fauno 378c10affc feat: block by owner 2025-11-04 18:05:41 -03:00
fauno 3e04ffd7f6 feat: retrieve asn owner 2025-11-04 18:05:23 -03:00
fauno b492755295 feat: block by owner 2025-11-04 18:04:10 -03:00
fauno a2c12edea9 feat: index asn ownership 2025-11-04 18:00:09 -03:00
fauno be75ab83f2 v0.6.1 2025-11-01 02:25:09 -03:00
fauno ccd54a93f0 ci: build ip2asn_server 2025-11-01 02:24:39 -03:00
fauno 81a7426e51 fix: update average intensity data 2025-11-01 02:23:01 -03:00
fauno ee40371272 feat: example ip2asn nginx config 2025-11-01 01:18:37 -03:00
fauno 838f9e970a fix: log not implemented command 2025-11-01 01:16:05 -03:00
fauno b1b0757e0d fix: don't cache status 2025-11-01 01:15:52 -03:00
fauno 4a062a4dae feat: asns can be blocked 2025-11-01 01:08:45 -03:00
fauno 857c39fdbb fix: rename 2025-11-01 00:33:15 -03:00
fauno 0326c60f90 feat: configurable 2025-11-01 00:32:50 -03:00
fauno f8a00dec1b refactor: reusable redis server 2025-11-01 00:24:55 -03:00
fauno ef975d1b98 feat: small redis server for nginx keyval 2025-10-31 23:29:29 -03:00
fauno d51d43d8a7 feat: http3 module 2025-10-30 18:48:31 -03:00
fauno b69e68b04b fix: tests 2025-07-24 12:34:57 -03:00
fauno cfdf9083ef v0.6.0 2025-07-24 12:29:15 -03:00
fauno 8c6c0e6aa2 fix: update intensity data
https://github.com/thegreenwebfoundation/co2.js/commit/b0a65fe073c8a4d7fc2adbafb1844e4cae3ee6c2
2025-07-24 11:41:40 -03:00
fauno 8440ba4cf8 feat: asn database 2025-07-24 11:35:05 -03:00
fauno 28f91eeecb fix: remove socket before start 2025-07-05 19:13:30 -03:00
fauno 61652d1e85 refactor: socket removal 2025-07-05 19:13:11 -03:00
fauno 4218f8b5bf fix: don't recalculate reading buffer 2025-07-05 18:42:03 -03:00
fauno 1aae89ed08 fix: match the internal version 2025-07-05 18:41:37 -03:00
fauno dae670f2b5 v0.5.12 2025-06-14 12:58:52 -03:00
fauno 413a4746cd feat: upgrade average intensity to 0.16.8 2025-06-14 12:58:33 -03:00
fauno 11cb80cf7c v0.5.11 2025-01-20 10:02:05 -03:00
fauno f984f6d47c feat: upgrade average intensity to 0.16.5 2025-01-20 10:01:24 -03:00
fauno d2f4907fcd v0.5.10 2024-12-10 10:19:17 -03:00
fauno c1ec450c33 feat: upgrade intensity data to v0.16.4 2024-12-10 10:18:42 -03:00
fauno 85dd3a58fd v0.5.9 2024-11-12 09:44:28 -03:00
fauno 83b3357e7b feat: 0.16.2 intensity data 2024-11-12 09:44:08 -03:00
fauno 6cc63614ea fix: don't fail if the intensity is integer 2024-11-12 09:43:09 -03:00
fauno befa954635 doc: how to update the data 2024-11-12 09:42:59 -03:00
fauno ebbae56efa v0.5.8 2024-03-21 12:03:16 -03:00
fauno 8e7053d756 fix: update average intensity data to match co2.js v0.14.4 2024-03-21 12:00:09 -03:00
19 changed files with 636 additions and 318 deletions
+3 -1
View File
@@ -25,9 +25,11 @@ build:
- "apk add sqlite-static"
- "shards install"
- "crystal build --release --static src/access_log.cr"
- "strip --strip-all access_log"
- "crystal build --release --static src/ip2asn_server.cr"
- "strip --strip-all access_log ip2asn_server"
- "install -Dm 644 LICENSE ${RELEASE_DIRECTORY}/LICENSE"
- "install -Dm 755 access_log ${RELEASE_DIRECTORY}/access_log"
- "install -Dm 755 ip2asn_server ${RELEASE_DIRECTORY}/ip2asn_server"
- "tar -cf ${RELEASE_TARBALL} ${RELEASE_DIRECTORY}"
artifacts:
paths:
+30 -52
View File
@@ -43,6 +43,22 @@ most countries.
`access_log` uses marginal data by default.
### Update the data
1. Go to the `data/output` directory on [co2.js
repository](https://github.com/thegreenwebfoundation/co2.js/tree/v0.16.2/data/output)
for the latest version released (in this example 0.16.2)
2. Download the `average-intensities.json` file.
3. Run `src/average_intensities_by_country.cr` with this file as stdin.
```
crystal run src/average_intensities_by_country.cr < average-intensities.json >> src/swd/average_intensity.cr
```
4. Modify `src/swd/average_intensity.cr` to fix the data.
## Create database
```bash
@@ -70,58 +86,8 @@ make alpine-build
## Database configuration
Create an `access_logs` database with the following schema:
| Field | Type | Reference | Index? |
| ----- | ---- | --------- | ------ |
| id | String | UUID | Unique |
| host | String | Host name | Yes |
| msec | Float | Unix timestamp of visit | ? |
| server_protocol | String | HTTP/Version | ? |
| request_method | String | GET/POST/etc. | ? |
| request_completion | String | "OK" | ? |
| uri | String | Request | True |
| query_string | String | Arguments | ? |
| status | Integer | HTTP status | ? |
| sent_http_content_type | String | MIME type of response | ? |
| sent_http_content_encoding | String | Compression | ? |
| sent_http_etag | String | ETag header | ? |
| sent_http_last_modified | String | Last modified date | ? |
| http_accept | String | MIME types requested | ? |
| http_accept_encoding | String | Compression accepted | ? |
| http_accept_language | String | Languages supported | ? |
| http_pragma | String | Pragma header | ? |
| http_cache_control | String | Cache requested | ? |
| http_if_none_match | String | ETag requested | ? |
| http_dnt | String | Do Not Track header | ? |
| http_user_agent | String | User Agent | Yes |
| http_origin | String | Request origin | Yes |
| http_referer | String | Referer (see Referrer Policy) | Yes |
| request_time | Float | Request duration | ? |
| bytes_sent | Integer | Bytes sent | ? |
| body_bytes_sent | Integer | Bytes sent not including headers | ? |
| request_length | Integer | Headers | ? |
| http_connection | String | Connection status | ? |
| pipe | String | Connection was multiplexed | ? |
| connection_requests | Integer | Requests done on the same connection | ? |
| geoip2_data_country_name | String | Country according to GeoIP | Yes |
| geoip2_data_city_name | String | City according to GeoIP | Yes |
| ssl_server_name | String | SNI | ? |
| ssl_protocol | String | SSL/TLS version used | ? |
| ssl_early_data | String | TLSv1.3 early data used | ? |
| ssl_session_reused | String | TLS session reused | ? |
| ssl_curves | String | Curves used | ? |
| ssl_ciphers | String | Ciphers available | ? |
| ssl_cipher | String | Cipher used | ? |
| sent_http_x_xss_protection | String | XSS Protection sent | ? |
| sent_http_x_frame_options | String | Frame protection sent | ? |
| sent_http_x_content_type_options | String | Content protection sent | ? |
| sent_http_strict_transport_security | String | HSTS sent | ? |
| nginx_version | String | Server version | ? |
| pid | Integer | Server PID | ? |
| crawler | Boolean | Web crawler detected | ? |
| remote_user | String | HTTP Basic auth user | ? |
Create an `access_logs` database with an schema similar to
[create.sql](contrib/schema.sql)
## Nginx configuration
@@ -179,6 +145,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
+12
View File
@@ -0,0 +1,12 @@
BEGIN;
CREATE TABLE IF NOT EXISTS asn_import (range_start INTEGER, range_end INTEGER, id INTEGER, country STRING, description TEXT);
CREATE TABLE IF NOT EXISTS asns (range_start INTEGER, range_end INTEGER, id INTEGER, country STRING, owner_id INTEGER);
CREATE UNIQUE INDEX IF NOT EXISTS asn_import_range_id ON asn_import (range_start, range_end);
CREATE UNIQUE INDEX IF NOT EXISTS asns_range_id ON asns (range_start, range_end);
CREATE INDEX IF NOT EXISTS asns_id_idx ON asns (id);
CREATE INDEX IF NOT EXISTS asns_owner_id_idx ON asns (owner_id);
CREATE TABLE IF NOT EXISTS owners (id INTEGER PRIMARY KEY, description TEXT);
CREATE TABLE IF NOT EXISTS status (id INTEGER PRIMARY KEY, blocked BOOLEAN DEFAULT FALSE);
CREATE TABLE IF NOT EXISTS owner_status (id INTEGER PRIMARY KEY, blocked BOOLEAN DEFAULT FALSE);
DELETE FROM asn_import;
COMMIT;
+12
View File
@@ -0,0 +1,12 @@
BEGIN;
INSERT INTO owners (description) SELECT DISTINCT asn_import.description FROM asn_import LEFT JOIN owners ON asn_import.description = owners.description WHERE owners.description IS NULL;
INSERT INTO asns (range_start, range_end, id, country, owner_id)
SELECT asn_import.range_start, asn_import.range_end, asn_import.id, asn_import.country, owners.id
FROM asn_import
LEFT JOIN owners
ON owners.description = asn_import.description
ON CONFLICT DO UPDATE SET id = excluded.id, country = excluded.country, owner_id = excluded.owner_id;
DROP TABLE asn_import;
DROP INDEX asns_range_id;
COMMIT;
VACUUM;
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -e
cr="$(dirname "$(readlink -f "$0")")"
db="${1:-$PWD/asn.sqlite3}"
cat "$cr/asn.sql" | sqlite3 "$db"
wget https://iptoasn.com/data/ip2asn-v4-u32.tsv.gz -O - | gunzip | sqlite3 -tabs "$db" ".import '|cat -' asn_import"
cat "$cr/asn_2.sql" | sqlite3 "$db"
echo "ASN database created/updated at $db"
+11 -1
View File
@@ -53,7 +53,17 @@ 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,
"http3" varchar(2) DEFAULT NULL,
"http_sec_fetch_mode" varchar DEFAULT NULL,
"http_sec_fetch_dest" varchar DEFAULT NULL,
"http_sec_fetch_site" varchar DEFAULT NULL,
"http_sec_fetch_user" varchar DEFAULT NULL,
"http_sec_purpose" varchar DEFAULT NULL,
"limit_req_status" varchar DEFAULT NULL,
"limit_conn_status" varchar DEFAULT NULL
);
CREATE INDEX IF NOT EXISTS "index_access_logs_on_host" ON "access_logs" ("host");
+19
View File
@@ -0,0 +1,19 @@
keyval_zone_redis zone=ip2asn hostname=::1;
keyval "$remote_addr:id" $asn zone=ip2asn;
keyval "$remote_addr:blocked" $asn_blocked zone=ip2asn;
limit_conn_zone $asn zone=asn_conn:10m;
limit_req_zone $asn zone=asn_req:10m rate=10r/s;
server {
server_name _;
listen 80 default_server;
if ($asn_blocked) {
return 444;
}
limit_conn asn_conn 1;
limit_req zone=asn_req nodelay;
}
+4
View File
@@ -1,5 +1,9 @@
version: 2.0
shards:
blank:
git: https://github.com/kostya/blank.git
version: 0.2.0
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.11.0
+3 -1
View File
@@ -1,5 +1,5 @@
name: "access_log"
version: "0.5.6"
version: "0.6.5"
authors:
- "f <f@sutty.nl>"
targets:
@@ -12,3 +12,5 @@ dependencies:
github: "will/crystal-pg"
sqlite3:
github: "crystal-lang/crystal-sqlite3"
blank:
github: "kostya/blank"
+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
+85 -51
View File
@@ -10,8 +10,10 @@ require "system"
require "./models/access_log"
require "./models/crawler"
require "./swd"
require "./asn"
require "./presence"
VERSION = "0.5.7"
VERSION = "0.6.5"
Log.setup_from_env
@@ -19,13 +21,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 http3 http_sec_fetch_mode http_sec_fetch_dest http_sec_fetch_site http_sec_fetch_user http_sec_purpose limit_req_status limit_conn_status]
# Params for the query
params = [] of String
# SWD
@@ -56,6 +60,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 +118,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,72 +160,83 @@ 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
# AccessLog as a NamedTuple.
db.exec query,
UUID.random.to_s,
access_log.remote_user,
access_log.host,
access_log.msec,
access_log.server_protocol,
access_log.request_method,
access_log.request_completion,
access_log.uri,
access_log.request_uri,
access_log.query_string,
access_log.remote_user.presence,
access_log.host.presence,
access_log.msec.presence,
access_log.server_protocol.presence,
access_log.request_method.presence,
access_log.request_completion.presence,
access_log.uri.presence,
access_log.request_uri.presence,
access_log.query_string.presence,
access_log.status,
access_log.sent_http_content_type,
access_log.sent_http_content_encoding,
access_log.sent_http_etag,
access_log.sent_http_last_modified,
access_log.http_accept,
access_log.http_accept_encoding,
access_log.http_accept_language,
access_log.http_pragma,
access_log.http_cache_control,
access_log.http_if_none_match,
access_log.http_dnt,
access_log.http_user_agent,
access_log.http_origin,
access_log.http_referer,
access_log.sent_http_content_type.presence,
access_log.sent_http_content_encoding.presence,
access_log.sent_http_etag.presence,
access_log.sent_http_last_modified.presence,
access_log.http_accept.presence,
access_log.http_accept_encoding.presence,
access_log.http_accept_language.presence,
access_log.http_pragma.presence,
access_log.http_cache_control.presence,
access_log.http_if_none_match.presence,
access_log.http_dnt.presence,
access_log.http_user_agent.presence,
access_log.http_origin.presence,
access_log.http_referer.presence,
access_log.request_time,
access_log.bytes_sent,
access_log.body_bytes_sent,
access_log.request_length,
access_log.http_connection,
access_log.pipe,
access_log.http_connection.presence,
access_log.pipe.presence,
access_log.connection_requests,
access_log.geoip2_data_country_name,
access_log.geoip2_data_city_name,
access_log.ssl_server_name,
access_log.ssl_protocol,
access_log.ssl_early_data,
access_log.ssl_session_reused,
access_log.ssl_curves,
access_log.ssl_ciphers,
access_log.ssl_cipher,
access_log.sent_http_x_xss_protection,
access_log.sent_http_x_frame_options,
access_log.sent_http_x_content_type_options,
access_log.sent_http_strict_transport_security,
access_log.nginx_version,
access_log.pid,
access_log.geoip2_data_country_name.presence,
access_log.geoip2_data_city_name.presence,
access_log.ssl_server_name.presence,
access_log.ssl_protocol.presence,
access_log.ssl_early_data.presence,
access_log.ssl_session_reused.presence,
access_log.ssl_curves.presence,
access_log.ssl_ciphers.presence,
access_log.ssl_cipher.presence,
access_log.sent_http_x_xss_protection.presence,
access_log.sent_http_x_frame_options.presence,
access_log.sent_http_x_content_type_options.presence,
access_log.sent_http_strict_transport_security.presence,
access_log.nginx_version.presence,
access_log.pid.presence,
(crawler ? !!(crawler_re =~ access_log.http_user_agent) : false),
(swd ? s.try(&.datacenter_co2) : nil),
(swd ? s.try(&.network_co2) : nil),
(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),
access_log.http3.presence,
access_log.http_sec_fetch_mode.presence,
access_log.http_sec_fetch_dest.presence,
access_log.http_sec_fetch_site.presence,
access_log.http_sec_fetch_user.presence,
access_log.http_sec_purpose.presence,
access_log.limit_req_status.presence,
access_log.limit_conn_status.presence
# 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
+55
View File
@@ -0,0 +1,55 @@
require "db"
require "sqlite3"
class AsnResult
include DB::Serializable
property id : Int64?
property country : String?
# TODO: Deprecate?
property description : String?
property owner_id : Int64?
property blocked : Bool?
end
class ASN
QUERY = "select asns.id, country, owners.description, owner_id from asns left join owners on owners.id = asns.owner_id where ? between range_start and range_end limit 1;"
STATUS = "select blocked from status where id = ? limit 1;"
OWNER_STATUS = "select blocked from owner_status where id = ? 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))).tap do |result|
result.blocked = rescue_query { @db.query_one(STATUS, result.id, &.read(Bool)) }
result.blocked ||= rescue_query { @db.query_one(OWNER_STATUS, result.owner_id, &.read(Bool)) }
end
rescue DB::NoResultsError
nil
end
def close
@db.close
end
def rescue_query(&)
yield
rescue DB::NoResultsError
nil
end
end
+6 -1
View File
@@ -23,7 +23,12 @@ JSON.parse(ARGF.gets_to_end).as_h.each do |country, data|
STDERR.puts "ok - #{country}"
average_intensities[countries[data["country_code"].as_s]] = data["emissions_intensity_gco2_per_kwh"].as_f
average_intensities[countries[data["country_code"].as_s]] =
begin
data["emissions_intensity_gco2_per_kwh"].as_f
rescue TypeCastError
data["emissions_intensity_gco2_per_kwh"].as_i.to_f
end
end
puts average_intensities.to_json
+93
View File
@@ -0,0 +1,93 @@
# This is a very minimal Redis server that works with Nginx keyval
# module. When set like this:
#
# keyval_zone_redis zone=redis hostname=::1;
# keyval "$remote_addr:id" $asn_id zone=redis;
#
# We can query the ASN id from a database and use it on Nginx config to
# rate limit entire subnets.
require "log"
require "option_parser"
require "./asn"
require "./redis_server"
Log.setup_from_env
VERSION = "0.2.0"
ASN_ID = "id"
ASN_DESCRIPTION = "description"
ASN_OWNER_ID = "owner_id"
ASN_COUNTRY = "country"
ASN_BLOCKED = "blocked"
asn_database = "sqlite3://./asn.sqlite3"
interface = "localhost"
port = 6379
OptionParser.parse do |p|
p.banner = "IP to ASN Redis server"
p.on "-v", "--version", "Show version" do
puts VERSION
exit
end
p.on "-h", "--help", "Show help" do
puts p
exit
end
p.on "-d #{asn_database}", "--asn-database=#{asn_database}", "ASN Database URI" do |d|
asn_database = d
end
p.on "-i #{interface}", "--interface=#{interface}", "Listening interface" do |i|
interface = i
end
p.on "-p #{port}", "--port=#{port}", "Listening port" do |p|
port = p.to_i
end
end
asn = ASN.new(asn_database)
# The client can be a long lived connection sending commands. We only
# answer to GET commands.
RedisServer.new(interface, port) do |client, _, key|
begin
_, ip, field = key.split(":", 3)
rescue
Log.warn &.emit("Error parsing key", key: key)
next
end
# This is cached in memory
result = asn.query(ip)
next if result.nil?
# XXX: Are we going to use the other fields?
response =
case field
when ASN_ID then result.try(&.id).try(&.to_s)
when ASN_OWNER_ID then result.try(&.owner_id).try(&.to_s)
when ASN_COUNTRY then result.try(&.country)
when ASN_DESCRIPTION then result.try(&.description)
when ASN_BLOCKED then
if result.try(&.blocked)
"1"
else
"0"
end
else nil
end
if response.nil?
RedisServer.null(client)
else
RedisServer.bulk_string(client, response)
end
end
asn.close
+9
View File
@@ -58,4 +58,13 @@ class AccessLog
property sent_http_strict_transport_security : String
property nginx_version : String
property pid : String
property remote_addr : String?
property http3 : String?
property http_sec_fetch_mode : String?
property http_sec_fetch_dest : String?
property http_sec_fetch_site : String?
property http_sec_fetch_user : String?
property http_sec_purpose : String?
property limit_req_status : String?
property limit_conn_status : String?
end
+9
View File
@@ -0,0 +1,9 @@
require "blank"
class Object
def presence
return nil if blank?
self
end
end
+64
View File
@@ -0,0 +1,64 @@
require "log"
require "socket"
class RedisServer
BULK_STRING = "$"
CRLF = "\r\n"
NOT_IMPLEMENTED = "-Not implemented#{CRLF}"
QUIT_COMMAND = "quit"
GET_COMMAND = "get"
NULL = "_#{CRLF}"
property server : TCPServer
def initialize(interface : String = "localhost", port : Int = 6379, &block : TCPSocket, String, String -> _)
@server = TCPServer.new(interface, port)
while (client = server.accept?)
spawn handle_client(client, block)
end
ensure
server.close
end
def handle_client(client : TCPSocket, block : Proc)
next_is_key = false
last_command = ""
while (line = client.gets)
# We're not a compliant RESP
next if line.starts_with?("*")
next if line.starts_with?("$")
# Execute the query when we get the key
if next_is_key
next_is_key = false
# Do something with the key
block.call(client, last_command, line)
# First we get the Redis command and we only support a small subset.
else
case (last_command = line.downcase)
when QUIT_COMMAND then client.close
when GET_COMMAND then next_is_key = true
else
client.print NOT_IMPLEMENTED
raise last_command
end
end
end
rescue ex
Log.warn &.emit("Error", exception: ex.class.name, error: ex.message)
ensure
client.close
end
def self.bulk_string(client : TCPSocket, string : String)
client.print(BULK_STRING, string.size, CRLF, string, CRLF)
end
def self.null(client : TCPSocket)
client.print(NULL)
end
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
+205 -205
View File
@@ -2,215 +2,215 @@ class SWD
module IntensityData
# @see {co2.js/data/output/average-intensities.json}
AVERAGE_INTENSITY_BY_ISO_CODE = {
"AE": 561.14,
"AF": 120.48,
"AG": 657.14,
"AL": 23.44,
"AM": 280.06,
"AO": 212.42,
"AR": 399.5,
"AS": 687.5,
"AT": 141.79,
"AU": 570.35,
"AW": 591.4,
"AZ": 719.39,
"BA": 670.09,
"BB": 644.86,
"BD": 719.78,
"BE": 139.79,
"BF": 611.43,
"BG": 476.9,
"BH": 956.55,
"BI": 250.0,
"BJ": 666.67,
"BN": 961.54,
"BO": 497.78,
"BR": 114.48,
"BS": 698.11,
"BT": 24.44,
"BW": 867.58,
"BY": 465.34,
"BZ": 484.38,
"CA": 163.01,
"CD": 25.36,
"AE": 467.51,
"AF": 123.71,
"AG": 611.11,
"AL": 24.42,
"AM": 243.52,
"AO": 167.22,
"AR": 344.83,
"AS": 647.06,
"AT": 102.62,
"AU": 553.76,
"AW": 550.0,
"AZ": 632.89,
"BA": 638.05,
"BB": 600.0,
"BD": 694.63,
"BE": 117.58,
"BF": 554.91,
"BG": 264.21,
"BH": 902.41,
"BI": 230.77,
"BJ": 590.0,
"BN": 892.67,
"BO": 468.02,
"BR": 106.06,
"BS": 653.66,
"BT": 24.19,
"BW": 849.42,
"BY": 323.63,
"BZ": 155.56,
"CA": 184.99,
"CD": 27.04,
"CF": 0.0,
"CG": 723.88,
"CH": 36.14,
"CI": 453.55,
"CK": 400.0,
"CL": 353.52,
"CM": 278.26,
"CN": 585.82,
"CO": 198.53,
"CR": 37.21,
"CU": 677.47,
"CV": 600.0,
"CY": 555.13,
"CZ": 488.3,
"DE": 420.06,
"DJ": 666.67,
"DK": 201.78,
"DM": 529.41,
"DO": 592.49,
"DZ": 672.64,
"EC": 192.39,
"EE": 489.89,
"EG": 612.16,
"ER": 688.89,
"ES": 217.77,
"ET": 25.19,
"FI": 130.42,
"FJ": 289.47,
"FK": 500.0,
"FO": 428.57,
"FR": 78.81,
"GA": 397.38,
"GB": 255.85,
"GD": 714.29,
"GE": 176.97,
"GF": 254.72,
"GH": 498.81,
"GL": 133.33,
"GM": 700.0,
"GN": 208.63,
"GP": 647.06,
"GQ": 492.96,
"GR": 378.18,
"GT": 331.72,
"GU": 670.33,
"GW": 750.0,
"GY": 642.28,
"HK": 733.05,
"HN": 378.97,
"HR": 242.28,
"HT": 606.06,
"HU": 228.75,
"ID": 675.93,
"IE": 333.23,
"IL": 641.17,
"IN": 705.13,
"IQ": 708.1,
"IR": 665.15,
"IS": 28.56,
"IT": 378.45,
"JM": 588.51,
"JO": 572.74,
"JP": 512.81,
"KE": 101.13,
"KG": 124.48,
"KH": 436.13,
"KI": 666.67,
"KM": 714.29,
"KN": 681.82,
"KP": 124.57,
"KR": 470.4,
"KW": 691.31,
"KY": 684.93,
"KZ": 830.41,
"LA": 284.21,
"LB": 663.1,
"LC": 685.71,
"LK": 543.12,
"LR": 304.35,
"LS": 20.0,
"LT": 177.18,
"LU": 144.07,
"LV": 136.27,
"LY": 861.69,
"MA": 705.67,
"MD": 871.66,
"ME": 483.38,
"MG": 497.61,
"MK": 639.86,
"ML": 463.13,
"MM": 575.77,
"MN": 852.82,
"MO": 627.12,
"MQ": 698.63,
"MR": 526.6,
"CG": 713.73,
"CH": 36.72,
"CI": 393.53,
"CK": 250.0,
"CL": 265.52,
"CM": 285.71,
"CN": 557.5,
"CO": 285.8,
"CR": 63.01,
"CU": 638.98,
"CV": 480.0,
"CY": 512.24,
"CZ": 413.86,
"DE": 342.06,
"DJ": 450.0,
"DK": 143.3,
"DM": 600.0,
"DO": 565.97,
"DZ": 633.65,
"EC": 209.7,
"EE": 341.02,
"EG": 574.5,
"ER": 590.91,
"ES": 146.15,
"ET": 23.55,
"FI": 72.25,
"FJ": 278.26,
"FK": 1000.0,
"FO": 354.17,
"FR": 44.18,
"GA": 429.47,
"GB": 215.79,
"GD": 666.67,
"GE": 143.06,
"GF": 204.08,
"GH": 452.86,
"GL": 111.11,
"GM": 666.67,
"GN": 182.72,
"GP": 493.9,
"GQ": 605.1,
"GR": 319.76,
"GT": 272.66,
"GU": 611.11,
"GW": 625.0,
"GY": 634.33,
"HK": 681.99,
"HN": 289.5,
"HR": 174.48,
"HT": 534.65,
"HU": 182.82,
"ID": 682.43,
"IE": 279.79,
"IL": 567.26,
"IN": 708.96,
"IQ": 689.4,
"IR": 648.68,
"IS": 28.33,
"IT": 287.75,
"JM": 561.25,
"JO": 539.21,
"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": 415.65,
"KW": 637.24,
"KY": 642.86,
"KZ": 801.79,
"LA": 232.12,
"LB": 369.47,
"LC": 650.0,
"LK": 509.78,
"LR": 435.9,
"LS": 20.83,
"LT": 139.34,
"LU": 134.62,
"LV": 136.22,
"LY": 830.53,
"MA": 577.65,
"MD": 629.56,
"ME": 413.51,
"MG": 477.27,
"MK": 568.97,
"ML": 394.5,
"MM": 569.69,
"MN": 784.01,
"MO": 448.98,
"MQ": 516.78,
"MR": 481.71,
"MS": 1000.0,
"MT": 500.0,
"MU": 631.94,
"MV": 651.52,
"MW": 133.8,
"MX": 515.62,
"MY": 605.83,
"MZ": 150.25,
"NA": 70.06,
"NC": 610.12,
"NE": 622.22,
"NG": 513.24,
"NI": 352.05,
"NL": 324.99,
"NO": 26.5,
"NP": 24.51,
"MT": 484.16,
"MU": 633.03,
"MV": 611.77,
"MW": 54.65,
"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": 252.7,
"NO": 30.75,
"NP": 23.36,
"NR": 750.0,
"NZ": 117.46,
"OM": 598.27,
"PA": 175.0,
"PE": 312.59,
"PF": 471.43,
"PG": 526.75,
"PH": 641.96,
"PK": 435.89,
"PL": 733.18,
"PM": 800.0,
"PR": 719.85,
"PS": 511.63,
"PT": 225.19,
"PY": 25.49,
"QA": 642.01,
"RE": 519.03,
"RO": 297.94,
"RS": 695.69,
"RU": 451.95,
"RW": 294.12,
"SA": 706.79,
"SB": 727.27,
"SC": 615.39,
"SD": 288.13,
"SE": 41.07,
"SG": 505.22,
"SI": 257.49,
"SK": 142.59,
"NZ": 120.11,
"OM": 545.33,
"PA": 258.74,
"PE": 263.27,
"PF": 436.62,
"PG": 513.74,
"PH": 613.38,
"PK": 398.61,
"PL": 614.98,
"PM": 600.0,
"PR": 664.53,
"PS": 460.78,
"PT": 111.8,
"PY": 24.86,
"QA": 602.83,
"RE": 525.22,
"RO": 245.55,
"RS": 670.8,
"RU": 446.17,
"RW": 301.89,
"SA": 691.95,
"SB": 636.36,
"SC": 571.43,
"SD": 214.33,
"SE": 35.89,
"SG": 498.74,
"SI": 227.11,
"SK": 96.49,
"SL": 47.62,
"SN": 685.05,
"SO": 634.15,
"SR": 356.44,
"SS": 684.21,
"ST": 600.0,
"SV": 211.29,
"SY": 758.35,
"SZ": 189.19,
"TC": 703.7,
"TD": 677.42,
"TG": 492.06,
"TH": 599.02,
"TJ": 99.34,
"TM": 1389.72,
"TN": 592.21,
"TO": 625.0,
"TR": 453.01,
"TT": 723.94,
"TW": 678.87,
"TZ": 374.08,
"UA": 271.41,
"UG": 52.27,
"US": 410.37,
"UY": 149.49,
"UZ": 1176.69,
"VC": 500.0,
"VE": 298.5,
"VG": 714.29,
"VI": 685.71,
"VN": 409.8,
"VU": 571.43,
"WS": 470.59,
"XK": 945.01,
"YE": 613.64,
"ZA": 762.46,
"ZM": 86.39,
"ZW": 433.38
"SN": 535.4,
"SO": 523.81,
"SR": 383.18,
"SS": 610.17,
"ST": 555.56,
"SV": 99.29,
"SY": 682.27,
"SZ": 142.86,
"TC": 653.85,
"TD": 615.39,
"TG": 478.26,
"TH": 555.43,
"TJ": 98.7,
"TM": 1306.3,
"TN": 560.25,
"TO": 571.43,
"TR": 469.7,
"TT": 682.11,
"TW": 635.15,
"TZ": 371.59,
"UA": 250.47,
"UG": 57.39,
"US": 383.55,
"UY": 96.7,
"UZ": 1121.18,
"VC": 600.0,
"VE": 180.25,
"VG": 647.06,
"VI": 641.79,
"VN": 486.13,
"VU": 500.0,
"WS": 400.0,
"XK": 958.72,
"YE": 586.32,
"ZA": 713.9,
"ZM": 111.0,
"ZW": 298.44
}
# @see {co2.js/data/output/marginal-intensities-2021.json}