implement maxmind geo-location for page connection info

This commit is contained in:
yggverse 2025-03-23 00:15:42 +02:00
parent c35db8c1ff
commit eb6da7a362
4 changed files with 67 additions and 20 deletions

View File

@ -36,6 +36,7 @@ ggemtext = "0.6.0"
indexmap = "2.7.0" indexmap = "2.7.0"
itertools = "0.14.0" itertools = "0.14.0"
libspelling = "0.3.0" libspelling = "0.3.0"
maxminddb = "0.25.0"
openssl = "0.10.70" openssl = "0.10.70"
plurify = "0.2.0" plurify = "0.2.0"
r2d2 = "0.8.10" r2d2 = "0.8.10"

View File

@ -105,7 +105,7 @@ impl Request {
if is_focused(e) { if is_focused(e) {
e.emit_activate() e.emit_activate()
} else { } else {
i.borrow().dialog(Some(e)); i.borrow().dialog(&p, Some(e));
} }
} }
_ => panic!(), _ => panic!(),

View File

@ -2,6 +2,7 @@ mod dialog;
mod event; mod event;
mod socket; mod socket;
use super::Profile;
use dialog::Dialog; use dialog::Dialog;
use event::Event; use event::Event;
use gtk::{gio::SocketAddress, prelude::IsA}; use gtk::{gio::SocketAddress, prelude::IsA};
@ -48,9 +49,9 @@ impl Info {
// Actions // Actions
pub fn dialog(&self, parent: Option<&impl IsA<gtk::Widget>>) { pub fn dialog(&self, profile: &Profile, parent: Option<&impl IsA<gtk::Widget>>) {
use adw::{PreferencesDialog, prelude::AdwDialogExt}; use adw::{PreferencesDialog, prelude::AdwDialogExt};
PreferencesDialog::info(self).present(parent) PreferencesDialog::info(self, profile).present(parent)
} }
/// Actualize `Self` /// Actualize `Self`

View File

@ -1,17 +1,16 @@
use super::Info; use super::{Info, Profile};
use adw::{ use adw::{
ActionRow, PreferencesDialog, PreferencesGroup, PreferencesPage, ActionRow, PreferencesDialog, PreferencesGroup, PreferencesPage,
prelude::{ActionRowExt, PreferencesDialogExt, PreferencesGroupExt, PreferencesPageExt}, prelude::{ActionRowExt, PreferencesDialogExt, PreferencesGroupExt, PreferencesPageExt},
}; };
use gtk::glib::gformat; use gtk::glib::gformat;
use sourceview::prelude::{SocketAddressExt, SocketConnectableExt};
pub trait Dialog { pub trait Dialog {
fn info(info: &Info) -> Self; fn info(this: &Info, profile: &Profile) -> Self;
} }
impl Dialog for PreferencesDialog { impl Dialog for PreferencesDialog {
fn info(info: &Info) -> Self { fn info(this: &Info, profile: &Profile) -> Self {
let d = PreferencesDialog::builder() let d = PreferencesDialog::builder()
.search_enabled(true) .search_enabled(true)
.title("Page info") .title("Page info")
@ -21,10 +20,10 @@ impl Dialog for PreferencesDialog {
.title("General") .title("General")
.icon_name("help-about-symbolic") .icon_name("help-about-symbolic")
.build(); .build();
if info.mime.is_some() { if this.mime.is_some() {
p.add(&{ p.add(&{
let g = PreferencesGroup::builder().title("Meta").build(); let g = PreferencesGroup::builder().title("Meta").build();
if let Some(ref mime) = info.mime { if let Some(ref mime) = this.mime {
g.add( g.add(
&ActionRow::builder() &ActionRow::builder()
.css_classes(["property"]) .css_classes(["property"])
@ -38,10 +37,10 @@ impl Dialog for PreferencesDialog {
g g
}); });
} // @TODO content language, header size, etc. } // @TODO content language, header size, etc.
if info.size.is_some() { if this.size.is_some() {
p.add(&{ p.add(&{
let g = PreferencesGroup::builder().title("Size").build(); let g = PreferencesGroup::builder().title("Size").build();
if let Some(ref size) = info.size { if let Some(ref size) = this.size {
g.add(&{ g.add(&{
use crate::tool::Format; use crate::tool::Format;
ActionRow::builder() ActionRow::builder()
@ -63,8 +62,10 @@ impl Dialog for PreferencesDialog {
.title("Connection") .title("Connection")
.icon_name("network-transmit-receive") .icon_name("network-transmit-receive")
.build(); .build();
if let Some(ref socket) = info.socket { if let Some(ref socket) = this.socket {
use gtk::gio::SocketFamily; use gtk::gio::{SocketAddress, SocketFamily};
use gtk::prelude::{SocketAddressExt, SocketConnectableExt};
/// Convert socket family to string
fn f2s(socket_family: &SocketFamily) -> &str { fn f2s(socket_family: &SocketFamily) -> &str {
match socket_family { match socket_family {
SocketFamily::Invalid => "Invalid", SocketFamily::Invalid => "Invalid",
@ -74,6 +75,7 @@ impl Dialog for PreferencesDialog {
_ => panic!(), _ => panic!(),
} }
} }
/// Build common `ActionRow` widget
fn r(title: &str, subtitle: &str) -> ActionRow { fn r(title: &str, subtitle: &str) -> ActionRow {
ActionRow::builder() ActionRow::builder()
.css_classes(["property"]) .css_classes(["property"])
@ -83,24 +85,67 @@ impl Dialog for PreferencesDialog {
.title(title) .title(title)
.build() .build()
} }
/// Lookup [MaxMind](https://www.maxmind.com) database
fn l(profile: &Profile, socket_address: &SocketAddress) -> Option<String> {
use maxminddb::{
MaxMindDBError, Reader,
geoip2::{/*City,*/ Country},
};
if !matches!(
socket_address.family(),
SocketFamily::Ipv4 | SocketFamily::Ipv6,
) {
return None;
}
let db = {
let mut c = profile.config_path.clone();
c.push("GeoLite2-Country.mmdb");
Reader::open_readfile(c)
}
.ok()?;
let lookup = {
let a: std::net::SocketAddr = socket_address.to_string().parse().unwrap();
let lookup: std::result::Result<Country, MaxMindDBError> =
db.lookup(a.ip());
lookup
}
.ok()?;
lookup.country.map(|c| {
let mut b = Vec::new();
if let Some(iso_code) = c.iso_code {
b.push(iso_code)
}
if let Some(n) = c.names {
if let Some(s) = n.get("en") {
b.push(s)
} // @TODO multi-lang
}
// @TODO city DB
b.join(", ")
})
}
p.add(&{ p.add(&{
let g = PreferencesGroup::builder().title("Remote").build(); let g = PreferencesGroup::builder().title("Remote").build();
g.add(&r("Address", &socket.remote_address.to_string())); g.add(&r("Address", &socket.remote_address.to_string()));
g.add(&r("Family", f2s(&socket.remote_address.family()))); g.add(&r("Family", f2s(&socket.remote_address.family())));
g.add(&r("Location", "-")); // @TODO optional, MaxMind DB if let Some(location) = l(profile, &socket.remote_address) {
g.add(&r("Location", &location));
}
g g
}); });
p.add(&{ p.add(&{
let g = PreferencesGroup::builder().title("Local").build(); let g = PreferencesGroup::builder().title("Local").build();
g.add(&r("Address", &socket.local_address.to_string())); g.add(&r("Address", &socket.local_address.to_string()));
g.add(&r("Family", f2s(&socket.local_address.family()))); g.add(&r("Family", f2s(&socket.local_address.family())));
g.add(&r("Location", "-")); // @TODO optional, MaxMind DB if let Some(location) = l(profile, &socket.local_address) {
g.add(&r("Location", &location));
}
g g
}); });
} }
p p
}); });
if info.redirect.is_some() { if this.redirect.is_some() {
d.add(&{ d.add(&{
let g = PreferencesGroup::new(); let g = PreferencesGroup::new();
let p = PreferencesPage::builder() let p = PreferencesPage::builder()
@ -119,7 +164,7 @@ impl Dialog for PreferencesDialog {
chain(b, r) chain(b, r)
} }
} }
chain(&mut b, info); chain(&mut b, this);
b.reverse(); b.reverse();
let l = b.len(); // calculate once let l = b.len(); // calculate once
let t = b[0].event[0].time(); // first event time to count from let t = b[0].event[0].time(); // first event time to count from
@ -165,7 +210,7 @@ impl Dialog for PreferencesDialog {
p p
}) // @TODO clickable navigation, test time values }) // @TODO clickable navigation, test time values
} }
if !info.event.is_empty() { if !this.event.is_empty() {
d.add(&{ d.add(&{
let p = PreferencesPage::builder() let p = PreferencesPage::builder()
.title("Events") .title("Events")
@ -173,7 +218,7 @@ impl Dialog for PreferencesDialog {
.build(); .build();
p.add(&{ p.add(&{
let g = PreferencesGroup::new(); let g = PreferencesGroup::new();
let e = &info.event[0]; let e = &this.event[0];
let t = e.time(); let t = e.time();
let n = e.name(); let n = e.name();
g.add( g.add(
@ -184,7 +229,7 @@ impl Dialog for PreferencesDialog {
.title(n) .title(n)
.build(), .build(),
); );
for e in &info.event[1..] { for e in &this.event[1..] {
g.add( g.add(
&ActionRow::builder() &ActionRow::builder()
.subtitle(gformat!( .subtitle(gformat!(