diff --git a/src/common/blib_graph.c b/src/common/blib_graph.c index dce0cba2..79fe7298 100644 --- a/src/common/blib_graph.c +++ b/src/common/blib_graph.c @@ -897,7 +897,7 @@ void chart_draw(int x1, int y1, int x2, int y2, var_num_t *vals, int count, var_num_t *xvals, int xcount, int chart, int marks) { var_num_t lx, ly; char buf[32]; - int32_t color = 0; + int32_t color = dev_fgcolor; int rx1 = x1; // ready @@ -1052,6 +1052,7 @@ void chart_draw(int x1, int y1, int x2, int y2, var_num_t *vals, int count, case 5: // line chart // points + dev_settextcolor(color, 15); if (chart == 5) { for (int i = 0; i < count; i++) { dev_setpixel(pts[i * 2], pts[i * 2 + 1]); @@ -1061,6 +1062,7 @@ void chart_draw(int x1, int y1, int x2, int y2, var_num_t *vals, int count, dev_line(pts[(i - 1) * 2], pts[(i - 1) * 2 + 1], pts[i * 2], pts[i * 2 + 1]); } } + dev_settextcolor(0, 15); // draw marks if (marks & 0x1) { @@ -1094,19 +1096,12 @@ void chart_draw(int x1, int y1, int x2, int y2, var_num_t *vals, int count, // draw rect color = 0; for (int i = 1; i < count; i++) { - if (os_color_depth > 2) { - dev_setcolor(color); - color++; - if (color >= 15) { - color = 0; - } - } + dev_setcolor(color); + color = (color + 1) % 16; dev_rect(pts[(i - 1) * 2], pts[(i - 1) * 2 + 1], pts[i * 2] - 2, y2, 1); } - if (os_color_depth > 2) { - dev_setcolor(color); - } + dev_setcolor(color); dev_rect(pts[(count - 1) * 2], pts[(count - 1) * 2 + 1], pts[(count - 1) * 2] + lx - 1, y2, 1); @@ -1121,23 +1116,18 @@ void chart_draw(int x1, int y1, int x2, int y2, var_num_t *vals, int count, int mx = pts[i * 2] + lx / 2 - fw / 2; int my = pts[i * 2 + 1]; - if (os_color_depth > 2) { - if (my - fh >= y1) { - dev_settextcolor(0, 15); + if (my - fh >= y1) { + dev_settextcolor(0, 15); + } else { + if (color >= 7 && color != 8) { + dev_settextcolor(0, color); } else { - if (color >= 7 && color != 8) { - dev_settextcolor(0, color); - } else { - dev_settextcolor(15, color); - } - } - - color++; - if (color >= 15) { - color = 0; + dev_settextcolor(15, color); } } + color = (color + 1) % 16; + if (my - fh >= y1) { my -= fh; } diff --git a/src/common/device.h b/src/common/device.h index 7e22d451..e31eb254 100644 --- a/src/common/device.h +++ b/src/common/device.h @@ -99,8 +99,6 @@ void g_line(int x1, int y1, int x2, int y2, void (*dotproc) (int, int)); * * @code * byte os_charset; // System's charset (see os_charset_codes) - * uint32_t os_color_depth; // The number of bits of the supported colors - * // (i.e.: 8 for 256 colors, 15 or 16 for 64K, 24 or 32 for 1.6M) * byte os_graphics; // Non-zero if the driver supports graphics * int os_graf_mx; // Graphic mode: screen width * int os_graf_my; // Graphic mode: screen height @@ -128,8 +126,6 @@ enum os_charset_codes { extern byte os_charset; extern byte os_color; // true if the output has real colors (256+ colors) -extern uint32_t os_color_depth; // the number of bits of the supported colors - // (ex: 8 for 256 colors, 15 or 16 for 64K, 24 or 32 for 1.6M) extern byte os_graphics; // non-zero if the driver supports graphics extern int os_graf_mx; // graphic mode: maximum x extern int os_graf_my; // graphic mode: maximum y diff --git a/src/common/screen.c b/src/common/screen.c index 03f5b62d..612e69b8 100644 --- a/src/common/screen.c +++ b/src/common/screen.c @@ -27,7 +27,6 @@ c |= ((y > dev_Vy2) << 3); } #define CLIPIN(c) ((c & 0xF) == 0) -uint32_t os_color_depth = 16; byte os_graphics = 0; // CONSOLE int os_graf_mx = 80; int os_graf_my = 25; diff --git a/src/platform/android/app/build.gradle b/src/platform/android/app/build.gradle index 9d5887aa..fb95a90b 100644 --- a/src/platform/android/app/build.gradle +++ b/src/platform/android/app/build.gradle @@ -9,7 +9,7 @@ android { applicationId 'net.sourceforge.smallbasic' minSdkVersion 16 targetSdkVersion 33 - versionCode 54 + versionCode 55 versionName '12.25' resConfigs 'en' } 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..ebcd3a0b 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 @@ -72,6 +72,7 @@ import java.util.Map; import java.util.Properties; import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -103,6 +104,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 ConcurrentHashMap<>(); private String[] _options = null; private MediaPlayer _mediaPlayer = null; private LocationAdapter _locationAdapter = null; @@ -149,13 +151,13 @@ public int ask(final byte[] titleBytes, final byte[] promptBytes, final boolean public void run() { AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(title).setMessage(prompt); - builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() { + builder.setPositiveButton(R.string.YES, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.setYes(); mutex.release(); } }); - builder.setNegativeButton("No", new DialogInterface.OnClickListener() { + builder.setNegativeButton(R.string.NO, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.setNo(); mutex.release(); @@ -169,7 +171,7 @@ public void onCancel(DialogInterface dialog) { } }); if (cancel) { - builder.setNeutralButton("Cancel", new DialogInterface.OnClickListener() { + builder.setNeutralButton(R.string.CANCEL, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.setCancel(); mutex.release(); @@ -501,8 +503,7 @@ public void setClipboardText(final byte[] textBytes) { final String text = new String(textBytes, CP1252); runOnUiThread(new Runnable() { public void run() { - ClipboardManager clipboard = - (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { ClipData clip = ClipData.newPlainText("text", text); clipboard.setPrimaryClip(clip); @@ -577,8 +578,9 @@ public void showAlert(final byte[] titleBytes, final byte[] messageBytes) { runOnUiThread(new Runnable() { public void run() { new AlertDialog.Builder(activity) - .setTitle(title).setMessage(message) - .setPositiveButton("OK", new DialogInterface.OnClickListener() { + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.OK, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) {} }).show(); } @@ -771,6 +773,14 @@ private void installSamples() { } } + private boolean isHostDenied(String remoteHost) { + return (remoteHost != null && permittedHost.get(remoteHost) != null && Boolean.FALSE.equals(permittedHost.get(remoteHost))); + } + + private boolean isHostNotPermitted(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); @@ -797,11 +807,11 @@ private void processSettings() { is = getApplication().openFileInput("settings.txt"); Properties p = new Properties(); p.load(is); - int socket = Integer.parseInt(p.getProperty("serverSocket", "-1")); + int port = Integer.parseInt(p.getProperty("serverSocket", "-1")); String token = p.getProperty("serverToken", new Date().toString()); - if (socket > 1023 && socket < 65536) { + if (port > 1023 && port < 65536) { WebServer webServer = new WebServerImpl(); - webServer.run(socket, token); + webServer.run(port, token); } else { Log.i(TAG, "Web service disabled"); } @@ -848,6 +858,27 @@ 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 +894,12 @@ private void setupStorageEnvironment() { setenv("LEGACY_DIR", _storage.getMedia()); } + private void validateAccess(String remoteHost) throws IOException { + if (isHostNotPermitted(remoteHost)) { + throw new IOException(getString(R.string.PORTAL_DENIED)); + } + } + private static class BasFileFilter implements FilenameFilter { @Override public boolean accept(File dir, String name) { @@ -944,7 +981,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"); } @@ -958,19 +996,30 @@ protected void deleteFile(String fileName) throws IOException { } @Override - protected void execStream(InputStream inputStream) throws IOException { - MainActivity.this.execStream(inputStream); + protected void execStream(String remoteHost, InputStream inputStream) throws IOException { + if (isHostDenied(remoteHost)) { + throw new IOException(getString(R.string.PORTAL_DENIED)); + } + if (isHostNotPermitted(remoteHost)) { + requestHostPermission(remoteHost); + } else { + MainActivity.this.execStream(inputStream); + } } @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) && isHostNotPermitted(remoteHost)) { + requestHostPermission(remoteHost); + } } else { + validateAccess(remoteHost); File file = getFile(path); if (file != null) { result = new Response(new FileInputStream(file), file.length()); @@ -982,7 +1031,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 +1051,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 +1070,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..156dfc0f 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"; @@ -49,11 +50,11 @@ public WebServer() { /** * Runs the WebServer in a new thread */ - public void run(final int socketNum, final String token) { + public void run(final int portNum, final String token) { Thread socketThread = new Thread(new Runnable() { public void run() { try { - runServer(socketNum, token); + runServer(portNum, token); } catch (Exception e) { log("Server failed to start: ", e); } @@ -62,25 +63,25 @@ public void run() { socketThread.start(); } - protected abstract void deleteFile(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 void deleteFile(String remoteHost, String fileName) throws IOException; + protected abstract void execStream(String remoteHost, InputStream inputStream) 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 */ - private void runServer(final int socketNum, final String token) throws IOException { - log("Listening :" + socketNum); + private void runServer(final int portNum, final String token) throws IOException { + log("Listening :" + portNum); log("Token :" + token); ServerSocket serverSocket; try { - serverSocket = new ServerSocket(socketNum); + serverSocket = new ServerSocket(portNum); } catch (IllegalArgumentException e) { log("Failed to start server: ", e); throw new IOException(e); @@ -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,19 +447,25 @@ private Response handleFileList() throws IOException { builder.append('['); long id = 0; char comma = 0; - for (FileData nextFile : getFileData()) { - builder.append(comma); - builder.append('{'); - builder.append("id", id++, true); - builder.append("fileName", nextFile.fileName, true); - builder.append("size", nextFile.size, true); - builder.append("date", nextFile.date, false); - builder.append('}'); - comma = ','; + Response result; + try { + for (FileData nextFile : getFileData(remoteHost)) { + builder.append(comma); + builder.append('{'); + builder.append("id", id++, true); + builder.append("fileName", nextFile.fileName, true); + builder.append("size", nextFile.size, true); + builder.append("date", nextFile.date, false); + builder.append('}'); + comma = ','; + } + builder.append(']'); + byte[] json = builder.getBytes(); + result = new Response(new ByteArrayInputStream(json), json.length); + } catch (Exception e) { + result = handleStatus(false, e.getMessage()); } - builder.append(']'); - byte[] json = builder.getBytes(); - return new Response(new ByteArrayInputStream(json), json.length); + return result; } /** @@ -516,10 +525,10 @@ 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) { + } catch (Exception e) { result = handleStatus(false, e.getMessage()); } return result; @@ -533,9 +542,9 @@ 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) { + } catch (Exception e) { result = handleStatus(false, e.getMessage()); } return result; @@ -543,7 +552,7 @@ private Response handleRename(Map data) throws IOException { private void handleRun(InputStream inputStream) throws IOException { if (tokenKey.equals(requestToken)) { - execStream(inputStream); + execStream(remoteHost, inputStream); } else { log("Invalid token"); } @@ -575,7 +584,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 +607,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..b29b714b 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,13 @@ SmallBASIC - Samsung keyboard not supported. Please use an alternative keyboard. + Access denied + Allow Web portal access? + Do you wish to allow anyone with IP address [%s] to gain access to the portal? + OK + Cancel + Yes + No