Skip to content

Commit

Permalink
Enable auth for [safdav]
Browse files Browse the repository at this point in the history
A security review revealed that the safdav component is not guarded
appropriately against unauthorized access. This commit enables basic
authentication that is configured automatically, with the auth
credentials being available via SafAccessProvider .getUsername() /
.getPassword().

As a result, thumbnails are loaded directly from the ContentResolver by
Glide.

Signed-off-by: x0b <[email protected]>
  • Loading branch information
x0b committed Oct 4, 2019
1 parent 54b130a commit bc2b012
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.content.Context;
import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
Expand All @@ -17,7 +18,7 @@
import ca.pkay.rcloneexplorer.R;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import io.github.x0b.safdav.file.SafConstants;
import io.github.x0b.safdav.SafAccessProvider;

import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -105,9 +106,10 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
.centerCrop()
.placeholder(R.drawable.ic_file);
if(localLoad) {
Uri contentUri = SafAccessProvider.getDirectServer(context).getDocumentUri('/'+item.getPath());
Glide
.with(context)
.load(SafConstants.SAF_REMOTE_URL + item.getPath())
.load(contentUri)
.apply(glideOption)
.thumbnail(0.1f)
.into(holder.fileIcon);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
Expand All @@ -21,10 +17,14 @@
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import ca.pkay.rcloneexplorer.R;
import ca.pkay.rcloneexplorer.Rclone;
import ca.pkay.rcloneexplorer.VirtualContentProvider;
import es.dmoral.toasty.Toasty;
import io.github.x0b.safdav.SafAccessProvider;
import io.github.x0b.safdav.file.SafConstants;

import java.util.ArrayList;
Expand Down Expand Up @@ -138,11 +138,17 @@ private void setSafEnabled(boolean isChecked) {
}

private void createSafRemote() {
String user = SafAccessProvider.getUser(getContext());
String pass = SafAccessProvider.getPassword(getContext());
ArrayList<String> options = new ArrayList<>();
options.add(SafConstants.SAF_REMOTE_NAME);
options.add("webdav");
options.add("url");
options.add(SafConstants.SAF_REMOTE_URL);
options.add("user");
options.add(user);
options.add("pass");
options.add(pass);

Process process = rclone.configCreate(options);
try {
Expand Down
46 changes: 42 additions & 4 deletions safdav/src/main/java/io/github/x0b/safdav/SafAccessProvider.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
package io.github.x0b.safdav;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;
import io.github.x0b.safdav.file.SafConstants;

import java.io.IOException;

import io.github.x0b.safdav.file.SafConstants;
import java.security.SecureRandom;

/**
* Saf Emulation Server
*/
public class SafAccessProvider {

private static final String PREF_KEY_SAF_USER = "io.github.x0b.safdav.safDavUser";
private static final String PREF_KEY_SAF_PASS = "io.github.x0b.safdav.safDavPass";

private static SafDAVServer davServer;
private static SafDirectServer directServer;
private static String user;
private static String password;

public static SafDAVServer getServer(Context context){
if(null == davServer){
davServer = new SafDAVServer(SafConstants.SAF_REMOTE_PORT, context);
if(null == davServer) {
initAuth(context);
davServer = new SafDAVServer(SafConstants.SAF_REMOTE_PORT, user, password, context);
try {
davServer.start();
} catch (IOException e) {
Expand All @@ -26,11 +35,40 @@ public static SafDAVServer getServer(Context context){
return davServer;
}

@SuppressLint("ApplySharedPref")
private static void initAuth(Context context){
SharedPreferences preferences = context.getSharedPreferences(
context.getPackageName() + "_preferences", Context.MODE_PRIVATE);
if(preferences.contains(PREF_KEY_SAF_PASS) && preferences.contains((PREF_KEY_SAF_USER))){
password = preferences.getString(PREF_KEY_SAF_PASS, "");
user = preferences.getString(PREF_KEY_SAF_USER, "dav");
} else {
SecureRandom random = new SecureRandom();
byte[] values = new byte[16];
random.nextBytes(values);
password = Base64.encodeToString(values, Base64.NO_WRAP);
user = "dav";
preferences.edit()
.putString(PREF_KEY_SAF_PASS, password)
.putString(PREF_KEY_SAF_USER, user)
.commit();
}
}

public static SafDirectServer getDirectServer(Context context) {
if(null == directServer) {
directServer = new SafDirectServer(context);
}
return directServer;
}

public static String getUser(Context context) {
initAuth(context);
return user;
}

public static String getPassword(Context context) {
initAuth(context);
return password;
}
}
33 changes: 33 additions & 0 deletions safdav/src/main/java/io/github/x0b/safdav/SafDAVServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.content.Context;
import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import fi.iki.elonen.NanoHTTPD;
import io.github.x0b.safdav.file.FileAccessError;
Expand All @@ -16,6 +17,7 @@
import io.github.x0b.safdav.xml.XmlResponseSerialization;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class SafDAVServer extends NanoHTTPD {

Expand All @@ -24,21 +26,44 @@ public class SafDAVServer extends NanoHTTPD {
private final ItemAccess itemAccess;
private final XmlResponseSerialization respSerializer;
private ProviderPaths paths;
private String requiredAuthHeader;

/**
* SafDAV binds to localhost. You must provide your own port availability checking mechanism
* Note that authentication is currently not supported.
* @param port the device port to listen on
*/
@Deprecated
public SafDAVServer(int port, Context context) {
super(hostname, port);
itemAccess = new SafFileAccess(context);
this.paths = new ProviderPaths(context);
respSerializer = new XmlResponseSerialization();
}

/**
* SafDAV binds to localhost. You must provide your own port availability checking mechanism
* Note that authentication is currently not supported.
* @param port the device port to listen on
* @param username username
* @param password password
* @param context context for file access
*/
public SafDAVServer(int port, String username, String password, Context context) {
super(hostname, port);
itemAccess = new SafFileAccess(context);
this.paths = new ProviderPaths(context);
respSerializer = new XmlResponseSerialization();
this.requiredAuthHeader = "Basic " + Base64.encodeToString(
(username + ':' + password).getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP) ;
}

@Override
public Response serve(IHTTPSession session) {
if(!checkAuthorization(session)){
Log.e(TAG, "serve: request not (correctly) authenticated");
return newFixedLengthResponse(Response.Status.UNAUTHORIZED, null, null);
}
switch (session.getMethod()) {
// reading methods
case GET:
Expand All @@ -64,6 +89,14 @@ public Response serve(IHTTPSession session) {
}
}

private boolean checkAuthorization(IHTTPSession session) {
if(null == requiredAuthHeader) {
return true;
}
String suppliedAuthHeader = session.getHeaders().get("authorization");
return requiredAuthHeader.equals(suppliedAuthHeader);
}

private Response badRequest(String signature) {
Log.d(TAG, signature + ": 400 Bad Request");
return newFixedLengthResponse(Response.Status.BAD_REQUEST, "text/html", "<!doctype html><html><body><h1>BAD REQEST</h1></body></html>");
Expand Down
17 changes: 16 additions & 1 deletion safdav/src/main/java/io/github/x0b/safdav/SafDirectServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import android.content.Context;
import android.net.Uri;
import io.github.x0b.safdav.saf.DocumentsContractAccess;
import io.github.x0b.safdav.file.ItemAccess;
import io.github.x0b.safdav.file.SafItem;
import io.github.x0b.safdav.saf.DocumentsContractAccess;
import io.github.x0b.safdav.saf.ProviderPaths;

import java.util.List;
Expand Down Expand Up @@ -65,4 +65,19 @@ public void delete(String path) {
Uri itemUri = providerPaths.getUriByMappedPath(path);
itemAccess.deleteItem(itemUri);
}

/**
* Get a content uri from a SafDAV server uri
* @param safDavUri the uri to decode - include any non-host path
* @return an accessible content uri
* @throws io.github.x0b.safdav.file.ItemNotFoundException if the uri is not accessible
*/
public Uri getContentUri(String safDavUri) {
return providerPaths.getUriByMappedPath(safDavUri);
}

public Uri getDocumentUri(String safDavUri) {
Uri itemUri = providerPaths.getUriByMappedPath(safDavUri);
return itemAccess.readMeta(itemUri).getUri();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public SafItem getSafRootDirectory() {
*/
public Uri getUriByMappedPath(String requestUri) {
if(null == requestUri || '/' != requestUri.charAt(0)){
throw new IllegalArgumentException("You must request an actual path permission");
throw new IllegalArgumentException("You must request an actual path permission, not " + requestUri);
}

Log.v(TAG, "Rewriting URI: " + requestUri);
Expand Down

0 comments on commit bc2b012

Please sign in to comment.