From 939758403edb80c580b70b3122086097600f3f20 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 29 May 2021 20:41:35 +0200 Subject: [PATCH] Add login for xLoveCam The login is not quite working. Probably something with the password encryption or the fnv hash --- CHANGELOG.md | 6 + .../ui/sites/xlovecam/XloveCamConfigUI.java | 34 ++++ .../ui/sites/xlovecam/XloveCamSiteUi.java | 3 + common/src/main/java/ctbrec/Settings.java | 2 + .../main/java/ctbrec/io/HttpConstants.java | 1 + .../java/ctbrec/sites/xlovecam/XloveCam.java | 9 +- .../sites/xlovecam/XloveCamHttpClient.java | 188 +++++++++++++++++- .../ctbrec/sites/xlovecam/XloveCamModel.java | 2 +- 8 files changed, 239 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c39e2ecc..d202bad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +4.4.0 +======================== +* Added Amateur.TV +* Added XloveCam +* Fixed tipping function + 4.3.1 ======================== * Fixed bug in server communication. The server always returned HTTP 400, diff --git a/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamConfigUI.java b/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamConfigUI.java index 1e9608b1..e78c7134 100644 --- a/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamConfigUI.java @@ -10,7 +10,10 @@ import javafx.scene.Parent; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; public class XloveCamConfigUI extends AbstractConfigUI { private XloveCam site; @@ -40,10 +43,41 @@ public class XloveCamConfigUI extends AbstractConfigUI { GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); layout.add(enabled, 1, row++); + layout.add(new Label("XloveCam User"), 0, row); + var username = new TextField(Config.getInstance().getSettings().xlovecamUsername); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().xlovecamUsername)) { + Config.getInstance().getSettings().xlovecamUsername = username.getText(); + site.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, row++); + + layout.add(new Label("XloveCam Password"), 0, row); + var password = new PasswordField(); + password.setText(Config.getInstance().getSettings().xlovecamPassword); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().xlovecamPassword)) { + Config.getInstance().getSettings().xlovecamPassword = password.getText(); + site.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, row++); + var createAccount = new Button("Create new Account"); createAccount.setOnAction(e -> DesktopIntegration.open(site.getAffiliateLink())); layout.add(createAccount, 1, row); GridPane.setColumnSpan(createAccount, 2); + GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); return layout; } diff --git a/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamSiteUi.java b/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamSiteUi.java index e595d313..d3453352 100644 --- a/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/xlovecam/XloveCamSiteUi.java @@ -31,6 +31,9 @@ public class XloveCamSiteUi extends AbstractSiteUi { @Override public synchronized boolean login() throws IOException { + if (!site.credentialsAvailable()) { + return false; + } return site.getHttpClient().login(); } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index e9877611..455e32b4 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -182,4 +182,6 @@ public class Settings { public boolean webinterface = false; public String webinterfaceUsername = "ctbrec"; public String webinterfacePassword = "sucks"; + public String xlovecamUsername = ""; + public String xlovecamPassword = ""; } diff --git a/common/src/main/java/ctbrec/io/HttpConstants.java b/common/src/main/java/ctbrec/io/HttpConstants.java index d99f6e69..aacdd03f 100644 --- a/common/src/main/java/ctbrec/io/HttpConstants.java +++ b/common/src/main/java/ctbrec/io/HttpConstants.java @@ -11,6 +11,7 @@ public class HttpConstants { public static final String CONTENT_ENCODING = "Content-Encoding"; public static final String CONTENT_TYPE = "Content-Type"; public static final String COOKIE = "Cookie"; + public static final String FORM_ENCODED = "application/x-www-form-urlencoded; charset=UTF-8"; public static final String KEEP_ALIVE = "keep-alive"; public static final String MIMETYPE_APPLICATION_JSON = "application/json"; public static final String MIMETYPE_TEXT_HTML = "text/html"; diff --git a/common/src/main/java/ctbrec/sites/xlovecam/XloveCam.java b/common/src/main/java/ctbrec/sites/xlovecam/XloveCam.java index ad89e34f..3ccd76f8 100644 --- a/common/src/main/java/ctbrec/sites/xlovecam/XloveCam.java +++ b/common/src/main/java/ctbrec/sites/xlovecam/XloveCam.java @@ -7,13 +7,16 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import ctbrec.Config; import ctbrec.Model; +import ctbrec.StringUtil; import ctbrec.io.HttpClient; import ctbrec.sites.AbstractSite; public class XloveCam extends AbstractSite { - public static String baseUrl = "https://mobile.xlovecam.com"; + public static String baseUrl = "https://www.xlovecam.com"; + public static String mobileUrl = "https://mobile.xlovecam.com"; private HttpClient httpClient; @Override @@ -28,7 +31,7 @@ public class XloveCam extends AbstractSite { @Override public String getBaseUrl() { - return baseUrl; + return mobileUrl; } @Override @@ -109,7 +112,7 @@ public class XloveCam extends AbstractSite { @Override public boolean credentialsAvailable() { - return false; + return StringUtil.isNotBlank(Config.getInstance().getSettings().xlovecamUsername); } @Override diff --git a/common/src/main/java/ctbrec/sites/xlovecam/XloveCamHttpClient.java b/common/src/main/java/ctbrec/sites/xlovecam/XloveCamHttpClient.java index 56176376..0485c323 100644 --- a/common/src/main/java/ctbrec/sites/xlovecam/XloveCamHttpClient.java +++ b/common/src/main/java/ctbrec/sites/xlovecam/XloveCamHttpClient.java @@ -1,17 +1,201 @@ package ctbrec.sites.xlovecam; -import java.io.IOException; +import static ctbrec.io.HttpConstants.*; +import static java.nio.charset.StandardCharsets.*; +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Locale; +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; public class XloveCamHttpClient extends HttpClient { + private static final Logger LOG = LoggerFactory.getLogger(XloveCamHttpClient.class); + private static final Pattern CSRF_PATTERN = Pattern.compile("CSRFToken\s*=\s*\"(.*?)\";"); + private final Random rng = new Random(); + public XloveCamHttpClient() { super("xlovecam"); } @Override public boolean login() throws IOException { - return false; + String username = Config.getInstance().getSettings().xlovecamUsername; + String csrfToken = getCsrfToken(); + JSONObject config = getConfig(); + String token = config.getString("token"); + byte[] passwordKey = getPasswordKey(config); + byte[] encryptedPassword = encryptPassword(Config.getInstance().getSettings().xlovecamPassword, passwordKey); + String base64EncryptedPassword = Base64.getEncoder().encodeToString(encryptedPassword); + LOG.debug("csrf:{} token:{} key:{}", csrfToken, token, Arrays.toString(passwordKey)); + LOG.debug("encrypted password: {}", base64EncryptedPassword); + + long time = System.currentTimeMillis() / 1000; + long rnd = rng.nextInt(100_000_000); + String rndTokenString = token + ':' + Long.toString(rnd) + ":\u20ac\u2716\u21aa:" + time + ':' + username; + long rndToken = fnv32a(rndTokenString.getBytes(UTF_8)); + + RequestBody body = new FormBody.Builder() // @formatter:off + .add("pseudo", username) + .add("passwd", "") + .add("keep", "1") + .add("isLayer", "1") + .add("token", token) + .add("grecaptchaLoaded", "0") + .add("extra[screen][width]", "1920") + .add("extra[screen][height]", "1080") + .add("extra[tmz]", "0") + .add("extra[blng]", "en") + .add("extra[clientTimezone]", "GMT") + .add("extra[flash][installed]", "false") + .add("extra[flash][raw]", "") + .add("extra[flash][major]", "-1") + .add("extra[flash][minor]", "-1") + .add("extra[flash][revision]", "-1") + .add("extra[flash][revisionStr]", "") + .add("extra[rnd][rnd]", Long.toString(rnd)) + .add("extra[rnd][time]", Long.toString(time)) + .add("extra[rnd][token]", Integer.toString((int)rndToken)) + .add("csrf", csrfToken) + .add("passwdEncoded", "b64:" + base64EncryptedPassword) + .add("g-recaptcha-response", "") + .build(); // @formatter:on + + String url = XloveCam.baseUrl + "/en/ajax/client/login2"; + Request req = new Request.Builder() + .url(url) + .method("POST", body) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(REFERER, XloveCam.baseUrl + "/en/login") + .header(ORIGIN, XloveCam.baseUrl) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(CONTENT_TYPE, FORM_ENCODED) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + try (Response resp = execute(req)) { + if (resp.isSuccessful()) { + String msg = resp.body().string(); + JSONObject json = new JSONObject(msg); + LOG.debug(json.toString(2)); + return json.optBoolean("success"); + } else { + throw new HttpException(resp.code(), resp.message()); + } + } + } + + private long fnv32a(byte[] a) { + long b = 2166136261l; + for (int d = 0; d < a.length; d++) { + b ^= (a[d] & 0xFF); + b += (b << 1) + (b << 4) + (b << 7) + (b << 8) + (b << 24) & 4294967295l; + } + return b >>> 0; + } + + private byte[] encryptPassword(String xlovecamPassword, byte[] passwordKey) { + byte[] password = xlovecamPassword.getBytes(UTF_8); + byte d = (byte) password.length; + byte[] c = new byte[password.length + rng.nextInt(20)]; + System.arraycopy(password, 0, c, 0, password.length); + for (int i = password.length; i < c.length; i++) { + c[i] = (byte)rng.nextInt(255); + } + + byte[] b = new byte[c.length + 1]; + b[0] = (byte)(d ^ (passwordKey[0] & 0xFF)); + + d = 1; + for (int e = 0; e < c.length; e++) { + b[e+1] = (byte)(c[e] ^ passwordKey[d]); + d++; + if(d >= passwordKey.length) { + d = 0; + } + } + return b; + } + + private byte[] getPasswordKey(JSONObject config) { + String passwordKeyString = config.getString("passwordKey"); + LOG.debug(passwordKeyString); + String[] numbers = passwordKeyString.split(","); + byte[] passwordKey = new byte[numbers.length]; + for (int i = 0; i < numbers.length; i++) { + passwordKey[i] = (byte)(Integer.parseInt(numbers[i]) & 0xFF); + } + return passwordKey; + } + + private String getCsrfToken() throws IOException { + Request req = new Request.Builder() + .url(XloveCam.baseUrl) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response resp = execute(req)) { + if (resp.isSuccessful()) { + String body = resp.body().string(); + Matcher m = CSRF_PATTERN.matcher(body); + if (m.find()) { + return m.group(1); + } else { + throw new IOException("CSRF token not found in landing page"); + } + } else { + throw new HttpException(resp.code(), resp.message()); + } + } + } + + private JSONObject getConfig() throws IOException { + String url = XloveCam.baseUrl + "/en/popup/login"; + LOG.debug("Calling {}", url); + RequestBody body = new FormBody.Builder() + .add("referrer", "https://www.xlovecam.com/en/") + .add("referrer_is_layer", "0") + .add("referrer_model_id", "") + .add("referrer_model_name", "") + .build(); + Request req = new Request.Builder() + .url(url) + .method("POST", body) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(REFERER, XloveCam.baseUrl) + .header(ORIGIN, XloveCam.baseUrl) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(CONTENT_TYPE, FORM_ENCODED) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + try (Response resp = execute(req)) { + if (resp.isSuccessful()) { + String msg = resp.body().string(); + JSONObject json = new JSONObject(msg); + if (json.has("config")) { + return json.getJSONObject("config"); + } else { + throw new IOException("JSON doesn't contain \"config\":\n" + json.toString(2)); + } + } else { + throw new HttpException(resp.code(), resp.message()); + } + } } } diff --git a/common/src/main/java/ctbrec/sites/xlovecam/XloveCamModel.java b/common/src/main/java/ctbrec/sites/xlovecam/XloveCamModel.java index 4d631e28..3e347dca 100644 --- a/common/src/main/java/ctbrec/sites/xlovecam/XloveCamModel.java +++ b/common/src/main/java/ctbrec/sites/xlovecam/XloveCamModel.java @@ -116,7 +116,7 @@ public class XloveCamModel extends AbstractModel { } private String getModelPage() throws IOException { - String url = XloveCam.baseUrl + "/en/model/" + getName() + '/'; + String url = XloveCam.mobileUrl + "/en/model/" + getName() + '/'; Request req = new Request.Builder() .url(url) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)