mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-31 06:05:12 +00:00
refactor: migrate SSHD Core to 1.0.0
. SAF is unavailable now.
This commit is contained in:
committed by
Albert Vaca Cintora
parent
6783f0a167
commit
e37a519e3a
@@ -132,7 +132,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.android.desugarJdkLibs)
|
||||
coreLibraryDesugaring(libs.android.desugarJdkLibsNio)
|
||||
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
|
@@ -32,13 +32,13 @@ reactiveStreams = "1.0.4"
|
||||
recyclerview = "1.3.2"
|
||||
rxjava = "2.2.21"
|
||||
sl4j = "2.0.4"
|
||||
sshdCore = "0.14.0"
|
||||
sshdCore = "1.0.0"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
uiToolingPreview = "1.6.7"
|
||||
univocityParsers = "2.9.1"
|
||||
|
||||
[libraries]
|
||||
android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
|
||||
android-desugarJdkLibsNio = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "androidDesugarJdkLibs" }
|
||||
android-smsmms = { module = "org.kde.invent.sredman:android-smsmms", version.ref = "androidSmsmms" }
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||
|
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.apache.sshd.common.Session;
|
||||
import org.apache.sshd.common.file.FileSystemFactory;
|
||||
import org.apache.sshd.common.file.FileSystemView;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class AndroidFileSystemFactory implements FileSystemFactory {
|
||||
final private Context context;
|
||||
final Map<String, String> roots;
|
||||
|
||||
AndroidFileSystemFactory(Context context) {
|
||||
this.context = context;
|
||||
this.roots = new HashMap<>();
|
||||
}
|
||||
|
||||
void initRoots(List<SftpPlugin.StorageInfo> storageInfoList) {
|
||||
for (SftpPlugin.StorageInfo curStorageInfo : storageInfoList) {
|
||||
if (curStorageInfo.isFileUri()) {
|
||||
if (curStorageInfo.uri.getPath() != null){
|
||||
roots.put(curStorageInfo.displayName, curStorageInfo.uri.getPath());
|
||||
}
|
||||
} else if (curStorageInfo.isContentUri()){
|
||||
roots.put(curStorageInfo.displayName, curStorageInfo.uri.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystemView createFileSystemView(final Session username) {
|
||||
return new AndroidSafFileSystemView(roots, username.getUsername(), context);
|
||||
}
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.apache.sshd.common.file.FileSystemView;
|
||||
import org.apache.sshd.common.file.SshFile;
|
||||
import org.apache.sshd.common.file.nativefs.NativeFileSystemView;
|
||||
import org.apache.sshd.common.file.nativefs.NativeSshFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
class AndroidFileSystemView extends NativeFileSystemView {
|
||||
final private String userName;
|
||||
final private Context context;
|
||||
private final Map<String, String> roots;
|
||||
private final RootFile rootFile;
|
||||
|
||||
AndroidFileSystemView(Map<String, String> roots, String currentRoot, final String userName, Context context) {
|
||||
super(userName, roots, currentRoot, File.separatorChar, true);
|
||||
this.roots = roots;
|
||||
this.userName = userName;
|
||||
this.context = context;
|
||||
this.rootFile = new RootFile( createFileList(), userName, true);
|
||||
}
|
||||
|
||||
private List<SshFile> createFileList() {
|
||||
List<SshFile> list = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : roots.entrySet()) {
|
||||
String displayName = entry.getKey();
|
||||
String path = entry.getValue();
|
||||
|
||||
list.add(createNativeSshFile(displayName, new File(path), userName));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshFile getFile(String file) {
|
||||
return getFile("/", file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshFile getFile(SshFile baseDir, String file) {
|
||||
return getFile(baseDir.getAbsolutePath(), file);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SshFile getFile(String dir, String file) {
|
||||
if (!dir.endsWith("/")) {
|
||||
dir = dir + "/";
|
||||
}
|
||||
|
||||
if (!file.startsWith("/")) {
|
||||
file = dir + file;
|
||||
}
|
||||
|
||||
String filename = NativeSshFile.getPhysicalName("/", "/", file, false);
|
||||
|
||||
if (filename.equals("/")) {
|
||||
return rootFile;
|
||||
}
|
||||
|
||||
for (String root : roots.keySet()) {
|
||||
if (filename.indexOf(root) == 1) {
|
||||
String nameWithoutRoot = filename.substring(root.length() + 1);
|
||||
String path = roots.get(root);
|
||||
|
||||
if (nameWithoutRoot.isEmpty()) {
|
||||
return createNativeSshFile(filename, new File(path), userName);
|
||||
} else {
|
||||
return createNativeSshFile(filename, new File(path, nameWithoutRoot), userName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//It's a file under / but not one covered by any Tree
|
||||
return new RootFile(new ArrayList<>(0), userName, false);
|
||||
}
|
||||
|
||||
// NativeFileSystemView.getFile(), NativeSshFile.getParentFile() and NativeSshFile.listSshFiles() call
|
||||
// createNativeSshFile to create new NativeSshFiles so override that instead of getFile() to always create an AndroidSshFile
|
||||
@Override
|
||||
public AndroidSshFile createNativeSshFile(String name, File file, String username) {
|
||||
return new AndroidSshFile(this, name, file, username, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystemView getNormalizedView() {
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
import org.apache.sshd.common.file.FileSystemView;
|
||||
import org.apache.sshd.common.file.SshFile;
|
||||
import org.apache.sshd.common.file.nativefs.NativeSshFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AndroidSafFileSystemView implements FileSystemView {
|
||||
final String userName;
|
||||
final Context context;
|
||||
private final Map<String, String> roots;
|
||||
private final RootFile rootFile;
|
||||
|
||||
AndroidSafFileSystemView(Map<String, String> roots, String userName, Context context) {
|
||||
this.roots = roots;
|
||||
this.userName = userName;
|
||||
this.context = context;
|
||||
this.rootFile = new RootFile( createFileList(), userName, true);
|
||||
}
|
||||
|
||||
private List<SshFile> createFileList() {
|
||||
List<SshFile> list = new ArrayList<>();
|
||||
for (Map.Entry<String, String> entry : roots.entrySet()) {
|
||||
String displayName = entry.getKey();
|
||||
String uri = entry.getValue();
|
||||
|
||||
Uri treeUri = Uri.parse(uri);
|
||||
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri));
|
||||
list.add(createAndroidSafSshFile(null, documentUri, File.separatorChar + displayName));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshFile getFile(String file) {
|
||||
return getFile("/", file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshFile getFile(SshFile baseDir, String file) {
|
||||
return getFile(baseDir.getAbsolutePath(), file);
|
||||
}
|
||||
|
||||
protected SshFile getFile(String dir, String file) {
|
||||
if (!dir.endsWith("/")) {
|
||||
dir = dir + "/";
|
||||
}
|
||||
|
||||
if (!file.startsWith("/")) {
|
||||
file = dir + file;
|
||||
}
|
||||
|
||||
String filename = NativeSshFile.getPhysicalName("/", "/", file, false);
|
||||
|
||||
if (filename.equals("/")) {
|
||||
return rootFile;
|
||||
}
|
||||
|
||||
for (String root : roots.keySet()) {
|
||||
if (filename.indexOf(root) == 1) {
|
||||
String nameWithoutRoot = filename.substring(root.length() + 1);
|
||||
String pathOrUri = roots.get(root);
|
||||
|
||||
Uri treeUri = Uri.parse(pathOrUri);
|
||||
if (nameWithoutRoot.isEmpty()) {
|
||||
//TreeDocument
|
||||
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri));
|
||||
|
||||
return createAndroidSafSshFile(documentUri, documentUri, filename);
|
||||
} else {
|
||||
/*
|
||||
When sharing a root document tree like "Internal Storage" documentUri looks like:
|
||||
content://com.android.externalstorage.documents/tree/primary:/document/primary:
|
||||
For a file or folder beneath that the uri looks like:
|
||||
content://com.android.externalstorage.documents/tree/primary:/document/primary:Folder/file.txt
|
||||
|
||||
Sharing a non root document tree the documentUri looks like:
|
||||
content://com.android.externalstorage.documents/tree/primary:Download/document/primary:Download
|
||||
For a file or folder beneath that the uri looks like:
|
||||
content://com.android.externalstorage.documents/tree/primary:Download/document/primary:Download/Folder/file.txt
|
||||
*/
|
||||
String treeDocumentId = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
File nameWithoutRootFile = new File(nameWithoutRoot);
|
||||
String parentSuffix = nameWithoutRootFile.getParent();
|
||||
String parentDocumentId = treeDocumentId + ("/".equals(parentSuffix) ? "" : parentSuffix);
|
||||
|
||||
Uri parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, parentDocumentId);
|
||||
|
||||
String documentId = treeDocumentId + (treeDocumentId.endsWith(":") ? nameWithoutRoot.substring(1) : nameWithoutRoot);
|
||||
Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId);
|
||||
|
||||
return createAndroidSafSshFile(parentUri, documentUri, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//It's a file under / but not one covered by any Tree
|
||||
return new RootFile(new ArrayList<>(0), userName, false);
|
||||
}
|
||||
|
||||
public AndroidSafSshFile createAndroidSafSshFile(Uri parentUri, Uri documentUri, String virtualFilename) {
|
||||
return new AndroidSafSshFile(this, parentUri, documentUri, virtualFilename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileSystemView getNormalizedView() {
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -1,480 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.sshd.common.file.SshFile;
|
||||
import org.kde.kdeconnect.Helpers.FilesHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class AndroidSafSshFile implements SshFile {
|
||||
private static final String TAG = AndroidSafSshFile.class.getSimpleName();
|
||||
|
||||
private final String virtualFileName;
|
||||
private DocumentInfo documentInfo;
|
||||
private Uri parentUri;
|
||||
private final AndroidSafFileSystemView fileSystemView;
|
||||
|
||||
AndroidSafSshFile(final AndroidSafFileSystemView fileSystemView, Uri parentUri, Uri uri, String virtualFileName) {
|
||||
this.fileSystemView = fileSystemView;
|
||||
this.parentUri = parentUri;
|
||||
this.documentInfo = new DocumentInfo(fileSystemView.context, uri);
|
||||
this.virtualFileName = virtualFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAbsolutePath() {
|
||||
return virtualFileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
/* From NativeSshFile, looks a lot like new File(virtualFileName).getName() to me */
|
||||
|
||||
// strip the last '/'
|
||||
String shortName = virtualFileName;
|
||||
int filelen = virtualFileName.length();
|
||||
if (shortName.charAt(filelen - 1) == File.separatorChar) {
|
||||
shortName = shortName.substring(0, filelen - 1);
|
||||
}
|
||||
|
||||
// return from the last '/'
|
||||
int slashIndex = shortName.lastIndexOf(File.separatorChar);
|
||||
if (slashIndex != -1) {
|
||||
shortName = shortName.substring(slashIndex + 1);
|
||||
}
|
||||
|
||||
return shortName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOwner() {
|
||||
return fileSystemView.userName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return documentInfo.isDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return documentInfo.isFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doesExist() {
|
||||
return documentInfo.exists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return documentInfo.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLastModified() {
|
||||
return documentInfo.lastModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setLastModified(long time) {
|
||||
//TODO
|
||||
/* Throws UnsupportedOperationException on API 26
|
||||
try {
|
||||
ContentValues updateValues = new ContentValues();
|
||||
updateValues.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, time);
|
||||
result = fileSystemView.context.getContentResolver().update(documentInfo.uri, updateValues, null, null) != 0;
|
||||
documentInfo.lastModified = time;
|
||||
} catch (NullPointerException ignored) {}
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable() {
|
||||
return documentInfo.canRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWritable() {
|
||||
return documentInfo.canWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExecutable() {
|
||||
return documentInfo.isDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRemovable() {
|
||||
Log.d(TAG, "isRemovable() - is this ever called?");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public SshFile getParentFile() {
|
||||
Log.d(TAG,"getParentFile() - is this ever called");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delete() {
|
||||
boolean ret;
|
||||
|
||||
try {
|
||||
ret = DocumentsContract.deleteDocument(fileSystemView.context.getContentResolver(), documentInfo.uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
ret = false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean create() {
|
||||
return create(parentUri, FilesHelper.getMimeTypeFromFile(virtualFileName), getName());
|
||||
}
|
||||
|
||||
private boolean create(Uri parentUri, String mimeType, String name) {
|
||||
Uri uri = null;
|
||||
try {
|
||||
uri = DocumentsContract.createDocument(fileSystemView.context.getContentResolver(), parentUri, mimeType, name);
|
||||
|
||||
if (uri != null) {
|
||||
documentInfo = new DocumentInfo(fileSystemView.context, uri);
|
||||
if (!name.equals(documentInfo.displayName)) {
|
||||
delete();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {}
|
||||
|
||||
return uri != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void truncate() {
|
||||
if (documentInfo.length > 0) {
|
||||
delete();
|
||||
create();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean move(final SshFile dest) {
|
||||
boolean success = false;
|
||||
|
||||
Uri destParentUri = ((AndroidSafSshFile)dest).parentUri;
|
||||
|
||||
if (destParentUri.equals(parentUri)) {
|
||||
//Rename
|
||||
try {
|
||||
Uri newUri = DocumentsContract.renameDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, dest.getName());
|
||||
if (newUri != null) {
|
||||
success = true;
|
||||
documentInfo.uri = newUri;
|
||||
}
|
||||
} catch (FileNotFoundException ignored) {}
|
||||
} else {
|
||||
// Move:
|
||||
String sourceTreeDocumentId = DocumentsContract.getTreeDocumentId(parentUri);
|
||||
String destTreeDocumentId = DocumentsContract.getTreeDocumentId(((AndroidSafSshFile) dest).parentUri);
|
||||
|
||||
if (sourceTreeDocumentId.equals(destTreeDocumentId) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
try {
|
||||
Uri newUri = DocumentsContract.moveDocument(fileSystemView.context.getContentResolver(), documentInfo.uri, parentUri, destParentUri);
|
||||
if (newUri != null) {
|
||||
success = true;
|
||||
parentUri = destParentUri;
|
||||
documentInfo.uri = newUri;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"DocumentsContract.moveDocument() threw an exception", e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (dest.create()) {
|
||||
try (InputStream in = createInputStream(0); OutputStream out = dest.createOutputStream(0)) {
|
||||
byte[] buffer = new byte[10 * 1024];
|
||||
int read;
|
||||
|
||||
while ((read = in.read(buffer)) > 0) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
|
||||
out.flush();
|
||||
|
||||
delete();
|
||||
success = true;
|
||||
} catch (IOException e) {
|
||||
if (dest.doesExist()) {
|
||||
dest.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mkdir() {
|
||||
return create(parentUri, DocumentsContract.Document.MIME_TYPE_DIR, getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SshFile> listSshFiles() {
|
||||
if (!documentInfo.isDirectory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ContentResolver resolver = fileSystemView.context.getContentResolver();
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(documentInfo.uri, DocumentsContract.getDocumentId(documentInfo.uri));
|
||||
final ArrayList<AndroidSafSshFile> results = new ArrayList<>();
|
||||
|
||||
Cursor c = resolver.query(childrenUri, new String[]
|
||||
{ DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DISPLAY_NAME }, null, null, null);
|
||||
|
||||
while (c != null && c.moveToNext()) {
|
||||
final String documentId = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
|
||||
final String displayName = c.getString(c.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
|
||||
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(documentInfo.uri, documentId);
|
||||
results.add(new AndroidSafSshFile(fileSystemView, parentUri, documentUri, virtualFileName + File.separatorChar + displayName));
|
||||
}
|
||||
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(results);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream createOutputStream(final long offset) throws IOException {
|
||||
if (offset != 0) {
|
||||
throw new IOException("Seeking is not supported.");
|
||||
}
|
||||
return fileSystemView.context.getContentResolver().openOutputStream(documentInfo.uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream createInputStream(final long offset) throws IOException {
|
||||
InputStream s = fileSystemView.context.getContentResolver().openInputStream(documentInfo.uri);
|
||||
final long sought = s.skip(offset);
|
||||
if (sought != offset) {
|
||||
throw new IOException(String.format("Unable to seek %d bytes, sought %d bytes.", offset, sought));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClose() {
|
||||
// Nop
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Attribute, Object> getAttributes(boolean followLinks) {
|
||||
Map<SshFile.Attribute, Object> attributes = new HashMap<>();
|
||||
for (SshFile.Attribute attr : SshFile.Attribute.values()) {
|
||||
switch (attr) {
|
||||
case Uid:
|
||||
case Gid:
|
||||
case NLink:
|
||||
continue;
|
||||
}
|
||||
attributes.put(attr, getAttribute(attr, followLinks));
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(Attribute attribute, boolean followLinks) {
|
||||
Object ret;
|
||||
|
||||
switch (attribute) {
|
||||
case Size:
|
||||
ret = documentInfo.length;
|
||||
break;
|
||||
case Uid:
|
||||
case Gid:
|
||||
ret = 1;
|
||||
break;
|
||||
case Owner:
|
||||
case Group:
|
||||
ret = getOwner();
|
||||
break;
|
||||
case IsDirectory:
|
||||
ret = documentInfo.isDirectory;
|
||||
break;
|
||||
case IsRegularFile:
|
||||
ret = documentInfo.isFile;
|
||||
break;
|
||||
case IsSymbolicLink:
|
||||
ret = false;
|
||||
break;
|
||||
case Permissions:
|
||||
Set<Permission> tmp = new HashSet<>();
|
||||
if (documentInfo.canRead) {
|
||||
tmp.add(SshFile.Permission.UserRead);
|
||||
tmp.add(SshFile.Permission.GroupRead);
|
||||
tmp.add(SshFile.Permission.OthersRead);
|
||||
}
|
||||
if (documentInfo.canWrite) {
|
||||
tmp.add(SshFile.Permission.UserWrite);
|
||||
tmp.add(SshFile.Permission.GroupWrite);
|
||||
tmp.add(SshFile.Permission.OthersWrite);
|
||||
}
|
||||
if (isExecutable()) {
|
||||
tmp.add(SshFile.Permission.UserExecute);
|
||||
tmp.add(SshFile.Permission.GroupExecute);
|
||||
tmp.add(SshFile.Permission.OthersExecute);
|
||||
}
|
||||
ret = tmp.isEmpty()
|
||||
? EnumSet.noneOf(SshFile.Permission.class)
|
||||
: EnumSet.copyOf(tmp);
|
||||
break;
|
||||
case CreationTime:
|
||||
case LastModifiedTime:
|
||||
case LastAccessTime:
|
||||
ret = documentInfo.lastModified;
|
||||
break;
|
||||
case NLink:
|
||||
ret = 0;
|
||||
break;
|
||||
default:
|
||||
ret = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttributes(Map<Attribute, Object> attributes) {
|
||||
//TODO: Using Java 7 NIO it should be possible to implement setting a number of attributes but does SaF allow that?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(Attribute attribute, Object value) {}
|
||||
|
||||
@Override
|
||||
public String readSymbolicLink() throws IOException {
|
||||
throw new IOException("Not Implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createSymbolicLink(SshFile destination) throws IOException {
|
||||
throw new IOException("Not Implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all file info using 1 query to speed things up
|
||||
* The only fields guaranteed to be initialized are uri and exists
|
||||
*/
|
||||
private static class DocumentInfo {
|
||||
private Uri uri;
|
||||
private boolean exists;
|
||||
private boolean canRead;
|
||||
private boolean canWrite;
|
||||
private boolean isDirectory;
|
||||
private boolean isFile;
|
||||
private long lastModified;
|
||||
private long length;
|
||||
@Nullable
|
||||
private String displayName;
|
||||
|
||||
private static final String[] columns;
|
||||
|
||||
static {
|
||||
columns = new String[]{
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||
//DocumentsContract.Document.COLUMN_ICON,
|
||||
DocumentsContract.Document.COLUMN_FLAGS,
|
||||
DocumentsContract.Document.COLUMN_SIZE
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
Based on https://github.com/rcketscientist/DocumentActivity
|
||||
Extracted from android.support.v4.provider.DocumentsContractAPI19 and android.support.v4.provider.DocumentsContractAPI21
|
||||
*/
|
||||
private DocumentInfo(Context c, Uri uri)
|
||||
{
|
||||
this.uri = uri;
|
||||
|
||||
try (Cursor cursor = c.getContentResolver().query(uri, columns, null, null, null)) {
|
||||
exists = cursor != null && cursor.getCount() > 0;
|
||||
|
||||
if (!exists)
|
||||
return;
|
||||
|
||||
cursor.moveToFirst();
|
||||
|
||||
//String documentId = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID));
|
||||
|
||||
final boolean readPerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
final boolean writePerm = c.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
final int flags = cursor.getInt(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS));
|
||||
final boolean supportsDelete = (flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0;
|
||||
final boolean supportsCreate = (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
|
||||
final boolean supportsWrite = (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0;
|
||||
String mimeType = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE));
|
||||
final boolean hasMime = !TextUtils.isEmpty(mimeType);
|
||||
|
||||
isDirectory = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
|
||||
isFile = !isDirectory && hasMime;
|
||||
|
||||
canRead = readPerm && hasMime;
|
||||
canWrite = writePerm && (supportsDelete || (isDirectory && supportsCreate) || (hasMime && supportsWrite));
|
||||
|
||||
displayName = cursor.getString(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
|
||||
lastModified = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED));
|
||||
length = cursor.getLong(cursor.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE));
|
||||
} catch (IllegalArgumentException e) {
|
||||
//File does not exist, it's probably going to be created
|
||||
exists = false;
|
||||
canWrite = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.sshd.common.file.nativefs.NativeSshFile
|
||||
import org.kde.kdeconnect.Helpers.MediaStoreHelper
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.io.RandomAccessFile
|
||||
|
||||
internal class AndroidSshFile(
|
||||
view: AndroidFileSystemView,
|
||||
name: String,
|
||||
file: File,
|
||||
userName: String,
|
||||
private val context: Context
|
||||
) : NativeSshFile(view, name, file, userName) {
|
||||
@Throws(IOException::class)
|
||||
override fun createOutputStream(offset: Long): OutputStream {
|
||||
if (!isWritable) {
|
||||
throw IOException("No write permission : ${file.name}")
|
||||
}
|
||||
|
||||
val raf = RandomAccessFile(file, "rw")
|
||||
try {
|
||||
if (offset < raf.length()) {
|
||||
throw IOException("Your SSHFS is bugged") // SSHFS 3.0 and 3.2 cause data corruption, abort the transfer if this happens
|
||||
}
|
||||
raf.setLength(offset)
|
||||
raf.seek(offset)
|
||||
|
||||
return object : FileOutputStream(raf.fd) {
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
super.close()
|
||||
raf.close()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
raf.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(): Boolean {
|
||||
return super.delete().also {
|
||||
if (it) {
|
||||
MediaStoreHelper.indexFile(context, Uri.fromFile(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun create(): Boolean {
|
||||
return super.create().also {
|
||||
if (it) {
|
||||
MediaStoreHelper.indexFile(context, Uri.fromFile(file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/wolpi/prim-ftpd/blob/master/primitiveFTPd/src/org/primftpd/filesystem/FsFile.java
|
||||
override fun doesExist(): Boolean {
|
||||
// file.exists() returns false when we don't have read permission
|
||||
// try to figure out if it really does not exist
|
||||
try {
|
||||
return file.exists() || FileUtils.directoryContains(file.parentFile, file)
|
||||
} catch (e: IOException) {
|
||||
// An IllegalArgumentException is thrown if the parent is null or not a directory.
|
||||
Log.d(TAG, "Exception: ", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.d(TAG, "Exception: ", e)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = AndroidSshFile::class.java.simpleName
|
||||
}
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||
|
||||
import org.apache.sshd.common.KeyExchange
|
||||
import org.apache.sshd.common.NamedFactory
|
||||
import org.apache.sshd.common.digest.SHA256
|
||||
import org.apache.sshd.common.kex.AbstractDH
|
||||
import org.apache.sshd.common.kex.DH
|
||||
import org.apache.sshd.common.kex.DHGroupData
|
||||
import org.apache.sshd.server.kex.AbstractDHGServer
|
||||
|
||||
class DHG14_256 : AbstractDHGServer() {
|
||||
class Factory : NamedFactory<KeyExchange> {
|
||||
override fun getName(): String = "diffie-hellman-group14-sha256"
|
||||
|
||||
override fun create(): KeyExchange {
|
||||
return DHG14_256()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getDH(): AbstractDH {
|
||||
return DH(SHA256.Factory()).apply {
|
||||
setG(DHGroupData.getG())
|
||||
setP(DHGroupData.getP14())
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||
|
||||
import org.apache.sshd.common.digest.BuiltinDigests
|
||||
import org.apache.sshd.common.kex.AbstractDH
|
||||
import org.apache.sshd.common.kex.DHFactory
|
||||
import org.apache.sshd.common.kex.DHG
|
||||
import org.apache.sshd.common.kex.DHGroupData
|
||||
import org.apache.sshd.common.util.SecurityUtils
|
||||
import java.math.BigInteger
|
||||
|
||||
object DHG14_256Factory : DHFactory {
|
||||
override fun getName(): String = "diffie-hellman-group14-sha256"
|
||||
|
||||
override fun isSupported(): Boolean = SecurityUtils.isBouncyCastleRegistered()
|
||||
|
||||
override fun isGroupExchange(): Boolean = false
|
||||
|
||||
override fun create(vararg params: Any?): AbstractDH {
|
||||
require(params.isEmpty()) { "No accepted parameters for $name" }
|
||||
return DHG(
|
||||
BuiltinDigests.sha256,
|
||||
BigInteger(DHGroupData.getP14()),
|
||||
BigInteger(DHGroupData.getG())
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||
|
||||
import org.apache.sshd.common.file.SshFile
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.Calendar
|
||||
import java.util.Collections
|
||||
import java.util.EnumMap
|
||||
import java.util.EnumSet
|
||||
|
||||
// TODO: ls .. and ls / only show .. and / respectively I would expect a listing
|
||||
// TODO: cd .. to / does not work and prints "Can't change directory: Can't check target"
|
||||
internal class RootFile(
|
||||
private val files: List<SshFile>,
|
||||
private val userName: String,
|
||||
private val exists: Boolean
|
||||
) : SshFile {
|
||||
override fun getAbsolutePath(): String = "/"
|
||||
|
||||
override fun getName(): String = "/"
|
||||
|
||||
override fun getAttributes(followLinks: Boolean): Map<SshFile.Attribute, Any> {
|
||||
val attrs: MutableMap<SshFile.Attribute, Any> = EnumMap(SshFile.Attribute::class.java)
|
||||
|
||||
attrs[SshFile.Attribute.Size] = 0
|
||||
attrs[SshFile.Attribute.Owner] = userName
|
||||
attrs[SshFile.Attribute.Group] = userName
|
||||
|
||||
val p = EnumSet.noneOf(
|
||||
SshFile.Permission::class.java
|
||||
)
|
||||
p.add(SshFile.Permission.UserExecute)
|
||||
p.add(SshFile.Permission.GroupExecute)
|
||||
p.add(SshFile.Permission.OthersExecute)
|
||||
attrs[SshFile.Attribute.Permissions] = p
|
||||
|
||||
val now = Calendar.getInstance().timeInMillis
|
||||
attrs[SshFile.Attribute.LastAccessTime] = now
|
||||
attrs[SshFile.Attribute.LastModifiedTime] = now
|
||||
|
||||
attrs[SshFile.Attribute.IsSymbolicLink] = false
|
||||
attrs[SshFile.Attribute.IsDirectory] = true
|
||||
attrs[SshFile.Attribute.IsRegularFile] = false
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
override fun setAttributes(attributes: Map<SshFile.Attribute, Any>) {}
|
||||
|
||||
override fun getAttribute(attribute: SshFile.Attribute, followLinks: Boolean): Any? = null
|
||||
|
||||
override fun setAttribute(attribute: SshFile.Attribute, value: Any) {}
|
||||
|
||||
override fun readSymbolicLink(): String = ""
|
||||
|
||||
override fun createSymbolicLink(destination: SshFile) {}
|
||||
|
||||
override fun getOwner(): String? = null
|
||||
|
||||
override fun isDirectory(): Boolean = true
|
||||
|
||||
override fun isFile(): Boolean = false
|
||||
|
||||
override fun doesExist(): Boolean = exists
|
||||
|
||||
override fun isReadable(): Boolean = true
|
||||
|
||||
override fun isWritable(): Boolean = false
|
||||
|
||||
override fun isExecutable(): Boolean = true
|
||||
|
||||
override fun isRemovable(): Boolean = false
|
||||
|
||||
override fun getParentFile(): SshFile = this
|
||||
|
||||
override fun getLastModified(): Long = 0
|
||||
|
||||
override fun setLastModified(time: Long): Boolean = false
|
||||
|
||||
override fun getSize(): Long = 0
|
||||
|
||||
override fun mkdir(): Boolean = false
|
||||
|
||||
override fun delete(): Boolean = false
|
||||
|
||||
override fun create(): Boolean = false
|
||||
|
||||
override fun truncate() {}
|
||||
|
||||
override fun move(destination: SshFile): Boolean = false
|
||||
|
||||
override fun listSshFiles(): List<SshFile> = Collections.unmodifiableList(files)
|
||||
|
||||
override fun createOutputStream(offset: Long): OutputStream? = null
|
||||
|
||||
override fun createInputStream(offset: Long): InputStream? = null
|
||||
|
||||
override fun handleClose() {
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
@@ -1,16 +1,20 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||
|
||||
import org.apache.sshd.common.NamedFactory
|
||||
import org.apache.sshd.common.Signature
|
||||
import org.apache.sshd.common.signature.AbstractSignature
|
||||
import org.apache.sshd.common.signature.Signature
|
||||
import org.apache.sshd.common.signature.SignatureFactory
|
||||
import org.apache.sshd.common.util.ValidateUtils
|
||||
|
||||
|
||||
class SignatureRSASHA256 : AbstractSignature("SHA256withRSA") {
|
||||
class Factory : NamedFactory<Signature> {
|
||||
object Factory : SignatureFactory {
|
||||
override fun isSupported(): Boolean = true
|
||||
override fun getName(): String = "rsa-sha2-256"
|
||||
|
||||
override fun create(): Signature {
|
||||
@@ -25,6 +29,18 @@ class SignatureRSASHA256 : AbstractSignature("SHA256withRSA") {
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun verify(sig: ByteArray): Boolean {
|
||||
return signature.verify(extractSig(sig))
|
||||
var data = sig
|
||||
val encoding = extractEncodedSignature(data)
|
||||
if (encoding != null) {
|
||||
val keyType = encoding.first
|
||||
ValidateUtils.checkTrue(
|
||||
"rsa-sha2-256" == keyType,
|
||||
"Mismatched key type: %s",
|
||||
keyType
|
||||
)
|
||||
data = encoding.second
|
||||
}
|
||||
|
||||
return signature.verify(data)
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2014 Samoilenko Yuri <kinnalru@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
@@ -8,30 +9,25 @@ package org.kde.kdeconnect.Plugins.SftpPlugin
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import org.apache.sshd.SshServer
|
||||
import org.apache.sshd.common.NamedFactory
|
||||
import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory
|
||||
import org.apache.sshd.common.kex.BuiltinDHFactories
|
||||
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider
|
||||
import org.apache.sshd.common.signature.SignatureDSA
|
||||
import org.apache.sshd.common.signature.SignatureECDSA.NISTP256Factory
|
||||
import org.apache.sshd.common.signature.SignatureECDSA.NISTP384Factory
|
||||
import org.apache.sshd.common.signature.SignatureECDSA.NISTP521Factory
|
||||
import org.apache.sshd.common.signature.SignatureRSA
|
||||
import org.apache.sshd.common.signature.BuiltinSignatures
|
||||
import org.apache.sshd.common.util.SecurityUtils
|
||||
import org.apache.sshd.server.Command
|
||||
import org.apache.sshd.server.PasswordAuthenticator
|
||||
import org.apache.sshd.server.PublickeyAuthenticator
|
||||
import org.apache.sshd.server.SshServer
|
||||
import org.apache.sshd.server.auth.password.PasswordAuthenticator
|
||||
import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator
|
||||
import org.apache.sshd.server.command.ScpCommandFactory
|
||||
import org.apache.sshd.server.kex.DHG14
|
||||
import org.apache.sshd.server.kex.ECDHP256
|
||||
import org.apache.sshd.server.kex.ECDHP384
|
||||
import org.apache.sshd.server.kex.ECDHP521
|
||||
import org.apache.sshd.server.kex.DHGServer
|
||||
import org.apache.sshd.server.session.ServerSession
|
||||
import org.apache.sshd.server.sftp.SftpSubsystem
|
||||
import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory
|
||||
import org.kde.kdeconnect.Device
|
||||
import org.kde.kdeconnect.Helpers.RandomHelper
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper
|
||||
import org.kde.kdeconnect.Helpers.SecurityHelpers.constantTimeCompare
|
||||
import org.kde.kdeconnect.Plugins.SftpPlugin.saf.SafFileSystemFactory
|
||||
import java.io.IOException
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.GeneralSecurityException
|
||||
@@ -53,7 +49,7 @@ internal class SimpleSftpServer {
|
||||
|
||||
private val sshd: SshServer = SshServer.setUpDefaultServer()
|
||||
|
||||
private var safFileSystemFactory: AndroidFileSystemFactory? = null
|
||||
private var safFileSystemFactory: SafFileSystemFactory? = null
|
||||
|
||||
fun setSafRoots(storageInfoList: List<SftpPlugin.StorageInfo>) {
|
||||
safFileSystemFactory!!.initRoots(storageInfoList)
|
||||
@@ -63,22 +59,25 @@ internal class SimpleSftpServer {
|
||||
fun initialize(context: Context?, device: Device) {
|
||||
sshd.signatureFactories =
|
||||
listOf(
|
||||
NISTP256Factory(),
|
||||
NISTP384Factory(),
|
||||
NISTP521Factory(),
|
||||
SignatureDSA.Factory(),
|
||||
SignatureRSASHA256.Factory(),
|
||||
SignatureRSA.Factory() // Insecure SHA1, left for backwards compatibility
|
||||
BuiltinSignatures.nistp256,
|
||||
BuiltinSignatures.nistp384,
|
||||
BuiltinSignatures.nistp521,
|
||||
BuiltinSignatures.dsa,
|
||||
SignatureRSASHA256.Factory,
|
||||
BuiltinSignatures.rsa // Insecure SHA1, left for backwards compatibility
|
||||
)
|
||||
|
||||
sshd.keyExchangeFactories =
|
||||
listOf(
|
||||
ECDHP256.Factory(), // ecdh-sha2-nistp256
|
||||
ECDHP384.Factory(), // ecdh-sha2-nistp384
|
||||
ECDHP521.Factory(), // ecdh-sha2-nistp521
|
||||
DHG14_256.Factory(), // diffie-hellman-group14-sha256
|
||||
DHG14.Factory() // Insecure diffie-hellman-group14-sha1, left for backwards-compatibility.
|
||||
)
|
||||
BuiltinDHFactories.ecdhp256, // ecdh-sha2-nistp256
|
||||
BuiltinDHFactories.ecdhp384, // ecdh-sha2-nistp384
|
||||
BuiltinDHFactories.ecdhp521, // ecdh-sha2-nistp521
|
||||
DHG14_256Factory, // diffie-hellman-group14-sha256
|
||||
BuiltinDHFactories.dhg14, // Insecure diffie-hellman-group14-sha1, left for backwards-compatibility.
|
||||
).map {
|
||||
DHGServer.newFactory(it)
|
||||
}
|
||||
|
||||
|
||||
// Reuse this device keys for the ssh connection as well
|
||||
val keyPair = KeyPair(
|
||||
@@ -92,12 +91,12 @@ internal class SimpleSftpServer {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
sshd.fileSystemFactory = NativeFileSystemFactory()
|
||||
} else {
|
||||
safFileSystemFactory = AndroidFileSystemFactory(context)
|
||||
sshd.fileSystemFactory = safFileSystemFactory
|
||||
safFileSystemFactory = SafFileSystemFactory(context!!)
|
||||
sshd.fileSystemFactory = safFileSystemFactory // FIXME: This is not working
|
||||
}
|
||||
sshd.commandFactory = ScpCommandFactory()
|
||||
sshd.subsystemFactories =
|
||||
listOf<NamedFactory<Command>>(SftpSubsystem.Factory())
|
||||
listOf<NamedFactory<Command>>(SftpSubsystemFactory())
|
||||
|
||||
keyAuth.deviceKey = device.certificate.publicKey
|
||||
|
||||
|
@@ -0,0 +1,37 @@
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.apache.sshd.common.file.util.BaseFileSystem
|
||||
import org.apache.sshd.common.file.util.ImmutableList
|
||||
import java.nio.file.attribute.UserPrincipalLookupService
|
||||
import java.nio.file.spi.FileSystemProvider
|
||||
|
||||
class SafFileSystem(
|
||||
fileSystemProvider: FileSystemProvider,
|
||||
roots: MutableMap<String, String?>,
|
||||
username: String,
|
||||
private val context: Context
|
||||
) : BaseFileSystem<SafPath>(fileSystemProvider) {
|
||||
override fun close() {
|
||||
// no-op
|
||||
Log.v(TAG, "close")
|
||||
}
|
||||
|
||||
override fun isOpen(): Boolean = true
|
||||
|
||||
override fun supportedFileAttributeViews(): Set<String> = setOf("basic")
|
||||
|
||||
override fun getUserPrincipalLookupService(): UserPrincipalLookupService {
|
||||
throw UnsupportedOperationException("SAF does not support user principal lookup")
|
||||
}
|
||||
|
||||
override fun create(root: String, names: ImmutableList<String>): SafPath {
|
||||
Log.v(TAG, "create: $root, $names")
|
||||
return SafPath(this, root, names)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SafFileSystem"
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2018 Erik Duisters <e.duisters1@gmail.com>
|
||||
* SPDX-FileCopyrightText: 2024 ShellWen Chen <me@shellwen.com>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.apache.sshd.common.session.Session
|
||||
import org.apache.sshd.common.file.FileSystemFactory
|
||||
import org.kde.kdeconnect.Plugins.SftpPlugin.SftpPlugin
|
||||
import java.nio.file.FileSystem
|
||||
|
||||
class SafFileSystemFactory(private val context: Context) : FileSystemFactory {
|
||||
private val provider = SafFileSystemProvider()
|
||||
private val roots: MutableMap<String, String?> = HashMap()
|
||||
|
||||
fun initRoots(storageInfoList: List<SftpPlugin.StorageInfo>) {
|
||||
Log.i(TAG, "initRoots: $storageInfoList")
|
||||
|
||||
for (curStorageInfo in storageInfoList) {
|
||||
when {
|
||||
curStorageInfo.isFileUri -> {
|
||||
TODO("File URI is not supported yet")
|
||||
// if (curStorageInfo.uri.path != null) {
|
||||
// roots[curStorageInfo.displayName] = curStorageInfo.uri.path
|
||||
// }
|
||||
}
|
||||
curStorageInfo.isContentUri -> {
|
||||
roots[curStorageInfo.displayName] = curStorageInfo.uri.toString()
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Unknown storage URI type: $curStorageInfo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun createFileSystem(session: Session): FileSystem {
|
||||
return SafFileSystem(provider, roots, session.username, context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SafFileSystemFactory"
|
||||
}
|
||||
}
|
@@ -0,0 +1,113 @@
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
|
||||
|
||||
import java.net.URI
|
||||
import java.nio.channels.SeekableByteChannel
|
||||
import java.nio.file.AccessMode
|
||||
import java.nio.file.CopyOption
|
||||
import java.nio.file.DirectoryStream
|
||||
import java.nio.file.FileStore
|
||||
import java.nio.file.FileSystem
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.OpenOption
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.BasicFileAttributes
|
||||
import java.nio.file.attribute.FileAttribute
|
||||
import java.nio.file.attribute.FileAttributeView
|
||||
import java.nio.file.spi.FileSystemProvider
|
||||
|
||||
class SafFileSystemProvider: FileSystemProvider() {
|
||||
override fun getScheme(): String = "saf"
|
||||
|
||||
override fun newFileSystem(uri: URI, env: MutableMap<String, *>?): FileSystem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getFileSystem(uri: URI): FileSystem {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getPath(uri: URI): Path {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun newByteChannel(
|
||||
path: Path,
|
||||
options: MutableSet<out OpenOption>,
|
||||
vararg attrs: FileAttribute<*>
|
||||
): SeekableByteChannel {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun newDirectoryStream(
|
||||
dir: Path,
|
||||
filter: DirectoryStream.Filter<in Path>
|
||||
): DirectoryStream<Path> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun createDirectory(dir: Path, vararg attrs: FileAttribute<*>) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun delete(path: Path) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun copy(source: Path, target: Path, vararg options: CopyOption) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun move(source: Path, target: Path, vararg options: CopyOption) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isSameFile(path: Path, path2: Path): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isHidden(path: Path): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getFileStore(path: Path): FileStore {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun checkAccess(path: Path, vararg modes: AccessMode) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun <V : FileAttributeView> getFileAttributeView(
|
||||
path: Path,
|
||||
type: Class<V>,
|
||||
vararg options: LinkOption?
|
||||
): V {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun <A : BasicFileAttributes> readAttributes(
|
||||
path: Path,
|
||||
type: Class<A>,
|
||||
vararg options: LinkOption?
|
||||
): A {
|
||||
// TODO
|
||||
throw UnsupportedOperationException("readAttributes($path)[${type.getSimpleName()}] N/A");
|
||||
}
|
||||
|
||||
override fun readAttributes(
|
||||
path: Path,
|
||||
attributes: String,
|
||||
vararg options: LinkOption?
|
||||
): MutableMap<String, Any?> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setAttribute(
|
||||
path: Path,
|
||||
attribute: String,
|
||||
value: Any?,
|
||||
vararg options: LinkOption?
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
15
src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt
Normal file
15
src/org/kde/kdeconnect/Plugins/SftpPlugin/saf/SafPath.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package org.kde.kdeconnect.Plugins.SftpPlugin.saf
|
||||
|
||||
import org.apache.sshd.common.file.util.BasePath
|
||||
import org.apache.sshd.common.file.util.ImmutableList
|
||||
import java.nio.file.LinkOption
|
||||
import java.nio.file.Path
|
||||
|
||||
class SafPath(
|
||||
fileSystem: SafFileSystem,
|
||||
root: String, names: ImmutableList<String>
|
||||
) : BasePath<SafPath, SafFileSystem>(fileSystem, root, names) {
|
||||
override fun toRealPath(vararg options: LinkOption?): Path {
|
||||
return this // FIXME
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user