/*
* Copyright 2012 The Stanford MobiSocial Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mobisocial.corral;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import mobisocial.comm.BluetoothDuplexSocket;
import mobisocial.comm.DuplexSocket;
import mobisocial.comm.StreamDuplexSocket;
import mobisocial.musubi.App;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MObject;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.ObjectManager;
import mobisocial.musubi.objects.StoryObj;
import mobisocial.musubi.provider.MusubiContentProvider;
import mobisocial.musubi.provider.MusubiContentProvider.Provided;
import mobisocial.musubi.ui.util.UiUtil;
import mobisocial.musubi.util.Util;
import mobisocial.socialkit.Obj;
import mobisocial.socialkit.musubi.DbFeed;
import mobisocial.socialkit.musubi.DbObj;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.http.entity.ContentProducer;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
/**
* Stores and retrieves large content associated with objs.
*/
public class ContentCorral {
private static final String PREF_CORRAL_BT_UUID = "corral_bt";
public static final int SERVER_PORT = 8225;
private static final String BT_CORRAL_NAME = "Content Corral";
private static final String TAG = "ContentCorral";
private static final boolean DBG = false;
private BluetoothAcceptThread mBluetoothAcceptThread;
private HttpAcceptThread mHttpAcceptThread;
private Context mContext;
/**
* A token generated for this corral instance.
*/
static SecureRandom sSecureRandom;
static String MOCK = "mock";
static final int RAW = 1;
static final int JSON = 2;
static final int NEWS = 3;
static UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sMatcher.addURI(MOCK, "raw/#", RAW);
sMatcher.addURI(MOCK, "json/#", JSON);
sMatcher.addURI(MOCK, "news", NEWS);
}
static final Map<String, AccessScope> sAppTokens = new HashMap<String, AccessScope>();
public static final String PICTURE_SUBFOLDER = "Pictures/Musubi";
public static final String HTML_SUBFOLDER = "Musubi/HTML";
public static final String FILES_SUBFOLDER = "Musubi/Files";
public static final String APPS_SUBFOLDER = "Musubi/Apps";
public ContentCorral(Context context) {
mContext = context;
}
public void start() {
startHttpServer();
startBluetoothService();
}
private void startBluetoothService() {
if (mBluetoothAcceptThread != null)
return;
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null || !adapter.isEnabled()) {
return;
}
mBluetoothAcceptThread = new BluetoothAcceptThread(adapter,
getLocalBluetoothServiceUuid(mContext));
mBluetoothAcceptThread.start();
}
/**
* Starts the simple image server
*/
private synchronized void startHttpServer() {
if (mHttpAcceptThread != null)
return;
/*String ip = getLocalIpAddress();
if (ip == null) {
Log.w(TAG, "No wifi ip address; corral not loaded.");
return;
}*/
mHttpAcceptThread = new HttpAcceptThread(SERVER_PORT, true);
mHttpAcceptThread.start();
}
public synchronized void stop() {
if (mHttpAcceptThread != null) {
mHttpAcceptThread.cancel();
mHttpAcceptThread = null;
}
}
static class AccessScope {
public String appId;
public long objId;
public AccessScope(String appId, long objId) {
this.appId = appId;
this.objId = objId;
}
}
public static synchronized String registerForAccessToken(String appId, long objId) {
String appToken = new BigInteger(130, initializeSecureRandom()).toString(32);
sAppTokens.put(appToken, new AccessScope(appId, objId));
return appToken;
}
public static synchronized void unregisterAppToken(String appToken) {
sAppTokens.remove(appToken);
}
static SecureRandom initializeSecureRandom() {
if (sSecureRandom == null) {
sSecureRandom = new SecureRandom();
}
return sSecureRandom;
}
public static Uri storeContent(Context context, Uri contentUri) {
return storeContent(context, contentUri, context.getContentResolver().getType(contentUri));
}
public static Uri storeContent(Context context, byte[] raw, String type) {
File contentDir;
if (type != null && (type.startsWith("image/") || type.startsWith("video/"))) {
contentDir = new File(Environment.getExternalStorageDirectory(), PICTURE_SUBFOLDER);
} else {
contentDir = new File(Environment.getExternalStorageDirectory(), FILES_SUBFOLDER);
}
if(!contentDir.exists() && !contentDir.mkdirs()) {
Log.e(TAG, "failed to create musubi corral directory");
return null;
}
int timestamp = (int) (System.currentTimeMillis() / 1000L);
String ext = CorralDownloadClient.extensionForType(type);
String fname = timestamp + "-" + "webapp-raw" + "." + ext;
File copy = new File(contentDir, fname);
FileOutputStream out = null;
try {
contentDir.mkdirs();
out = new FileOutputStream(copy);
out.write(raw);
return Uri.fromFile(copy);
} catch (IOException e) {
Log.w(TAG, "Error copying file", e);
if (copy.exists()) {
copy.delete();
}
return null;
} finally {
try {
if (out != null)
out.close();
} catch (IOException e) {
Log.e(TAG, "failed to close handle on store corral content", e);
}
}
}
public static Uri storeContent(Context context, Uri contentUri, String type) {
File contentDir;
if (type != null && (type.startsWith("image/") || type.startsWith("video/"))) {
contentDir = new File(Environment.getExternalStorageDirectory(), PICTURE_SUBFOLDER);
} else {
contentDir = new File(Environment.getExternalStorageDirectory(), FILES_SUBFOLDER);
}
if(!contentDir.exists() && !contentDir.mkdirs()) {
Log.e(TAG, "failed to create musubi corral directory");
return null;
}
int timestamp = (int) (System.currentTimeMillis() / 1000L);
String ext = CorralDownloadClient.extensionForType(type);
String fname = timestamp + "-" + contentUri.getLastPathSegment() + "." + ext;
File copy = new File(contentDir, fname);
FileOutputStream out = null;
InputStream in = null;
try {
contentDir.mkdirs();
in = context.getContentResolver().openInputStream(contentUri);
BufferedInputStream bin = new BufferedInputStream(in);
byte[] buff = new byte[1024];
out = new FileOutputStream(copy);
int r;
while ((r = bin.read(buff)) > 0) {
out.write(buff, 0, r);
}
bin.close();
return Uri.fromFile(copy);
} catch (IOException e) {
Log.w(TAG, "Error copying file", e);
if (copy.exists()) {
copy.delete();
}
return null;
} finally {
try {
if (in != null)
in.close();
if (out != null)
out.close();
} catch (IOException e) {
Log.e(TAG, "failed to close handle on store corral content", e);
}
}
}
private class HttpAcceptThread extends Thread {
// The local server socket
private final ServerSocket mmServerSocket;
public HttpAcceptThread(int port, boolean allowRemote) {
ServerSocket tmp = null;
// Create a new listening server socket
try {
tmp = new ServerSocket(port);
if (!allowRemote) {
tmp.bind(new InetSocketAddress("127.0.0.1", port));
} else {
tmp.bind(new InetSocketAddress("0.0.0.0", port));
}
} catch (IOException e) {
System.err.println("Could not open server socket");
e.printStackTrace(System.err);
}
mmServerSocket = tmp;
}
public void run() {
if (mmServerSocket == null) {
return;
}
// Log.d(TAG, "BEGIN mAcceptThread" + this);
setName("AcceptThread");
Socket socket = null;
// Listen to the server socket always
while (true) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
if (DBG)
Log.d(TAG, "corral waiting for client...");
socket = mmServerSocket.accept();
if (DBG)
Log.d(TAG, "corral client connected!");
} catch (SocketException e) {
Log.e(TAG, "accept() failed", e);
break;
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
}
// If a connection was accepted
if (socket == null) {
break;
}
DuplexSocket duplex;
try {
duplex = new StreamDuplexSocket(socket.getInputStream(),
socket.getOutputStream());
} catch (IOException e) {
Log.e(TAG, "Failed to connect to socket", e);
return;
}
HttpConnectedThread conThread = new HttpConnectedThread(socket, duplex);
conThread.start();
}
Log.d(TAG, "END mAcceptThread");
}
public void cancel() {
Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}
private class BluetoothAcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
public BluetoothAcceptThread(BluetoothAdapter adapter, UUID coralUuid) {
BluetoothServerSocket tmp = null;
// Create a new listening server socket
try {
try {
if (DBG)
Log.d(TAG, "Bluetooth corral listening on " + adapter.getAddress() + ":"
+ coralUuid);
tmp = adapter.listenUsingRfcommWithServiceRecord(BT_CORRAL_NAME, coralUuid);
} catch (NoSuchMethodError e) {
// Let's not deal with pairing UI.
}
} catch (IOException e) {
Log.e(TAG, "Could not open bt server socket");
e.printStackTrace(System.err);
} catch (NoSuchMethodError e) {
Log.e(TAG, "Bluetooth Corral not available for this Android version.");
}
mmServerSocket = tmp;
}
public void run() {
if (mmServerSocket == null) {
return;
}
// Log.d(TAG, "BEGIN mAcceptThread" + this);
setName("AcceptThread");
BluetoothSocket socket = null;
// Listen to the server socket always
while (true) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
if (DBG)
Log.d(TAG, "Corral bluetooth server waiting for client...");
socket = mmServerSocket.accept();
if (DBG)
Log.d(TAG, "Corral bluetooth server connected!");
} catch (SocketException e) {
Log.e(TAG, "accept() failed", e);
break;
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
}
// If a connection was accepted
if (socket == null) {
break;
}
DuplexSocket duplex = new BluetoothDuplexSocket(socket);
CorralConnectedThread conThread = new CorralConnectedThread(duplex);
conThread.start();
}
Log.d(TAG, "END mAcceptThread");
}
@SuppressWarnings("unused")
public void cancel() {
Log.d(TAG, "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of server failed", e);
}
}
}
/**
* This thread runs during a connection with a remote device. It supports
* incoming and outgoing transmissions over HTTP.
*/
private class HttpConnectedThread extends Thread {
private final DuplexSocket mmDuplexSocket;
private final Socket mmRealSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
private final int BUFFER_LENGTH = 1024;
public HttpConnectedThread(Socket socket, DuplexSocket streams) {
// Log.d(TAG, "create ConnectedThread");
mmRealSocket = socket;
mmDuplexSocket = streams;
InputStream tmpIn = null;
OutputStream tmpOut = null;
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.d(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[BUFFER_LENGTH];
int bytes;
if (mmInStream == null || mmOutStream == null)
return;
// Read header information, determine connection type
try {
bytes = mmInStream.read(buffer);
if (DBG) Log.d(TAG, "read " + bytes + " header bytes");
String header = new String(buffer, 0, bytes);
if (DBG) Log.d(TAG, header);
// determine request type
if (header.startsWith("GET ")) {
doGetRequest(header);
}
} catch (Exception e) {
Log.e(TAG, "Error reading connection header", e);
}
// No longer listening.
cancel();
}
class CorralHttpRequest {
final String mRequest;
final Map<String, String> mHeaders = new HashMap<String, String>();
String mMethod;
String mPath;
public CorralHttpRequest(String httpRequest) {
mRequest = httpRequest;
parseRequest();
}
void parseRequest() {
String[] headers = mRequest.split("\r\n");
if (headers.length == 0) {
throw new IllegalArgumentException("Bad http request");
}
for (int i = 1; i < headers.length; i++) {
String h = headers[i];
int col = h.indexOf(':');
if (col > -1) {
String v = h.substring(col+1).trim();
mHeaders.put(h.substring(0, col).trim().toLowerCase(), v);
}
}
String[] request = headers[0].split(" ");
if (request.length == 0) {
throw new IllegalArgumentException("No method");
}
mMethod = request[0];
mPath = request[1];
}
}
void handleRaw(Uri targetUri) {
long objId = Long.parseLong(targetUri.getLastPathSegment());
String ticket = targetUri.getQueryParameter("ticket");
if (ticket == null) {
notAuthorized();
return;
}
AccessScope scope = sAppTokens.get(ticket);
if (scope == null || scope.objId != objId) {
// TODO: grant access based on appId/feed etc.
notAuthorized();
return;
}
Uri uri = MusubiContentProvider.uriForItem(Provided.OBJS_ID, objId);
String[] projection = new String[] { DbObj.COL_RAW, DbObj.COL_JSON };
String selection = DbObj.COL_ID + "=?";
String[] selectionArgs = new String[] { Long.toString(objId) };
String sortOrder = null;
Cursor obj = mContext.getContentResolver().query(uri,
projection, selection, selectionArgs, sortOrder);
try {
if (obj.moveToFirst()) {
try {
String type = "application/octet";
byte[] bytes = obj.getBlob(0);
String jsonSrc = obj.getString(1);
try {
JSONObject json = new JSONObject(jsonSrc);
if (json.has(CorralDownloadClient.OBJ_MIME_TYPE)) {
type = json.getString(CorralDownloadClient.OBJ_MIME_TYPE);
}
} catch (JSONException e) {
}
sendRaw(type, bytes);
mmOutStream.close();
} catch(IOException e) {
if (DBG) Log.w(TAG, "corral http", e);
}
} else {
try {
mmOutStream.write(header("HTTP/1.1 404 NOT FOUND"));
mmOutStream.close();
} catch (IOException e) {
if (DBG) Log.w(TAG, "corral http", e);
}
}
} finally {
obj.close();
}
return;
}
void handleNews(Uri targetUri) {
if (DBG) Log.d(TAG, "reading the news");
PrintWriter pw = new PrintWriter(mmOutStream);
if (!"/127.0.0.1".equals(mmRealSocket.getLocalAddress().toString())) {
pw.append("HTTP/1.1 403 FORBIDDEN\r\n\r\n");
return;
}
Uri uri = MusubiContentProvider.uriForDir(Provided.OBJECTS);
String[] projection = new String[] { MObject.COL_ID };
String selection = "type=?";
String[] selectionArgs = new String[] { StoryObj.TYPE };
String sortOrder = DbObj.COL_ID + " DESC LIMIT 10";
Cursor c = mContext.getContentResolver().query(
uri, projection, selection, selectionArgs, sortOrder);
try {
PulseFeed pulse = new PulseFeed(mContext, c);
String news = pulse.toJson().toString();
if (DBG) Log.d(TAG, "News feed: " + news);
pw.append("HTTP/1.1 200 OK\r\n");
pw.append("Content-Type: application/json\r\n");
pw.append("Content-Length: " + news.getBytes().length + "\r\n");
pw.append("\r\n");
pw.append(news); // TODO: memory management; streaming interface
pw.close();
mmOutStream.close();
} catch (IOException e) {
if (DBG) Log.d(TAG, "io", e);
} finally {
c.close();
}
/*Log.d(TAG, "mocking pulse data");
String PULSE_ASSET = "pulse.txt";
try {
String content = IOUtils.toString(mContext.getResources().getAssets().open(PULSE_ASSET));
pw.append("HTTP/1.1 200 OK\r\n");
pw.append("Content-Type: application/json\r\n");
pw.append("Content-Length: " + content.getBytes().length);
pw.append("\r\n\r\n");
pw.append(content);
pw.close();
mmOutStream.close();
} catch (IOException e) {
Log.e(TAG, "failed to pulse", e);
}*/
}
void handleApp(Uri targetUri) {
if (targetUri.getPath().contains("..")) {
return;
}
File appFolder = new File(Environment.getExternalStorageDirectory(), APPS_SUBFOLDER);
String filePath;
if (targetUri.getPathSegments().size() == 2) {
filePath = targetUri.getPathSegments().get(1) + "/index.html";
} else {
filePath = targetUri.getPath();
filePath = filePath.replaceFirst("/app", "");
}
try {
FileInputStream fileInputStream = new FileInputStream(new File(appFolder, filePath));
mmOutStream.write(header("HTTP/1.1 200 OK"));
//mmOutStream.write(header("Content-Type: " + type));
mmOutStream.write(header("Content-Length: " + fileInputStream.available()));
mmOutStream.write(header(""));
IOUtils.copy(fileInputStream, mmOutStream);
} catch (IOException e) {
Log.e(TAG, "Error sending app file", e);
}
}
/**
* Handles an HTTP GET request for objects, raw content,
* and Corral images.
*/
private void doGetRequest(String httpRequest) {
CorralHttpRequest request = new CorralHttpRequest(httpRequest);
if (!"GET".equals(request.mMethod)) {
throw new IllegalArgumentException();
}
Uri targetUri = Uri.parse("content://" + MOCK + request.mPath);
int match = sMatcher.match(targetUri);
boolean handled = true;
switch (match) {
case RAW:
handleRaw(targetUri);
break;
case NEWS:
handleNews(targetUri);
break;
default:
handled = false;
}
if (handled) {
return;
}
if (request.mPath.startsWith("/app")) {
handleApp(targetUri);
return;
}
// Old-School:
if (targetUri.getQueryParameter("content") == null) {
try {
mmOutStream.write(header("HTTP/1.1 404 NOT FOUND\r\n\r\n"));
mmOutStream.close();
} catch (IOException e) {
}
return;
}
if (targetUri.getQueryParameter("hash") == null) {
notAuthorized();
return;
}
// Verify the hash is for an obj with the given filepath.
// TODO: This is not secure. Require challenge/response authentication.
String universalHashStr = targetUri.getQueryParameter("hash");
String contentPath = targetUri.getQueryParameter("content");
byte[] universalHashBytes = Util.convertToByteArray(universalHashStr);
ObjectManager om = new ObjectManager(App.getDatabaseSource(mContext));
long objId = om.getObjectIdForHash(universalHashBytes);
if (objId == -1) {
try {
mmOutStream.write(header("HTTP/1.1 410 GONE\r\n\r\n"));
mmOutStream.close();
} catch (IOException e) {
}
return;
}
JSONObject json;
MObject obj = om.getObjectForId(objId);
if (obj.json_ == null) {
notAuthorized();
return;
}
try {
json = new JSONObject(obj.json_);
} catch (JSONException e) {
notAuthorized();
return;
}
String localPath = json.optString(CorralDownloadClient.OBJ_LOCAL_URI);
if (!contentPath.equals(localPath)) {
try {
mmOutStream.write(header("HTTP/1.1 400 BAD REQUEST\r\n\r\n"));
mmOutStream.close();
} catch (IOException e) {
}
return;
}
// OK to download:
Uri requestPath = Uri.parse(contentPath);
String scheme = requestPath.getScheme();
if ("content".equals(scheme) || "file".equals(scheme)) {
if (DBG) Log.d(TAG, "Retrieving for " + requestPath.getAuthority());
if (MusubiContentProvider.AUTHORITY.equals(requestPath.getAuthority())) {
if (requestPath.getQueryParameter("obj") != null) {
int objIndex = Integer.parseInt(requestPath.getQueryParameter("obj"));
// sendObj(requestPath, objIndex);
Log.w(TAG, "Unknown corral request");
} else {
Log.w(TAG, "Unknown corral request");
}
} else {
sendContent(requestPath);
}
}
}
class PulseFeed {
final IdentitiesManager mIdentityManager;
final JSONArray mEntries;
public PulseFeed(Context context, Cursor c){
mIdentityManager = new IdentitiesManager(App.getDatabaseSource(context));
mEntries = new JSONArray();
try {
while (c.moveToNext()) {
mEntries.put(getEntry(c.getLong(0)));
}
} catch (JSONException e) {
throw new IllegalArgumentException(e);
}
}
public JSONObject toJson() {
try {
JSONObject pulse = new JSONObject();
pulse.put("responseData", getResponse());
return pulse;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
JSONObject getEntry(long id) throws JSONException {
JSONArray categories = new JSONArray();
categories.put("friends");
JSONObject entry = new JSONObject();
// Pulse: title, link, author, publishedDate, contentSnippet, content, categories
// Musubi: title, text, favicon_length, original_url, [raw]
DbObj story = App.getMusubi(mContext).objForId(id);
MIdentity senderId = mIdentityManager.getIdentityForId(story.getSenderId());
String sender = UiUtil.safeNameForIdentity(senderId);
JSONObject meta = story.getJson();
entry.put("title", meta.optString(StoryObj.TITLE));
entry.put("link", meta.optString(StoryObj.ORIGINAL_URL));
entry.put("author", sender);
entry.put("publishDate", new Date(story.getTimestamp()));
entry.put("contentSnippet", meta.optString(StoryObj.TEXT));
entry.put("content", meta.optString(StoryObj.TEXT));
entry.put("categories", categories);
return entry;
}
JSONObject getResponse() throws JSONException {
JSONObject response = new JSONObject();
response.put("feed", getFeed());
return response;
}
JSONObject getFeed() throws JSONException {
JSONObject feed = new JSONObject();
feed.put("feedUrl", "content://org.musubi.db/news");
feed.put("title", "Musubi");
feed.put("link", "http://127.0.0.1:" + SERVER_PORT + "/news");
feed.put("author", "Stanford");
feed.put("description", "Stories from Friends");
feed.put("type", "rss20");
feed.put("entries", getEntries());
return feed;
}
JSONArray getEntries() {
return mEntries;
}
}
void notAuthorized() {
try {
mmOutStream.write(header("HTTP/1.1 401 UNAUTHORIZED\r\n\r\n"));
mmOutStream.close();
} catch (IOException e) {
}
}
void sendRaw(String type, byte[] bytes) throws IOException {
mmOutStream.write(header("HTTP/1.1 200 OK"));
mmOutStream.write(header("Content-Type: " + type));
mmOutStream.write(header("Content-Length: " + bytes.length));
mmOutStream.write(header(""));
InputStream in = new ByteArrayInputStream(bytes);
byte[] buf = new byte[1024];
int r;
while ((r = in.read(buf)) > 0) {
mmOutStream.write(buf, 0, r);
};
}
private void sendContent(Uri requestPath) {
InputStream in;
try {
// img = Uri.withAppendedPath(Images.Media.EXTERNAL_CONTENT_URI,
// imgId);
in = mContext.getContentResolver().openInputStream(requestPath);
} catch (Exception e) {
Log.d(TAG, "Error opening file", e);
return;
}
try {
byte[] buffer = new byte[4096];
int r = 0;
// Gross way to get length. What's the right way??
int size = 0;
while ((r = in.read(buffer)) > 0) {
size += r;
}
String type = mContext.getContentResolver().getType(requestPath);
if (type == null) {
int p = requestPath.toString().lastIndexOf(".");
if (p > 0) {
String ext = requestPath.toString().substring(p + 1);
type = CorralDownloadClient.typeForExtension(ext);
}
}
in = mContext.getContentResolver().openInputStream(requestPath);
mmOutStream.write(header("HTTP/1.1 200 OK"));
if (type != null) {
mmOutStream.write(header("Content-Type: " + type));
}
mmOutStream.write(header("Content-Length: " + size));
// mmOutStream.write(header("Content-Disposition: attachment; filename=\""+filename+"\""));
mmOutStream.write(header(""));
while ((r = in.read(buffer)) > 0) {
mmOutStream.write(buffer, 0, r);
}
} catch (Exception e) {
Log.e(TAG, "Error sending file", e);
} finally {
try {
mmOutStream.close();
} catch (IOException e) {
}
}
}
private void sendObj(Uri requestPath, int objIndex) {
InputStream in;
byte[] bytes;
try {
Uri uri;
try {
uri = DbFeed.uriForId(Long.parseLong(requestPath.getPath().substring(1)));
} catch (NumberFormatException e) {
return;
}
String[] projection = new String[] {
DbObj.COL_ID, MObject.COL_JSON
};
String selection = MObject.COL_RENDERABLE + " = 1";
String[] selectionArgs = null;
String sortOrder = DbObj.COL_ID + " ASC";
Cursor cursor = mContext.getContentResolver().query(uri, projection, selection,
selectionArgs, sortOrder);
try {
if (!cursor.moveToPosition(objIndex)) {
Log.d(TAG, "No obj found for " + uri);
return;
}
String jsonStr = cursor.getString(1);
bytes = jsonStr.getBytes();
in = new ByteArrayInputStream(bytes);
} finally {
cursor.close();
}
} catch (Exception e) {
Log.d(TAG, "Error opening obj", e);
return;
}
try {
byte[] buffer = new byte[4096];
int r = 0;
mmOutStream.write(header("HTTP/1.1 200 OK"));
mmOutStream.write(header("Content-Type: text/plain"));
mmOutStream.write(header("Content-Length: " + bytes.length));
// mmOutStream.write(header("Content-Disposition: attachment; filename=\""+filename+"\""));
mmOutStream.write(header(""));
while ((r = in.read(buffer)) > 0) {
Log.d(TAG, "sending: " + new String(buffer));
mmOutStream.write(buffer, 0, r);
}
} catch (Exception e) {
Log.e(TAG, "Error sending file", e);
} finally {
try {
mmOutStream.close();
} catch (IOException e) {
}
}
}
private void sendObjs(Uri requestPath) {
// TODO: hard-coded limit of 30 in place.
InputStream in;
byte[] bytes;
StringBuilder jsonArrayBuilder = new StringBuilder("[");
try {
Uri uri;
try {
uri = DbFeed.uriForId(Long.parseLong(requestPath.getPath().substring(1)));
} catch (NumberFormatException e) {
return;
}
String[] projection = new String[] {
DbObj.COL_ID, MObject.COL_JSON
};
String selection = MObject.COL_RENDERABLE + " = 1";
String[] selectionArgs = null;
String sortOrder = DbObj.COL_ID + " ASC LIMIT 30";
Cursor cursor = mContext.getContentResolver().query(uri, projection, selection,
selectionArgs, sortOrder);
try {
if (!cursor.moveToFirst()) {
Log.d(TAG, "No objs found for " + uri);
return;
}
jsonArrayBuilder.append(cursor.getString(1));
while (!cursor.isLast()) {
cursor.moveToNext();
String jsonStr = cursor.getString(1);
jsonArrayBuilder.append(",").append(jsonStr);
}
} finally {
cursor.close();
}
} catch (Exception e) {
Log.d(TAG, "Error opening obj", e);
return;
}
bytes = jsonArrayBuilder.append("]").toString().getBytes();
in = new ByteArrayInputStream(bytes);
try {
byte[] buffer = new byte[4096];
int r = 0;
mmOutStream.write(header("HTTP/1.1 200 OK"));
mmOutStream.write(header("Content-Type: text/plain"));
mmOutStream.write(header("Content-Length: " + bytes.length));
// mmOutStream.write(header("Content-Disposition: attachment; filename=\""+filename+"\""));
mmOutStream.write(header(""));
while ((r = in.read(buffer)) > 0) {
Log.d(TAG, "sending: " + new String(buffer));
mmOutStream.write(buffer, 0, r);
}
} catch (Exception e) {
Log.e(TAG, "Error sending file", e);
} finally {
try {
mmOutStream.close();
} catch (IOException e) {
}
}
}
public void cancel() {
try {
mmDuplexSocket.close();
} catch (IOException e) {
}
}
}
private byte[] header(String str) {
return (str + "\r\n").getBytes();
}
public static String getLocalIpAddress() {
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en
.hasMoreElements();) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr
.hasMoreElements();) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress()) {
// not ready for IPv6, apparently.
if (!inetAddress.getHostAddress().contains(":")) {
return inetAddress.getHostAddress().toString();
}
}
}
}
} catch (SocketException ex) {
}
return null;
}
/**
* This thread runs during a connection with a remote device. It supports
* incoming and outgoing transmissions over HTTP.
*/
private class CorralConnectedThread extends Thread {
private final DuplexSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
private final int BUFFER_LENGTH = 1024;
public CorralConnectedThread(DuplexSocket socket) {
if (DBG)
Log.d(TAG, "create CorralConnectedThread");
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.d(TAG, "BEGIN CorralConnectedThread");
byte[] buffer = new byte[BUFFER_LENGTH];
int bytes;
if (mmInStream == null || mmOutStream == null)
return;
// Read header information, determine connection type
try {
PosiServerProtocol protocol = new PosiServerProtocol(mmSocket);
CorralRequestHandler handler = protocol.getRequestHandler();
/**
* TODO: SNEP-like protocol here, for ObjEx. Remember, we have
* authenticated objs, ndef does not. server: NONCE CHALLENGE
* client: AUTHED REQUEST server: AUTHED RESPONSE
*/
bytes = mmInStream.read(buffer);
Log.d(TAG, "read " + bytes + " header bytes");
String header = new String(buffer, 0, bytes);
/**
* Your task is to find out which friends are nearby. We're just
* going to try to connect to all of their CORRAL_BLUETOOTH
* ports and send a quick HELLO. First visual is to show this in
* a "nearby" list. We'll easily up-convert to groups. Dumb
* algorithm for now just iterates over MACs and tries to
* connect, following protocol.
*/
// TODO
Log.d(TAG, "BJD BLUETOOTH CORRAL NOT READY: ObjEx needs defining.");
} catch (Exception e) {
Log.e(TAG, "Error reading connection header", e);
}
// No longer listening.
cancel();
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
}
}
}
public static UUID getLocalBluetoothServiceUuid(Context c) {
SharedPreferences prefs = c.getSharedPreferences("main", 0);
if (!prefs.contains(PREF_CORRAL_BT_UUID)) {
UUID btUuid = UUID.randomUUID();
prefs.edit().putString(PREF_CORRAL_BT_UUID, btUuid.toString()).commit();
}
String uuidStr = prefs.getString(PREF_CORRAL_BT_UUID, null);
return (uuidStr == null) ? null : UUID.fromString(uuidStr);
}
static class PosiServerProtocol {
public static final int POSI_MARKER = 0x504f5349;
public static final int POSI_VERSION = 0x01;
static SecureRandom sSecureRandom;
private final DuplexSocket mmDuplexSocket;
public PosiServerProtocol(DuplexSocket socket) {
if (sSecureRandom == null) {
sSecureRandom = new SecureRandom();
}
mmDuplexSocket = socket;
}
private byte[] getHeader() {
byte[] header = new byte[16];
ByteBuffer buffer = ByteBuffer.wrap(header);
buffer.putInt(POSI_MARKER);
buffer.putInt(POSI_VERSION);
buffer.putLong(sSecureRandom.nextLong());
return header;
}
// TODO:
public CorralRequestHandler getRequestHandler() throws IOException {
if (DBG)
Log.d(TAG, "Getting request handler for posi session");
OutputStream out = mmDuplexSocket.getOutputStream();
byte[] header = getHeader();
if (DBG)
Log.d(TAG, "Writing header " + new String(header));
out.write(header);
if (DBG)
Log.d(TAG, "Flushing header bytes");
out.flush();
if (DBG)
Log.d(TAG, "Done writing header.");
/**
* TODO: SignedObj obj = ObjDecoder.decode(readObj()) Authenticate
* signer and select protocol. Authentication verifies nonce and
* ensures timestamp is more recent than the users' last transmitted
* obj's timestamp.
*/
return new NonceRequestHandler();
}
}
/**
* The trivial request handler that sends a nonce and hangs up.
*/
static class NonceRequestHandler implements CorralRequestHandler {
// Does nothing.
}
interface CorralRequestHandler {
}
/**
* Makes a webapp available offline via the Corral.
*/
public static Uri cacheWebApp(Uri appUri) {
String appName = getWebappCacheName(appUri);
return cacheWebApp(appUri, appName);
}
/**
* Makes a webapp available offline via the Corral.
*/
public static Uri cacheWebApp(Uri appUri, String name) {
File localAppDir = new File(
new File(Environment.getExternalStorageDirectory(),
ContentCorral.APPS_SUBFOLDER), name);
try {
FileUtils.deleteDirectory(localAppDir);
} catch (IOException e) {}
localAppDir.mkdirs();
String indexPath = getIndexPath(appUri);
try {
downloadFile(localAppDir, appUri, indexPath);
scrapePage(localAppDir, appUri.buildUpon().path("").build(), indexPath);
} catch (IOException e) {
Log.e(TAG, "Failed to cache webapp", e);
}
return getWebappCacheUrl(appUri, localAppDir.getName());
}
public static Uri getWebappCacheUrl(Uri webapp) {
return getWebappCacheUrl(webapp, getWebappCacheName(webapp));
}
public static Uri getWebappCacheUrl(Uri webapp, String localName) {
File appPath = new File(Environment.getExternalStorageDirectory(), APPS_SUBFOLDER);
appPath = new File(appPath, localName);
if (appPath.exists()) {
return Uri.parse("http://127.0.0.1:" + SERVER_PORT + "/app/" +
localName + getIndexPath(webapp));
}
return null;
}
public static String getWebappCacheName(Uri webapp) {
return Util.convertToHex(Util.sha256(webapp.toString().getBytes())).substring(0, 10);
}
private static Pattern sScriptRegex = Pattern.compile("<\\s*script\\s+[^>]+>", Pattern.CASE_INSENSITIVE);
private static Pattern sSrcRegex = Pattern.compile("\\bsrc\\s*=\\s*(\"[^\"]+\"|'[^']+')", Pattern.CASE_INSENSITIVE);
private static void scrapePage(File storageDir, Uri baseUri, String relativePath) throws IOException {
File sourceFile = new File(storageDir, relativePath);
String page = IOUtils.toString(new FileInputStream(sourceFile));
Matcher matcher = sScriptRegex.matcher(page);
int offset = 0;
while (matcher.find(offset)) {
try {
String tag = matcher.group();
Matcher srcMatcher = sSrcRegex.matcher(tag);
if(!srcMatcher.find())
continue;
String srcPath = srcMatcher.group(1);
srcPath = srcPath.substring(1, srcPath.length() - 1);
srcPath = StringEscapeUtils.unescapeHtml4(srcPath);
//srcPath = absoluteToRelative(baseUri, srcPath);
if (!srcPath.contains("://")) {
Uri absolutePath = getAbsoluteUri(baseUri, relativePath, srcPath);
downloadFile(storageDir, absolutePath, absolutePath.getPath());
}
} finally {
offset = matcher.end();
}
}
}
/**
* If the given path is provisioned by baseUri, returns a localized
* representation of that path.
*/
private String absoluteToRelative(Uri baseUri, String path) {
if (path.startsWith(baseUri.toString())) {
path = path.substring(baseUri.toString().length());
Log.d(TAG, "TRUNCATED TO " + path);
}
return path;
}
private static String getIndexPath(Uri uri) {
String seg = uri.getLastPathSegment();
if (seg.endsWith(".html") || seg.endsWith(".htm") || seg.endsWith(".php")) {
return uri.getPath();
}
return uri.buildUpon().appendPath("index.html").build().getPath();
}
private static Uri getAbsoluteUri(Uri baseUri, String parentFile, String src) {
if (src.startsWith("/")) {
return baseUri.buildUpon().path(src).build();
}
Uri.Builder builder = baseUri.buildUpon();
String[] parentPath = parentFile.split("/");
for (int i = 0; i < parentPath.length - 1; i++) {
builder.appendPath(parentPath[i]);
}
String[] srcParts = src.split("/");
for (String part : srcParts) {
builder.appendPath(part);
}
return builder.build();
}
private static void downloadFile(File storageDir, Uri page, String localPath) throws IOException {
URL url = new URL(page.toString());
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
File outFile = new File(storageDir, localPath);
outFile.getParentFile().mkdirs();
FileOutputStream outStream = new FileOutputStream(outFile);
byte[] buffer = new byte[2048];
int readLength;
while ( (readLength = inputStream.read(buffer)) > 0) {
outStream.write(buffer, 0, readLength);
}
outStream.close();
inputStream.close();
}
}