Initial commit.
This commit is contained in:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 디폴트 무시된 파일
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 에디터 기반 HTTP 클라이언트 요청
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
7
.idea/encodings.xml
generated
Normal file
7
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
14
.idea/misc.xml
generated
Normal file
14
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Loco wrapper
|
||||
|
||||
테스트 해볼거면 본계정 쓰지마세요. 영정먹을 수 있음
|
||||
|
||||

|
||||
56
dependency-reduced-pom.xml
Normal file
56
dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.github.netricecake</groupId>
|
||||
<artifactId>loco-wrapper</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>com.github.netricecake.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp-bom</artifactId>
|
||||
<version>5.2.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<properties>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
91
pom.xml
Normal file
91
pom.xml
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.github.netricecake</groupId>
|
||||
<artifactId>loco-wrapper</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.github.netricecake.Main</mainClass> </transformer>
|
||||
</transformers>
|
||||
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp-bom</artifactId>
|
||||
<version>5.2.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp-jvm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.13.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.2.7.Final</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.42</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>bson</artifactId>
|
||||
<version>5.6.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
218
src/main/java/com/github/netricecake/KakaoApi.java
Normal file
218
src/main/java/com/github/netricecake/KakaoApi.java
Normal file
@@ -0,0 +1,218 @@
|
||||
package com.github.netricecake;
|
||||
|
||||
import com.github.netricecake.message.request.GetConfRequest;
|
||||
import com.github.netricecake.message.response.GetConfResponse;
|
||||
import com.github.netricecake.util.ByteUtil;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import okhttp3.*;
|
||||
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.security.auth.login.AccountException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Map;
|
||||
|
||||
public class KakaoApi {
|
||||
|
||||
public final static String ALLOW_LIST_URL = "https://katalk.kakao.com/android/account/allowlist.json";
|
||||
public final static String LOGIN_URL = "https://katalk.kakao.com/android/account/login.json";
|
||||
public final static String PASSCODE_GENERATE_URL = "https://katalk.kakao.com/android/account/passcodeLogin/generate";
|
||||
public final static String REGISTER_DEVICE_URL = "https://katalk.kakao.com/android/account/passcodeLogin/registerDevice";
|
||||
public final static String CANCEL_REGISTER_URL = "https://katalk.kakao.com/android/account/passcodeLogin/cancel";
|
||||
public final static String BOOKING_URL = "booking-loco.kakao.com";
|
||||
public final static int BOOKING_PORT = 443;
|
||||
|
||||
public final static String AGENT = "android";
|
||||
public final static String VERSION = "25.9.2";
|
||||
public final static String OS_VERSION = "13";
|
||||
public final static String API_LEVEL = "33";
|
||||
public final static String LANGUAGE = "ko";
|
||||
public final static String AUTH_USER_AGENT = String.format("KT/%s An/%s %s", VERSION, OS_VERSION, LANGUAGE);
|
||||
public final static String AUTH_HEADER_AGENT = String.format("%s/%s/%s", AGENT, VERSION, LANGUAGE);
|
||||
|
||||
public final static int UUID_LENGTH = 64;
|
||||
|
||||
private final static OkHttpClient client = new OkHttpClient();;
|
||||
|
||||
private final static Gson gson = new Gson();
|
||||
|
||||
public static LoginData loginRequest(String email, String password, String deviceName, String deviceUuid) throws IOException, InvalidParameterException, IllegalStateException {
|
||||
if (deviceUuid == null || deviceUuid.length() != UUID_LENGTH) throw new InvalidParameterException("invalid deviceUuid");
|
||||
if (!checkAllowedDevice(deviceName)) throw new InvalidParameterException("This device does not support sub device login");
|
||||
RequestBody body = new FormBody.Builder().add("password", password)
|
||||
.add("device_name", deviceName)
|
||||
.add("foced", "false")
|
||||
.add("permanent", "true")
|
||||
.add("email", email)
|
||||
.add("device_uuid", deviceUuid).build();
|
||||
Request.Builder builder = new Request.Builder().url(LOGIN_URL).post(body);
|
||||
builder.addHeader("X-VC", calculateXVC(email))
|
||||
.addHeader("Accept-Language", LANGUAGE)
|
||||
.addHeader("User-Agent", AUTH_USER_AGENT)
|
||||
.addHeader("A", AUTH_HEADER_AGENT);
|
||||
|
||||
Response response = client.newCall(builder.build()).execute();
|
||||
JsonObject jsonObject = new JsonParser().parse(response.body().string()).getAsJsonObject();
|
||||
int status = jsonObject.get("status").getAsInt();
|
||||
|
||||
// 12 비번 틀림 30 이메일 틀림
|
||||
if (status == 12 || status == 30) throw new InvalidParameterException("Email or password is invalid");
|
||||
if (status == -100) throw new IllegalStateException("Register device before login");
|
||||
if (status == 0) {
|
||||
|
||||
LoginData data = new LoginData();
|
||||
data.userId = jsonObject.get("userId").getAsInt();
|
||||
data.countryIso = jsonObject.get("countryIso").getAsString();
|
||||
data.countryCode = jsonObject.get("countryCode").getAsString();
|
||||
data.accountId = jsonObject.get("accountId").getAsInt();
|
||||
data.accessToken = jsonObject.get("access_token").getAsString();
|
||||
data.refreshToken = jsonObject.get("refresh_token").getAsString();
|
||||
data.tokenType = jsonObject.get("token_type").getAsString();
|
||||
data.autoLoginAccountId = jsonObject.get("autoLoginAccountId").getAsString();
|
||||
data.displayAccountId = jsonObject.get("displayAccountId").getAsString();
|
||||
data.mainDeviceAgentName = jsonObject.get("mainDeviceAgentName").getAsString();
|
||||
data.mainDeviceAppVersion = jsonObject.get("mainDeviceAppVersion").getAsString();
|
||||
data.recipe = jsonObject.get("recipe").getAsString();
|
||||
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Map.Entry<String, Integer> generatePasscode(String email, String password, String device_name, String deviceUuid) throws IOException {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("email", email);
|
||||
jsonObject.addProperty("password", password);
|
||||
jsonObject.addProperty("permanent", true);
|
||||
JsonObject deviceObject = new JsonObject();
|
||||
deviceObject.addProperty("name", device_name);
|
||||
deviceObject.addProperty("uuid", deviceUuid);
|
||||
deviceObject.addProperty("model", device_name);
|
||||
deviceObject.addProperty("osVersion", API_LEVEL);
|
||||
jsonObject.add("device", deviceObject);
|
||||
|
||||
Request.Builder builder = generateHeader(email).url(PASSCODE_GENERATE_URL).post(RequestBody.create(gson.toJson(jsonObject), MediaType.parse("application/json; charset=utf-8")));
|
||||
String json = client.newCall(builder.build()).execute().body().string();
|
||||
JsonObject body = JsonParser.parseString(json).getAsJsonObject();
|
||||
return Map.entry(body.get("passcode").getAsString(), body.get("remainingSeconds").getAsInt());
|
||||
}
|
||||
|
||||
public static boolean registerDevice(String email, String password, String deviceUuid) {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("email", email);
|
||||
jsonObject.addProperty("password", password);
|
||||
JsonObject deviceObject = new JsonObject();
|
||||
deviceObject.addProperty("uuid", deviceUuid);
|
||||
jsonObject.add("device", deviceObject);
|
||||
|
||||
Request.Builder builder = generateHeader(email).url(REGISTER_DEVICE_URL).post(RequestBody.create(gson.toJson(jsonObject), MediaType.parse("application/json; charset=utf-8")));
|
||||
|
||||
Request request = builder.build();
|
||||
try {
|
||||
int remainTime = -7777;
|
||||
do {
|
||||
JsonObject resBody = JsonParser.parseString(client.newCall(request).execute().body().string()).getAsJsonObject();
|
||||
if (resBody.get("status").getAsInt() == 0) return true;
|
||||
if (resBody.get("status").getAsInt() != -100) return false;
|
||||
int remain = resBody.get("remainingSeconds").getAsInt();
|
||||
int interval = resBody.get("nextRequestIntervalInSeconds").getAsInt();
|
||||
remainTime = remain - interval;
|
||||
Thread.sleep((long) interval * 1000);
|
||||
} while(remainTime > 0);
|
||||
builder = generateHeader(email).url(CANCEL_REGISTER_URL).post(RequestBody.create(gson.toJson(jsonObject), MediaType.parse("application/json; charset=utf-8")));
|
||||
client.newCall(builder.build()).execute();
|
||||
} catch (Exception e) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static GetConfResponse getBookingData(String MCCMNC, int userId) {
|
||||
byte[] id = ByteUtil.intToByteArrayLE(1000);
|
||||
byte[] method = new byte[11];
|
||||
System.arraycopy("GETCONF".getBytes(), 0, method, 0, 7);
|
||||
byte[] b = new GetConfRequest(MCCMNC, AGENT, userId).toBson();
|
||||
byte[] m = ByteUtil.concatBytes(id, new byte[2], method, new byte[1], ByteUtil.intToByteArrayLE(b.length), b);
|
||||
try {
|
||||
SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
|
||||
SSLSocket socket = (SSLSocket) socketFactory.createSocket(BOOKING_URL, BOOKING_PORT);
|
||||
socket.startHandshake();
|
||||
|
||||
OutputStream writer = socket.getOutputStream();
|
||||
InputStream reader = socket.getInputStream();
|
||||
|
||||
writer.write(m);
|
||||
writer.flush();
|
||||
|
||||
byte[] res = new byte[4096];
|
||||
reader.read(res);
|
||||
|
||||
int len = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(res, 18, 4));
|
||||
GetConfResponse r = new GetConfResponse();
|
||||
r.fromBson(ByteUtil.sliceBytes(res, 22, len));
|
||||
|
||||
socket.close();
|
||||
|
||||
return r;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkAllowedDevice(String deviceName) throws IOException {
|
||||
Request.Builder builder = new Request.Builder().url(String.format("%s?model_name=%s", ALLOW_LIST_URL, deviceName)).get();
|
||||
builder.addHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
builder.addHeader("Accept-Language", LANGUAGE);
|
||||
builder.addHeader("User-Agent", AUTH_USER_AGENT);
|
||||
builder.addHeader("A", AUTH_HEADER_AGENT);
|
||||
builder.addHeader("Accept-Encoding", "gzip");
|
||||
|
||||
Request request = builder.build();
|
||||
|
||||
return JsonParser.parseString(client.newCall(request).execute().body().string()).getAsJsonObject().get("allowlisted").getAsBoolean();
|
||||
}
|
||||
|
||||
public static Request.Builder generateHeader(String email) {
|
||||
Request.Builder builder = new Request.Builder();
|
||||
builder.addHeader("X-VC", calculateXVC(email))
|
||||
.addHeader("User-Agent", AUTH_USER_AGENT)
|
||||
.addHeader("A", AUTH_HEADER_AGENT);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static String calculateXVC(String email) {
|
||||
try {
|
||||
String str = String.format("BARD|%s|DANTE|%s|SIAN", AUTH_USER_AGENT, email);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
digest.reset();
|
||||
digest.update(str.getBytes());
|
||||
|
||||
return ByteUtil.byteArrayToHexString(digest.digest()).substring(0, 16).toLowerCase();
|
||||
} catch (NoSuchAlgorithmException e) {}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static class LoginData {
|
||||
public int userId;
|
||||
public String countryIso;
|
||||
public String countryCode;
|
||||
public int accountId;
|
||||
public String accessToken;
|
||||
public String refreshToken;
|
||||
public String tokenType;
|
||||
public String autoLoginAccountId;
|
||||
public String displayAccountId;
|
||||
public String mainDeviceAgentName;
|
||||
public String mainDeviceAppVersion;
|
||||
public String recipe;
|
||||
}
|
||||
|
||||
}
|
||||
89
src/main/java/com/github/netricecake/KakaoTalkClient.java
Normal file
89
src/main/java/com/github/netricecake/KakaoTalkClient.java
Normal file
@@ -0,0 +1,89 @@
|
||||
package com.github.netricecake;
|
||||
|
||||
import com.github.netricecake.kakao.KakaoDefaultValues;
|
||||
import com.github.netricecake.message.request.*;
|
||||
import com.github.netricecake.message.response.CheckInResponse;
|
||||
import com.github.netricecake.message.response.GetConfResponse;
|
||||
import com.github.netricecake.message.response.MessageResponse;
|
||||
import com.github.netricecake.network.LocoPacket;
|
||||
import com.github.netricecake.network.LocoSocket;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class KakaoTalkClient {
|
||||
|
||||
private final static String DEFAULT_MCCMNC = KakaoDefaultValues.MCCMNC;
|
||||
|
||||
private KakaoApi.LoginData loginData;
|
||||
private GetConfResponse bookingData;
|
||||
private CheckInResponse checkInData;
|
||||
|
||||
LocoSocket socket;
|
||||
|
||||
public void login(String email, String password, String deviceName, String deviceUuid) throws Exception, IOException, InvalidParameterException, IllegalStateException {
|
||||
try {
|
||||
loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid);
|
||||
} catch (IllegalStateException e) {
|
||||
Map.Entry<String, Integer> registerInfo = KakaoApi.generatePasscode(email, password, deviceName, deviceUuid);
|
||||
System.out.println("디바이스 등록이 필요합니다.");
|
||||
System.out.println("카카오톡에서 " + registerInfo.getValue() + "초 안에 코드를 입력해주세요. 코드 : " + registerInfo.getKey());
|
||||
boolean registerResult = KakaoApi.registerDevice(email, password, deviceUuid);
|
||||
if (!registerResult) throw new IllegalStateException("기기 등록 실패");
|
||||
System.out.println("기기 등록 성공");
|
||||
loginData = KakaoApi.loginRequest(email, password, deviceName, deviceUuid);
|
||||
}
|
||||
System.out.println("로그인 성공");
|
||||
|
||||
bookingData = KakaoApi.getBookingData(DEFAULT_MCCMNC, loginData.userId);
|
||||
|
||||
LocoSocket checkInSocket = new LocoSocket(bookingData.getAddr(), bookingData.getPort());
|
||||
|
||||
try {
|
||||
CheckInRequest checkInRequest = new CheckInRequest();
|
||||
checkInRequest.setUserId(loginData.userId);
|
||||
byte[] body = checkInRequest.toBson();
|
||||
checkInSocket.connect();
|
||||
checkInSocket.write(new LocoPacket(1001, checkInRequest.getMethod(), body));
|
||||
checkInData = new CheckInResponse();
|
||||
checkInData.fromBson(checkInSocket.read().getBody());
|
||||
checkInSocket.close();
|
||||
} catch (Exception e) {
|
||||
System.out.println("오류 : " + e.getMessage());
|
||||
}
|
||||
|
||||
socket = new LocoSocket(checkInData.getHost(), checkInData.getPort());
|
||||
socket.connect();
|
||||
LoginListRequest req = new LoginListRequest();
|
||||
req.setDuuid(deviceUuid);
|
||||
req.setOauthToken(loginData.accessToken);
|
||||
socket.write(new LocoPacket(1002, "LOGINLIST", req.toBson()));
|
||||
}
|
||||
|
||||
public void test() throws Exception {
|
||||
while(true) {
|
||||
LocoPacket packet = socket.read();
|
||||
if (packet == null) continue;
|
||||
if (!packet.getMethod().equals("MSG")) continue;
|
||||
|
||||
socket.write(new LocoPacket(packet.getPacketId(), "MSG", new MessageRequest().toBson()));
|
||||
MessageResponse res = new MessageResponse();
|
||||
res.fromBson(packet.getBody());
|
||||
if (res.getType() != 1) return;
|
||||
if (res.getMessage().trim().equals("!!test")) {
|
||||
WriteRequest req = new WriteRequest();
|
||||
req.setChatId(res.getChatId());
|
||||
req.setMessage("test!!");
|
||||
socket.write(new LocoPacket(1003, "WRITE", req.toBson()));
|
||||
LocoPacket t = socket.read();
|
||||
System.out.println(BsonUtil.bsonToJson(t.getBody()));
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/java/com/github/netricecake/Main.java
Normal file
22
src/main/java/com/github/netricecake/Main.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.github.netricecake;
|
||||
|
||||
import com.github.netricecake.util.ByteUtil;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class Main {
|
||||
|
||||
static String EMAIL = "";
|
||||
static String PASSWORD = "";
|
||||
static String DEVICE_NAME = "SM-X930"; // 갤럭시 탭 s11 울트라
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
byte[] uuid = new byte[32];
|
||||
new SecureRandom().nextBytes(uuid);
|
||||
String deviceUuid = ByteUtil.byteArrayToHexString(uuid);
|
||||
KakaoTalkClient client = new KakaoTalkClient();
|
||||
client.login(EMAIL, PASSWORD, DEVICE_NAME, deviceUuid);
|
||||
client.test();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.github.netricecake.crypto;
|
||||
|
||||
import com.github.netricecake.util.ByteUtil;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import java.security.*;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
|
||||
public class CryptoManager {
|
||||
|
||||
public final static int HANDSHAKE_BODY_SIZE = 256; // ENCRYPTED KEY
|
||||
|
||||
public final static String RSA_ALGORITHM = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
public final static int RSA_LOCO_HEADER = 16;
|
||||
public final static byte[] RSA_PUBLIC_KEY_BYTES = ByteUtil.hexStringToByteArray("30820120300D06092A864886F70D01010105000382010D00308201080282010100A3B076E8C445851F19A670C231AAC6DB42EFD09717D06048A5CC56906CD1AB27B9DF37FFD5017E7C13A1405B5D1C3E4879A6A499D3C618A72472B0B50CA5EF1EF6EEA70369D9413FE662D8E2B479A9F72142EE70CEE6C2AD12045D52B25C4A204A28968E37F0BA6A49EE3EC9F2AC7A65184160F22F62C43A4067CD8D2A6F13D9B8298AB002763D236C9D1879D7FCE5B8FA910882B21E15247E0D0A24791308E51983614402E9FA03057C57E9E178B1CC39FE67288EFC461945CBCAA11D1FCC123E750B861F0D447EBE3C115F411A42DC95DDB21DA42774A5BCB1DDF7FA5F10628010C74F36F31C40EFCFE289FD81BABA44A6556A6C301210414B6023C3F46371020103");
|
||||
|
||||
public final static String AES_ALGORITHM = "AES/GCM/NoPadding";
|
||||
public final static int AES_KEY_SIZE = 128;
|
||||
public final static int AES_NONCE_SIZE = 12;
|
||||
public final static int AES_LOCO_HEADER = 3;
|
||||
|
||||
private Key aesKey;
|
||||
private final SecureRandom generator = new SecureRandom();
|
||||
|
||||
public CryptoManager() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
|
||||
keyGenerator.init(AES_KEY_SIZE);
|
||||
aesKey = keyGenerator.generateKey();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public CryptoManager(Key aesKey) {
|
||||
this.aesKey = aesKey;
|
||||
}
|
||||
|
||||
public byte[] generateHandshakeMessage() {
|
||||
try {
|
||||
PublicKey rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(RSA_PUBLIC_KEY_BYTES));
|
||||
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, rsaPublicKey);
|
||||
byte[] encryptedKey = cipher.doFinal(aesKey.getEncoded());
|
||||
byte[] length = ByteUtil.intToByteArrayLE(HANDSHAKE_BODY_SIZE);
|
||||
return ByteUtil.concatBytes(length, ByteUtil.intToByteArrayLE(RSA_LOCO_HEADER), ByteUtil.intToByteArrayLE(AES_LOCO_HEADER), encryptedKey);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// 바디 사이즈가 131067가 최대인거 같은데 잘 모르겠음
|
||||
public byte[] encryptMessage(byte[] message) {
|
||||
try {
|
||||
byte[] nonce = new byte[AES_NONCE_SIZE];
|
||||
generator.nextBytes(nonce);
|
||||
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(AES_KEY_SIZE, nonce));
|
||||
byte[] encryptedBody = cipher.doFinal(message);
|
||||
return ByteUtil.concatBytes(nonce, encryptedBody);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public byte[] decryptMessage(byte[] message) {
|
||||
try {
|
||||
byte[] nonce = ByteUtil.sliceBytes(message, 0, AES_NONCE_SIZE);
|
||||
Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(AES_KEY_SIZE, nonce));
|
||||
return cipher.doFinal(ByteUtil.sliceBytes(message, AES_NONCE_SIZE, message.length - nonce.length));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.netricecake.kakao;
|
||||
|
||||
public class KakaoDefaultValues {
|
||||
|
||||
public final static String MCCMNC = "45006";
|
||||
public final static int ntype = 0;
|
||||
public final static String os = "android";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.netricecake.message;
|
||||
|
||||
public interface LocoRequest {
|
||||
|
||||
String getMethod();
|
||||
|
||||
byte[] toBson();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.github.netricecake.message;
|
||||
|
||||
public interface LocoResponse {
|
||||
|
||||
String getMethod();
|
||||
|
||||
void fromBson(byte[] bson);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.KakaoApi;
|
||||
import com.github.netricecake.kakao.KakaoDefaultValues;
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class CheckInRequest implements LocoRequest {
|
||||
|
||||
private int userId;
|
||||
|
||||
private String os = KakaoDefaultValues.os;
|
||||
|
||||
private int ntype = KakaoDefaultValues.ntype;
|
||||
|
||||
private String appVer = KakaoApi.VERSION;
|
||||
|
||||
private String lang = KakaoApi.LANGUAGE;
|
||||
|
||||
private String MCCMNC = KakaoDefaultValues.MCCMNC;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "CHECKIN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("userId", userId);
|
||||
jsonObject.addProperty("os", os);
|
||||
jsonObject.addProperty("ntype", ntype);
|
||||
jsonObject.addProperty("appVer", appVer);
|
||||
jsonObject.addProperty("lang", lang);
|
||||
jsonObject.addProperty("MCCMNC", MCCMNC);
|
||||
|
||||
return BsonUtil.jsonObjectToBson(jsonObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
public class GetConfRequest implements LocoRequest {
|
||||
|
||||
private String MCCMNC;
|
||||
|
||||
private String os;
|
||||
|
||||
private int userId;
|
||||
|
||||
public GetConfRequest(String MCCMNC, String os, int userId) {
|
||||
this.MCCMNC = MCCMNC;
|
||||
this.os = os;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "GETCONF";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("MCCMNC", MCCMNC);
|
||||
jsonObject.addProperty("os", os);
|
||||
jsonObject.addProperty("userId", userId);
|
||||
return BsonUtil.jsonObjectToBson(jsonObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.KakaoApi;
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class LoginListRequest implements LocoRequest {
|
||||
|
||||
private String appVer = KakaoApi.VERSION;
|
||||
|
||||
private String prtVer = "1";
|
||||
|
||||
private String os = KakaoApi.AGENT;
|
||||
|
||||
private String lang = "ko";
|
||||
|
||||
private String duuid;
|
||||
|
||||
private int ntype = 0; // 0 : WIFI, 3: Cellular
|
||||
|
||||
private String MCCMNC = "45006"; // 앞자리 세자리(한국) 450 고정, 뒤에 두자리 SKT: 05 KT: 08 LGU+: 06 ex) 45006
|
||||
|
||||
private int revision = 0; // TODO 이거뭐임
|
||||
|
||||
private JsonArray chatIds = new JsonArray();
|
||||
|
||||
private JsonArray maxIds = new JsonArray();
|
||||
|
||||
private int lastTokenId = 0;
|
||||
|
||||
private int lbk = 0; // TODO 이거 뭐임 2
|
||||
|
||||
private JsonObject rp = new JsonObject(); // TODO 이거 뭐임 3
|
||||
|
||||
private boolean bg = true; // TODO 이거 뭐임 4
|
||||
|
||||
private String oauthToken;
|
||||
|
||||
public LoginListRequest() {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("base64", "AAD//wAA");
|
||||
jsonObject.addProperty("subType", "00");
|
||||
rp.add("$binary", jsonObject);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "LOGINLIST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("appVer", appVer);
|
||||
jsonObject.addProperty("prtVer", prtVer);
|
||||
jsonObject.addProperty("os", os);
|
||||
jsonObject.addProperty("lang", lang);
|
||||
jsonObject.addProperty("duuid", duuid);
|
||||
jsonObject.addProperty("ntype", ntype);
|
||||
jsonObject.addProperty("MCCMNC", MCCMNC);
|
||||
jsonObject.addProperty("revision", revision);
|
||||
jsonObject.add("chatIds", chatIds);
|
||||
jsonObject.add("maxIds", maxIds);
|
||||
jsonObject.addProperty("lastTokenId", lastTokenId);
|
||||
jsonObject.addProperty("lbk", lbk);
|
||||
jsonObject.add("rp", rp);
|
||||
jsonObject.addProperty("bg", bg);
|
||||
jsonObject.addProperty("oauthToken", oauthToken);
|
||||
|
||||
return BsonUtil.jsonObjectToBson(jsonObject);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
|
||||
public class MessageRequest implements LocoRequest {
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "MSG";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
return BsonUtil.jsonToBson("{ notiRead: false }");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
|
||||
public class PingRequest implements LocoRequest {
|
||||
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "PING";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
return BsonUtil.jsonToBson("{}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class SetStatusRequest implements LocoRequest {
|
||||
|
||||
/*
|
||||
카톡 켜고 있는지 안켜고 있는지 알리는 패킷인듯
|
||||
*/
|
||||
|
||||
private int status; // 1 : 본다, 2 : 안본다
|
||||
|
||||
public SetStatusRequest(int atatus) {
|
||||
this.status = atatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "SETST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("st", status);
|
||||
return BsonUtil.jsonObjectToBson(jsonObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.github.netricecake.message.request;
|
||||
|
||||
import com.github.netricecake.message.LocoRequest;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class WriteRequest implements LocoRequest {
|
||||
|
||||
private long chatId;
|
||||
private long msgId; // 이거 뭐임???
|
||||
private String message;
|
||||
private int type = 1;
|
||||
private boolean noSeen = false;
|
||||
private String extra = "{}";
|
||||
private int scope = 1;
|
||||
|
||||
public WriteRequest() {
|
||||
msgId = (long) Math.ceil(Math.random() * 99999999);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "WRITE";
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBson() {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("chatId", chatId);
|
||||
jsonObject.addProperty("msgId", msgId);
|
||||
jsonObject.addProperty("msg", message);
|
||||
jsonObject.addProperty("type", type);
|
||||
jsonObject.addProperty("noSeen", noSeen);
|
||||
jsonObject.addProperty("extra", extra);
|
||||
jsonObject.addProperty("scope", scope);
|
||||
return BsonUtil.jsonObjectToBson(jsonObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.github.netricecake.message.response;
|
||||
|
||||
import com.github.netricecake.message.LocoResponse;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.github.netricecake.util.ByteUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class CheckInResponse implements LocoResponse {
|
||||
|
||||
private String host;
|
||||
|
||||
private String host6;
|
||||
|
||||
private int port;
|
||||
|
||||
private String cshost;
|
||||
|
||||
private String cshost6;
|
||||
|
||||
private int csport;
|
||||
|
||||
private String vshost;
|
||||
|
||||
private String vshost6;
|
||||
|
||||
private int vsport;
|
||||
|
||||
private int cacheExpire;
|
||||
|
||||
private String MCCMNC;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "CHECKIN";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBson(byte[] bson) {
|
||||
JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson);
|
||||
this.host = jsonObject.get("host").getAsString();
|
||||
this.host6 = jsonObject.get("host6").getAsString();
|
||||
this.port = jsonObject.get("port").getAsInt();
|
||||
this.cshost = jsonObject.get("cshost").getAsString();
|
||||
this.cshost6 = jsonObject.get("cshost6").getAsString();
|
||||
this.csport = jsonObject.get("csport").getAsInt();
|
||||
this.vshost = jsonObject.get("vsshost").getAsString();
|
||||
this.vshost6 = jsonObject.get("vsshost6").getAsString();
|
||||
this.vsport = jsonObject.get("vssport").getAsInt();
|
||||
this.cacheExpire = jsonObject.get("cacheExpire").getAsInt();
|
||||
this.MCCMNC = jsonObject.get("MCCMNC").getAsString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.github.netricecake.message.response;
|
||||
|
||||
import com.github.netricecake.message.LocoResponse;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class GetConfResponse implements LocoResponse {
|
||||
|
||||
private String addr;
|
||||
|
||||
private int port;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "GetConf";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBson(byte[] bson) {
|
||||
JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson);
|
||||
this.addr = jsonObject.get("ticket").getAsJsonObject().get("lsl").getAsJsonArray().get(0).getAsString();
|
||||
this.port = jsonObject.get("wifi").getAsJsonObject().get("ports").getAsJsonArray().get(0).getAsInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.netricecake.message.response;
|
||||
|
||||
import com.github.netricecake.message.LocoResponse;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class LoginListResponse implements LocoResponse {
|
||||
|
||||
private int status;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "LOGINLIST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBson(byte[] bson) {
|
||||
// 너무 많음;;
|
||||
JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson);
|
||||
this.status = jsonObject.get("status").getAsInt();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.github.netricecake.message.response;
|
||||
|
||||
import com.github.netricecake.message.LocoResponse;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class MessageResponse implements LocoResponse {
|
||||
|
||||
private long chatId;
|
||||
private long logId;
|
||||
private int type;
|
||||
private long authorId;
|
||||
private String message;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "MSG";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBson(byte[] bson) {
|
||||
JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson);
|
||||
chatId = jsonObject.get("chatId").getAsLong();
|
||||
logId = jsonObject.get("logId").getAsLong();
|
||||
type = jsonObject.get("chatLog").getAsJsonObject().get("type").getAsInt();
|
||||
message = jsonObject.get("chatLog").getAsJsonObject().get("message").getAsString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.github.netricecake.message.response;
|
||||
|
||||
import com.github.netricecake.message.LocoResponse;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class PingResponse implements LocoResponse {
|
||||
|
||||
private int status;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "PING";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBson(byte[] bson) {
|
||||
JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson);
|
||||
this.status = jsonObject.get("status").getAsInt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.github.netricecake.message.response;
|
||||
|
||||
import com.github.netricecake.message.LocoResponse;
|
||||
import com.github.netricecake.util.BsonUtil;
|
||||
import com.google.gson.JsonObject;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
public class SetStatusResponse implements LocoResponse {
|
||||
|
||||
private int status;
|
||||
|
||||
@Override
|
||||
public String getMethod() {
|
||||
return "SETST";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBson(byte[] bson) {
|
||||
JsonObject jsonObject = BsonUtil.bsonToJsonObject(bson);
|
||||
status = jsonObject.get("status").getAsInt();
|
||||
}
|
||||
}
|
||||
40
src/main/java/com/github/netricecake/network/LocoPacket.java
Normal file
40
src/main/java/com/github/netricecake/network/LocoPacket.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.github.netricecake.network;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class LocoPacket {
|
||||
|
||||
@Getter
|
||||
private final int packetId;
|
||||
|
||||
@Getter
|
||||
private final short statusCode;
|
||||
|
||||
@Getter
|
||||
private String method;
|
||||
|
||||
@Getter
|
||||
private final byte bodyType;
|
||||
|
||||
@Getter
|
||||
private int bodyLength;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private byte[] body;
|
||||
|
||||
public LocoPacket(int packetId, short statusCode, String method, byte bodyType, int bodyLength, byte[] body) {
|
||||
this.packetId = packetId;
|
||||
this.statusCode = statusCode;
|
||||
this.method = method;
|
||||
this.bodyType = bodyType;
|
||||
this.bodyLength = bodyLength;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public LocoPacket(int packetId, String method, byte[] body) {
|
||||
this(packetId, (short) 0, method, (byte) 0, body.length, body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.github.netricecake.network;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface LocoPacketHandler {
|
||||
void onPacket(LocoPacket packet);
|
||||
}
|
||||
92
src/main/java/com/github/netricecake/network/LocoSocket.java
Normal file
92
src/main/java/com/github/netricecake/network/LocoSocket.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.github.netricecake.network;
|
||||
|
||||
import com.github.netricecake.crypto.CryptoManager;
|
||||
import com.github.netricecake.network.codec.LocoCodec;
|
||||
import com.github.netricecake.network.codec.SecureLayerCodec;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.*;
|
||||
import io.netty.channel.nio.NioIoHandler;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.codec.bytes.ByteArrayDecoder;
|
||||
import io.netty.handler.codec.bytes.ByteArrayEncoder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LocoSocket {
|
||||
|
||||
@Getter
|
||||
private String ip;
|
||||
@Getter
|
||||
private int port;
|
||||
|
||||
private CryptoManager cryptoManager;
|
||||
|
||||
private Channel channel;
|
||||
private EventLoopGroup eventLoopGroup;
|
||||
|
||||
@Getter
|
||||
private boolean alive = false;
|
||||
|
||||
private BlockingQueue<LocoPacket> locoPacketQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
public LocoSocket(String ip, int port) {
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
cryptoManager = new CryptoManager();
|
||||
}
|
||||
|
||||
public void connect() throws Exception {
|
||||
byte[] handshakePacket = cryptoManager.generateHandshakeMessage();
|
||||
eventLoopGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory());
|
||||
Bootstrap bootstrap = new Bootstrap();
|
||||
bootstrap.remoteAddress(new InetSocketAddress(ip, port))
|
||||
.group(eventLoopGroup)
|
||||
.channel(NioSocketChannel.class)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel socketChannel) throws Exception {
|
||||
ChannelPipeline pipeline = socketChannel.pipeline();
|
||||
pipeline.addLast(new ByteArrayEncoder());
|
||||
pipeline.addLast(new ByteArrayDecoder());
|
||||
}
|
||||
});
|
||||
channel = bootstrap.connect().sync().channel();
|
||||
alive = true;
|
||||
channel.writeAndFlush(handshakePacket).sync();
|
||||
channel.pipeline().addLast(new SecureLayerCodec(cryptoManager));
|
||||
channel.pipeline().addLast(new LocoCodec(locoPacketQueue));
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
channel.closeFuture().sync();
|
||||
eventLoopGroup.shutdownGracefully();
|
||||
locoPacketQueue.offer(null);
|
||||
alive = false;
|
||||
} catch (Exception e) {}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void write(LocoPacket packet) {
|
||||
if (!alive) return;
|
||||
channel.writeAndFlush(packet);
|
||||
}
|
||||
|
||||
public LocoPacket read() throws Exception {
|
||||
if (!alive) return null;
|
||||
return locoPacketQueue.poll(100000, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
channel.close();
|
||||
eventLoopGroup.shutdownGracefully();
|
||||
alive = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.github.netricecake.network.codec;
|
||||
|
||||
import com.github.netricecake.network.LocoPacket;
|
||||
import com.github.netricecake.util.ByteUtil;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageCodec;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
public class LocoCodec extends MessageToMessageCodec<byte[], LocoPacket> {
|
||||
|
||||
private LocoPacket currentLocoPacket = null;
|
||||
private byte[] buffer = new byte[0];
|
||||
|
||||
private BlockingQueue<LocoPacket> locoPacketQueue;
|
||||
|
||||
public LocoCodec(BlockingQueue<LocoPacket> locoPacketHandler) {
|
||||
this.locoPacketQueue = locoPacketHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext channelHandlerContext, LocoPacket packet, List<Object> list) throws Exception {
|
||||
byte[] packetId = ByteUtil.intToByteArrayLE(packet.getPacketId());
|
||||
byte[] statusCode = ByteUtil.shortToByteArrayLE(packet.getStatusCode());
|
||||
byte[] method = new byte[11];
|
||||
System.arraycopy(packet.getMethod().getBytes(), 0, method, 0, packet.getMethod().length());
|
||||
byte[] bodyType = { packet.getBodyType() };
|
||||
byte[] body = packet.getBody();
|
||||
byte[] bodyLength = ByteUtil.intToByteArrayLE(body.length);
|
||||
list.add(ByteUtil.concatBytes(packetId, statusCode, method, bodyType, bodyLength, body));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List<Object> list) throws Exception {
|
||||
if (bytes == null) return;
|
||||
buffer = ByteUtil.concatBytes(buffer, bytes);
|
||||
do {
|
||||
if (currentLocoPacket == null) {
|
||||
if (buffer.length < 22) return;
|
||||
int id = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(buffer, 0, 4));
|
||||
short statusCode = ByteUtil.byteArrayToShortLE(ByteUtil.sliceBytes(buffer, 4, 2));
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (int i = 0; i < 11; i++) {
|
||||
if ((buffer[6 + i] & 0xFF) == 0) break;
|
||||
sb.append((char) buffer[6 + i]);
|
||||
}
|
||||
String method = sb.toString();
|
||||
byte bodyType = buffer[17];
|
||||
int bodyLength = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(buffer, 18, 4));
|
||||
currentLocoPacket = new LocoPacket(id, statusCode, method, bodyType, bodyLength, null);
|
||||
buffer = ByteUtil.sliceBytes(buffer, 22, buffer.length - 22);
|
||||
}
|
||||
if (currentLocoPacket.getBodyLength() > buffer.length) break;
|
||||
byte[] body = ByteUtil.sliceBytes(buffer, 0, currentLocoPacket.getBodyLength());
|
||||
buffer = ByteUtil.sliceBytes(buffer, currentLocoPacket.getBodyLength(), buffer.length - currentLocoPacket.getBodyLength());
|
||||
currentLocoPacket.setBody(body);
|
||||
locoPacketQueue.put(currentLocoPacket);
|
||||
currentLocoPacket = null;
|
||||
} while (buffer.length > 0);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.github.netricecake.network.codec;
|
||||
|
||||
import com.github.netricecake.crypto.CryptoManager;
|
||||
import com.github.netricecake.util.ByteUtil;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToMessageCodec;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SecureLayerCodec extends MessageToMessageCodec<byte[], byte[]> {
|
||||
|
||||
private CryptoManager cryptoManager;
|
||||
|
||||
private int currentLength = -1;
|
||||
private byte[] buffer = new byte[0];
|
||||
|
||||
public SecureLayerCodec(CryptoManager cryptoManager) {
|
||||
this.cryptoManager = cryptoManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List<Object> list) throws Exception {
|
||||
byte[] encryptedBody = cryptoManager.encryptMessage(bytes);
|
||||
byte[] packet = ByteUtil.concatBytes(ByteUtil.intToByteArrayLE(encryptedBody.length), encryptedBody);
|
||||
list.add(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext channelHandlerContext, byte[] bytes, List<Object> list) throws Exception {
|
||||
buffer = ByteUtil.concatBytes(buffer, bytes);
|
||||
do {
|
||||
if (currentLength == -1) {
|
||||
if (buffer.length < 4) break;
|
||||
currentLength = ByteUtil.byteArrayToIntLE(ByteUtil.sliceBytes(buffer, 0, 4));
|
||||
buffer = ByteUtil.sliceBytes(buffer, 4, buffer.length - 4);
|
||||
}
|
||||
if (currentLength > buffer.length) break;
|
||||
byte[] packet = ByteUtil.sliceBytes(buffer, 0, currentLength);
|
||||
buffer = ByteUtil.sliceBytes(buffer, currentLength, buffer.length - currentLength);
|
||||
currentLength = -1;
|
||||
list.add(cryptoManager.decryptMessage(packet));
|
||||
} while (buffer.length > 0);
|
||||
}
|
||||
|
||||
}
|
||||
42
src/main/java/com/github/netricecake/util/BsonUtil.java
Normal file
42
src/main/java/com/github/netricecake/util/BsonUtil.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.github.netricecake.util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.bson.BsonBinaryReader;
|
||||
import org.bson.RawBsonDocument;
|
||||
import org.bson.codecs.BsonDocumentCodec;
|
||||
import org.bson.codecs.DecoderContext;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class BsonUtil {
|
||||
|
||||
private final static BsonDocumentCodec bsonDocumentCodec = new BsonDocumentCodec();
|
||||
private final static Gson gson = new Gson();
|
||||
|
||||
public static byte[] jsonToBson(String json) {
|
||||
var rawBson = RawBsonDocument.parse(json);
|
||||
ByteBuffer buffer = rawBson.getByteBuffer().asNIO();
|
||||
byte[] exactBytes = new byte[buffer.remaining()];
|
||||
buffer.get(exactBytes);
|
||||
return exactBytes;
|
||||
}
|
||||
|
||||
public static byte[] jsonObjectToBson(JsonObject jsonObject) {
|
||||
return jsonToBson(gson.toJson(jsonObject));
|
||||
}
|
||||
|
||||
public static String bsonToJson(byte[] bson) {
|
||||
var doc = bsonDocumentCodec.decode(
|
||||
new BsonBinaryReader(ByteBuffer.wrap(bson)),
|
||||
DecoderContext.builder().build()
|
||||
);
|
||||
return doc.toString();
|
||||
}
|
||||
|
||||
public static JsonObject bsonToJsonObject(byte[] bson) {
|
||||
return JsonParser.parseString(bsonToJson(bson)).getAsJsonObject();
|
||||
}
|
||||
|
||||
}
|
||||
74
src/main/java/com/github/netricecake/util/ByteUtil.java
Normal file
74
src/main/java/com/github/netricecake/util/ByteUtil.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.github.netricecake.util;
|
||||
|
||||
public class ByteUtil {
|
||||
|
||||
public static byte[] intToByteArrayLE(int value) {
|
||||
byte[] byteArray = new byte[4];
|
||||
byteArray[0] = (byte)(value);
|
||||
byteArray[1] = (byte)(value >> 8);
|
||||
byteArray[2] = (byte)(value >> 16);
|
||||
byteArray[3] = (byte)(value >> 24);
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
public static int byteArrayToIntLE(byte[] bytes) {
|
||||
return ((bytes[0] & 0xFF)) |
|
||||
((bytes[1] & 0xFF) << 8) |
|
||||
((bytes[2] & 0xFF) << 16) |
|
||||
((bytes[3] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
public static byte[] shortToByteArrayLE(short value) {
|
||||
byte[] byteArray = new byte[2];
|
||||
byteArray[0] = (byte)(value);
|
||||
byteArray[1] = (byte)(value >> 8);
|
||||
return byteArray;
|
||||
}
|
||||
|
||||
public static short byteArrayToShortLE(byte[] bytes) {
|
||||
return (short) ((bytes[0] & 0xFF) |
|
||||
((bytes[1] & 0xFF) << 8));
|
||||
}
|
||||
|
||||
public static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
public static String byteArrayToHexString(byte[] bytes){
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(byte b : bytes){
|
||||
sb.append(String.format("%02X", b&0xff));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static byte[] concatBytes(byte[]... bytes) {
|
||||
int len = 0;
|
||||
for (byte[] i: bytes) len += i.length;
|
||||
|
||||
byte[] r = new byte[len];
|
||||
int c = 0;
|
||||
for (byte[] i: bytes) {
|
||||
System.arraycopy(i, 0, r, c, i.length);
|
||||
c += i.length;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public static byte[] sliceBytes(byte[] bytes, int offset, int length) {
|
||||
byte[] r = new byte[length];
|
||||
System.arraycopy(bytes, offset, r, 0, length);
|
||||
return r;
|
||||
}
|
||||
|
||||
}
|
||||
3
src/main/resources/META-INF/MANIFEST.MF
Normal file
3
src/main/resources/META-INF/MANIFEST.MF
Normal file
@@ -0,0 +1,3 @@
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: com.github.netricecake.Main
|
||||
|
||||
Reference in New Issue
Block a user