Support for ftps

This commit is contained in:
Alexander Rosenberg 2024-09-14 23:51:17 -07:00
parent 240e7f3677
commit d76fda9bfd
Signed by: Zander671
GPG Key ID: 5FD0394ADBD72730
8 changed files with 262 additions and 109 deletions

View File

@ -117,8 +117,9 @@ public class Library {
private LocalCache localCache;
private RemoteStore remoteStore;
public Library(File local, URI remote, Secrets secrets) throws ManifestParseException, IOException {
public Library(File local, URI remote, boolean passiveMode, Secrets secrets) throws ManifestParseException, IOException {
remoteStore = new RemoteStore(remote, secrets);
remoteStore.setPassiveMode(passiveMode);
localCache = new LocalCache(local);
}

View File

@ -7,6 +7,7 @@ import java.net.ProtocolException;
import java.net.URI;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
@ -34,6 +35,7 @@ public class RemoteStore {
private final FTPClient ftp;
private int connectionLevel = 0;
private boolean ignoreManifest = false;
private boolean passiveMode;
public RemoteStoreFileTypeHandler fileTypeHandler = new RemoteStoreFileTypeHandler();
@ -42,10 +44,13 @@ public class RemoteStore {
if (!url.isAbsolute() || url.getScheme().equals("ftp")) {
ftp = new FTPClient();
} else if (url.getScheme().equals("ftps")) {
ftp = new FTPSClient(false);
ftp = new FTPSClient("TLS", false);
((FTPSClient) ftp).setEndpointCheckingEnabled(true);
} else {
throw new ProtocolException("unknown protocol: '" + url.getScheme() + "'");
}
ftp.addProtocolCommandListener(new PrintCommandListener(System.out, true));
ftp.setListHiddenFiles(true);
this.secrets = secrets;
}
@ -61,6 +66,24 @@ public class RemoteStore {
this.ignoreManifest = ignoreManifest;
}
public boolean isPassiveMode() {
return passiveMode;
}
public void setPassiveMode(boolean passiveMode) throws IOException {
this.passiveMode = passiveMode;
if (connectionLevel != 0) {
String modeString = passiveMode ? "passive" : "active";
LOGGER.info("Switching to " + modeString + " mode");
if (passiveMode) {
ftp.enterLocalPassiveMode();
} else {
ftp.enterLocalActiveMode();
}
checkFtpResponse("Failed to switch to " + modeString + " mode");
}
}
public void fetchManifest() throws IOException {
doFtpActions(() -> {
FTPFile info = fileInformation(MANIFEST_PATH);
@ -199,10 +222,31 @@ public class RemoteStore {
return fileInformation(path) != null;
}
private boolean isRootDir(String path) {
return path.matches("^(/+\\.{0,2})+$");
}
public FTPFile fileInformation(String path) throws IOException {
if (isRootDir(path)) {
return null;
}
AtomicReference<FTPFile> file = new AtomicReference<FTPFile>();
doFtpActions(() -> {
file.set(ftp.mlistFile(resolvePath(path)));
String pp = getParentPath(path);
if (pp == null) {
pp = getBasePath();
}
String name = getFileName(path);
FTPFile[] files = ftp.listFiles(pp);
System.out.println("STAT " + name + " in " + pp);
for (FTPFile f : files) {
System.out.println(
(f.isDirectory() ? "D " : "F ") +
f.getName());
if (f.getName().equals(name)) {
file.set(f);
}
}
});
return file.get();
}
@ -216,6 +260,14 @@ public class RemoteStore {
throw new IOException("Invalid ftp secrets! Username: '" + secrets.getUsername() + "'");
}
LOGGER.info("Logged info ftp as user '{}'", secrets.getUsername());
if (ftp instanceof FTPSClient) {
FTPSClient ftps = (FTPSClient) ftp;
ftps.execPBSZ(0);
checkFtpResponse("Failed to send PBSZ command");
ftps.execPROT("P");
checkFtpResponse("Failed to send PROT command");
}
setPassiveMode(passiveMode);
}
}
@ -307,7 +359,10 @@ public class RemoteStore {
pb.append("/");
ftp.makeDirectory(pb.toString());
}
checkFtpResponse("Could not create directory: " + pp);
info = fileInformation(pp);
// if (info == null || !info.isDirectory()) {
// throw new IOException("Failed to create directory: " + pp);
// }
LOGGER.info("Created directory: {}", pp);
} else if (!info.isDirectory()) {
throw new IOException("Not a directory: " + pp);
@ -319,8 +374,15 @@ public class RemoteStore {
if (ftp.isConnected()) {
int r = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(r)) {
LOGGER.info(error);
throw new IOException(error);
String replyString = ftp.getReplyString();
if (replyString.endsWith("\n")) {
replyString = replyString.substring(0, replyString.length() - 1);
}
if (replyString.endsWith(".")) {
replyString = replyString.substring(0, replyString.length() - 1);
}
LOGGER.info("{}: {}", error, replyString);
throw new IOException(error + ": " + replyString);
}
}
}
@ -365,6 +427,19 @@ public class RemoteStore {
if (i == -1) {
return null;
}
return cp.substring(0, i);
String pp = cp.substring(0, i);
while (!pp.isEmpty() && pp.charAt(pp.length() - 1) == '/') {
pp = pp.substring(0, pp.length() - 1);
}
return pp;
}
private static String getFileName(String path) {
String cp = cleanPath(path);
int i = cp.lastIndexOf("/");
if (i == -1) {
return cp;
}
return cp.substring(i + 1);
}
}

View File

@ -33,7 +33,7 @@ public class PassSecretsFactory extends SecretsFactory {
public static boolean isBackendSupported() {
try {
Process p = Runtime.getRuntime().exec("which pass");
Process p = Runtime.getRuntime().exec(new String[] {"which", "pass"});
if (!p.waitFor(1000, TimeUnit.MILLISECONDS)) {
return false;
}

View File

@ -12,6 +12,7 @@ import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
@ -44,6 +45,8 @@ public class LibraryCreateDialog extends JDialog {
private final JTextField usernameField;
private final JLabel passwordLabel;
private final JPasswordField passwordField;
private final JLabel ftpModeLabel;
private final JComboBox<String> ftpModeCombo;
private final JCheckBox defaultPathCheck;
private final JLabel pathLabel;
private final FileChooserField pathField;
@ -66,6 +69,8 @@ public class LibraryCreateDialog extends JDialog {
usernameField = new JTextField(10);
passwordLabel = new JLabel("Password:");
passwordField = new JPasswordField(10);
ftpModeLabel = new JLabel("FTP Mode:");
ftpModeCombo = new JComboBox<String>(new String[] { "Active", "Passive" });
defaultPathCheck = new JCheckBox("Use default library location");
defaultPathCheck.setSelected(true);
pathLabel = new JLabel("Library Path:");
@ -223,12 +228,21 @@ public class LibraryCreateDialog extends JDialog {
entryPanel.add(passwordField, gbc);
gbc.gridy = 5;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(ftpModeLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.LINE_START;
entryPanel.add(ftpModeCombo, gbc);
gbc.gridy = 6;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.CENTER;
gbc.fill = GridBagConstraints.NONE;
gbc.gridwidth = 2;
entryPanel.add(defaultPathCheck, gbc);
gbc.gridwidth = 1;
gbc.gridy = 6;
gbc.gridy = 7;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(pathLabel, gbc);
@ -266,4 +280,8 @@ public class LibraryCreateDialog extends JDialog {
return pathField.getPathField().getText();
}
public boolean isFtpPassiveMode() {
return ftpModeCombo.getSelectedItem().equals("Passive");
}
}

View File

@ -12,6 +12,7 @@ import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
@ -44,6 +45,8 @@ public class LibraryEditDialog extends JDialog {
private final JTextField usernameField;
private final JLabel passwordLabel;
private final JPasswordField passwordField;
private final JLabel ftpModeLabel;
private final JComboBox<String> ftpModeCombo;
private final JCheckBox defaultPathCheck;
private final JLabel pathLabel;
private final FileChooserField pathField;
@ -55,7 +58,9 @@ public class LibraryEditDialog extends JDialog {
private final JPanel buttonPanel;
private final JPanel panel;
public LibraryEditDialog(Window parent, String name, String url, String ftp, String username, String password, String file, Runnable cacheClearCallback) {
public LibraryEditDialog(Window parent, String name, String url, String ftp,
String username, String password, boolean passiveMode, String file,
Runnable cacheClearCallback) {
super(parent, "Edit Library");
nameLabel = new JLabel("Name:");
nameField = new JTextField(10);
@ -72,6 +77,9 @@ public class LibraryEditDialog extends JDialog {
passwordLabel = new JLabel("Password:");
passwordField = new JPasswordField(10);
passwordField.setText(password);
ftpModeLabel = new JLabel("FTP Mode:");
ftpModeCombo = new JComboBox<String>(new String[] { "Active", "Passive" });
ftpModeCombo.setSelectedIndex(passiveMode ? 1 : 0);
defaultPathCheck = new JCheckBox("Use default library location");
pathLabel = new JLabel("Library Path:");
pathField = new FileChooserField(10, file, new File(LibrarySelectFrame.DEFAULT_LIBRARY_PATH));
@ -245,11 +253,20 @@ public class LibraryEditDialog extends JDialog {
gbc.gridy = 5;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(ftpModeLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.LINE_START;
entryPanel.add(ftpModeCombo, gbc);
gbc.gridy = 6;
gbc.gridx = 0;
gbc.fill = GridBagConstraints.NONE;
gbc.anchor = GridBagConstraints.CENTER;
gbc.gridwidth = 2;
entryPanel.add(defaultPathCheck, gbc);
gbc.gridwidth = 1;
gbc.gridy = 6;
gbc.gridy = 7;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
entryPanel.add(pathLabel, gbc);
@ -287,4 +304,8 @@ public class LibraryEditDialog extends JDialog {
return pathField.getPathField().getText();
}
public boolean isFtpPassiveMode() {
return ftpModeCombo.getSelectedItem().equals("Passive");
}
}

View File

@ -10,6 +10,7 @@ import java.awt.event.KeyEvent;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
@ -39,6 +40,8 @@ public class LibraryImportDialog extends JDialog {
private final JTextField usernameField;
private final JLabel passwordLabel;
private final JPasswordField passwordField;
private final JLabel ftpModeLabel;
private final JComboBox<String> ftpModeCombo;
private final JLabel fileLabel;
private final FileChooserField fileField;
private final JPanel entryPanel;
@ -58,6 +61,8 @@ public class LibraryImportDialog extends JDialog {
usernameField = new JTextField(10);
passwordLabel = new JLabel("Password:");
passwordField = new JPasswordField(10);
ftpModeLabel = new JLabel("FTP Mode:");
ftpModeCombo = new JComboBox<String>(new String[] { "Active", "Passive" });
fileLabel = new JLabel("Source:");
fileField = new FileChooserField(10);
fileField.getChooser().setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
@ -167,6 +172,15 @@ public class LibraryImportDialog extends JDialog {
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
gbc.fill = GridBagConstraints.NONE;
entryPanel.add(ftpModeLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.LINE_START;
entryPanel.add(ftpModeCombo, gbc);
gbc.gridy = 5;
gbc.gridx = 0;
gbc.anchor = GridBagConstraints.LINE_END;
gbc.fill = GridBagConstraints.NONE;
entryPanel.add(fileLabel, gbc);
gbc.gridx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
@ -198,4 +212,8 @@ public class LibraryImportDialog extends JDialog {
return fileField.getPathField().getText();
}
public boolean isFtpPassiveMode() {
return ftpModeCombo.getSelectedItem().equals("Passive");
}
}

View File

@ -80,12 +80,15 @@ public class LibrarySelectFrame extends JFrame {
public String name;
public String url;
public String ftp;
public boolean passiveMode;
public File file;
public LibraryEntry(String name, String url, String ftp, File file) {
public LibraryEntry(String name, String url, String ftp,
boolean passiveMode, File file) {
this.name = name;
this.url = url;
this.ftp = ftp;
this.passiveMode = passiveMode;
this.file = file;
}
@ -101,7 +104,9 @@ public class LibrarySelectFrame extends JFrame {
public boolean equals(Object o) {
if (o instanceof LibraryEntry) {
LibraryEntry e = (LibraryEntry) o;
return e.name.equals(name) && e.url.equals(url) && e.file.getPath().equals(file.getPath());
return e.name.equals(name) && e.url.equals(url) &&
e.file.getPath().equals(file.getPath()) &&
e.passiveMode == passiveMode;
}
return false;
}
@ -201,7 +206,8 @@ public class LibrarySelectFrame extends JFrame {
nld.setVisible(true);
if (nld.getResponse() == LibraryCreateDialog.RESPONSE_CREATE) {
createLibrary(nld.getLibraryName(), nld.getURL(), nld.getFTP(),
nld.getUsername(), nld.getPassword(), new File(nld.getLibraryFile()));
nld.getUsername(), nld.getPassword(),
nld.isFtpPassiveMode(), new File(nld.getLibraryFile()));
}
});
importItem = new JMenuItem("Import");
@ -209,7 +215,8 @@ public class LibrarySelectFrame extends JFrame {
LibraryImportDialog lid = new LibraryImportDialog(this);
lid.setVisible(true);
if (lid.getResponse() == LibraryImportDialog.RESPONSE_IMPORT) {
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(), lid.getPassword(), new File(lid.getFile()));
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(),
lid.getPassword(), lid.isFtpPassiveMode(), new File(lid.getFile()));
}
});
listPopup = new JPopupMenu();
@ -287,7 +294,8 @@ public class LibrarySelectFrame extends JFrame {
LibraryImportDialog lid = new LibraryImportDialog(this);
lid.setVisible(true);
if (lid.getResponse() == LibraryImportDialog.RESPONSE_IMPORT) {
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(), lid.getPassword(), new File(lid.getFile()));
importLibrary(lid.getURL(), lid.getFTP(), lid.getUsername(),
lid.getPassword(), lid.isFtpPassiveMode(), new File(lid.getFile()));
}
});
newButton = new JButton("New");
@ -297,7 +305,8 @@ public class LibrarySelectFrame extends JFrame {
nld.setVisible(true);
if (nld.getResponse() == LibraryCreateDialog.RESPONSE_CREATE) {
createLibrary(nld.getLibraryName(), nld.getURL(), nld.getFTP(),
nld.getUsername(), nld.getPassword(), new File(nld.getLibraryFile()));
nld.getUsername(), nld.getPassword(),
nld.isFtpPassiveMode(), new File(nld.getLibraryFile()));
}
});
deleteButton = new JButton("Delete");
@ -468,7 +477,9 @@ public class LibrarySelectFrame extends JFrame {
}
private String formatLibraryUrl(String url) {
if (!url.matches("^https?\\:\\/\\/.*")) {
if (url.isBlank()) {
return "";
} else if (!url.matches("^https?\\:\\/\\/.*")) {
return "https://" + url;
}
return url;
@ -476,8 +487,9 @@ public class LibrarySelectFrame extends JFrame {
private void editLibrary(LibraryEntry en) {
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + en.name);
LibraryEditDialog led = new LibraryEditDialog(this, en.name, en.url, en.ftp,
s.getUsername(), s.getPassword(), en.file.getPath(),
LibraryEditDialog led = new LibraryEditDialog(this, en.name, en.url,
en.ftp, s.getUsername(), s.getPassword(),
en.passiveMode, en.file.getPath(),
() -> clearLibraryCache(en));
led.setVisible(true);
if (led.getResponse() == LibraryEditDialog.RESPONSE_SAVE) {
@ -506,6 +518,7 @@ public class LibrarySelectFrame extends JFrame {
en.name = led.getLibraryName();
en.url = formatLibraryUrl(led.getURL());
en.ftp = led.getFTP();
en.passiveMode = led.isFtpPassiveMode();
en.file = nf;
saveLibraryList();
list.repaint();
@ -548,7 +561,7 @@ public class LibrarySelectFrame extends JFrame {
settingsPutObject("last-library", en);
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + en.name);
try {
Library lib = new Library(en.file, new URI(en.ftp), s);
Library lib = new Library(en.file, new URI(en.ftp), en.passiveMode, s);
LibrarySyncDialog lsd = new LibrarySyncDialog(null, lib);
if (lsd.sync() == LibrarySyncDialog.STATUS_OK) {
return lib;
@ -622,7 +635,8 @@ public class LibrarySelectFrame extends JFrame {
}
}
private void importLibrary(String url, String ftp, String username, String password, File dir) {
private void importLibrary(String url, String ftp, String username,
String password, boolean passiveMode, File dir) {
File cf = null;
try {
cf = dir.getCanonicalFile();
@ -640,7 +654,7 @@ public class LibrarySelectFrame extends JFrame {
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + name);
s.setUsername(username);
s.setPassword(password);
LibraryEntry en = new LibraryEntry(name, url, ftp, cf);
LibraryEntry en = new LibraryEntry(name, url, ftp, passiveMode, cf);
LIBRARY_ENTRIES.add(0, en);
openLibrary(en, false);
}
@ -702,7 +716,8 @@ public class LibrarySelectFrame extends JFrame {
return true;
}
private void createLibrary(String name, String url, String ftp, String username, String password, File file) {
private void createLibrary(String name, String url, String ftp,
String username, String password, boolean passiveMode, File file) {
File cf = null;
try {
cf = file.getCanonicalFile();
@ -726,7 +741,7 @@ public class LibrarySelectFrame extends JFrame {
recursiveDelete(cf);
break;
case LibraryExistsDialog.RESPONSE_IMPORT:
importLibrary(url, ftp, username, password, file);
importLibrary(url, ftp, username, password, passiveMode, file);
return;
default:
return;
@ -756,7 +771,7 @@ public class LibrarySelectFrame extends JFrame {
Secrets s = SECRETS_FACTORY.createSecrets(SECRETS_KEY + name);
s.setUsername(username);
s.setPassword(password);
LibraryEntry en = new LibraryEntry(name, url, ftp, cf);
LibraryEntry en = new LibraryEntry(name, url, ftp, passiveMode, cf);
LIBRARY_ENTRIES.add(0, en);
openLibrary(en, false);
}

View File

@ -3,6 +3,8 @@ package zander.ui.media.map;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
@ -128,9 +130,12 @@ public class OSMLocationEncoder implements LocationEncoder {
private static URL getURLForQuery(String query) {
try {
final String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
return new URL("https://nominatim.openstreetmap.org/search.php?format=jsonv2&q=" + encodedQuery);
} catch (MalformedURLException e) {
final String encodedQuery = URLEncoder.encode(query,
StandardCharsets.UTF_8);
return new URI(
"https://nominatim.openstreetmap.org/search.php?format=jsonv2&q=" +
encodedQuery).toURL();
} catch (URISyntaxException | MalformedURLException e) {
LOGGER.error("Could not generate url for query: '{}'", query, e);
throw new Error("Could not create URL!", e);
}