mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-25 11:05:25 +00:00
370 lines
12 KiB
Java
370 lines
12 KiB
Java
package org.thoughtcrime.securesms.attachments;
|
|
|
|
|
|
import android.content.Context;
|
|
import android.net.Uri;
|
|
import android.support.annotation.NonNull;
|
|
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
|
import org.thoughtcrime.securesms.util.Hex;
|
|
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 = Hex.toStringCondensed(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;
|
|
}
|
|
}
|
|
}
|
|
} |