You've already forked KsuWebUIStandalone
mirror of
https://github.com/5ec1cff/KsuWebUIStandalone.git
synced 2025-09-06 06:37:11 +00:00
187 lines
8.0 KiB
Java
187 lines
8.0 KiB
Java
package io.github.a13e300.ksuwebui;
|
|
|
|
import android.content.Context;
|
|
import android.util.Log;
|
|
import android.webkit.WebResourceResponse;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.WorkerThread;
|
|
import androidx.webkit.WebViewAssetLoader;
|
|
|
|
import com.topjohnwu.superuser.nio.FileSystemManager;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.zip.GZIPInputStream;
|
|
|
|
/**
|
|
* Handler class to open files from file system by root access
|
|
* For more information about android storage please refer to
|
|
* <a href="https://developer.android.com/guide/topics/data/data-storage">Android Developers
|
|
* Docs: Data and file storage overview</a>.
|
|
* <p class="note">
|
|
* To avoid leaking user or app data to the web, make sure to choose {@code directory}
|
|
* carefully, and assume any file under this directory could be accessed by any web page subject
|
|
* to same-origin rules.
|
|
* <p>
|
|
* A typical usage would be like:
|
|
* <pre class="prettyprint">
|
|
* File publicDir = new File(context.getFilesDir(), "public");
|
|
* // Host "files/public/" in app's data directory under:
|
|
* // http://appassets.androidplatform.net/public/...
|
|
* WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
|
|
* .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
|
|
* .build();
|
|
* </pre>
|
|
*/
|
|
public final class RemoteFsPathHandler implements WebViewAssetLoader.PathHandler {
|
|
private static final String TAG = "FsServicePathHandler";
|
|
|
|
/**
|
|
* Default value to be used as MIME type if guessing MIME type failed.
|
|
*/
|
|
public static final String DEFAULT_MIME_TYPE = "text/plain";
|
|
|
|
/**
|
|
* Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this
|
|
* handler. They are forbidden as they often contain sensitive information.
|
|
* <p class="note">
|
|
* Note: Any future addition to this list will be considered breaking changes to the API.
|
|
*/
|
|
private static final String[] FORBIDDEN_DATA_DIRS =
|
|
new String[] {"/data/data", "/data/system"};
|
|
|
|
@NonNull
|
|
private final File mDirectory;
|
|
|
|
private final FileSystemManager mFs;
|
|
|
|
/**
|
|
* Creates PathHandler for app's internal storage.
|
|
* The directory to be exposed must be inside either the application's internal data
|
|
* directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}.
|
|
* External storage is not supported for security reasons, as other apps with
|
|
* {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the
|
|
* files.
|
|
* <p>
|
|
* Exposing the entire data or cache directory is not permitted, to avoid accidentally
|
|
* exposing sensitive application files to the web. Certain existing subdirectories of
|
|
* {@link Context#getDataDir} are also not permitted as they are often sensitive.
|
|
* These files are ({@code "app_webview/"}, {@code "databases/"}, {@code "lib/"},
|
|
* {@code "shared_prefs/"} and {@code "code_cache/"}).
|
|
* <p>
|
|
* The application should typically use a dedicated subdirectory for the files it intends to
|
|
* expose and keep them separate from other files.
|
|
*
|
|
* @param context {@link Context} that is used to access app's internal storage.
|
|
* @param directory the absolute path of the exposed app internal storage directory from
|
|
* which files can be loaded.
|
|
* @throws IllegalArgumentException if the directory is not allowed.
|
|
*/
|
|
public RemoteFsPathHandler(@NonNull Context context, @NonNull File directory, FileSystemManager fs) {
|
|
try {
|
|
mDirectory = new File(getCanonicalDirPath(directory));
|
|
if (!isAllowedInternalStorageDir(context)) {
|
|
throw new IllegalArgumentException("The given directory \"" + directory
|
|
+ "\" doesn't exist under an allowed app internal storage directory");
|
|
}
|
|
mFs = fs;
|
|
} catch (IOException e) {
|
|
throw new IllegalArgumentException(
|
|
"Failed to resolve the canonical path for the given directory: "
|
|
+ directory.getPath(), e);
|
|
}
|
|
}
|
|
|
|
private boolean isAllowedInternalStorageDir(@NonNull Context context) throws IOException {
|
|
String dir = getCanonicalDirPath(mDirectory);
|
|
|
|
for (String forbiddenPath : FORBIDDEN_DATA_DIRS) {
|
|
if (dir.startsWith(forbiddenPath)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Opens the requested file from the exposed data directory.
|
|
* <p>
|
|
* The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the
|
|
* requested file cannot be found or is outside the mounted directory a
|
|
* {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be
|
|
* returned instead of {@code null}. This saves the time of falling back to network and
|
|
* trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with
|
|
* {@code null} {@link InputStream} will be received as an HTTP response with status code
|
|
* {@code 404} and no body.
|
|
* <p class="note">
|
|
* The MIME type for the file will be determined from the file's extension using
|
|
* {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that
|
|
* files are named using standard file extensions. If the file does not have a
|
|
* recognised extension, {@code "text/plain"} will be used by default.
|
|
*
|
|
* @param path the suffix path to be handled.
|
|
* @return {@link WebResourceResponse} for the requested file.
|
|
*/
|
|
@Override
|
|
@WorkerThread
|
|
@NonNull
|
|
public WebResourceResponse handle(@NonNull String path) {
|
|
try {
|
|
File file = getCanonicalFileIfChild(mDirectory, path);
|
|
if (file != null) {
|
|
InputStream is = openFile(file, mFs);
|
|
String mimeType = guessMimeType(path);
|
|
return new WebResourceResponse(mimeType, null, is);
|
|
} else {
|
|
Log.e(TAG, String.format(
|
|
"The requested file: %s is outside the mounted directory: %s", path,
|
|
mDirectory));
|
|
}
|
|
} catch (IOException e) {
|
|
Log.e(TAG, "Error opening the requested path: " + path, e);
|
|
}
|
|
return new WebResourceResponse(null, null, null);
|
|
}
|
|
|
|
public static String getCanonicalDirPath(@NonNull File file) throws IOException {
|
|
String canonicalPath = file.getCanonicalPath();
|
|
if (!canonicalPath.endsWith("/")) canonicalPath += "/";
|
|
return canonicalPath;
|
|
}
|
|
|
|
public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child)
|
|
throws IOException {
|
|
String parentCanonicalPath = getCanonicalDirPath(parent);
|
|
String childCanonicalPath = new File(parent, child).getCanonicalPath();
|
|
if (childCanonicalPath.startsWith(parentCanonicalPath)) {
|
|
return new File(childCanonicalPath);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@NonNull
|
|
private static InputStream handleSvgzStream(@NonNull String path,
|
|
@NonNull InputStream stream) throws IOException {
|
|
return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream;
|
|
}
|
|
|
|
public static InputStream openFile(@NonNull File file, @NonNull FileSystemManager fs) throws IOException {
|
|
return handleSvgzStream(file.getPath(), fs.getFile(file.getAbsolutePath()).newInputStream());
|
|
}
|
|
|
|
/**
|
|
* Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the
|
|
* {@link #DEFAULT_MIME_TYPE} if it can't guess.
|
|
*
|
|
* @param filePath path of the file to guess its MIME type.
|
|
* @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}.
|
|
*/
|
|
@NonNull
|
|
public static String guessMimeType(@NonNull String filePath) {
|
|
String mimeType = MimeUtil.getMimeFromFileName(filePath);
|
|
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
|
|
}
|
|
}
|