PageRenderTime 141ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/org/thoughtcrime/securesms/audio/AudioAttachmentServer.java

https://gitlab.com/ayham-hassan/Signal-Android
Java | 376 lines | 285 code | 56 blank | 35 comment | 47 complexity | 9507ed84dbd5b726cd0d8abe182cf2e6 MD5 | raw file
  1. package org.thoughtcrime.securesms.audio;
  2. import android.content.Context;
  3. import android.net.Uri;
  4. import android.support.annotation.NonNull;
  5. import android.util.Log;
  6. import org.spongycastle.util.encoders.Hex;
  7. import org.thoughtcrime.securesms.attachments.Attachment;
  8. import org.thoughtcrime.securesms.crypto.MasterSecret;
  9. import org.thoughtcrime.securesms.mms.PartAuthority;
  10. import org.thoughtcrime.securesms.util.Util;
  11. import java.io.BufferedOutputStream;
  12. import java.io.BufferedReader;
  13. import java.io.ByteArrayInputStream;
  14. import java.io.IOException;
  15. import java.io.InputStream;
  16. import java.io.InputStreamReader;
  17. import java.io.OutputStream;
  18. import java.net.InetAddress;
  19. import java.net.ServerSocket;
  20. import java.net.Socket;
  21. import java.net.SocketException;
  22. import java.net.SocketTimeoutException;
  23. import java.net.UnknownHostException;
  24. import java.security.MessageDigest;
  25. import java.util.Map;
  26. import java.util.Properties;
  27. import java.util.StringTokenizer;
  28. /**
  29. * @author Stefan "frostymarvelous" Froelich <stefan d0t froelich At whisppa DoT com>
  30. */
  31. public class AudioAttachmentServer implements Runnable {
  32. private static final String TAG = AudioAttachmentServer.class.getSimpleName();
  33. private final Context context;
  34. private final MasterSecret masterSecret;
  35. private final Attachment attachment;
  36. private final ServerSocket socket;
  37. private final int port;
  38. private final String auth;
  39. private volatile boolean isRunning;
  40. public AudioAttachmentServer(Context context, MasterSecret masterSecret, Attachment attachment)
  41. throws IOException
  42. {
  43. try {
  44. this.context = context;
  45. this.masterSecret = masterSecret;
  46. this.attachment = attachment;
  47. this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
  48. this.port = socket.getLocalPort();
  49. this.auth = new String(Hex.encode(Util.getSecretBytes(16)));
  50. this.socket.setSoTimeout(5000);
  51. } catch (UnknownHostException e) {
  52. throw new AssertionError(e);
  53. }
  54. }
  55. public Uri getUri() {
  56. return Uri.parse(String.format("http://127.0.0.1:%d/%s", port, auth));
  57. }
  58. public void start() {
  59. isRunning = true;
  60. new Thread(this).start();
  61. }
  62. public void stop() {
  63. isRunning = false;
  64. }
  65. @Override
  66. public void run() {
  67. while (isRunning) {
  68. Socket client = null;
  69. try {
  70. client = socket.accept();
  71. if (client != null) {
  72. StreamToMediaPlayerTask task = new StreamToMediaPlayerTask(client, "/" + auth);
  73. if (task.processRequest()) {
  74. task.execute();
  75. }
  76. }
  77. } catch (SocketTimeoutException e) {
  78. Log.w(TAG, e);
  79. } catch (IOException e) {
  80. Log.e(TAG, "Error connecting to client", e);
  81. } finally {
  82. try {if (client != null) client.close();} catch (IOException e) {}
  83. }
  84. }
  85. Log.d(TAG, "Proxy interrupted. Shutting down.");
  86. }
  87. private class StreamToMediaPlayerTask {
  88. private final @NonNull Socket client;
  89. private final @NonNull String auth;
  90. private long cbSkip;
  91. private Properties parameters;
  92. private Properties request;
  93. private Properties requestHeaders;
  94. // private String filePath;
  95. public StreamToMediaPlayerTask(@NonNull Socket client, @NonNull String auth) {
  96. this.client = client;
  97. this.auth = auth;
  98. }
  99. public boolean processRequest() throws IOException {
  100. InputStream is = client.getInputStream();
  101. final int bufferSize = 8192;
  102. byte[] buffer = new byte[bufferSize];
  103. int splitByte = 0;
  104. int readLength = 0;
  105. {
  106. int read = is.read(buffer, 0, bufferSize);
  107. while (read > 0) {
  108. readLength += read;
  109. splitByte = findHeaderEnd(buffer, readLength);
  110. if (splitByte > 0)
  111. break;
  112. read = is.read(buffer, readLength, bufferSize - readLength);
  113. }
  114. }
  115. // Create a BufferedReader for parsing the header.
  116. ByteArrayInputStream hbis = new ByteArrayInputStream(buffer, 0, readLength);
  117. BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
  118. request = new Properties();
  119. parameters = new Properties();
  120. requestHeaders = new Properties();
  121. try {
  122. decodeHeader(hin, request, parameters, requestHeaders);
  123. } catch (InterruptedException e1) {
  124. Log.e(TAG, "Exception: " + e1.getMessage());
  125. e1.printStackTrace();
  126. }
  127. for (Map.Entry<Object, Object> e : requestHeaders.entrySet()) {
  128. Log.i(TAG, "Header: " + e.getKey() + " : " + e.getValue());
  129. }
  130. String range = requestHeaders.getProperty("range");
  131. if (range != null) {
  132. Log.i(TAG, "range is: " + range);
  133. range = range.substring(6);
  134. int charPos = range.indexOf('-');
  135. if (charPos > 0) {
  136. range = range.substring(0, charPos);
  137. }
  138. cbSkip = Long.parseLong(range);
  139. Log.i(TAG, "range found!! " + cbSkip);
  140. }
  141. if (!"GET".equals(request.get("method"))) {
  142. Log.e(TAG, "Only GET is supported: " + request.get("method"));
  143. return false;
  144. }
  145. String receivedAuth = request.getProperty("uri");
  146. if (receivedAuth == null || !MessageDigest.isEqual(receivedAuth.getBytes(), auth.getBytes())) {
  147. Log.w(TAG, "Bad auth token!");
  148. return false;
  149. }
  150. // filePath = request.getProperty("uri");
  151. return true;
  152. }
  153. protected void execute() throws IOException {
  154. InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, attachment.getDataUri());
  155. long fileSize = attachment.getSize();
  156. String headers = "";
  157. if (cbSkip > 0) {// It is a seek or skip request if there's a Range
  158. // header
  159. headers += "HTTP/1.1 206 Partial Content\r\n";
  160. headers += "Content-Type: " + attachment.getContentType() + "\r\n";
  161. headers += "Accept-Ranges: bytes\r\n";
  162. headers += "Content-Length: " + (fileSize - cbSkip) + "\r\n";
  163. headers += "Content-Range: bytes " + cbSkip + "-" + (fileSize - 1) + "/" + fileSize + "\r\n";
  164. headers += "Connection: Keep-Alive\r\n";
  165. headers += "\r\n";
  166. } else {
  167. headers += "HTTP/1.1 200 OK\r\n";
  168. headers += "Content-Type: " + attachment.getContentType() + "\r\n";
  169. headers += "Accept-Ranges: bytes\r\n";
  170. headers += "Content-Length: " + fileSize + "\r\n";
  171. headers += "Connection: Keep-Alive\r\n";
  172. headers += "\r\n";
  173. }
  174. Log.i(TAG, "headers: " + headers);
  175. OutputStream output = null;
  176. byte[] buff = new byte[64 * 1024];
  177. try {
  178. output = new BufferedOutputStream(client.getOutputStream(), 32 * 1024);
  179. output.write(headers.getBytes());
  180. inputStream.skip(cbSkip);
  181. // dataSource.skipFully(data, cbSkip);//try to skip as much as possible
  182. // Loop as long as there's stuff to send and client has not closed
  183. int cbRead;
  184. while (!client.isClosed() && (cbRead = inputStream.read(buff, 0, buff.length)) != -1) {
  185. output.write(buff, 0, cbRead);
  186. }
  187. }
  188. catch (SocketException socketException) {
  189. Log.e(TAG, "SocketException() thrown, proxy client has probably closed. This can exit harmlessly");
  190. }
  191. catch (Exception e) {
  192. Log.e(TAG, "Exception thrown from streaming task:");
  193. Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
  194. }
  195. // Cleanup
  196. try {
  197. if (output != null) {
  198. output.close();
  199. }
  200. client.close();
  201. }
  202. catch (IOException e) {
  203. Log.e(TAG, "IOException while cleaning up streaming task:");
  204. Log.e(TAG, e.getClass().getName() + " : " + e.getLocalizedMessage());
  205. e.printStackTrace();
  206. }
  207. }
  208. /**
  209. * Find byte index separating header from body. It must be the last byte of
  210. * the first two sequential new lines.
  211. **/
  212. private int findHeaderEnd(final byte[] buf, int rlen) {
  213. int splitbyte = 0;
  214. while (splitbyte + 3 < rlen) {
  215. if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n'
  216. && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n')
  217. return splitbyte + 4;
  218. splitbyte++;
  219. }
  220. return 0;
  221. }
  222. /**
  223. * Decodes the sent headers and loads the data into java Properties' key -
  224. * value pairs
  225. **/
  226. private void decodeHeader(BufferedReader in, Properties pre,
  227. Properties parms, Properties header) throws InterruptedException {
  228. try {
  229. // Read the request line
  230. String inLine = in.readLine();
  231. if (inLine == null)
  232. return;
  233. StringTokenizer st = new StringTokenizer(inLine);
  234. if (!st.hasMoreTokens())
  235. Log.e(TAG,
  236. "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
  237. String method = st.nextToken();
  238. pre.put("method", method);
  239. if (!st.hasMoreTokens())
  240. Log.e(TAG,
  241. "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
  242. String uri = st.nextToken();
  243. // Decode parameters from the URI
  244. int qmi = uri.indexOf('?');
  245. if (qmi >= 0) {
  246. decodeParms(uri.substring(qmi + 1), parms);
  247. uri = decodePercent(uri.substring(0, qmi));
  248. } else
  249. uri = decodePercent(uri);
  250. // If there's another token, it's protocol version,
  251. // followed by HTTP headers. Ignore version but parse headers.
  252. // NOTE: this now forces header names lowercase since they are
  253. // case insensitive and vary by client.
  254. if (st.hasMoreTokens()) {
  255. String line = in.readLine();
  256. while (line != null && line.trim().length() > 0) {
  257. int p = line.indexOf(':');
  258. if (p >= 0)
  259. header.put(line.substring(0, p).trim().toLowerCase(),
  260. line.substring(p + 1).trim());
  261. line = in.readLine();
  262. }
  263. }
  264. pre.put("uri", uri);
  265. } catch (IOException ioe) {
  266. Log.e(TAG,
  267. "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
  268. }
  269. }
  270. /**
  271. * Decodes parameters in percent-encoded URI-format ( e.g.
  272. * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
  273. * Properties. NOTE: this doesn't support multiple identical keys due to the
  274. * simplicity of Properties -- if you need multiples, you might want to
  275. * replace the Properties with a Hashtable of Vectors or such.
  276. */
  277. private void decodeParms(String parms, Properties p)
  278. throws InterruptedException {
  279. if (parms == null)
  280. return;
  281. StringTokenizer st = new StringTokenizer(parms, "&");
  282. while (st.hasMoreTokens()) {
  283. String e = st.nextToken();
  284. int sep = e.indexOf('=');
  285. if (sep >= 0)
  286. p.put(decodePercent(e.substring(0, sep)).trim(),
  287. decodePercent(e.substring(sep + 1)));
  288. }
  289. }
  290. /**
  291. * Decodes the percent encoding scheme. <br/>
  292. * For example: "an+example%20string" -> "an example string"
  293. */
  294. private String decodePercent(String str) throws InterruptedException {
  295. try {
  296. StringBuffer sb = new StringBuffer();
  297. for (int i = 0; i < str.length(); i++) {
  298. char c = str.charAt(i);
  299. switch (c) {
  300. case '+':
  301. sb.append(' ');
  302. break;
  303. case '%':
  304. sb.append((char) Integer.parseInt(
  305. str.substring(i + 1, i + 3), 16));
  306. i += 2;
  307. break;
  308. default:
  309. sb.append(c);
  310. break;
  311. }
  312. }
  313. return sb.toString();
  314. } catch (Exception e) {
  315. Log.e(TAG, "BAD REQUEST: Bad percent-encoding.");
  316. return null;
  317. }
  318. }
  319. }
  320. }