From 40668d63da1ea1e9ee4c2400bf8ad84f1e577340 Mon Sep 17 00:00:00 2001 From: Patryk Kaczmarkiewicz Date: Wed, 19 Sep 2018 19:13:49 -0600 Subject: [PATCH] Add options to Serve feature Option allow remote access. This fixes previous security bug where external access was enabled by default. Option to specify user and password for authentication --- .../rcloneexplorer/Dialogs/ServeDialog.java | 148 ++++++++++++++++++ .../Fragments/FileExplorerFragment.java | 63 ++++---- .../java/ca/pkay/rcloneexplorer/Rclone.java | 50 +++--- .../Services/StreamingService.java | 10 +- .../Services/ThumbnailsLoadingService.java | 2 +- app/src/main/res/layout/dialog_serve.xml | 100 ++++++++++++ app/src/main/res/values/strings.xml | 4 + 7 files changed, 327 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/ServeDialog.java create mode 100644 app/src/main/res/layout/dialog_serve.xml diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/ServeDialog.java b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/ServeDialog.java new file mode 100644 index 0000000..84cad14 --- /dev/null +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Dialogs/ServeDialog.java @@ -0,0 +1,148 @@ +package ca.pkay.rcloneexplorer.Dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.TextInputLayout; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.FragmentActivity; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.RadioGroup; + +import ca.pkay.rcloneexplorer.R; +import ca.pkay.rcloneexplorer.Rclone; + +public class ServeDialog extends DialogFragment { + + private Context context; + private boolean isDarkTheme; + private Callback callback; + private RadioGroup protocol; + private CheckBox allowRemoteAccess; + private EditText user; + private EditText password; + + public interface Callback { + void onServeOptionsSelected(int protocol, boolean allowRemoteAccess, String user, String password); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + if (getParentFragment() != null) { + callback = (Callback) getParentFragment(); + } + + if (savedInstanceState != null) { + isDarkTheme = savedInstanceState.getBoolean("isDarkTheme"); + } + + AlertDialog.Builder builder; + if (isDarkTheme) { + builder = new AlertDialog.Builder(context, R.style.DarkDialogTheme); + } else { + builder = new AlertDialog.Builder(context); + } + + LayoutInflater layoutInflater = ((FragmentActivity)context).getLayoutInflater(); + View view = layoutInflater.inflate(R.layout.dialog_serve, null); + + protocol = view.findViewById(R.id.radio_group_protocol); + allowRemoteAccess = view.findViewById(R.id.checkbox_allow_remote_access); + user = view.findViewById(R.id.edit_text_user); + password = view.findViewById(R.id.edit_text_password); + + ((TextInputLayout) view.findViewById(R.id.text_input_layout_user)).setHint("Username"); + ((TextInputLayout) view.findViewById(R.id.text_input_layout_password)).setHint("Password"); + + builder.setTitle(R.string.serve_dialog_title); + builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + sendCallback(); + } + }); + + builder.setNegativeButton(R.string.cancel, null); + + builder.setView(view); + + return builder.show(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean("isDarkTheme", isDarkTheme); + outState.putInt("protocol", protocol.getCheckedRadioButtonId()); + outState.putBoolean("allowRemoteAccess", allowRemoteAccess.isChecked()); + if (!user.getText().toString().trim().isEmpty()) { + outState.putString("user", user.getText().toString()); + } + if (!password.getText().toString().trim().isEmpty()) { + outState.putString("password", password.getText().toString()); + } + } + + @Override + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + if (savedInstanceState == null) { + return; + } + + allowRemoteAccess.setChecked(savedInstanceState.getBoolean("allowRemoteAccess", false)); + String savedUser = savedInstanceState.getString("user"); + if (savedUser != null) { + user.setText(savedUser); + } + + String savedPassword = savedInstanceState.getString("password"); + if (savedPassword != null) { + password.setText(savedPassword); + } + + int savedProtocol = savedInstanceState.getInt("protocol", -1); + if (savedProtocol == R.id.radio_http || savedProtocol == R.id.radio_webdav) { + protocol.check(savedProtocol); + } + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + this.context = context; + + if (context instanceof Callback) { + callback = (Callback) context; + } + } + + public ServeDialog setDarkTheme(boolean isDarkTheme) { + this.isDarkTheme = isDarkTheme; + return this; + } + + private void sendCallback() { + int selectedProtocolId = protocol.getCheckedRadioButtonId(); + int selectedProtocol; + switch (selectedProtocolId) { + case R.id.radio_webdav: + selectedProtocol = Rclone.SERVE_PROTOCOL_WEBDAV; + break; + case R.id.radio_http: + default: + selectedProtocol = Rclone.SERVE_PROTOCOL_HTTP; + break; + } + + callback.onServeOptionsSelected(selectedProtocol, allowRemoteAccess.isChecked(), user.getText().toString(), password.getText().toString()); + } +} diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java index b7c8544..343e040 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Fragments/FileExplorerFragment.java @@ -62,6 +62,7 @@ import ca.pkay.rcloneexplorer.Dialogs.InputDialog; import ca.pkay.rcloneexplorer.Dialogs.LinkDialog; import ca.pkay.rcloneexplorer.Dialogs.LoadingDialog; +import ca.pkay.rcloneexplorer.Dialogs.ServeDialog; import ca.pkay.rcloneexplorer.Dialogs.SortDialog; import ca.pkay.rcloneexplorer.FileComparators; import ca.pkay.rcloneexplorer.Dialogs.FilePropertiesDialog; @@ -90,7 +91,8 @@ public class FileExplorerFragment extends Fragment implements FileExplorerRecy OpenAsDialog.OnClickListener, InputDialog.OnPositive, GoToDialog.Callbacks, - SortDialog.OnClickListener { + SortDialog.OnClickListener, + ServeDialog.Callback { private static final String ARG_REMOTE = "remote_param"; private static final String SHARED_PREFS_SORT_ORDER = "ca.pkay.rcexplorer.sort_order"; @@ -593,34 +595,7 @@ public boolean onOptionsItemSelected(MenuItem item) { recyclerViewAdapter.toggleSelectAll(); return true; case R.id.action_serve: - String[] serveOptions = new String[] {"HTTP", "Webdav"}; - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setItems(serveOptions, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(getContext(), StreamingService.class); - switch (which) { - case 0: // HTTP - intent.putExtra(StreamingService.SERVE_PATH_ARG, directoryObject.getCurrentPath()); - intent.putExtra(StreamingService.REMOTE_ARG, remote); - intent.putExtra(StreamingService.SERVE_PROTOCOL, StreamingService.SERVE_HTTP); - intent.putExtra(StreamingService.SHOW_NOTIFICATION_TEXT, true); - break; - case 1: // Webdav - intent.putExtra(StreamingService.SERVE_PATH_ARG, directoryObject.getCurrentPath()); - intent.putExtra(StreamingService.REMOTE_ARG, remote); - intent.putExtra(StreamingService.SERVE_PROTOCOL, StreamingService.SERVE_WEBDAV); - intent.putExtra(StreamingService.SHOW_NOTIFICATION_TEXT, true); - break; - default: - return; - } - context.startService(intent); - } - }); - builder.setTitle(R.string.pick_a_protocol); - builder.show(); - + serve(); return true; case R.id.action_empty_trash: emptyTrash(); @@ -650,6 +625,36 @@ public void onClick(DialogInterface dialog, int which) { } } + private void serve() { + ServeDialog serveDialog = new ServeDialog(); + serveDialog.setDarkTheme(isDarkTheme); + serveDialog.show(getChildFragmentManager(), "serve dialog"); + } + + // serve callback + @Override + public void onServeOptionsSelected(int protocol, boolean allowRemoteAccess, String user, String password) { + Intent intent = new Intent(getContext(), StreamingService.class); + intent.putExtra(StreamingService.SERVE_PATH_ARG, directoryObject.getCurrentPath()); + intent.putExtra(StreamingService.REMOTE_ARG, remote); + intent.putExtra(StreamingService.SHOW_NOTIFICATION_TEXT, true); + intent.putExtra(StreamingService.ALLOW_REMOTE_ACCESS, allowRemoteAccess); + intent.putExtra(StreamingService.AUTHENTICATION_USERNAME, user); + intent.putExtra(StreamingService.AUTHENTICATION_PASSWORD, password); + + switch (protocol) { + case Rclone.SERVE_PROTOCOL_HTTP: // HTTP + intent.putExtra(StreamingService.SERVE_PROTOCOL, StreamingService.SERVE_HTTP); + break; + case Rclone.SERVE_PROTOCOL_WEBDAV: // Webdav + intent.putExtra(StreamingService.SERVE_PROTOCOL, StreamingService.SERVE_WEBDAV); + break; + default: + return; + } + context.startService(intent); + } + private void emptyTrash() { AlertDialog.Builder builder; diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java index af6c92d..0ba69c9 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Rclone.java @@ -33,8 +33,10 @@ public class Rclone { - public static int SYNC_DIRECTION_LOCAL_TO_REMOTE = 1; - public static int SYNC_DIRECTION_REMOTE_TO_LOCAL = 2; + public static final int SYNC_DIRECTION_LOCAL_TO_REMOTE = 1; + public static final int SYNC_DIRECTION_REMOTE_TO_LOCAL = 2; + public static final int SERVE_PROTOCOL_HTTP = 1; + public static final int SERVE_PROTOCOL_WEBDAV = 2; private Context context; private String rclone; private String rcloneConf; @@ -353,37 +355,49 @@ public String obscure(String pass) { } } - public Process serveHttp(RemoteItem remote, String servePath, int port) { + public Process serve(int protocol, int port, boolean allowRemoteAccess, String user, String password, RemoteItem remote, String servePath) { String remoteName = remote.getName(); String localRemotePath = (remote.isRemoteType(RemoteItem.LOCAL)) ? Environment.getExternalStorageDirectory().getAbsolutePath() + "/" : ""; String path = (servePath.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath : remoteName + ":" + localRemotePath + servePath; - String[] command = createCommandWithOptions("serve", "http", "--addr", ":" + String.valueOf(port), path); - try { - return Runtime.getRuntime().exec(command); - } catch (IOException e) { - e.printStackTrace(); - return null; + String commandProtocol = protocol == SERVE_PROTOCOL_HTTP ? "http" : "webdav"; + String address; + if (allowRemoteAccess) { + address = ":" + String.valueOf(port); + } else { + address = "127.0.0.1:" + String.valueOf(port); } - } - - public Process serveWebdav(RemoteItem remote, String servePath, int port) { - String remoteName = remote.getName(); - String localRemotePath = (remote.isRemoteType(RemoteItem.LOCAL)) ? Environment.getExternalStorageDirectory().getAbsolutePath() + "/" : ""; - String path = (servePath.compareTo("//" + remoteName) == 0) ? remoteName + ":" + localRemotePath : remoteName + ":" + localRemotePath + servePath; - String[] command = createCommandWithOptions("serve", "webdav", "--addr", ":" + String.valueOf(port), path); - String cachePath = context.getCacheDir().getAbsolutePath(); String[] environmentalVariables = {"TMPDIR=" + cachePath}; // this is a fix for #199 + String[] command; + + if (user == null && password != null) { + command = createCommandWithOptions("serve", commandProtocol, "--addr", address, path, "--pass", password); + } else if (user != null && password == null) { + command = createCommandWithOptions("serve", commandProtocol, "--addr", address, path, "--user", user); + } else if (user != null) { + command = createCommandWithOptions("serve", commandProtocol, "--addr", address, path, "--user", user, "--pass", password); + } else { + command = createCommandWithOptions("serve", commandProtocol, "--addr", address, path); + } + try { - return Runtime.getRuntime().exec(command, environmentalVariables); + if (protocol == SERVE_PROTOCOL_WEBDAV) { + return Runtime.getRuntime().exec(command, environmentalVariables); + } else { + return Runtime.getRuntime().exec(command); + } } catch (IOException e) { e.printStackTrace(); return null; } } + public Process serve(int protocol, int port, boolean localhostOnly, RemoteItem remote, String servePath) { + return serve(protocol, port, localhostOnly, null, null, remote, servePath); + } + public Process sync(RemoteItem remoteItem, String remote, String localPath, int syncDirection) { String[] command; String remoteName = remoteItem.getName(); diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Services/StreamingService.java b/app/src/main/java/ca/pkay/rcloneexplorer/Services/StreamingService.java index 2a794d5..ce5733b 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Services/StreamingService.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Services/StreamingService.java @@ -23,6 +23,9 @@ public class StreamingService extends IntentService { public static final String REMOTE_ARG = "ca.pkay.rcexplorer.streaming_service.arg2"; public static final String SHOW_NOTIFICATION_TEXT = "ca.pkay.rcexplorer.streaming_service.arg3"; public static final String SERVE_PROTOCOL = "ca.pkay.rcexplorer.serve_protocol"; + public static final String ALLOW_REMOTE_ACCESS = "ca.pkay.rcexplorer.allow_remote_access"; + public static final String AUTHENTICATION_USERNAME = "ca.pkay.rcexplorer.username"; + public static final String AUTHENTICATION_PASSWORD = "ca.pkay.rcexplorer.password"; public static final int SERVE_HTTP = 11; public static final int SERVE_WEBDAV = 12; private final String CHANNEL_ID = "ca.pkay.rcexplorer.streaming_channel"; @@ -54,6 +57,9 @@ protected void onHandleIntent(@Nullable Intent intent) { final RemoteItem remote = intent.getParcelableExtra(REMOTE_ARG); final Boolean showNotificationText = intent.getBooleanExtra(SHOW_NOTIFICATION_TEXT, false); final int protocol = intent.getIntExtra(SERVE_PROTOCOL, SERVE_HTTP); + final Boolean allowRemoteAccess = intent.getBooleanExtra(ALLOW_REMOTE_ACCESS, false); + final String authenticationUsername = intent.getStringExtra(AUTHENTICATION_USERNAME); + final String authenticationPassword = intent.getStringExtra(AUTHENTICATION_PASSWORD); Intent foregroundIntent = new Intent(this, StreamingService.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, foregroundIntent, 0); @@ -81,11 +87,11 @@ protected void onHandleIntent(@Nullable Intent intent) { switch (protocol) { case SERVE_WEBDAV: - runningProcess = rclone.serveWebdav(remote, servePath, 8080); + runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_WEBDAV, 8080, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath); break; case SERVE_HTTP: default: - runningProcess = rclone.serveHttp(remote, servePath, 8080); + runningProcess = rclone.serve(Rclone.SERVE_PROTOCOL_HTTP, 8080, allowRemoteAccess, authenticationUsername, authenticationPassword, remote, servePath); break; } diff --git a/app/src/main/java/ca/pkay/rcloneexplorer/Services/ThumbnailsLoadingService.java b/app/src/main/java/ca/pkay/rcloneexplorer/Services/ThumbnailsLoadingService.java index 3a2632c..030e793 100644 --- a/app/src/main/java/ca/pkay/rcloneexplorer/Services/ThumbnailsLoadingService.java +++ b/app/src/main/java/ca/pkay/rcloneexplorer/Services/ThumbnailsLoadingService.java @@ -30,7 +30,7 @@ protected void onHandleIntent(@Nullable Intent intent) { } RemoteItem remote = intent.getParcelableExtra(REMOTE_ARG); - process = rclone.serveHttp(remote, "", 29170); + process = rclone.serve(Rclone.SERVE_PROTOCOL_HTTP, 29170, true, remote, ""); if (process != null) { try { process.waitFor(); diff --git a/app/src/main/res/layout/dialog_serve.xml b/app/src/main/res/layout/dialog_serve.xml new file mode 100644 index 0000000..c8d5d29 --- /dev/null +++ b/app/src/main/res/layout/dialog_serve.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b6f330..b2661bb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -361,4 +361,8 @@ Hidden remotes shared_preferences_hidden_remotes Select remotes to hide + Allow remote access + Authentication + Optional + Serve a remote