]> jfr.im git - irc/quakenet/newserv.git/blob - geoip/geoip.c
Merge pull request #132 from retropc/lua_country
[irc/quakenet/newserv.git] / geoip / geoip.c
1 /*
2 Geoip module
3 Copyright (C) 2004-2023 Chris Porter.
4 */
5
6 #include "../nick/nick.h"
7 #include "../core/error.h"
8 #include "../core/config.h"
9 #include "../core/hooks.h"
10 #include "../control/control.h"
11 #include "../lib/version.h"
12
13 #include <string.h>
14
15 #include <maxminddb.h>
16 #include "geoip.h"
17
18 MODULE_VERSION("");
19
20 #define COUNTRY_MAX (26 * 26 + 1)
21 #define UNKNOWN_COUNTRY (COUNTRY_MAX - 1)
22 #define COUNTRY_NAME_LEN 40
23
24 struct country {
25 int total;
26 char code[3];
27 char name[COUNTRY_NAME_LEN + 1];
28 };
29
30 static struct country countries[COUNTRY_MAX] = { [UNKNOWN_COUNTRY] = { 0, "??", "Unknown" } };
31 static struct country *unknown_country = &countries[UNKNOWN_COUNTRY];
32
33 static int nickext = -1;
34 static MMDB_s db;
35
36 static struct country *lookup_country(struct sockaddr *addr) {
37 int mmdb_error;
38 MMDB_lookup_result_s result = MMDB_lookup_sockaddr(&db, addr, &mmdb_error);
39 if (mmdb_error != MMDB_SUCCESS || !result.found_entry) {
40 return NULL;
41 }
42
43 MMDB_entry_data_s entry_data;
44 int status = MMDB_get_value(&result.entry, &entry_data, "country", "iso_code", NULL);
45 if (status != MMDB_SUCCESS || !entry_data.has_data || entry_data.type != MMDB_DATA_TYPE_UTF8_STRING || entry_data.data_size != 2) {
46 return NULL;
47 }
48
49 /* not null terminated, sad */
50 const char *code_u = entry_data.utf8_string;
51 const char code[3] = { code_u[0], code_u[1], '\0' };
52
53 struct country *c = geoip_lookup_code(code);
54 if (!c) {
55 return NULL;
56 }
57
58 if (c->code[0] == '\0') {
59 status = MMDB_get_value(&result.entry, &entry_data, "country", "names", "en", NULL);
60 if (status != MMDB_SUCCESS || !entry_data.has_data || entry_data.type != MMDB_DATA_TYPE_UTF8_STRING) {
61 return NULL;
62 }
63
64 /* not null terminated, sad */
65 const char *name = entry_data.utf8_string;
66 size_t name_len = entry_data.data_size;
67 if (name_len > COUNTRY_NAME_LEN) {
68 name_len = COUNTRY_NAME_LEN;
69 }
70
71 memcpy(c->code, code, 3);
72 memcpy(c->name, name, name_len);
73 c->name[name_len] = '\0';
74 }
75
76 return c;
77 }
78
79 static void nick_setup(nick *np) {
80 union {
81 struct sockaddr_in sin;
82 struct sockaddr_in6 sin6;
83 } u = {};
84
85 struct irc_in_addr *ip = &np->ipaddress;
86 if (irc_in_addr_is_ipv4(ip)) {
87 u.sin.sin_family = AF_INET;
88 u.sin.sin_addr.s_addr = htonl(irc_in_addr_v4_to_int(ip));
89 } else {
90 u.sin6.sin6_family = AF_INET6;
91 memcpy(&u.sin6.sin6_addr.s6_addr, ip->in6_16, sizeof(ip->in6_16));
92 }
93
94 struct country *c = lookup_country((struct sockaddr *)&u);
95 if (!c) {
96 c = unknown_country;
97 }
98
99 c->total++;
100 np->exts[nickext] = c;
101 }
102
103 static void nick_new(int hook, void *args) {
104 nick_setup(args);
105 }
106
107 static void nick_lost(int hook, void *args) {
108 nick *np = args;
109
110 struct country *c = geoip_lookup_nick(np);
111 if (!c) {
112 return;
113 }
114
115 c->total--;
116 }
117
118 static void whois_handler(int hooknum, void *arg) {
119 nick *np = arg;
120 if (!np) {
121 return;
122 }
123
124 struct country *c = geoip_lookup_nick(np);
125 if (!c) {
126 return;
127 }
128
129 char buf[512];
130 snprintf(buf, sizeof(buf), "Country : %s (%s)", geoip_code(c), geoip_name(c));
131 triggerhook(HOOK_CONTROL_WHOISREPLY, buf);
132 }
133
134 struct country *geoip_lookup_code(const char *code) {
135 if (code[0] < 'A' || code[0] > 'Z' || code[1] < 'A' || code[1] > 'Z' || code[2] != '\0') {
136 if (!strcmp("??", code)) {
137 return unknown_country;
138 }
139
140 return NULL;
141 }
142
143 return &countries[(code[0] - 'A') * 26 + (code[1] - 'A')];
144 }
145
146 const char *geoip_code(struct country *c) {
147 return c->code;
148 }
149
150 const char *geoip_name(struct country *c) {
151 return c->name;
152 }
153
154 int geoip_total(struct country *c) {
155 return c->total;
156 }
157
158 struct country *geoip_next(struct country *c) {
159 int pos;
160 if (c == NULL) {
161 pos = 0;
162 } else {
163 pos = c - countries + 1;
164 }
165
166 for (; pos < COUNTRY_MAX; pos++) {
167 c = &countries[pos];
168 if (!c->total) {
169 continue;
170 }
171
172 return c;
173 }
174
175 return NULL;
176 }
177
178 struct country *geoip_lookup_nick(nick *np) {
179 if (nickext == -1) {
180 return NULL;
181 }
182
183 return np->exts[nickext];
184 }
185
186 void _init(void) {
187 nickext = registernickext("geoip");
188 if (nickext == -1) {
189 return;
190 }
191
192 sstring *filename = getcopyconfigitem("geoip", "db", "GeoLite2-Country.mmdb", 256);
193 int result = MMDB_open(filename->content, MMDB_MODE_MMAP, &db);
194 freesstring(filename);
195 if (result != MMDB_SUCCESS) {
196 Error("geoip", ERR_WARNING, "Unable to load geoip database [filename: %s] [code: %d]", filename->content, result);
197 return;
198 }
199
200 for (int i = 0; i < NICKHASHSIZE; i++) {
201 for (nick *np = nicktable[i]; np; np=np->next) {
202 nick_setup(np);
203 }
204 }
205
206 registerhook(HOOK_NICK_NEWNICK, nick_new);
207 registerhook(HOOK_NICK_LOSTNICK, nick_lost);
208 registerhook(HOOK_CONTROL_WHOISREQUEST, whois_handler);
209 }
210
211 void _fini(void) {
212 if (nickext != -1) {
213 releasenickext(nickext);
214 }
215
216 MMDB_close(&db);
217
218 deregisterhook(HOOK_NICK_NEWNICK, nick_new);
219 deregisterhook(HOOK_NICK_LOSTNICK, nick_lost);
220 deregisterhook(HOOK_CONTROL_WHOISREQUEST, whois_handler);
221 }