/** * The MIT License * ------------------------------------------------------------- * Copyright (c) 2008, Rob Ellis, Brock Whitten, Brian Leroux, Joe Bowser, Dave Johnson, Nitobi * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.phonegap.io; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Hashtable; import java.util.Stack; import javax.microedition.io.Connection; import javax.microedition.io.Connector; import javax.microedition.io.HttpConnection; import javax.microedition.io.InputConnection; import com.phonegap.util.MD5; import net.rim.blackberry.api.browser.Browser; import net.rim.blackberry.api.browser.BrowserSession; import net.rim.device.api.browser.field.RequestedResource; import net.rim.device.api.io.Base64OutputStream; import net.rim.device.api.io.http.HttpHeaders; import net.rim.device.api.io.http.HttpProtocolConstants; import net.rim.device.api.system.Application; import net.rim.device.api.util.StringUtilities; /** * Manages all HTTP connections. * * @author Jose Noheda * */ public final class ConnectionManager { public static final String DATA = "data"; public static final String DATA_PROTOCOL = DATA + ":///"; public static final String ROOT = "/www/"; public static final String URI_SUFFIX = ";charset=utf-8;base64,"; public static final String REFERRER_KEY = "referer"; public static final String HTTP = "http"; private static final String LOG_CONN_OK = "[PhoneGap] Successfully retrieved last URL, returning now."; public static Hashtable dirHash = new Hashtable(); public static Stack history = new Stack(); private static final byte[] DATA_URL_HTML = (ConnectionManager.DATA + ":text/html" + URI_SUFFIX).getBytes(); private static final byte[] DATA_URL_JS = (ConnectionManager.DATA + ":text/javascript" + URI_SUFFIX).getBytes(); private static final byte[] DATA_URL_IMG_JPG = (ConnectionManager.DATA + ":image/jpeg" + URI_SUFFIX).getBytes(); private static final byte[] DATA_URL_IMG_PNG = (ConnectionManager.DATA + ":image/png" + URI_SUFFIX).getBytes(); private static final byte[] DATA_URL_CSS = (ConnectionManager.DATA + ":text/css" + URI_SUFFIX).getBytes(); private static final byte[] DATA_URL_PLAIN = (ConnectionManager.DATA + ":text/plain" + URI_SUFFIX).getBytes(); /** * Creates a connection and returns it. Calling this method without care may saturate BB capacity. * * @param url a http:// or data:// URL */ public static HttpConnection getUnmanagedConnection(String url, HttpHeaders requestHeaders, byte[] postData) { HttpConnection conn = null; OutputStream out = null; boolean internalReferrer = false; String referrer = ""; // Check if the HttpHeaders are null. If not, dive into them to see if the referrer is internal or not. if (requestHeaders != null) { referrer = requestHeaders.getPropertyValue(REFERRER_KEY); if (referrer != null && referrer.length() > 0) { if (referrer.startsWith(DATA + ":text")) { internalReferrer = true; } } } if ((url != null) && (url.trim().length() > 0)) { if (internalReferrer && !url.startsWith(HTTP)) { System.out.println("[PhoneGap] Retrieving internal resource '" + url + "' with referrer '" + referrer); if (url.endsWith(".html") || url.endsWith(".htm")) { String fullURL = DATA + "://" + cleanUpRequestURL(url, referrer)[0]; history.push(fullURL); fullURL = null; } conn = getInternalConnection(url, referrer); } else { if (isInternal(url,null)) { // Add URL to our own history stack. if (url.endsWith(".html") || url.endsWith(".htm")) { history.push(url); } conn = getInternalConnection(url,null); } else { try { return (HttpConnection)Connector.open(url); } catch (IOException e) { // Invoke native browser. ConnectionManager.invokeBrowser(url); } } } } else { return conn; } referrer = null; try { //conn = setConnectionRequestHeaders(url, requestHeaders, conn); if (postData == null) { conn.setRequestMethod(HttpConnection.GET); } else { conn.setRequestMethod(HttpConnection.POST); conn.setRequestProperty(HttpProtocolConstants.HEADER_CONTENT_LENGTH, String.valueOf(postData.length)); out = conn.openOutputStream(); out.write(postData); } } catch (IOException e1) { } finally { if (out != null) { try { out.close(); } catch (IOException e2) { } } out = null; } return conn; } public static HttpConnection setConnectionRequestHeaders(String url, HttpHeaders requestHeaders, HttpConnection conn) { HttpConnection returnConn = conn; if (requestHeaders != null) { // From // http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3 // // Clients SHOULD NOT include a Referer header field in a // (non-secure) HTTP // request if the referring page was transferred with a secure // protocol. String referer = requestHeaders.getPropertyValue(REFERRER_KEY); boolean sendReferrer = true; if (referer != null && StringUtilities.startsWithIgnoreCase(referer,"https:") && !StringUtilities.startsWithIgnoreCase(url, "https:")) { sendReferrer = false; } referer = null; int size = requestHeaders.size(); for (int i = 0; i < size;) { String header = requestHeaders.getPropertyKey(i); // Remove referer header if needed. if (!sendReferrer && header.equals(REFERRER_KEY)) { requestHeaders.removeProperty(i); --size; continue; } String value = requestHeaders.getPropertyValue(i++); if (value != null) { try { returnConn.setRequestProperty(header, value); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } value = null; header = null; } } return returnConn; } /** * Loads an external URL and provides a connection that holds the array of bytes. Internal * URLs (data://) simply pass through. * * @param url a http:// or data:// URL */ public InputConnection getPreLoadedConnection(String url) { InputConnection connection = getUnmanagedConnection(url, null, null); if ((connection != null) && (!isInternal(url, null))) { try { final byte[] data = read(connection.openInputStream()); close(connection); if (data != null) { connection = new InputConnection() { public DataInputStream openDataInputStream() throws IOException { return new DataInputStream(openInputStream()); } public InputStream openInputStream() throws IOException { return new ByteArrayInputStream(data); } public void close() throws IOException { return; } }; } } catch(IOException ioe) { close(connection); System.out.println("Problems reading an external URL"); } } return connection; } /** * Determines whether a URL/RequestedResource parameter combination is requesting an internal (to device) or external (web) resource. * @param url The URL of the resource. * @param resource The RIM resource that is being requested. */ public static boolean isInternal(String url, RequestedResource resource) { if (resource != null) { HttpHeaders header = resource.getRequestHeaders(); if (header != null) { String referrer = header.getPropertyValue(REFERRER_KEY); if (referrer != null && referrer.length() > 0) { // TODO: Weakness here that external URLs must be specified with the full protocol at the start of the URL. if (referrer.startsWith("data:text") && !url.startsWith(HTTP)) { return true; } } referrer = null; } header = null; } return (url != null) && url.startsWith(ConnectionManager.DATA_PROTOCOL); } private static void close(Connection connection) { if (connection != null) { try { connection.close(); } catch(Exception ioe) { System.out.println("Problem closing a connection"); } } } /** * Invokes the native browser on the BlackBerry and sends it to a specific URL. * @param url The external URL to load into the native browser app. */ private static void invokeBrowser(String url) { BrowserSession bSession = Browser.getDefaultSession(); bSession.displayPage(url); } /** * Returns an HttpConnection to a resource local to the device. * @param url The URL of the local reference. * @param referrer An ID / referrer tag identifying the resource that requested the specified URL. * @return HttpConnection object instantiated to the local resource. */ private static HttpConnection getInternalConnection(String url, String referrer) { HttpConnection outputHttp = null; Base64OutputStream boutput = null; ByteArrayOutputStream output = new ByteArrayOutputStream(); String[] URLandDirectory = cleanUpRequestURL(url, referrer); String dataUrl = URLandDirectory[0]; String directory = URLandDirectory[1]; // Read internal resource and encode as Base64, then return as HttpConnection object. System.out.println("[PhoneGap] Begin retrieval of internal URL '" + dataUrl + "' and DIRECTORY '" + directory + "'"); try { // Identify file type and include proper MIME type in data URI. if (dataUrl.endsWith(".html") || dataUrl.endsWith(".htm")) { output.write(ConnectionManager.DATA_URL_HTML); } else if (dataUrl.endsWith(".js")) { output.write(ConnectionManager.DATA_URL_JS); } else if (dataUrl.endsWith(".jpg") || dataUrl.endsWith(".jpeg")) { output.write(ConnectionManager.DATA_URL_IMG_JPG); } else if (dataUrl.endsWith(".png")) { output.write(ConnectionManager.DATA_URL_IMG_PNG); } else if (dataUrl.endsWith(".css")) { output.write(ConnectionManager.DATA_URL_CSS); } else { output.write(ConnectionManager.DATA_URL_PLAIN); } // Create stream to resource and cast as HttpConnection. boutput = new Base64OutputStream(output); InputStream theResource = null; if (dataUrl.startsWith("file://")) { theResource = Connector.openInputStream(dataUrl); } else { theResource = Application.class.getResourceAsStream(dataUrl); } if (theResource != null) { byte[] resourceBytes = read(theResource); theResource = null; boutput.write(resourceBytes); } boutput.flush(); boutput.close(); output.flush(); output.close(); String outString = output.toString(); Connection outputCon = Connector.open(outString); outputHttp = (HttpConnection) outputCon; outputCon = null; // Add the Base64 encoded resource to the directory reference hash, after MD5 hashing the key. String outMD5 = MD5.hash(outString); outString = null; if (!dirHash.containsKey(outMD5)) { dirHash.put(outMD5, directory); } outMD5 = null; System.out.println(LOG_CONN_OK); } catch (IOException ex) { System.out.println("[PhoneGap] *ERROR* during retrieval of internal URL '" + dataUrl + "'"); System.out.println("[PhoneGap] Exception " + ex.toString() + ", message: " + ex.getMessage()); outputHttp = null; } finally { dataUrl = null; directory = null; output = null; boutput = null; URLandDirectory = null; } return outputHttp; } public static String[] cleanUpRequestURL(String inURL, String referrer) { String dataUrl = inURL.startsWith(ConnectionManager.DATA_PROTOCOL) ? inURL.substring(ConnectionManager.DATA_PROTOCOL.length() - 1) : inURL; int slash = dataUrl.indexOf('/'); // Clean up the URL from BB's weird bullshit - they change the URL for (I think) locally requested resources that are referenced with relative URLs. if (inURL.startsWith("data://text/html;charset=utf-8;base64,") && referrer != null) { dataUrl = dataUrl.substring(slash+1); // TODO: Shitty hack, to clean up the URL based on length. Can we do it any other way ? while (dataUrl.length() > 150) { slash = dataUrl.indexOf('/'); dataUrl = dataUrl.substring(slash+1); } } else if (dataUrl.startsWith("data://text/")) { dataUrl = dataUrl.substring(12); } else if (dataUrl.startsWith("data:text/")) { // second test is 4.7.1 and later dataUrl = dataUrl.substring(10); } // Save local directory. int slashPos = dataUrl.lastIndexOf('/'); String directory = dataUrl.substring(0,slashPos+1); if (directory.length() == 0) { directory = ConnectionManager.ROOT; if (dataUrl.indexOf('/') == -1) { dataUrl = directory + dataUrl; } } // Check whether the referrer has already been processed (ignore if URL uses an absolute path reference). if (referrer != null && !dataUrl.startsWith("/") && !dataUrl.startsWith("file://")) { String MD5key = MD5.hash(referrer); if (dirHash.containsKey(MD5key)) { String referrerDirectory = ((String) dirHash.get(MD5key)); dataUrl = referrerDirectory + dataUrl; directory = referrerDirectory + directory; referrerDirectory = null; } MD5key = null; } return new String[]{dataUrl, directory}; } public static byte[] read(InputStream input) throws IOException { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try { int bytesRead = -1; byte[] buffer = new byte[1024]; while ((bytesRead = input.read(buffer)) != -1) bytes.write(buffer, 0, bytesRead); } finally { try { input.close(); } catch (IOException ex) {} } return bytes.toByteArray(); } }