/** * Copyright (c) 2010 Marc A. Paradise This file is part of "BBSSH" BBSSH is based upon MidpSSH by Karl von Randow. * MidpSSH was based upon Telnet Floyd and FloydSSH by Radek Polak. This program is free software; you can redistribute * it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; * either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU * General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, * Cambridge, MA 02139, USA. */ package org.bbssh.util; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.Hashtable; import java.util.Vector; import javax.microedition.io.Connector; import javax.microedition.io.HttpConnection; import javax.microedition.io.file.FileConnection; import net.rim.blackberry.api.invoke.Invoke; import net.rim.blackberry.api.invoke.MessageArguments; import net.rim.blackberry.api.mail.Address; import net.rim.blackberry.api.mail.Message; import net.rim.blackberry.api.mail.Message.RecipientType; import net.rim.blackberry.api.mail.Multipart; import net.rim.blackberry.api.mail.SupportedAttachmentPart; import net.rim.blackberry.api.mail.TextBodyPart; import net.rim.device.api.applicationcontrol.ApplicationPermissions; import net.rim.device.api.crypto.CryptoTokenException; import net.rim.device.api.crypto.HMAC; import net.rim.device.api.i18n.ResourceBundle; import net.rim.device.api.io.FileNotFoundException; import net.rim.device.api.math.Fixed32; import net.rim.device.api.system.Bitmap; import net.rim.device.api.system.ControlledAccessException; import net.rim.device.api.system.CoverageInfo; import net.rim.device.api.system.DeviceInfo; import net.rim.device.api.system.EncodedImage; import net.rim.device.api.system.PNGEncodedImage; import net.rim.device.api.system.RadioInfo; import net.rim.device.api.ui.Font; import net.rim.device.api.ui.component.Dialog; import net.rim.device.api.util.Arrays; import org.bbssh.BBSSHApp; import org.bbssh.i18n.BBSSHResource; import org.bbssh.io.LineInputStream; import org.bbssh.net.ConnectionHelper; import org.bbssh.net.session.SshSession; import org.bbssh.session.RemoteSessionInstance; import org.bbssh.session.SessionManager; import org.bbssh.ssh.kex.KexAgreement; /** * Utility class containing various helper/tool functions. */ public class Tools { public static final long NOTIFICATION_GUID = 0x148f158c8bac0113L; // org.bbssh.BBSSH Notifications public static Object[] vectorToArray(Vector v, int frontPadding) { int limit = (v == null) ? 0 : v.size(); int size = limit + frontPadding; Object ret[] = new Object[size]; for (int x = 0; x < limit; x++) { ret[x + frontPadding] = v.elementAt(x); } return ret; } public static Object[] vectorToArray(Vector v) { return vectorToArray(v, 0); } public static String byteCountToHumanReadableString(int bytes) { if (bytes < 1024) { return bytes + " bytes"; } else if (bytes < 1024 * 1024) { return getIntAsStringWithTwoDecimals(bytes * 100 / 1024) + " KB"; } else { return getIntAsStringWithTwoDecimals(bytes * 100 / (1024 * 1024)) + " MB"; } } private static String getIntAsStringWithTwoDecimals(int i) { String str = "" + i; return str.substring(0, str.length() - 2) + "." + str.substring(str.length() - 2); } /** * Splits a string using the provided delimeter. * * @param data * @param splitChar * @return array of strings */ public static final String[] splitString(final String data, final char splitChar) { if (data == null || data.length() == 0) return new String[] {}; Vector v = new Vector(); // @todo - more efficient to count iterations first? // @todo - implement "max" number of elements to return. int indexStart = 0; int indexEnd = data.indexOf(splitChar); while (indexEnd != -1) { String s = data.substring(indexStart, indexEnd); if (s.length() > 0) { v.addElement(s); } indexStart = indexEnd + 1; indexEnd = data.indexOf(splitChar, indexStart); } if (indexStart != data.length()) { String s = data.substring(indexStart); if (s.length() > 0) { v.addElement(s); } } String[] result = new String[v.size()]; v.copyInto(result); return result; } static final String HEXES = "0123456789ABCDEF"; /** * Converts a string representation of a hex value into byte array. * * @param data * @return hex string as bytes * @throws NumberFormatException * if string is not valid hex format or is does not contain an even number of digits. */ public static byte[] getHexStringAsBytes(String data) throws NumberFormatException { int len = data.length(); byte[] output = new byte[len / 2]; for (int i = 0; i < len; i += 2) { output[i / 2] = (byte) ((Character.digit(data.charAt(i), 16) << 4) + Character.digit(data.charAt(i + 1), 16)); } return output; } public static String getBytesAsUnpaddedHexString(byte[] raw, int count) { if (raw == null) { return null; } final StringBuffer hex = new StringBuffer(3 * raw.length); for (int x = 0; x < count; x++) { byte b = raw[x]; hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))); } return hex.toString(); } public static String getBytesAsHexString(byte[] raw, int count) { if (raw == null) { return null; } final StringBuffer hex = new StringBuffer(3 * raw.length); for (int x = 0; x < count; x++) { byte b = raw[x]; hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F))).append(" "); } return hex.toString(); } public static byte[] fillPattern(byte pattern, int length) { byte[] val = new byte[length]; for (int x = 0; x < length; x++) { val[x] = (byte) pattern; } return val; } /** * A simple helper function primarily used during key negotiations - in many cases the server and client exchange a * list of supported protocols, and the protocl to be used will be the first common protocol listed from clietn and * server. This function simply identifies the first common elements in two string arrays by performing a string * comparison, with first match from "a" found in "b" being the accepted value. * * @param a * first non-null string array to compare * @param b * second non-null string array to compare. * @return the first matching element from "a" that is in "b", or null if none. */ public static String findFirstMatchingElement(String[] a, String[] b) { for (int x = 0; x < a.length; x++) { for (int y = 0; y < b.length; y++) { if (a[x].equals(b[y])) { return a[x]; } } } return null; } /** * Compares the first elements of two string arrays and returns true if and only if they match exactly * * @param a * @param b * @return true if a[0] and b[0] are valid and match. */ public static boolean doFirstElementsMatch(String[] a, String[] b) { if (a == null || b == null) { return false; } if (a.length == 0 && b.length == 0) { return true; } if (a.length == 0 || b.length == 0) { return false; } return a[0].equals(b[0]); } public static StringBuffer getLocalFileContents(String url) throws IOException { FileConnection fc = (FileConnection) Connector.open(url); LineInputStream stream = new LineInputStream(fc.openInputStream()); StringBuffer data = readContentIntoFile(stream); fc.close(); return data; } /** * This is something of a kludge - attempts to load a file via the first valid connection type. * * @param url * @return a valid HttpConnection * @throws IOException * if a connection can't be established. */ public static HttpConnection getHTTPConnectionForURL(String url) throws IOException { int accessCount = 0; for (int x = 0; x < ConnectionHelper.CONNECTION_TYPE_COUNT; x++) { try { return ConnectionHelper.getHttpConnection((byte) x, url); } catch (IOException e) { Logger.error("Failed to load URL via type " + x + ": " + url + " : " + e.getMessage()); } catch (ControlledAccessException ex2) { Logger.error("Access denied for connection type " + x); accessCount++; } } if (accessCount == ConnectionHelper.CONNECTION_TYPE_COUNT) { throw new IOException("No permissions to connect via any available networking mode."); } throw new IOException("Unable to load file via any available connection method."); } /** * Retrieve the file specified by URL and return its contents in a stringbuffer. * * @param url * file to retrieve * @return file contents in stringbuffer. * @throws IOException */ public static StringBuffer getHTTPFileContents(String url) throws IOException { HttpConnection conn = getHTTPConnectionForURL(url); if (conn == null) { Logger.error("Could not obtain valid connection for URL"); return new StringBuffer(); } LineInputStream stream = new LineInputStream(conn.openInputStream()); StringBuffer data = readContentIntoFile(stream); conn.close(); return data; } /** * Reads the full contents of a LineInputStream into a StringBuffer * * @param stream * @return contents of stream. * @throws IOException */ public static StringBuffer readContentIntoFile(LineInputStream stream) throws IOException { String line; StringBuffer data = new StringBuffer(); while ((line = stream.readLine(true)) != null) { data.append(line); } return data; } /** * Sends feedback, optionally including the provided bitmap as an attachement. it is the caller's responsibility to * ensure that this is invoked in a properly synchronized manner. * * @param screenshot * - if not null, this function prompts the user to include the screenshot as an attachment. */ public static void sendFeedback(Bitmap screenshot) { BBSSHApp.inst().requestPermission(ApplicationPermissions.PERMISSION_EMAIL, BBSSHResource.MSG_PERMISSIONS_MISSING_EMAIL_FEEDBACK); ResourceBundle b = ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME); try { Multipart mp = new Multipart(); Message msg = new Message(); // @todo - prompt whether they want to send bug or feature request request or feedback/comments. // if bug/feature, use SUPPORT_EMAIL_ADDRESS and SUPPORT_EMAIL_NAME // otherwise use SUPPORT_EMAIL_FEEDBACK_ADDRESS and SUPPORT_EMAIL_FEEDBACK_NAME Address a = new Address(b.getString(BBSSHResource.SUPPORT_EMAIL_FEEDBACK_ADDRESS), b.getString(BBSSHResource.SUPPORT_EMAIL_FEEDBACK_NAME)); Address[] addresses = { a }; if (screenshot == null || Dialog.ask(Dialog.D_YES_NO, b.getString(BBSSHResource.MSG_FEEDBACK_INCLUDE_SCREENSHOT), Dialog.YES) == Dialog.NO) { } else { PNGEncodedImage img = PNGEncodedImage.encode(screenshot); SupportedAttachmentPart pt = new SupportedAttachmentPart(mp, img.getMIMEType(), "bbssh-screen.png", img.getData()); mp.addBodyPart(pt); } if (Logger.isFileLoggingEnabled()) { if (Dialog.ask(Dialog.D_YES_NO, b.getString(BBSSHResource.MSG_FEEDBACK_INCLUDE_LOGS), Dialog.YES) == Dialog.YES) { byte[] data = Logger.getFileContent(); if (data == null || data.length == 0) { Dialog.inform(b.getString(BBSSHResource.MSG_FEEDBACK_COULD_NOT_READ_LOG)); } else { mp.addBodyPart(new SupportedAttachmentPart(mp, "text/plain", "BBSSH-LOG.TXT", data)); } } } StringBuffer data = new StringBuffer(2048); data.append("\r\n\r\n---------------------\r\n") .append("Please allow us to include the following information in order" + " to help troubleshoot any problems you may be having.") .append("\r\nNone of this information is personally identifiable.\r\n"); buildDiagnosticString(data); TextBodyPart tb = new TextBodyPart(mp, data.toString()); mp.addBodyPart(tb); msg.setContent(mp); msg.addRecipients(RecipientType.TO, addresses); Invoke.invokeApplication(Invoke.APP_TYPE_MESSAGES, new MessageArguments(msg)); } catch (Throwable ex) { Logger.error("Unable to send feedback: " + ex.getMessage()); } } public static void buildDiagnosticString(StringBuffer data) { data.append("BBSSH version: ").append(Version.getAppVersion()).append("\r\n").append("Hardware: ") .append(DeviceInfo.getDeviceName()).append("\r\n").append("Plat version: ") .append(DeviceInfo.getPlatformVersion()).append("\r\n").append("SW version: ") .append(DeviceInfo.getSoftwareVersion()).append("\r\n").append("Mfg: ") .append(DeviceInfo.getManufacturerName()).append("\r\n").append("Release mode: ") .append(Version.isReleaseMode()).append("\r\n").append("Direct: ") .append(CoverageInfo.isCoverageSufficient(CoverageInfo.COVERAGE_DIRECT)).append("\r\n") .append("WiFi Connected: " + ConnectionHelper.isWifiAvailable()).append("\r\n").append("BIS-B: ") .append(CoverageInfo.isCoverageSufficient(CoverageInfo.COVERAGE_BIS_B)).append("\r\n").append("BES: ") .append(CoverageInfo.isCoverageSufficient(CoverageInfo.COVERAGE_MDS)).append("\r\n") .append("Sig Strength: ").append(RadioInfo.getSignalLevel()).append("dB\r\n"); RemoteSessionInstance i = SessionManager.getInstance().activeSession; if (i != null) { if (i.state != null) { data.append("Cols Visible: ").append(i.state.numColsVisible).append("\r\n").append("Rows: ") .append(i.state.numRows).append("\r\n").append("Top Offset: ").append(i.state.topTermRow) .append("\r\n").append("Term Left Offset: ").append(i.state.left).append("\r\n") .append("Paint Stats: (FR PR LE LP PBS PC) ").append(i.state.debugFullRefreshCount).append(' ') .append(i.state.debugPartialRefreshCount).append(' ').append(i.state.debugLineEvalCount) .append(' ').append(i.state.debugLinePaintCount).append(' ') .append(i.state.debugPaintBackStoreCount).append(' ').append(i.state.debugPaintCount) .append(' ').append("Redraw Request Wait Time: ").append(i.state.debugRedrawRequestWaitTime) .append(' ').append("Redraw Paint Wait Time: ").append(i.state.debugRedrawStartWaitTime) .append(' '); if (i.session != null) { data.append("WiFi Override: ").append(i.session.isWifiOverrideConnection()).append("\r\n"); } if (i.session instanceof SshSession) { SshSession s = (SshSession) i.session; data.append("Packet In/Out: ").append(s.inputPacketCount).append('/').append(s.outputPacketCount) .append("\r\n").append("Data Received/Accepted bytes: ").append(s.getReceivedDataBytes()) .append('/').append(s.getAcceptedDataBytes()).append("\r\n"); } if (i.emulator != null) { data.append("Max Rows: ").append(i.emulator.getMaxBufferSize()).append("\r\n") .append("Curr Rows: ").append(i.emulator.getBufferSize()).append("\r\n") .append("Processed data bytes: ").append(i.emulator.handledCharCount).append("\r\n"); } } } } /** * Packs the keystroke * * @param keyCode * @param status * @return packed value of the keycode and status. */ public static long packToLong(int keyCode, int status) { return ((long) keyCode << 32) | status; } private static Hashtable bitmaps = new Hashtable(); private static Hashtable images = new Hashtable(); public static Bitmap loadBitmap(String fileName) { Bitmap image = (Bitmap) bitmaps.get(fileName); Logger.debug("Loading resource: " + fileName); if (image == null) { image = Bitmap.getBitmapResource(fileName); if (image == null) { image = Bitmap.getBitmapResource("cod://" + fileName); } if (image == null) { image = Bitmap.getBitmapResource("/" + fileName); } if (image == null) { Logger.error("Failed to load image file: " + fileName); } else { bitmaps.put(fileName, image); } } return image; } // @todo can we replace Bitmap with this too? public static EncodedImage loadEncodedImage(String fileName) { EncodedImage image = (EncodedImage) images.get(fileName); Logger.debug("Loading resource: " + fileName); if (image == null) { image = EncodedImage.getEncodedImageResource(fileName); if (image == null) { image = EncodedImage.getEncodedImageResource("cod://" + fileName); } if (image == null) { image = EncodedImage.getEncodedImageResource("/" + fileName); } if (image == null) { Logger.error("Failed to load encoded image file: " + fileName); } else { images.put(fileName, image); } } return image; } public static EncodedImage scaleImage(String name, int width, int height) { EncodedImage image = loadEncodedImage(name); if (image == null) return null; int currentWidthFixed32 = Fixed32.toFP(image.getWidth()); int currentHeightFixed32 = Fixed32.toFP(image.getHeight()); int requiredWidthFixed32 = Fixed32.toFP(width); int requiredHeightFixed32 = Fixed32.toFP(height); int scaleXFixed32 = Fixed32.div(currentWidthFixed32, requiredWidthFixed32); int scaleYFixed32 = Fixed32.div(currentHeightFixed32, requiredHeightFixed32); return image.scaleImage32(scaleXFixed32, scaleYFixed32); } /** * Makes every possible attempt to find a file resource by name and returns it as an InputStream if found. If not fo * und, an exception is thrown. It is the caller's responsibility to close the input stream if it opens the stream. * * @param fileName * file name of the resource. * @return InputStream of the requestedresource. * @throws FileNotFoundException * when resource/file can't be found. */ public static InputStream getResourceInputStream(String fileName) throws FileNotFoundException { Object o = new Object(); InputStream stream = o.getClass().getResourceAsStream("cod://" + fileName); if (stream == null) { stream = o.getClass().getResourceAsStream("/" + fileName); } if (stream == null) { stream = o.getClass().getResourceAsStream(fileName); } if (stream == null) throw new FileNotFoundException(); return stream; } public static void copyIntToByteArray(int value, byte[] target, int offset) { target[offset + 3] = (byte) (value & 0xff); target[offset + 2] = (byte) ((value >> 8) & 0xff); target[offset + 1] = (byte) ((value >> 16) & 0xff); target[offset + 0] = (byte) ((value >> 24) & 0xff); } public static int byteArrayToInt(byte[] source, int offset) { return (source[offset + 3] | (source[offset + 2] << 8) | (source[offset + 1] << 16) | (source[offset + 0] << 24)); } private static final byte[] converter = new byte[4]; /** * This works around an apparent flaw in HMAC.updateInt wherein it's not correctly calculating the hash when an int * is used. * * @param val * @param mac */ public static void updateMACForInt(int val, HMAC mac) throws CryptoTokenException { converter[0] = (byte) (val >>> 24); converter[1] = (byte) (val >>> 16); converter[2] = (byte) (val >>> 8); converter[3] = (byte) val; mac.update(converter); } public static byte[] insertByteValue(byte value, byte[] in) { byte[] out = new byte[in.length + 1]; out[0] = value; System.arraycopy(in, 0, out, 1, in.length); return out; } /** * @param input * @param length * @return byte array no longer than the specified length, truncating as required. . */ public static byte[] trimBytesToLength(byte[] input, int length) { byte[] out = input; if (input.length > length) { out = new byte[length]; System.arraycopy(input, 0, out, 0, length); } return out; } /** * Safely swaps the two provided indices. If either index is invalid for any reason no action is taken. * * @param v * @param idx1 * @param idx2 */ public static void swapVectorElements(Vector v, int idx1, int idx2) { if (idx1 == idx2 || idx1 < 0 || idx2 < 0 || idx1 >= v.size() || idx2 >= v.size()) { return; } Object o = v.elementAt(idx1); v.setElementAt(v.elementAt(idx2), idx1); v.setElementAt(o, idx2); } public static String[] buildConnectionDataString(KexAgreement a) { if (a == null) return null; return new String[] { "Crypto S -> C: " + a.serverToClientCryptoAlgorithm, "Crypto C -> S: " + a.clientToServerCryptoAlgorithm, "Compression S -> C: " + a.compressionClientToServer, "Compression C -> S: " + a.compressionServerToClient, "KEX Algorithm: " + a.kexAlgorithm, "Language S -> C: " + a.languageClientToServer, "Language C -> S: " + a.languageServerToClient, "MAC S -> C: " + a.MACClientToServer, "MAC C -> S: " + a.MACServerToClient, "Server Key Algorithm: " + a.serverHostKeyAlgorithm }; } public static ResourceBundle getResourceBundle() { return ResourceBundle.getBundle(BBSSHResource.BUNDLE_ID, BBSSHResource.BUNDLE_NAME); } public static String getStringResource(int id) { return getResourceBundle().getString(id); } public static void sortVector(Vector v) { Object[] o = new Object[v.size()]; v.copyInto(o); // ..getInstance(false) Arrays.sort(o, ObjectStringComparator.CASE_SENSITIVE_COMPARATOR); for (int x = v.size() - 1; x >= 0; x--) { v.setElementAt(o[x], x); } } public static Font deriveBBSSHDialogFont(Font f) { return f.derive(Font.PLAIN, (f.getHeight() / 4) * 3); } public static byte[] removeBytePadding(byte[] b) { // Remove padding // @todo ... shouldn't this remove ALL leading? if (b[0] == 0) { // trim leading zero. byte[] temp = new byte[b.length - 1]; System.arraycopy(b, 1, temp, 0, temp.length); return temp; } return b; } private static DataOutputStream openNewOutputImpl(String name) { try { FileConnection fconn = (FileConnection) Connector.open(name); if (fconn.exists()) { fconn.delete(); } fconn.create(); DataOutputStream s = fconn.openDataOutputStream(); fconn.close(); return s; } catch (Exception e) { Logger.error("Failed openNewOutput " + name + " " + e.getMessage()); } return null; } /** * destructively opens the requested file if possible. creates it if it doesn't exist; otherwise deletes it and * ercreates it. If the file can't be created, this returns null. File will be attempted first on the user's SDcard; * if that's not available it will be created on device storage. * * @param name * unqualified name of the file. * @return */ public static DataOutputStream openNewOutput(String name) { DataOutputStream r = openNewOutputImpl("file:///SDCard/" + name); if (r == null) r = openNewOutputImpl("file:///store/home/user/" + name); return r; } /** * returns the standard color index for the specified color. reference: * http://en.wikipedia.org/wiki/ANSI_escape_code#Colors - color table * * @param color * @param default * @return color index 0-7; if no match, returns defValue. */ public static int convertColorToANSITable(int color, int defValue) { // // (color names are standard, appearance is impl specific. switch (color) { case 0: // black, dim black case 0x333333: // bold black return 0; case 0xcc0000: // red case 0x990000: // dim case 0xff0000: // bold red return 1; case 0x00cc00: // green case 0x009900: // dim green case 0x00FF00: // bold green return 2; case 0xcccc00: // yellow case 0x999900: // dim case 0xFFFF00: // bold return 3; case 0x0000cc: // blue case 0x000099: // dim case 0x0000FF: // bold return 4; case 0xcc00cc: // magenta case 0x990099: // dim case 0xFF00FF: // bold return 5; case 0x00cccc: // cyan case 0x009999: // dim case 0x00FFFF: // bold return 6; case 0xcccccc: // white case 0x999999: // dim case 0xFFFFFF: // bold return 7; default: return defValue; } } // A list of colors used for representation of the display */ public static final int color[] = { 0x000000,// black 0xcc0000, // red 0x00cc00, // green 0xcccc00, // yellow 0x0000cc, // blue 0xcc00cc, // magenta 0x00cccc, // cyan 0xcccccc // white }; public static final int boldcolor[] = { 0x333333, // black 0xff0000, // red 0x00ff00, // green 0xffff00, // yellow 0x0000ff, // blue 0xff00ff, // magenta 0x00ffff, // cyan 0xffffff // white }; public static final int lowcolor[] = { 0x000000,// black 0x990000, // red 0x009900, // green 0x999900, // yellow 0x000099, // blue 0x990099, // magenta 0x009999, // cyan 0x999999 // white }; public static final String CRLF = "\r\n"; }