Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/main/java/com/rarchives/ripme/ui/CookieSettingsDialog.java
Original file line number Diff line number Diff line change
@@ -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("<html>" + Utils.getLocalizedString("cookies.help") + "</html>"), 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);
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/rarchives/ripme/ui/MainWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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));
Expand Down
9 changes: 7 additions & 2 deletions src/main/java/com/rarchives/ripme/utils/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ private Map<String, String> 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
Expand Down Expand Up @@ -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);
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/com/rarchives/ripme/utils/RipUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,17 @@ else if (album.length() == 5 || album.length() == 6) {
*/
public static Map<String, String> getCookiesFromString(String line) {
Map<String,String> 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;
}
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/com/rarchives/ripme/utils/SiteCookieStorage.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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));
}
}
13 changes: 13 additions & 0 deletions src/main/resources/LabelsBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <b>How to copy cookies (Chrome, Edge, Firefox, Safari):</b><br>1. Log into the site in your browser.<br>2. Open developer tools &rarr; <b>Network</b> tab.<br>3. Reload the page.<br>4. Click a request to the site (e.g. www.reddit.com).<br>5. Under <b>Request Headers</b>, copy the full <b>Cookie</b> value.<br>6. Paste below. Domain: <b>reddit.com</b>.
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?
Expand Down
56 changes: 56 additions & 0 deletions src/test/java/com/rarchives/ripme/tst/SiteCookieStorageTest.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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"));
}
}