From f2e2b357be72396154e200658e952753bc38c17b Mon Sep 17 00:00:00 2001 From: hyper Date: Tue, 9 Jun 2026 19:19:24 +0300 Subject: [PATCH] Add in-app UI to paste site cookies for logged-in ripping Stores cookies in config-dir sidecar files with Network-tab paste instructions. Fixes JWT/JSON cookie parsing and wires Http to load per-domain cookies. --- .../ripme/ui/CookieSettingsDialog.java | 104 ++++++++++++++++++ .../com/rarchives/ripme/ui/MainWindow.java | 10 ++ .../java/com/rarchives/ripme/utils/Http.java | 9 +- .../com/rarchives/ripme/utils/RipUtils.java | 12 +- .../ripme/utils/SiteCookieStorage.java | 64 +++++++++++ src/main/resources/LabelsBundle.properties | 13 +++ .../ripme/tst/SiteCookieStorageTest.java | 56 ++++++++++ 7 files changed, 264 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/rarchives/ripme/ui/CookieSettingsDialog.java create mode 100644 src/main/java/com/rarchives/ripme/utils/SiteCookieStorage.java create mode 100644 src/test/java/com/rarchives/ripme/tst/SiteCookieStorageTest.java diff --git a/src/main/java/com/rarchives/ripme/ui/CookieSettingsDialog.java b/src/main/java/com/rarchives/ripme/ui/CookieSettingsDialog.java new file mode 100644 index 000000000..e3c4b4c7f --- /dev/null +++ b/src/main/java/com/rarchives/ripme/ui/CookieSettingsDialog.java @@ -0,0 +1,104 @@ +package com.rarchives.ripme.ui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.Frame; +import java.io.IOException; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import com.rarchives.ripme.utils.SiteCookieStorage; +import com.rarchives.ripme.utils.Utils; + +public class CookieSettingsDialog extends JDialog { + + private final JTextField domainField; + private final JTextArea cookieArea; + + public CookieSettingsDialog(Frame owner) { + super(owner, Utils.getLocalizedString("cookies.dialog.title"), true); + setLayout(new BorderLayout(8, 8)); + + JPanel top = new JPanel(new BorderLayout(4, 4)); + top.add(new JLabel(Utils.getLocalizedString("cookies.domain")), BorderLayout.WEST); + domainField = new JTextField("reddit.com", 24); + top.add(domainField, BorderLayout.CENTER); + add(top, BorderLayout.NORTH); + + cookieArea = new JTextArea(8, 50); + cookieArea.setLineWrap(true); + cookieArea.setWrapStyleWord(true); + add(new JScrollPane(cookieArea), BorderLayout.CENTER); + + JPanel bottom = new JPanel(new BorderLayout(4, 4)); + bottom.add(new JLabel("" + Utils.getLocalizedString("cookies.help") + ""), BorderLayout.NORTH); + JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton saveButton = new JButton(Utils.getLocalizedString("cookies.save")); + JButton clearButton = new JButton(Utils.getLocalizedString("cookies.clear")); + JButton closeButton = new JButton(Utils.getLocalizedString("cookies.close")); + buttons.add(saveButton); + buttons.add(clearButton); + buttons.add(closeButton); + bottom.add(buttons, BorderLayout.SOUTH); + add(bottom, BorderLayout.SOUTH); + + saveButton.addActionListener(e -> saveCookies()); + clearButton.addActionListener(e -> clearCookies()); + closeButton.addActionListener(e -> dispose()); + + pack(); + setLocationRelativeTo(owner); + } + + public void loadDomain(String domain) { + domainField.setText(domain); + try { + cookieArea.setText(SiteCookieStorage.load(domain)); + } catch (IOException ex) { + cookieArea.setText(""); + } + } + + private void saveCookies() { + String domain = domainField.getText(); + if (SiteCookieStorage.normalizeDomain(domain).isEmpty()) { + JOptionPane.showMessageDialog(this, Utils.getLocalizedString("cookies.domain.required"), + Utils.getLocalizedString("cookies.dialog.title"), JOptionPane.WARNING_MESSAGE); + return; + } + try { + SiteCookieStorage.save(domain, cookieArea.getText()); + JOptionPane.showMessageDialog(this, Utils.getLocalizedString("cookies.saved"), + Utils.getLocalizedString("cookies.dialog.title"), JOptionPane.INFORMATION_MESSAGE); + } catch (IllegalArgumentException ex) { + JOptionPane.showMessageDialog(this, Utils.getLocalizedString("cookies.invalid"), + Utils.getLocalizedString("cookies.dialog.title"), JOptionPane.WARNING_MESSAGE); + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, ex.getMessage(), + Utils.getLocalizedString("cookies.dialog.title"), JOptionPane.ERROR_MESSAGE); + } + } + + private void clearCookies() { + String domain = domainField.getText(); + if (SiteCookieStorage.normalizeDomain(domain).isEmpty()) { + return; + } + try { + SiteCookieStorage.clear(domain); + cookieArea.setText(""); + JOptionPane.showMessageDialog(this, Utils.getLocalizedString("cookies.cleared"), + Utils.getLocalizedString("cookies.dialog.title"), JOptionPane.INFORMATION_MESSAGE); + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, ex.getMessage(), + Utils.getLocalizedString("cookies.dialog.title"), JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/src/main/java/com/rarchives/ripme/ui/MainWindow.java b/src/main/java/com/rarchives/ripme/ui/MainWindow.java index 35357830c..f41e695e8 100644 --- a/src/main/java/com/rarchives/ripme/ui/MainWindow.java +++ b/src/main/java/com/rarchives/ripme/ui/MainWindow.java @@ -119,6 +119,7 @@ public final class MainWindow implements Runnable, RipStatusHandler { private static JLabel configRetrySleepLabel; // This doesn't really belong here but I have no idea where else to put it private static JButton configUrlFileChooserButton; + private static JButton configCookiesButton; private static TrayIcon trayIcon; private static MenuItem trayMenuMain; @@ -598,6 +599,7 @@ public void setValueAt(Object value, int row, int col) { configSaveDirLabel.setToolTipText(configSaveDirLabel.getText()); configSaveDirLabel.setHorizontalAlignment(JLabel.RIGHT); configSaveDirButton = new JButton(Utils.getLocalizedString("select.save.dir") + "..."); + configCookiesButton = new JButton(Utils.getLocalizedString("cookies.configure")); var idx = 0; addItemToConfigGridBagConstraints(gbc, idx++, configUpdateLabel, configUpdateButton); @@ -615,6 +617,14 @@ public void setValueAt(Object value, int row, int col) { addItemToConfigGridBagConstraints(gbc, idx++, configSSLVerifyOff, configSSLVerifyOff); addItemToConfigGridBagConstraints(gbc, idx++, configSelectLangComboBox, configUrlFileChooserButton); addItemToConfigGridBagConstraints(gbc, idx++, configSaveDirLabel, configSaveDirButton); + addItemToConfigGridBagConstraints(gbc, idx++, + new JLabel(Utils.getLocalizedString("cookies.configure"), JLabel.RIGHT), configCookiesButton); + + configCookiesButton.addActionListener(event -> { + CookieSettingsDialog dialog = new CookieSettingsDialog(mainFrame); + dialog.loadDomain("reddit.com"); + dialog.setVisible(true); + }); emptyPanel = new JPanel(); emptyPanel.setPreferredSize(new Dimension(0, 0)); diff --git a/src/main/java/com/rarchives/ripme/utils/Http.java b/src/main/java/com/rarchives/ripme/utils/Http.java index a1705f5a9..a09964720 100644 --- a/src/main/java/com/rarchives/ripme/utils/Http.java +++ b/src/main/java/com/rarchives/ripme/utils/Http.java @@ -90,7 +90,12 @@ private Map cookiesForURL(String u) { String domain = String.join(".", parts); // Try to get cookies for this host from config logger.info("Trying to load cookies from config for " + domain); - cookieStr = Utils.getConfigString("cookies." + domain, ""); + try { + cookieStr = SiteCookieStorage.load(domain); + } catch (IOException e) { + logger.warn("Failed to load cookies for {}", domain, e); + cookieStr = ""; + } if (!cookieStr.equals("")) { cookieDomain = domain; // we found something, start parsing @@ -215,7 +220,7 @@ public Response response() throws IOException { int status = ex.getStatusCode(); if (status == 401 || status == 403) { - throw new IOException("Failed to load " + url + ": Status Code " + status + ". You might be able to circumvent this error by setting cookies for this domain", e); + throw new IOException("Failed to load " + url + ": Status Code " + status + ". You might be able to circumvent this error by setting cookies for this domain (Configuration → Site cookies...)", e); } if (status == 404) { throw new IOException("File not found " + url + ": Status Code " + status + ". ", e); diff --git a/src/main/java/com/rarchives/ripme/utils/RipUtils.java b/src/main/java/com/rarchives/ripme/utils/RipUtils.java index 4fee248da..7e026f509 100644 --- a/src/main/java/com/rarchives/ripme/utils/RipUtils.java +++ b/src/main/java/com/rarchives/ripme/utils/RipUtils.java @@ -287,9 +287,17 @@ else if (album.length() == 5 || album.length() == 6) { */ public static Map getCookiesFromString(String line) { Map cookies = new HashMap<>(); + line = SiteCookieStorage.normalizeCookieString(line); for (String pair : line.split(";")) { - String[] kv = pair.split("="); - cookies.put(kv[0].trim(), kv[1]); + pair = pair.trim(); + if (!pair.contains("=")) { + continue; + } + String[] kv = pair.split("=", 2); + String name = kv[0].trim(); + if (!name.isEmpty()) { + cookies.put(name, kv[1]); + } } return cookies; } diff --git a/src/main/java/com/rarchives/ripme/utils/SiteCookieStorage.java b/src/main/java/com/rarchives/ripme/utils/SiteCookieStorage.java new file mode 100644 index 000000000..38424969b --- /dev/null +++ b/src/main/java/com/rarchives/ripme/utils/SiteCookieStorage.java @@ -0,0 +1,64 @@ +package com.rarchives.ripme.utils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +/** + * Stores per-domain cookie strings in the config directory, outside rip.properties. + */ +public final class SiteCookieStorage { + + private SiteCookieStorage() { + } + + public static String normalizeDomain(String domain) { + String normalized = domain.trim().toLowerCase(); + if (normalized.startsWith("www.")) { + normalized = normalized.substring(4); + } + return normalized; + } + + public static String normalizeCookieString(String raw) { + String normalized = raw.trim(); + if (normalized.regionMatches(true, 0, "cookie:", 0, 7)) { + normalized = normalized.substring(7).trim(); + } + return normalized; + } + + public static Path cookieFilePath(String domain) { + return Paths.get(Utils.getConfigDir(), "cookies." + normalizeDomain(domain)); + } + + public static String load(String domain) throws IOException { + String normalizedDomain = normalizeDomain(domain); + String fromConfig = Utils.getConfigString("cookies." + normalizedDomain, ""); + if (!fromConfig.isEmpty()) { + return fromConfig; + } + Path cookieFile = cookieFilePath(normalizedDomain); + if (!Files.exists(cookieFile)) { + return ""; + } + return Files.readString(cookieFile).trim(); + } + + public static void save(String domain, String raw) throws IOException { + String cookieString = normalizeCookieString(raw); + Map cookies = RipUtils.getCookiesFromString(cookieString); + if (cookies.isEmpty()) { + throw new IllegalArgumentException("No valid name=value cookie pairs found"); + } + Path configDir = Paths.get(Utils.getConfigDir()); + Files.createDirectories(configDir); + Files.writeString(cookieFilePath(domain), cookieString); + } + + public static void clear(String domain) throws IOException { + Files.deleteIfExists(cookieFilePath(domain)); + } +} diff --git a/src/main/resources/LabelsBundle.properties b/src/main/resources/LabelsBundle.properties index 6a48b245e..30aaffbab 100644 --- a/src/main/resources/LabelsBundle.properties +++ b/src/main/resources/LabelsBundle.properties @@ -29,6 +29,19 @@ remember.url.history = Remember URL history ssl.verify.off = SSL verify off loading.history.from = Loading history from +# Site cookies +cookies.configure = Site cookies... +cookies.dialog.title = Site cookies +cookies.domain = Domain: +cookies.help = How to copy cookies (Chrome, Edge, Firefox, Safari):
1. Log into the site in your browser.
2. Open developer tools → Network tab.
3. Reload the page.
4. Click a request to the site (e.g. www.reddit.com).
5. Under Request Headers, copy the full Cookie value.
6. Paste below. Domain: reddit.com. +cookies.save = Save +cookies.clear = Clear +cookies.close = Close +cookies.saved = Cookies saved. +cookies.cleared = Cookies cleared. +cookies.invalid = No valid name=value pairs found. +cookies.domain.required = Enter a domain (e.g. reddit.com). + # Queue keys queue.remove.all = Remove All queue.validation = Are you sure you want to remove all elements from the queue? diff --git a/src/test/java/com/rarchives/ripme/tst/SiteCookieStorageTest.java b/src/test/java/com/rarchives/ripme/tst/SiteCookieStorageTest.java new file mode 100644 index 000000000..a9b2ac860 --- /dev/null +++ b/src/test/java/com/rarchives/ripme/tst/SiteCookieStorageTest.java @@ -0,0 +1,56 @@ +package com.rarchives.ripme.tst; + +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.rarchives.ripme.utils.RipUtils; +import com.rarchives.ripme.utils.SiteCookieStorage; + +public class SiteCookieStorageTest { + + @Test + public void testNormalizeDomainStripsWww() { + Assertions.assertEquals("reddit.com", SiteCookieStorage.normalizeDomain("www.reddit.com")); + } + + @Test + public void testNormalizeCookieStringStripsCookiePrefix() { + Assertions.assertEquals("a=b; c=d", SiteCookieStorage.normalizeCookieString("Cookie: a=b; c=d")); + } + + @Test + public void testGetCookiesFromStringSplitsOnFirstEquals() { + Map cookies = RipUtils.getCookiesFromString("token_v2=abc=def; reddit_session=xyz"); + Assertions.assertEquals("abc=def", cookies.get("token_v2")); + Assertions.assertEquals("xyz", cookies.get("reddit_session")); + } + + @Test + public void testGetCookiesFromStringParsesJwtLikeValues() { + String jwtLike = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.signature=with=equals"; + Map cookies = RipUtils.getCookiesFromString("reddit_session=" + jwtLike); + Assertions.assertEquals(jwtLike, cookies.get("reddit_session")); + } + + @Test + public void testGetCookiesFromStringParsesJsonCookieValue() { + String line = "g_state={\"i_l\":0,\"i_ll\":1781015544601}; csv=2; pc=xg"; + Map cookies = RipUtils.getCookiesFromString(line); + Assertions.assertEquals("2", cookies.get("csv")); + Assertions.assertEquals("xg", cookies.get("pc")); + Assertions.assertEquals("{\"i_l\":0,\"i_ll\":1781015544601}", cookies.get("g_state")); + } + + @Test + public void testGetCookiesFromStringParsesNetworkHeaderStyleLine() { + String line = "edgebucket=abc; loid=loidval; reddit_session=jwt=a=b=c; token_v2=tok=x=y=z; seeker_session=true"; + Map cookies = RipUtils.getCookiesFromString(line); + Assertions.assertEquals(5, cookies.size()); + Assertions.assertEquals("abc", cookies.get("edgebucket")); + Assertions.assertEquals("jwt=a=b=c", cookies.get("reddit_session")); + Assertions.assertEquals("tok=x=y=z", cookies.get("token_v2")); + Assertions.assertEquals("true", cookies.get("seeker_session")); + } +}