keepass2android/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java

478 lines
16 KiB
Java

package keepass2android.javafilestorage;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.DispatchingAuthenticator;
import com.burgstaller.okhttp.basic.BasicAuthenticator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import keepass2android.javafilestorage.webdav.ConnectionInfo;
import keepass2android.javafilestorage.webdav.DecoratedTrustManager;
import keepass2android.javafilestorage.webdav.PropfindXmlParser;
import keepass2android.javafilestorage.webdav.WebDavUtil;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class WebDavStorage extends JavaFileStorageBase {
private final ICertificateErrorHandler mCertificateErrorHandler;
public WebDavStorage(ICertificateErrorHandler certificateErrorHandler)
{
mCertificateErrorHandler = certificateErrorHandler;
}
public String buildFullPath(String url, String username, String password) throws UnsupportedEncodingException {
String scheme = url.substring(0, url.indexOf("://"));
url = url.substring(scheme.length() + 3);
return scheme + "://" + encode(username)+":"+encode(password)+"@"+url;
}
private ConnectionInfo splitStringToConnectionInfo(String filename)
throws UnsupportedEncodingException {
ConnectionInfo ci = new ConnectionInfo();
String scheme = filename.substring(0, filename.indexOf("://"));
filename = filename.substring(scheme.length() + 3);
String userPwd = filename.substring(0, filename.indexOf('@'));
ci.username = decode(userPwd.substring(0, userPwd.indexOf(":")));
ci.password = decode(userPwd.substring(userPwd.indexOf(":") + 1));
ci.URL = scheme + "://" +filename.substring(filename.indexOf('@') + 1);
return ci;
}
private static final String HTTP_PROTOCOL_ID = "http";
private static final String HTTPS_PROTOCOL_ID = "https";
@Override
public boolean checkForFileChangeFast(String path,
String previousFileVersion) throws Exception {
String currentVersion = getCurrentFileVersionFast(path);
if (currentVersion == null)
return false;
return currentVersion.equals(previousFileVersion) == false;
}
@Override
public String getCurrentFileVersionFast(String path) {
return null; // no simple way to get the version "fast"
}
@Override
public InputStream openFileForRead(String path) throws Exception {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.method("GET", null)
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
return response.body().byteStream();
} catch (Exception e) {
throw convertException(e);
}
}
private OkHttpClient getClient(ConnectionInfo ci) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
com.burgstaller.okhttp.digest.Credentials credentials = new com.burgstaller.okhttp.digest.Credentials(ci.username, ci.password);
final BasicAuthenticator basicAuthenticator = new BasicAuthenticator(credentials);
final DigestAuthenticator digestAuthenticator = new DigestAuthenticator(credentials);
// note that all auth schemes should be registered as lowercase!
DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
.with("digest", digestAuthenticator)
.with("basic", basicAuthenticator)
.build();
builder = builder.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
.addInterceptor(new AuthenticationCacheInterceptor(authCache));
if ((mCertificateErrorHandler != null) && (mCertificateErrorHandler.alwaysFailOnValidationError())) {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
trustManager = new DecoratedTrustManager(trustManager, mCertificateErrorHandler);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
builder = builder.sslSocketFactory(sslSocketFactory, trustManager);
}
OkHttpClient client = builder.build();
return client;
}
@Override
public void uploadFile(String path, byte[] data, boolean writeTransactional)
throws Exception {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.put(RequestBody.create(MediaType.parse("application/binary"), data))
.build();
//TODO consider writeTransactional
//TODO check for error
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
} catch (Exception e) {
throw convertException(e);
}
}
@Override
public String createFolder(String parentPath, String newDirName)
throws Exception {
try {
String newFolder = createFilePath(parentPath, newDirName);
ConnectionInfo ci = splitStringToConnectionInfo(newFolder);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.method("MKCOL", null)
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
return newFolder;
} catch (Exception e) {
throw convertException(e);
}
}
private String concatPaths(String parentPath, String newDirName) {
String res = parentPath;
if (!res.endsWith("/"))
res += "/";
res += newDirName;
return res;
}
@Override
public String createFilePath(String parentPath, String newFileName)
throws Exception {
if (parentPath.endsWith("/") == false)
parentPath += "/";
return parentPath + newFileName;
}
public List<FileEntry> listFiles(String parentPath, int depth) throws Exception {
ArrayList<FileEntry> result = new ArrayList<>();
try {
if (parentPath.endsWith("/"))
parentPath = parentPath.substring(0,parentPath.length()-1);
ConnectionInfo ci = splitStringToConnectionInfo(parentPath);
String requestBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<d:propfind xmlns:d=\"DAV:\">\n" +
" <d:prop><d:displayname/><d:getlastmodified/><d:getcontentlength/></d:prop>\n" +
"</d:propfind>\n";
Log.d("WEBDAV", "starting query for " + ci.URL);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.method("PROPFIND", RequestBody.create(MediaType.parse("application/xml"),requestBody))
.addHeader("Depth",String.valueOf(depth))
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
String xml = response.body().string();
PropfindXmlParser parser = new PropfindXmlParser();
List<PropfindXmlParser.Response> responses = parser.parse(new StringReader(xml));
for (PropfindXmlParser.Response r: responses)
{
PropfindXmlParser.Response.PropStat.Prop okprop =r.getOkProp();
if (okprop != null)
{
FileEntry e = new FileEntry();
e.canRead = e.canWrite = true;
Date lastMod = WebDavUtil.parseDate(okprop.LastModified);
if (lastMod != null)
e.lastModifiedTime = lastMod.getTime();
if (okprop.ContentLength != null)
{
try {
e.sizeInBytes = Integer.parseInt(okprop.ContentLength);
} catch (NumberFormatException exc) {
e.sizeInBytes = -1;
}
}
e.isDirectory = r.href.endsWith("/");
e.displayName = okprop.DisplayName;
if (e.displayName == null)
{
e.displayName = getDisplayNameFromHref(r.href);
}
e.path = r.href;
if (e.path.indexOf("://") == -1)
{
//relative path:
e.path = buildPathFromHref(parentPath, r.href);
}
if ((depth == 1) && e.isDirectory)
{
String path = e.path;
if (!path.endsWith("/"))
path += "/";
String parentPathWithTrailingSlash = parentPath + "/";
//for depth==1 only list children, not directory itself
if (path.equals(parentPathWithTrailingSlash))
continue;
}
result.add(e);
}
}
return result;
} catch (Exception e) {
throw convertException(e);
}
}
private String buildPathFromHref(String parentPath, String href) throws UnsupportedEncodingException {
String scheme = parentPath.substring(0, parentPath.indexOf("://"));
String filename = parentPath.substring(scheme.length() + 3);
String userPwd = filename.substring(0, filename.indexOf('@'));
String username_enc = (userPwd.substring(0, userPwd.indexOf(":")));
String password_enc = (userPwd.substring(userPwd.indexOf(":") + 1));
String host = filename.substring(filename.indexOf('@')+1);
int firstSlashPos = host.indexOf("/");
if (firstSlashPos >= 0)
{
host = host.substring(0,firstSlashPos);
}
if (!href.startsWith("/"))
href = "/" + href;
return scheme + "://" + username_enc + ":" + password_enc + "@" + host + href;
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
return listFiles(parentPath, 1);
}
private void checkStatus(Response response) throws Exception {
if((response.code() < 200)
|| (response.code() >= 300))
{
if (response.code() == 404)
throw new FileNotFoundException();
throw new Exception("Received unexpected response: " + response.toString());
}
}
private Exception convertException(Exception e) {
return e;
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
List<FileEntry> list = listFiles(filename,0);
if (list.size() != 1)
throw new FileNotFoundException();
return list.get(0);
}
@Override
public void delete(String path) throws Exception {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
Request request = new Request.Builder()
.url(new URL(ci.URL))
.delete()
.build();
Response response = getClient(ci).newCall(request).execute();
checkStatus(response);
} catch (Exception e) {
throw convertException(e);
}
}
@Override
public void startSelectFile(
JavaFileStorage.FileStorageSetupInitiatorActivity activity,
boolean isForSave, int requestCode) {
activity.performManualFileSelect(isForSave, requestCode, getProtocolId());
}
@Override
protected String decode(String encodedString)
throws UnsupportedEncodingException {
return java.net.URLDecoder.decode(encodedString, UTF_8);
}
@Override
protected String encode(final String unencoded)
throws UnsupportedEncodingException {
return java.net.URLEncoder.encode(unencoded, UTF_8);
}
@Override
public void prepareFileUsage(JavaFileStorage.FileStorageSetupInitiatorActivity activity, String path, int requestCode, boolean alwaysReturnSuccess) {
Intent intent = new Intent();
intent.putExtra(EXTRA_PATH, path);
activity.onImmediateResult(requestCode, RESULT_FILEUSAGE_PREPARED, intent);
}
@Override
public String getProtocolId() {
return HTTPS_PROTOCOL_ID;
}
@Override
public void onResume(JavaFileStorage.FileStorageSetupActivity setupAct) {
}
@Override
public boolean requiresSetup(String path) {
return false;
}
@Override
public void onCreate(FileStorageSetupActivity activity,
Bundle savedInstanceState) {
}
String getDisplayNameFromHref(String href)
{
if (href.endsWith("/"))
href = href.substring(0, href.length()-1);
int lastIndex = href.lastIndexOf("/");
if (lastIndex >= 0)
return href.substring(lastIndex + 1);
else
return href;
}
@Override
public String getDisplayName(String path) {
try {
ConnectionInfo ci = splitStringToConnectionInfo(path);
return ci.URL;
}
catch (Exception e)
{
return getDisplayNameFromHref(path);
}
}
@Override
public String getFilename(String path) throws Exception {
if (path.endsWith("/"))
path = path.substring(0, path.length() - 1);
int lastIndex = path.lastIndexOf("/");
if (lastIndex >= 0)
return path.substring(lastIndex + 1);
else
return path;
}
@Override
public void onStart(FileStorageSetupActivity activity) {
}
@Override
public void onActivityResult(FileStorageSetupActivity activity,
int requestCode, int resultCode, Intent data) {
}
@Override
public void prepareFileUsage(Context appContext, String path) {
//nothing to do
}
}