From 1e362ed09e53613d034f9be5ec3d5112daa7e124 Mon Sep 17 00:00:00 2001 From: chrisws Date: Sun, 20 Nov 2022 10:20:38 +1000 Subject: [PATCH 1/5] ANDROID: implemented IP based authentication for the web portal --- .../sourceforge/smallbasic/MainActivity.java | 51 ++++++++++++++++--- .../net/sourceforge/smallbasic/WebServer.java | 31 ++++++----- .../app/src/main/res/values/strings.xml | 5 +- .../net/sourceforge/smallbasic/Server.java | 10 ++-- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java index 6d968edb..c5e759aa 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/MainActivity.java @@ -103,6 +103,7 @@ public class MainActivity extends NativeActivity { private final ExecutorService _audioExecutor = Executors.newSingleThreadExecutor(); private final Queue _sounds = new ConcurrentLinkedQueue<>(); private final Handler _keypadHandler = new Handler(Looper.getMainLooper()); + private final Map permittedHost = new HashMap<>(); private String[] _options = null; private MediaPlayer _mediaPlayer = null; private LocationAdapter _locationAdapter = null; @@ -578,7 +579,7 @@ public void showAlert(final byte[] titleBytes, final byte[] messageBytes) { public void run() { new AlertDialog.Builder(activity) .setTitle(title).setMessage(message) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.OK, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) {} }).show(); } @@ -771,6 +772,10 @@ private void installSamples() { } } + private boolean isHostPermitted(String remoteHost) { + return (remoteHost != null && permittedHost.get(remoteHost) != null && Boolean.TRUE.equals(permittedHost.get(remoteHost))); + } + private boolean locationPermitted() { String permission = Manifest.permission.ACCESS_FINE_LOCATION; return (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED); @@ -848,6 +853,26 @@ private String readLine(InputStream inputReader) throws IOException { return b == -1 ? null : out.size() == 0 ? "" : out.toString(); } + private void requestHostPermission(String remoteHost) { + final Activity activity = this; + runOnUiThread(new Runnable() { + public void run() { + new AlertDialog.Builder(activity) + .setTitle(R.string.PORTAL_PROMPT).setMessage(getString(R.string.PORTAL_QUESTION, remoteHost)) + .setPositiveButton(R.string.OK, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + permittedHost.put(remoteHost, Boolean.TRUE); + } + }) + .setNegativeButton(R.string.Cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + permittedHost.put(remoteHost, Boolean.FALSE); + } + }).show(); + } + }); + } + private String saveSchemeData(final String buffer) throws IOException { File outputFile = new File(_storage.getInternal(), SCHEME_BAS); BufferedWriter output = new BufferedWriter(new FileWriter(outputFile)); @@ -863,6 +888,12 @@ private void setupStorageEnvironment() { setenv("LEGACY_DIR", _storage.getMedia()); } + private void validateAccess(String remoteHost) throws IOException { + if (!isHostPermitted(remoteHost)) { + throw new IOException("Access denied"); + } + } + private static class BasFileFilter implements FilenameFilter { @Override public boolean accept(File dir, String name) { @@ -944,7 +975,8 @@ protected byte[] decodeBase64(String data) { } @Override - protected void deleteFile(String fileName) throws IOException { + protected void deleteFile(String remoteHost, String fileName) throws IOException { + validateAccess(remoteHost); if (fileName == null) { throw new IOException("Empty file name"); } @@ -963,14 +995,18 @@ protected void execStream(InputStream inputStream) throws IOException { } @Override - protected Response getFile(String path, boolean asset) throws IOException { + protected Response getFile(String remoteHost, String path, boolean asset) throws IOException { Response result; if (asset) { String name = "webui/" + path; long length = getFileLength(name); log("Opened " + name + " " + length + " bytes"); result = new Response(getAssets().open(name), length); + if ("index.html".equals(path) && !isHostPermitted(remoteHost)) { + requestHostPermission(remoteHost); + } } else { + validateAccess(remoteHost); File file = getFile(path); if (file != null) { result = new Response(new FileInputStream(file), file.length()); @@ -982,7 +1018,8 @@ protected Response getFile(String path, boolean asset) throws IOException { } @Override - protected Collection getFileData() throws IOException { + protected Collection getFileData(String remoteHost) throws IOException { + validateAccess(remoteHost); Collection result = new ArrayList<>(); result.addAll(getFiles(new File(_storage.getExternal()))); result.addAll(getFiles(new File(_storage.getMedia()))); @@ -1001,7 +1038,8 @@ protected void log(String message) { } @Override - protected void renameFile(String from, String to) throws IOException { + protected void renameFile(String remoteHost, String from, String to) throws IOException { + validateAccess(remoteHost); if (to == null) { throw new IOException("Empty file name"); } @@ -1019,7 +1057,8 @@ protected void renameFile(String from, String to) throws IOException { } @Override - protected void saveFile(String fileName, byte[] content) throws IOException { + protected void saveFile(String remoteHost, String fileName, byte[] content) throws IOException { + validateAccess(remoteHost); File file = new File(_storage.getExternal(), fileName); if (file.exists()) { throw new IOException("File already exists: " + fileName); diff --git a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java index 2818ba05..c2890e74 100644 --- a/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java +++ b/src/platform/android/app/src/main/java/net/sourceforge/smallbasic/WebServer.java @@ -8,6 +8,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URLDecoder; @@ -32,7 +33,7 @@ * @author chrisws */ public abstract class WebServer { - private static final int BUFFER_SIZE = 32768 / 2; + private static final int BUFFER_SIZE = 32768; private static final int SEND_SIZE = BUFFER_SIZE / 4; private static final int LINE_SIZE = 128; private static final String UTF_8 = "utf-8"; @@ -62,15 +63,15 @@ public void run() { socketThread.start(); } - protected abstract void deleteFile(String fileName) throws IOException; + protected abstract void deleteFile(String remoteHost, String fileName) throws IOException; protected abstract void execStream(InputStream inputStream) throws IOException; - protected abstract Response getFile(String path, boolean asset) throws IOException; - protected abstract Collection getFileData() throws IOException; + protected abstract Response getFile(String remoteHost, String path, boolean asset) throws IOException; + protected abstract Collection getFileData(String remoteHost) throws IOException; protected abstract byte[] decodeBase64(String data); protected abstract void log(String message); protected abstract void log(String message, Exception exception); - protected abstract void renameFile(String from, String to) throws IOException; - protected abstract void saveFile(String fileName, byte[] content) throws IOException; + protected abstract void renameFile(String remoteHost, String from, String to) throws IOException; + protected abstract void saveFile(String remoteHost, String fileName, byte[] content) throws IOException; /** * WebServer main loop to be run in a separate thread @@ -108,6 +109,7 @@ public abstract class AbstractRequest { final String tokenKey; final List headers; final InputStream inputStream; + final String remoteHost; public AbstractRequest(Socket socket, String tokenKey) throws IOException { this.socket = socket; @@ -115,6 +117,7 @@ public AbstractRequest(Socket socket, String tokenKey) throws IOException { this.inputStream = socket.getInputStream(); this.headers = getHeaders(); this.requestToken = getToken(headers); + this.remoteHost = ((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(); String first = headers.size() > 0 ? headers.get(0) : null; String[] fields; if (first != null) { @@ -387,7 +390,7 @@ private void afterRun() { */ private Collection getAllFileNames() throws IOException { Collection result = new ArrayList<>(); - for (FileData fileData : getFileData()) { + for (FileData fileData : getFileData(remoteHost)) { result.add(fileData.fileName); } return result; @@ -416,13 +419,13 @@ private Response handleDownload(Map> data) throws IOE result = handleStatus(false, "File list is empty"); } else if (fileNames.size() == 1) { // plain text download single file - result = getFile(fileNames.iterator().next(), false); + result = getFile(remoteHost, fileNames.iterator().next(), false); } else { // download multiple as zip ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream); for (String fileName : fileNames) { - Response response = getFile(fileName, false); + Response response = getFile(remoteHost, fileName, false); ZipEntry entry = new ZipEntry(fileName); zipOutputStream.putNextEntry(entry); response.toStream(zipOutputStream); @@ -444,7 +447,7 @@ private Response handleFileList() throws IOException { builder.append('['); long id = 0; char comma = 0; - for (FileData nextFile : getFileData()) { + for (FileData nextFile : getFileData(remoteHost)) { builder.append(comma); builder.append('{'); builder.append("id", id++, true); @@ -516,7 +519,7 @@ private Response handleDelete(Map data) throws IOException { String fileName = getString(data, "fileName"); Response result; try { - deleteFile(fileName); + deleteFile(remoteHost, fileName); log("Deleted " + fileName); result = handleFileList(); } catch (IOException e) { @@ -533,7 +536,7 @@ private Response handleRename(Map data) throws IOException { String to = getString(data, "to"); Response result; try { - renameFile(from, to); + renameFile(remoteHost, from, to); result = handleStatus(true, "File renamed"); } catch (IOException e) { result = handleStatus(false, e.getMessage()); @@ -575,7 +578,7 @@ private Response handleUpload(Map data) throws IOException { if (fileName == null || content == null) { result = handleStatus(false, "Invalid input"); } else { - saveFile(fileName, content); + saveFile(remoteHost, fileName, content); result = handleStatus(true, "File saved"); } } catch (Exception e) { @@ -598,7 +601,7 @@ private Response handleWebResponse(String asset) throws IOException { } Response result; try { - result = getFile(path, true); + result = getFile(remoteHost, path, true); } catch (Exception e) { log("Error: " + e); result = null; diff --git a/src/platform/android/app/src/main/res/values/strings.xml b/src/platform/android/app/src/main/res/values/strings.xml index bb11245f..a07a6a8c 100644 --- a/src/platform/android/app/src/main/res/values/strings.xml +++ b/src/platform/android/app/src/main/res/values/strings.xml @@ -1,7 +1,10 @@ SmallBASIC - Samsung keyboard not supported. Please use an alternative keyboard. + Allow Web portal access? + Do you wish to allow anyone with IP address [%s] to gain access to the portal? + OK + Cancel