session-android/src/org/thoughtcrime/securesms/attachments/AttachmentServer.java

373 lines
12 KiB
Java

package org.thoughtcrime.securesms.attachments;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.util.Log;
import org.spongycastle.util.encoders.Hex;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.Util;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
/**
* @author Stefan "frostymarvelous" Froelich <stefan d0t froelich At whisppa DoT com>
*/
public class AttachmentServer implements Runnable {
private static final String TAG = AttachmentServer.class.getSimpleName();
private final Context context;
private final Attachment attachment;
private final ServerSocket socket;
private final int port;
private final String auth;
private volatile boolean isRunning;
public AttachmentServer(Context context, Attachment attachment)
throws IOException
{
try {
this.context = context;
this.attachment = attachment;
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
this.port = socket.getLocalPort();
this.auth = new String(Hex.encode(Util.getSecretBytes(16)));
this.socket.setSoTimeout(5000);
} catch (UnknownHostException e) {
throw new AssertionError(e);
}
}
public Uri getUri() {
return Uri.parse(String.format(Locale.ROOT, "http://127.0.0.1:%d/%s", port, auth));
}
public void start() {
isRunning = true;
new Thread(this).start();
}
public void stop() {
isRunning = false;
}
@Override
public void run() {
while (isRunning) {
Socket client = null;
try {
client = socket.accept();
if (client != null) {
StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client, "/" + auth);
if (task.processRequest()) {
task.execute();
}
}
} catch (SocketTimeoutException e) {
Log.w(TAG, e);
} catch (IOException e) {
Log.e(TAG, "Error connecting to client", e);
} finally {
try {if (client != null) client.close();} catch (IOException e) {}
}
}
Log.d(TAG, "Proxy interrupted. Shutting down.");
}
private class StreamToMediaPlayerTask {
private final @NonNull Socket client;
private final @NonNull String auth;
private long cbSkip;
private Properties parameters;
private Properties request;
private Properties requestHeaders;
// private String filePath;
public StreamToMediaPlayerTask(@NonNull Socket client, @NonNull String auth) {
this.client = client;
this.auth = auth;
}
public boolean processRequest() throws IOException {
InputStream is = client.getInputStream();
final int bufferSize = 8192;
byte[] buffer = new byte[bufferSize];
int splitByte = 0;
int readLength = 0;
{
int read = is.read(buffer, 0, bufferSize);
while (read > 0) {
readLength += read;
splitByte = findHeaderEnd(buffer, readLength);
if (splitByte > 0)
break;
read = is.read(buffer, readLength, bufferSize - readLength);
}
}
// Create a BufferedReader for parsing the header.
ByteArrayInputStream hbis = new ByteArrayInputStream(buffer, 0, readLength);
BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
request = new Properties();
parameters = new Properties();
requestHeaders = new Properties();
try {
decodeHeader(hin, request, parameters, requestHeaders);
} catch (InterruptedException e1) {
Log.e(TAG, "Exception: " + e1.getMessage());
e1.printStackTrace();
}
for (Map.Entry<Object, Object> e : requestHeaders.entrySet()) {
Log.i(TAG, "Header: " + e.getKey() + " : " + e.getValue());
}
String range = requestHeaders.getProperty("range");
if (range != null) {
Log.i(TAG, "range is: " + range);
range = range.substring(6);
int charPos = range.indexOf('-');
if (charPos > 0) {
range = range.substring(0, charPos);
}
cbSkip = Long.parseLong(range);
Log.i(TAG, "range found!! " + cbSkip);
}
if (!"GET".equals(request.get("method"))) {
Log.e(TAG, "Only GET is supported: " + request.get("method"));
return false;
}
String receivedAuth = request.getProperty("uri");
if (receivedAuth == null || !MessageDigest.isEqual(receivedAuth.getBytes(), auth.getBytes())) {
Log.w(TAG, "Bad auth token!");
return false;
}
// filePath = request.getProperty("uri");
return true;
}
protected void execute() throws IOException {
InputStream inputStream = PartAuthority.getAttachmentStream(context, attachment.getDataUri());
long fileSize = attachment.getSize();
String headers = "";
if (cbSkip > 0) {// It is a seek or skip request if there's a Range
// header
headers += "HTTP/1.1 206 Partial Content\r\n";
headers += "Content-Type: " + attachment.getContentType() + "\r\n";
headers += "Accept-Ranges: bytes\r\n";
headers += "Content-Length: " + (fileSize - cbSkip) + "\r\n";
headers += "Content-Range: bytes " + cbSkip + "-" + (fileSize - 1) + "/" + fileSize + "\r\n";
headers += "Connection: Keep-Alive\r\n";
headers += "\r\n";
} else {
headers += "HTTP/1.1 200 OK\r\n";
headers += "Content-Type: " + attachment.getContentType() + "\r\n";
headers += "Accept-Ranges: bytes\r\n";
headers += "Content-Length: " + fileSize + "\r\n";
headers += "Connection: Keep-Alive\r\n";
headers += "\r\n";
}
Log.i(TAG, "headers: " + headers);
OutputStream output = null;
byte[] buff = new byte[64 * 1024];
try {
output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024);
output.write(headers.getBytes());
inputStream.skip(cbSkip);
// dataSource.skipFully(data, cbSkip);//try to skip as much as possible
// Loop as long as there's stuff to send and client has not closed
int cbRead;
while (!client.isClosed() && (cbRead = inputStream.read(buff, 0, buff.length)) != -1) {
output.write(buff, 0, cbRead);
}
}
catch (SocketException socketException) {
Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
}
catch (Exception e) {
Log.e(TAG, "Exception thrown from streaming task:");
Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
}
// Cleanup
try {
if (output != null) {
output.close();
}
client.close();
}
catch (IOException e) {
Log.e(TAG, "IOException while cleaning up streaming task:");
Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
e.printStackTrace();
}
}
/**
* Find byte index separating header from body. It must be the last byte of
* the first two sequential new lines.
**/
private int findHeaderEnd(final byte[] buf, int rlen) {
int splitbyte = 0;
while (splitbyte + 3 < rlen) {
if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n'
&& buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n')
return splitbyte + 4;
splitbyte++;
}
return 0;
}
/**
* Decodes the sent headers and loads the data into java Properties' key -
* value pairs
**/
private void decodeHeader(BufferedReader in, Properties pre,
Properties parms, Properties header) throws InterruptedException {
try {
// Read the request line
String inLine = in.readLine();
if (inLine == null)
return;
StringTokenizer st = new StringTokenizer(inLine);
if (!st.hasMoreTokens())
Log.e(TAG,
"BAD REQUEST: Syntax error. Usage: GET /example/file.html");
String method = st.nextToken();
pre.put("method", method);
if (!st.hasMoreTokens())
Log.e(TAG,
"BAD REQUEST: Missing URI. Usage: GET /example/file.html");
String uri = st.nextToken();
// Decode parameters from the URI
int qmi = uri.indexOf('?');
if (qmi >= 0) {
decodeParms(uri.substring(qmi + 1), parms);
uri = decodePercent(uri.substring(0, qmi));
} else
uri = decodePercent(uri);
// If there's another token, it's protocol version,
// followed by HTTP headers. Ignore version but parse headers.
// NOTE: this now forces header names lowercase since they are
// case insensitive and vary by client.
if (st.hasMoreTokens()) {
String line = in.readLine();
while (line != null && line.trim().length() > 0) {
int p = line.indexOf(':');
if (p >= 0)
header.put(line.substring(0, p).trim().toLowerCase(),
line.substring(p + 1).trim());
line = in.readLine();
}
}
pre.put("uri", uri);
} catch (IOException ioe) {
Log.e(TAG,
"SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
}
}
/**
* Decodes parameters in percent-encoded URI-format ( e.g.
* "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
* Properties. NOTE: this doesn't support multiple identical keys due to the
* simplicity of Properties -- if you need multiples, you might want to
* replace the Properties with a Hashtable of Vectors or such.
*/
private void decodeParms(String parms, Properties p)
throws InterruptedException {
if (parms == null)
return;
StringTokenizer st = new StringTokenizer(parms, "&");
while (st.hasMoreTokens()) {
String e = st.nextToken();
int sep = e.indexOf('=');
if (sep >= 0)
p.put(decodePercent(e.substring(0, sep)).trim(),
decodePercent(e.substring(sep + 1)));
}
}
/**
* Decodes the percent encoding scheme. <br/>
* For example: "an+example%20string" -> "an example string"
*/
private String decodePercent(String str) throws InterruptedException {
try {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
switch (c) {
case '+':
sb.append(' ');
break;
case '%':
sb.append((char) Integer.parseInt(
str.substring(i + 1, i + 3), 16));
i += 2;
break;
default:
sb.append(c);
break;
}
}
return sb.toString();
} catch (Exception e) {
Log.e(TAG, "BAD REQUEST: Bad percent-encoding.");
return null;
}
}
}
}