Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anonymous fix, connection limiter, and chroot selection #228

Merged
Merged
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
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ along with SwiFTP. If not, see <http://www.gnu.org/licenses/>.
android:parentActivityName=".gui.MainActivity"
android:theme="@style/AppThemeDark" />

<activity
android:name=".gui.ManageAnonActivity"
android:parentActivityName=".gui.MainActivity"
android:theme="@style/AppThemeDark" />

<service android:name="be.ppareit.swiftp.FsService" />

<service android:name="be.ppareit.swiftp.NsdService" />
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/be/ppareit/swiftp/FsSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ public static int getPortNumber() {
return port;
}

public static int getAnonMaxConNumber() {
final SharedPreferences sp = getSharedPreferences();
String s = sp.getString("anon_max", "1");
int i = Integer.parseInt(s);
Log.v(TAG, "Using anon max connections: " + i);
return i;
}

public static boolean shouldTakeFullWakeLock() {
final SharedPreferences sp = getSharedPreferences();
return sp.getBoolean("stayAwake", false);
Expand Down
136 changes: 136 additions & 0 deletions app/src/main/java/be/ppareit/swiftp/gui/ManageAnonActivity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package be.ppareit.swiftp.gui;


import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;

import androidx.core.app.NavUtils;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;

import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;

import net.vrallev.android.cat.Cat;

import be.ppareit.swiftp.App;
import be.ppareit.swiftp.FsSettings;
import be.ppareit.swiftp.R;
import be.ppareit.swiftp.utils.ChrootPicker;

public class ManageAnonActivity extends AppCompatActivity {

private static final int ACTION_OPEN_DOCUMENT_TREE = 42;
ChrootPicker chrootPicker = null;

@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
setTheme(FsSettings.getTheme());
super.onCreate(savedInstanceState);

setContentView(R.layout.anon_layout);

ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setHomeButtonEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
}

chrootPicker = new ChrootPicker();
TextView chroot = findViewById(R.id.anon_chroot);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
String chrootString = sp.getString("anonChroot", "");
chroot.setText(chrootString);
chroot.setOnTouchListener((v, event) -> {
sp.edit().remove("anonChroot").apply();
sp.edit().remove("anonUriString").apply();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
chrootPicker.showFolderPicker(chroot.getText().toString(), this, null);
}
return true;
});
chrootPicker.setOnTextEventListener(s -> {
chroot.setText(s);
sp.edit().putString("anonChroot", s).apply();
});
chrootPicker.setOnActionTreeEventListener(() -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE);
});

EditText anonMaxCon = findViewById(R.id.anon_max);
anonMaxCon.setText(String.valueOf(FsSettings.getAnonMaxConNumber()));
anonMaxCon.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

@Override
public void afterTextChanged(Editable s) {
sp.edit().putString("anon_max", s.toString()).apply();
}
});

CheckBox anonCB = findViewById(R.id.anon_enable);
anonCB.setChecked(sp.getBoolean("allow_anonymous", false));
anonCB.setOnCheckedChangeListener((buttonView, isChecked) -> {
SharedPreferences sp1 = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
if (isChecked ) {
if (sp.getString("anonChroot", "").isEmpty()) {
// Deny as workaround to problems like app crashing with scoped storage.
anonCB.setChecked(false);
final ObjectAnimator loading = ObjectAnimator.ofObject((TextView) chroot, "backgroundColor",
new ArgbEvaluator(), Color.RED, Color.TRANSPARENT).setDuration(1000);
loading.start();
return;
}
sp1.edit().putBoolean("allow_anonymous", true).apply();
}
else sp1.edit().putBoolean("allow_anonymous", false).apply();
});
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
super.onActivityResult(requestCode, resultCode, resultData);
Cat.d("onActivityResult called");
if (requestCode == ACTION_OPEN_DOCUMENT_TREE && resultCode == Activity.RESULT_OK) {
if (resultData == null) return;
Uri treeUri = resultData.getData();
if (treeUri == null) return;
String path = treeUri.getPath();
Cat.d("Action Open Document Tree on path " + path);
chrootPicker.save(this.getApplicationContext(), treeUri);
String uriString = treeUri.getPath();
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
sp.edit().putString("anonUriString", uriString).apply();
}
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ public void onCreate(Bundle savedInstanceState) {
return true;
});

Preference manageAnonPref = findPref("manage_anon");
manageAnonPref.setOnPreferenceClickListener((preference) -> {
startActivity(new Intent(getActivity(), ManageAnonActivity.class));
return true;
});

EditTextPreference portNumberPref = findPref("portNum");
portNumberPref.setSummary(String.valueOf(FsSettings.getPortNumber()));
portNumberPref.setOnPreferenceChangeListener((preference, newValue) -> {
Expand Down
71 changes: 38 additions & 33 deletions app/src/main/java/be/ppareit/swiftp/gui/UserEditFragment.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
package be.ppareit.swiftp.gui;


import android.app.AlertDialog;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import java.io.File;
import net.vrallev.android.cat.Cat;

import be.ppareit.swiftp.FsSettings;
import be.ppareit.swiftp.R;
import be.ppareit.swiftp.server.FtpUser;
import be.ppareit.swiftp.utils.ChrootPicker;

public class UserEditFragment extends Fragment {

private static final int ACTION_OPEN_DOCUMENT_TREE = 42;

private FtpUser item;
private OnEditFinishedListener editFinishedListener;
private boolean isShowingFolderPicker = false;
private TextView chroot = null;
private String uriString = "";
private ChrootPicker chrootPicker = null;

public static UserEditFragment newInstance(@Nullable FtpUser item, @NonNull OnEditFinishedListener listener) {
UserEditFragment fragment = new UserEditFragment();
Expand All @@ -40,14 +48,21 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
View root = inflater.inflate(R.layout.user_edit_layout, container, false);
EditText username = (EditText) root.findViewById(R.id.user_edit_name);
EditText password = (EditText) root.findViewById(R.id.user_edit_password);
TextView chroot = (TextView) root.findViewById(R.id.user_edit_chroot);
chrootPicker = new ChrootPicker();
chroot = (TextView) root.findViewById(R.id.user_edit_chroot);
chroot.setText(FsSettings.getDefaultChrootDir().getPath());
chroot.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus)
return;
showFolderPicker(chroot);
if (!hasFocus) return;
chrootPicker.showFolderPicker(chroot.getText().toString(), null, getContext());
});
chroot.setOnClickListener(v -> {
chrootPicker.showFolderPicker(chroot.getText().toString(), null, getContext());
});
chrootPicker.setOnTextEventListener(s -> chroot.setText(s));
chrootPicker.setOnActionTreeEventListener(() -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, ACTION_OPEN_DOCUMENT_TREE);
});
chroot.setOnClickListener(view -> showFolderPicker(chroot));

if (item != null) {
username.setText(item.getUsername());
Expand All @@ -60,38 +75,28 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
String newPassword = password.getText().toString();
String newChroot = chroot.getText().toString();
if (validateInput(newUsername, newPassword)) {
editFinishedListener.onEditActionFinished(item, new FtpUser(newUsername, newPassword, newChroot));
editFinishedListener.onEditActionFinished(item, new FtpUser(newUsername, newPassword, newChroot, uriString));
getActivity().onBackPressed();
}
});
root.findViewById(R.id.user_cancel_btn).setOnClickListener((buttonView) -> getActivity().onBackPressed());
return root;
}

private void showFolderPicker(TextView chrootView) {
if (isShowingFolderPicker)
return;
isShowingFolderPicker = true;
final File startDir;
if (chrootView.getText().toString().isEmpty()) {
startDir = Environment.getExternalStorageDirectory();
} else {
startDir = new File((chrootView.getText().toString()));
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
Cat.d("onActivityResult called");
if (requestCode == ACTION_OPEN_DOCUMENT_TREE && resultCode == Activity.RESULT_OK) {
if (resultData == null) return;
Uri treeUri = resultData.getData();
if (treeUri == null) return;
String path = treeUri.getPath();
Cat.d("Action Open Document Tree on path " + path);
// *************************************
// The order following here is critical. They must stay ordered as they are.
chrootPicker.save(this.getContext(), treeUri);
uriString = treeUri.getPath();
}
AlertDialog folderPicker = new FolderPickerDialogBuilder(getActivity(), startDir)
.setSelectedButton(R.string.select, path -> {
final File root = new File(path);
if (!root.canRead()) {
showToast(R.string.notice_cant_read_write);
} else if (!root.canWrite()) {
showToast(R.string.notice_cant_write);
}
chrootView.setText(path);
})
.setNegativeButton(R.string.cancel, null)
.create();
folderPicker.setOnDismissListener(dialog -> isShowingFolderPicker = false);
folderPicker.show();
}

private boolean validateInput(String username, String password) {
Expand Down
40 changes: 38 additions & 2 deletions app/src/main/java/be/ppareit/swiftp/server/CmdPASS.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@

package be.ppareit.swiftp.server;

import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

import be.ppareit.swiftp.App;
import be.ppareit.swiftp.FsSettings;
import be.ppareit.swiftp.Util;
import be.ppareit.swiftp.utils.AnonymousLimit;
import be.ppareit.swiftp.utils.Logging;

public class CmdPASS extends FtpCmd implements Runnable {
private static final String TAG = CmdPASS.class.getSimpleName();
Expand All @@ -44,8 +50,35 @@ public void run() {
return;
}
if (attemptUsername.equals("anonymous") && FsSettings.allowAnonymous()) {
Log.i(TAG, "Guest logged in with email: " + attemptPassword);
sessionThread.writeString("230 Guest login ok, read only access.\r\n");
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(App.getAppContext());
final int anonMaxCon = Integer.parseInt(sp.getString("anon_max", "1"));
Logging logging = new Logging();
final int newCount = AnonymousLimit.incrementAndGet();
logging.appendLog("anon CURRENT client conn count: " + (newCount - 1));
logging.appendLog("anon MAX conn count: " + anonMaxCon);
if (newCount > anonMaxCon) {
Log.i(TAG, "Failed authentication, too many anonymous users connected.");
Util.sleepIgnoreInterrupt(1000); // sleep to foil brute force attack
sessionThread.writeString("421 too many anonymous users connected.\r\n");
sessionThread.authAttempt(false);
} else {
Log.i(TAG, "Guest logged in with email: " + attemptPassword);
sessionThread.writeString("230 Guest login ok, read only access.\r\n");
final String anonChroot = sp.getString("anonChroot", "/storage/emulated/0" /*backwards compat*/);
final String anonUriString = sp.getString("anonUriString", "");
if (!anonChroot.isEmpty()) {
sessionThread.setChrootDir(anonChroot);
if (!anonUriString.isEmpty()) {
SessionThread.putUriString(Thread.currentThread().getName(), anonUriString);
} else if (Util.useScopedStorage()) {
// Protect against app crashes/problems
Log.i(TAG, "Failed authentication, too many anonymous users connected.");
Util.sleepIgnoreInterrupt(1000); // sleep to foil brute force attack
sessionThread.writeString("421 too many anonymous users connected.\r\n");
sessionThread.authAttempt(false);
}
}
}
return;
}
FtpUser user = FsSettings.getUser(attemptUsername);
Expand All @@ -59,6 +92,9 @@ public void run() {
sessionThread.writeString("230 Access granted\r\n");
sessionThread.authAttempt(true);
sessionThread.setChrootDir(user.getChroot());
if (Util.useScopedStorage()) {
SessionThread.putUriString(Thread.currentThread().getName(), user.getUriString());
}
} else {
Log.i(TAG, "Failed authentication, incorrect password");
Util.sleepIgnoreInterrupt(1000); // sleep to foil brute force attack
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/be/ppareit/swiftp/server/SessionThread.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

import be.ppareit.swiftp.App;
import be.ppareit.swiftp.FsSettings;
import be.ppareit.swiftp.utils.AnonymousLimit;

public class SessionThread extends Thread {

Expand Down Expand Up @@ -256,6 +257,7 @@ public void run() {
Cat.i("Connection was dropped");
}
closeSocket();
AnonymousLimit.decrement();
}

public void closeSocket() {
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/be/ppareit/swiftp/utils/AnonymousLimit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package be.ppareit.swiftp.utils;

import java.util.concurrent.atomic.AtomicInteger;

public class AnonymousLimit {
static AtomicInteger anonUsersConnected = new AtomicInteger(0);

public static int incrementAndGet() {
return anonUsersConnected.incrementAndGet();
}

public static void decrement() {
if (anonUsersConnected.get() >= 1) anonUsersConnected.decrementAndGet();
}
}
Loading