Skip to content

Commit ec52e9d

Browse files
committed
Implements IP2ASN - and fixes quarts logging
1 parent 82f7f14 commit ec52e9d

10 files changed

Lines changed: 269 additions & 9 deletions

File tree

README.MD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ Java application designed to automate various backend tasks we have within Parad
77
It handles:
88

99
- Bouncer Restart
10+
- IP2ASN
1011
- Profiler Daemon Ingest
1112
- Profiler DB Cleanup
1213

1314
### Bouncer Restart
1415

1516
Restarts our "bouncer" server every day. This is the thing that does 2FA, server queue, and region redirection.
1617

18+
### IP2ASN
19+
20+
Caches ASNs for IP addresses of recent players.
21+
1722
### Profiler Daemon Ingest
1823

1924
This takes BYOND Profiler results and split them apart to be indexed at a later date, allowing multiple procs from multiple rounds to easily be visualised alongside eachother over time. An example SS13 codebase implementation can be found at <https://github.com/ParadiseSS13/Paradise/pull/17459>.

TaskDaemon.Core/pom.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
<name>TaskDaemon.Core</name>
1313

1414
<dependencies>
15+
<dependency>
16+
<groupId>me.aa07.paradise</groupId>
17+
<artifactId>taskdaemon-database-gamedb</artifactId>
18+
<version>dev-SNAPSHOT</version>
19+
</dependency>
1520
<dependency>
1621
<groupId>me.aa07.paradise</groupId>
1722
<artifactId>taskdaemon-database-profiler</artifactId>
@@ -42,8 +47,8 @@
4247
<artifactId>quartz</artifactId>
4348
</dependency>
4449
<dependency>
45-
<groupId>org.slf4j</groupId>
46-
<artifactId>slf4j-log4j12</artifactId>
50+
<groupId>org.apache.logging.log4j</groupId>
51+
<artifactId>log4j-slf4j2-impl</artifactId>
4752
</dependency>
4853
</dependencies>
4954

TaskDaemon.Core/src/me/aa07/paradise/taskdaemon/core/Core.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import me.aa07.paradise.taskdaemon.core.config.ConfigHolder;
77
import me.aa07.paradise.taskdaemon.core.database.DbCore;
88
import me.aa07.paradise.taskdaemon.core.modules.bouncerrestart.BouncerRestartJob;
9+
import me.aa07.paradise.taskdaemon.core.modules.ip2asn.Ip2AsnJob;
910
import me.aa07.paradise.taskdaemon.core.modules.profilercleanup.ProfilerCleanupJob;
1011
import me.aa07.paradise.taskdaemon.core.modules.profileringest.ProfilerWorker;
1112
import me.aa07.paradise.taskdaemon.core.redis.RedisManager;
@@ -93,6 +94,21 @@ private void setupJobs(Scheduler scheduler, DbCore dbCore, ConfigHolder config,
9394
.withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?")) // Every day - midnight
9495
.build();
9596

97+
// IP2ASN
98+
JobDataMap jdm_ip2asn = new JobDataMap();
99+
jdm_ip2asn.put("LOGGER", logger);
100+
jdm_ip2asn.put("DBCORE", dbCore);
101+
jdm_ip2asn.put("IP2ASNCFG", config.ip2asn);
102+
JobDetail jd_ip2asn = JobBuilder.newJob(Ip2AsnJob.class)
103+
.withIdentity("ip2asn", "ip2asn")
104+
.usingJobData(jdm_ip2asn)
105+
.build();
106+
CronTrigger ct_ip2asn = TriggerBuilder.newTrigger()
107+
.withIdentity("ip2asn", "ip2asn")
108+
//.withSchedule(CronScheduleBuilder.cronSchedule("0 */10 * * * ?")) // Every 10 minutes
109+
.withSchedule(CronScheduleBuilder.cronSchedule("0 * * * * ?"))
110+
.build();
111+
96112
// Profiler cleanup
97113
JobDataMap jdm_profilercleanup = new JobDataMap();
98114
jdm_profilercleanup.put("LOGGER", logger);
@@ -109,6 +125,7 @@ private void setupJobs(Scheduler scheduler, DbCore dbCore, ConfigHolder config,
109125

110126
// Schedule all
111127
scheduler.scheduleJob(jd_bouncerrestart, ct_bouncerrestart);
128+
scheduler.scheduleJob(jd_ip2asn, ct_ip2asn);
112129
scheduler.scheduleJob(jd_profilercleanup, ct_profilercleanup);
113130
}
114131

TaskDaemon.Core/src/me/aa07/paradise/taskdaemon/core/config/ConfigHolder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package me.aa07.paradise.taskdaemon.core.config;
22

33
public class ConfigHolder {
4+
public DatabaseConfig gameDatabase;
5+
public Ip2AsnSerivceConfig ip2asn;
46
public DatabaseConfig profilerDatabase;
57
public RedisConfig redis;
68
public TgsConfig tgs;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package me.aa07.paradise.taskdaemon.core.config;
2+
3+
public class Ip2AsnSerivceConfig {
4+
public String host;
5+
}

TaskDaemon.Core/src/me/aa07/paradise/taskdaemon/core/database/DbCore.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@
1616
import org.jooq.impl.DSL;
1717

1818
public class DbCore {
19-
private ConfigHolder config;
2019
private HashMap<DatabaseType, DataSource> connectionMap;
2120

2221
public DbCore(ConfigHolder config, Logger logger) {
23-
this.config = config;
2422
// Suppress JOOQ console spam
2523
System.getProperties().setProperty("org.jooq.no-logo", "true");
2624
System.getProperties().setProperty("org.jooq.no-tips", "true");
2725

2826
connectionMap = new HashMap<DatabaseType, DataSource>();
2927

30-
establishConnections();
28+
establishConnections(config);
3129
logger.info("Ready to handle DB requests");
3230
}
3331

@@ -36,6 +34,7 @@ private DataSource openDataSource(String url, String username, String password)
3634
source.addConnectionProperty("autoReconnect", "true");
3735
source.addConnectionProperty("allowMultiQueries", "true");
3836
source.addConnectionProperty("zeroDateTimeBehavior", "convertToNull");
37+
source.addConnectionProperty("connectionTimeZone", "UTC");
3938
source.setDefaultTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
4039
source.setDriverClassName("com.mysql.cj.jdbc.Driver");
4140
source.setUrl(url);
@@ -49,9 +48,10 @@ private DataSource openDataSource(String url, String username, String password)
4948
return source;
5049
}
5150

52-
private void establishConnections() {
51+
private void establishConnections(ConfigHolder config) {
5352
HashMap<DatabaseType, DatabaseConfig> db_types = new HashMap<DatabaseType, DatabaseConfig>();
5453

54+
db_types.put(DatabaseType.GameDb, config.gameDatabase);
5555
db_types.put(DatabaseType.ProfilerDb, config.profilerDatabase);
5656

5757
for (DatabaseType dbtype : db_types.keySet()) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package me.aa07.paradise.taskdaemon.core.models.ip2asn;
2+
3+
import com.google.gson.annotations.SerializedName;
4+
5+
public class Ip2AsnResponseModel {
6+
@SerializedName("as_number") // Cant use underscores in class fields - checkstyle gets unhappy
7+
public int asn;
8+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package me.aa07.paradise.taskdaemon.core.modules.ip2asn;
2+
3+
import com.google.gson.Gson;
4+
import java.net.URI;
5+
import java.net.http.HttpClient;
6+
import java.net.http.HttpRequest;
7+
import java.net.http.HttpResponse;
8+
import java.net.http.HttpResponse.BodyHandlers;
9+
import java.time.LocalDateTime;
10+
import java.util.ArrayList;
11+
import java.util.List;
12+
import java.util.Optional;
13+
import me.aa07.paradise.taskdaemon.core.config.Ip2AsnSerivceConfig;
14+
import me.aa07.paradise.taskdaemon.core.database.DatabaseType;
15+
import me.aa07.paradise.taskdaemon.core.database.DbCore;
16+
import me.aa07.paradise.taskdaemon.core.models.ip2asn.Ip2AsnResponseModel;
17+
import me.aa07.paradise.taskdaemon.database.gamedb.Tables;
18+
import me.aa07.paradise.taskdaemon.database.gamedb.tables.records.Ip2groupRecord;
19+
import org.apache.logging.log4j.Logger;
20+
import org.jooq.DSLContext;
21+
import org.jooq.Record1;
22+
import org.jooq.Select;
23+
import org.jooq.types.UInteger;
24+
import org.quartz.DisallowConcurrentExecution;
25+
import org.quartz.Job;
26+
import org.quartz.JobDataMap;
27+
import org.quartz.JobExecutionContext;
28+
import org.quartz.JobExecutionException;
29+
30+
@DisallowConcurrentExecution // NO
31+
public class Ip2AsnJob implements Job {
32+
33+
@Override
34+
public void execute(JobExecutionContext event) throws JobExecutionException {
35+
JobDataMap datamap = event.getMergedJobDataMap();
36+
37+
// Get our logger - important
38+
Object raw_logger = datamap.get("LOGGER");
39+
Optional<Logger> logger_holder = Optional.empty();
40+
41+
if (raw_logger instanceof Logger l2) {
42+
logger_holder = Optional.of(l2);
43+
}
44+
45+
if (!logger_holder.isPresent()) {
46+
System.out.println("[Ip2Asn] LOGGER WAS SOMEHOW NULL - THIS IS VERY BAD");
47+
return;
48+
}
49+
50+
Logger logger = logger_holder.get();
51+
52+
// Now get our DB
53+
Object raw_db = datamap.get("DBCORE");
54+
Optional<DbCore> dbcore_holder = Optional.empty();
55+
56+
if (raw_db instanceof DbCore db2) {
57+
dbcore_holder = Optional.of(db2);
58+
}
59+
60+
if (!dbcore_holder.isPresent()) {
61+
logger.error("[Ip2Asn] DBCORE WAS SOMEHOW NULL - THIS IS VERY BAD");
62+
return;
63+
}
64+
65+
DbCore dbcore = dbcore_holder.get();
66+
67+
// Now get our config
68+
Object raw_ip2asn_cfg = datamap.get("IP2ASNCFG");
69+
Optional<Ip2AsnSerivceConfig> ip2asn_cfg_holder = Optional.empty();
70+
71+
if (raw_ip2asn_cfg instanceof Ip2AsnSerivceConfig ip2asnCfg2) {
72+
ip2asn_cfg_holder = Optional.of(ip2asnCfg2);
73+
}
74+
75+
if (!ip2asn_cfg_holder.isPresent()) {
76+
logger.error("[Ip2Asn] IP2ASNCFG WAS SOMEHOW NULL - THIS IS VERY BAD");
77+
return;
78+
}
79+
80+
Ip2AsnSerivceConfig config = ip2asn_cfg_holder.get();
81+
82+
DSLContext ctx = dbcore.jooq(DatabaseType.GameDb);
83+
84+
// So
85+
// JOOQ doesnt have INET_ATON, presumably because its a MySQL native
86+
// So we have to do the manual selects then work out the differences
87+
// To do this we need:
88+
// 1. All IPs from player seen in the last 30 days
89+
// 2. Then remove the IPs in ip2group that have been updated in the last 7 days
90+
// Then we need to update IP2Group or insert where required
91+
LocalDateTime last_30_days = LocalDateTime.now().minusDays(30);
92+
LocalDateTime last_7_days = LocalDateTime.now().minusDays(7);
93+
94+
logger.info("[Ip2Asn] Pulling player IPs...");
95+
96+
// Get the player IPs
97+
List<String> player_ips = ctx.select(Tables.PLAYER.IP).from(Tables.PLAYER)
98+
.where(Tables.PLAYER.LASTSEEN.gt(last_30_days)).fetch()
99+
.getValues(Tables.PLAYER.IP, String.class);
100+
101+
logger.info(String.format("[Ip2Asn] Pulled %s player IPs, now pulling ip2group IPs...", player_ips.size()));
102+
103+
List<UInteger> ip2group_ips = ctx.select(Tables.IP2GROUP.IP).from(Tables.IP2GROUP)
104+
.where(Tables.IP2GROUP.DATE.gt(last_7_days)).fetch()
105+
.getValues(Tables.IP2GROUP.IP, UInteger.class);
106+
107+
logger.info(String.format("[Ip2Asn] Pulled %s ip2group IPs, now parsing...", ip2group_ips.size()));
108+
109+
// Now turn that to regular IPs
110+
ArrayList<String> parsed_ip2group_ips = new ArrayList<String>();
111+
112+
for (UInteger ip_uint : ip2group_ips) {
113+
String ip_str = uint2ip(ip_uint);
114+
parsed_ip2group_ips.add(ip_str);
115+
}
116+
117+
ArrayList<String> ips_to_get = new ArrayList<String>();
118+
119+
// Start with the IPs in player - then see if they exist in the last 7 days in
120+
// ip2group
121+
for (String ip : player_ips) {
122+
if (!parsed_ip2group_ips.contains(ip)) {
123+
ips_to_get.add(ip);
124+
}
125+
}
126+
127+
logger.info(String.format("[Ip2Asn] Found %s IPs to refresh groups for", ips_to_get.size()));
128+
129+
if (ips_to_get.size() == 0) {
130+
logger.info("[Ip2Asn] No work to do");
131+
return;
132+
}
133+
134+
// Now do the big refresh
135+
HttpClient client = HttpClient.newHttpClient();
136+
Gson gson = new Gson();
137+
for (String ip : ips_to_get) {
138+
try {
139+
// Get our AS - probably a more elegant way to do this but meh
140+
HttpRequest httpreq = HttpRequest.newBuilder().uri(new URI(String.format("%s%s", config.host, ip)))
141+
.GET().build();
142+
143+
// Send the request off
144+
HttpResponse<String> response = client.send(httpreq, BodyHandlers.ofString());
145+
146+
// Decode the shenanigans
147+
Ip2AsnResponseModel res_model = gson.fromJson(response.body(), Ip2AsnResponseModel.class);
148+
UInteger ip_uint = ip2uint(ip);
149+
UInteger asn = UInteger.valueOf(res_model.asn);
150+
151+
// See if our row exists, we update if it does
152+
Select<Record1<UInteger>> fetch_test = ctx.select(Tables.IP2GROUP.IP).from(Tables.IP2GROUP)
153+
.where(Tables.IP2GROUP.IP.eq(ip_uint));
154+
155+
if (ctx.fetchExists(fetch_test)) {
156+
// It exists, update
157+
logger.info(String.format("[Ip2Asn] Updating row for %s", ip));
158+
ctx.update(Tables.IP2GROUP)
159+
.set(Tables.IP2GROUP.GROUPSTR, asn)
160+
.set(Tables.IP2GROUP.DATE, dbcore.now())
161+
.where(Tables.IP2GROUP.IP.eq(ip_uint))
162+
.execute();
163+
} else {
164+
// It dont exist, insert
165+
logger.info(String.format("[Ip2Asn] Inserting row for %s", ip));
166+
Ip2groupRecord record = ctx.newRecord(Tables.IP2GROUP);
167+
record.setIp(ip_uint);
168+
record.setDate(dbcore.now());
169+
record.setGroupstr(asn);
170+
record.store();
171+
}
172+
173+
} catch (Exception e) {
174+
logger.warn(String.format("[Ip2Asn] Failed to get AS info for IP %s - skipping", ip));
175+
logger.warn(e);
176+
}
177+
}
178+
179+
logger.info("[Ip2Asn] Processing complete");
180+
}
181+
182+
// Turns a uint IP into its string form
183+
private String uint2ip(UInteger ip) {
184+
long ip_long = ip.longValue();
185+
return String.format("%d.%d.%d.%d",
186+
(ip_long >> 24 & 0xff),
187+
(ip_long >> 16 & 0xff),
188+
(ip_long >> 8 & 0xff),
189+
(ip_long & 0xff));
190+
}
191+
192+
// Turns a string IP into its uint form
193+
private UInteger ip2uint(String ip) {
194+
String[] octets = ip.split("\\.");
195+
196+
long ip_long = 0;
197+
for (int i = 0; i < 4; i++) {
198+
int octet = Integer.parseInt(octets[i]);
199+
if (octet < 0 || octet > 255) {
200+
throw new IllegalArgumentException("Invalid octet in IP address: " + octets[i]);
201+
}
202+
// This does the shift magic with the for loop,
203+
// with each iteration shifting by 8
204+
ip_long |= ((long) octet << (24 - 8 * i));
205+
}
206+
207+
return UInteger.valueOf(ip_long);
208+
}
209+
}

config.toml.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# This is an example configuration. Please see README.MD
22

3+
[gameDatabase]
4+
host = "172.16.0.200"
5+
username = "myuser"
6+
password = "mypassword"
7+
database = "paradise_profilerdaemon"
8+
9+
[ip2asn]
10+
host = "http://127.0.0.1:53661/v1/as/ip/" # Fill out URL - IP will be appended at the end
11+
312
[profilerDatabase]
413
host = "172.16.0.200"
514
username = "myuser"

pom.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@
7373
</dependency>
7474
<!-- Log4J injector -->
7575
<dependency>
76-
<groupId>org.slf4j</groupId>
77-
<artifactId>slf4j-log4j12</artifactId>
78-
<version>2.0.17</version>
76+
<groupId>org.apache.logging.log4j</groupId>
77+
<artifactId>log4j-slf4j2-impl</artifactId>
78+
<version>2.25.0</version>
7979
</dependency>
8080
</dependencies>
8181
</dependencyManagement>

0 commit comments

Comments
 (0)