AI2 Component  (Version nb184)
AppInvHTTPD.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2011-2018 MIT, All rights reserved
3 // Released under the Apache License, Version 2.0
4 // http://www.apache.org/licenses/LICENSE-2.0
5 // This work is licensed under a Creative Commons Attribution 3.0 Unported License.
6 
7 package com.google.appinventor.components.runtime.util;
8 import android.os.Looper;
10 
11 import java.util.ArrayList;
12 import java.util.Enumeration;
13 import java.util.Formatter;
14 import java.util.List;
15 import java.util.Properties;
16 import java.io.File;
17 import java.io.IOException;
18 import java.io.FileInputStream;
19 import java.io.FileOutputStream;
20 import java.net.InetAddress;
21 import java.net.Socket;
22 
23 import javax.crypto.Mac;
24 import javax.crypto.spec.SecretKeySpec;
25 
26 import android.os.Build;
27 import android.os.Handler;
28 import android.util.Log;
29 
30 import kawa.standard.Scheme;
31 import gnu.expr.Language;
32 
33 import android.content.Intent;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageManager.NameNotFoundException;
36 import android.net.Uri;
37 import org.json.JSONArray;
38 import org.json.JSONException;
39 import org.json.JSONObject;
40 
41 public class AppInvHTTPD extends NanoHTTPD {
42 
43  private File rootDir;
44  private Language scheme;
45  private ReplForm form;
46  private boolean secure; // Should we only accept from 127.0.0.1?
47 
48  private static final int YAV_SKEW_FORWARD = 1;
49  private static final int YAV_SKEW_BACKWARD = 4;
50  private static final String LOG_TAG = "AppInvHTTPD";
51  private static byte[] hmacKey;
52  private static int seq;
53  private static final String MIME_JSON = "application/json"; // Other mime types defined in NanoHTTPD
54  private final Handler androidUIHandler = new Handler();
55 
56  public AppInvHTTPD( int port, File wwwroot, boolean secure, ReplForm form) throws IOException
57  {
58  super(port, wwwroot);
59  this.rootDir = wwwroot;
60  this.scheme = Scheme.getInstance("scheme");
61  this.form = form;
62  this.secure = secure;
63  gnu.expr.ModuleExp.mustNeverCompile();
64  }
65 
74  public Response serve( String uri, String method, Properties header, Properties parms, Properties files, Socket mySocket )
75  {
76  Log.d(LOG_TAG, method + " '" + uri + "' " );
77 
78  // Check to see where the connection is from. If we are in "secure" mode (aka running
79  // in the emulator or via the USB Cable, then we should only accept connections from 127.0.0.1
80  // which is the address that "adb" uses when forwarding the connection from the blocks
81  // editor to the Companion.
82 
83  if (secure) {
84  InetAddress myAddress = mySocket.getInetAddress();
85  String hostAddress = myAddress.getHostAddress();
86  if (!hostAddress.equals("127.0.0.1")) {
87  Log.d(LOG_TAG, "Debug: hostAddress = " + hostAddress + " while in secure mode, closing connection.");
88  Response res = new Response(HTTP_OK, MIME_JSON, "{\"status\" : \"BAD\", \"message\" : \"Security Error: Invalid Source Location " + hostAddress + "\"}");
89  // Even though we are blowing this guy off, we return the headers below so the browser
90  // will deliver the status message above. Otherwise it won't due to browser security
91  // restrictions
92  res.addHeader("Access-Control-Allow-Origin", "*");
93  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
94  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
95  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
96  return (res);
97  }
98  }
99 
100  if (method.equals("OPTIONS")) { // This is a complete hack. OPTIONS requests are used
101  // by Cross Origin Resource Sharing. We give a response
102  // that permits connections to us from Javascript
103  // loaded from other pages (like the App Inventor Blocks Editor)
104  Enumeration e = header.propertyNames();
105  while ( e.hasMoreElements())
106  {
107  String value = (String)e.nextElement();
108  Log.d(LOG_TAG, " HDR: '" + value + "' = '" +
109  header.getProperty( value ) + "'" );
110  }
111  Response res = new Response(HTTP_OK, MIME_PLAINTEXT, "OK");
112  res.addHeader("Access-Control-Allow-Origin", "*");
113  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
114  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
115  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
116  return (res);
117  }
118 
119 
120  if (uri.equals("/_newblocks")) { // Handle AJAX calls from the newblocks code
121  adoptMainThreadClassLoader();
122  String inSeq = parms.getProperty("seq", "0");
123  int iseq = Integer.parseInt(inSeq);
124  String blockid = parms.getProperty("blockid");
125  String code = parms.getProperty("code");
126  String inMac = parms.getProperty("mac", "no key provided");
127  String compMac = "";
128  String input_code = code;
129  if (hmacKey != null) {
130  try {
131  Mac hmacSha1 = Mac.getInstance("HmacSHA1");
132  SecretKeySpec key = new SecretKeySpec(hmacKey, "RAW");
133  hmacSha1.init(key);
134  byte [] tmpMac = hmacSha1.doFinal((code + inSeq + blockid).getBytes());
135  StringBuffer sb = new StringBuffer(tmpMac.length * 2);
136  Formatter formatter = new Formatter(sb);
137  for (byte b : tmpMac)
138  formatter.format("%02x", b);
139  compMac = sb.toString();
140  } catch (Exception e) {
141  Log.e(LOG_TAG, "Error working with hmac", e);
142  form.dispatchErrorOccurredEvent(form, "AppInvHTTPD",
143  ErrorMessages.ERROR_REPL_SECURITY_ERROR, "Exception working on HMAC");
144  Response res = new Response(HTTP_OK, MIME_PLAINTEXT, "NOT");
145  return(res);
146  }
147  Log.d(LOG_TAG, "Incoming Mac = " + inMac);
148  Log.d(LOG_TAG, "Computed Mac = " + compMac);
149  Log.d(LOG_TAG, "Incoming seq = " + inSeq);
150  Log.d(LOG_TAG, "Computed seq = " + seq);
151  Log.d(LOG_TAG, "blockid = " + blockid);
152  if (!inMac.equals(compMac)) {
153  Log.e(LOG_TAG, "Hmac does not match");
154  form.dispatchErrorOccurredEvent(form, "AppInvHTTPD",
155  ErrorMessages.ERROR_REPL_SECURITY_ERROR, "Invalid HMAC");
156  Response res = new Response(HTTP_OK, MIME_JSON, "{\"status\" : \"BAD\", \"message\" : \"Security Error: Invalid MAC\"}");
157  return(res);
158  }
159  if ((seq != iseq) && (seq != (iseq+1))) {
160  Log.e(LOG_TAG, "Seq does not match");
161  form.dispatchErrorOccurredEvent(form, "AppInvHTTPD",
163  Response res = new Response(HTTP_OK, MIME_JSON, "{\"status\" : \"BAD\", \"message\" : \"Security Error: Invalid Seq\"}");
164  return(res);
165  }
166  // Seq Fixup: Sometimes the Companion doesn't increment it's seq if it is in the middle of a project switch
167  // so we tolerate an off-by-one here.
168  if (seq == (iseq+1))
169  Log.e(LOG_TAG, "Seq Fixup Invoked");
170  seq = iseq + 1;
171  } else { // No hmacKey
172  Log.e(LOG_TAG, "No HMAC Key");
173  form.dispatchErrorOccurredEvent(form, "AppInvHTTPD",
175  Response res = new Response(HTTP_OK, MIME_JSON, "{\"status\" : \"BAD\", \"message\" : \"Security Error: No HMAC Key\"}");
176  return(res);
177  }
178 
179  code = "(begin (require <com.google.youngandroid.runtime>) (process-repl-input " + blockid + " (begin " +
180  code + " )))";
181 
182  Log.d(LOG_TAG, "To Eval: " + code);
183 
184  Response res;
185  try {
186  // Don't evaluate a simple "#f" which is used by the poller
187  if (input_code.equals("#f")) {
188  Log.e(LOG_TAG, "Skipping evaluation of #f");
189  } else {
190  scheme.eval(code);
191  }
192  res = new Response(HTTP_OK, MIME_JSON, RetValManager.fetch(false));
193  } catch (Throwable ex) {
194  Log.e(LOG_TAG, "newblocks: Scheme Failure", ex);
195  RetValManager.appendReturnValue(blockid, "BAD", ex.toString());
196  res = new Response(HTTP_OK, MIME_JSON, RetValManager.fetch(false));
197  }
198  res.addHeader("Access-Control-Allow-Origin", "*");
199  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
200  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
201  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
202  return(res);
203  } else if (uri.equals("/_values")) {
204  Response res = new Response(HTTP_OK, MIME_JSON, RetValManager.fetch(true)); // Blocking Fetch
205  res.addHeader("Access-Control-Allow-Origin", "*");
206  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
207  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
208  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
209  return(res);
210  } else if (uri.equals("/_getversion")) {
211  Response res;
212  try {
213  String packageName = form.getPackageName();
214  PackageInfo pInfo = form.getPackageManager().getPackageInfo(packageName, 0);
215  String installer;
217  installer = EclairUtil.getInstallerPackageName("edu.mit.appinventor.aicompanion3", form);
218  } else {
219  installer = "Not Known"; // So we *will* auto-update old phones, no way to find out
220  // from wence they came!
221  }
222 
223  // installer will be "com.android.vending" if installed from the play store.
224  String versionName = pInfo.versionName;
225  if (installer == null)
226  installer = "Not Known";
227  // fcqn = true indicates we accept FullyQualifiedComponentNames (FQCN)
228  // This informs the blocks editor whether or not we can accept the new style
229  // fully qualified component names
230  res = new Response(HTTP_OK, MIME_JSON, "{\"version\" : \"" + versionName +
231  "\", \"fingerprint\" : \"" + Build.FINGERPRINT + "\"," +
232  " \"installer\" : \"" + installer + "\", \"package\" : \"" +
233  packageName + "\", \"fqcn\" : true }");
234  } catch (NameNotFoundException n) {
235  n.printStackTrace();
236  res = new Response(HTTP_OK, MIME_JSON, "{\"verison\" : \"Unknown\"");
237  }
238  res.addHeader("Access-Control-Allow-Origin", "*");
239  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
240  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
241  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
242  if (secure) { // Only do this for USB and Emulator (secure = true)
243  seq = 1;
244  androidUIHandler.post(new Runnable() { // Must run on the UI Thread
245  public void run() {
246  form.clear();
247  }
248  });
249  }
250  return (res);
251  } else if (uri.equals("/_extensions")) {
252  return processLoadExtensionsRequest(parms);
253  }
254 
255  if (method.equals("PUT")) { // Asset File Upload for newblocks
256  Boolean error = false;
257  String tmpFileName = (String) files.getProperty("content", null);
258  if (tmpFileName != null) { // We have content
259  File fileFrom = new File(tmpFileName);
260  String filename = parms.getProperty("filename", null);
261  if (filename != null) {
262  if (filename.startsWith("..") || filename.endsWith("..")
263  || filename.indexOf("../") >= 0) {
264  Log.d(LOG_TAG, " Ignoring invalid filename: " + filename);
265  filename = null;
266  }
267  }
268  if (filename != null) { // We have a filename and it has not been declared
269  // invalid by the code above
270  File fileTo = new File(rootDir + "/" + filename);
271  File parentFileTo = fileTo.getParentFile();
272  if (!parentFileTo.exists()) {
273  parentFileTo.mkdirs();
274  }
275  if (!fileFrom.renameTo(fileTo)) { // First try rename
276  error = copyFile(fileFrom, fileTo);
277  fileFrom.delete(); // Remove temp file
278  }
279  } else {
280  fileFrom.delete(); // We have content but no file name
281  Log.e(LOG_TAG, "Received content without a file name!");
282  error = true;
283  }
284  } else {
285  Log.e(LOG_TAG, "Received PUT without content.");
286  error = true;
287  }
288  if (error) {
289  Response res = new Response(HTTP_INTERNALERROR, MIME_PLAINTEXT, "NOTOK");
290  res.addHeader("Access-Control-Allow-Origin", "*");
291  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
292  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
293  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
294  return (res);
295  } else {
296  Response res = new Response(HTTP_OK, MIME_PLAINTEXT, "OK");
297  res.addHeader("Access-Control-Allow-Origin", "*");
298  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
299  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
300  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
301  return (res);
302  }
303  }
304 
305  return serveFile( uri, header, rootDir, true );
306  }
307 
308  private boolean copyFile(File infile, File outfile) {
309  try {
310  FileInputStream in = new FileInputStream(infile);
311  FileOutputStream out = new FileOutputStream(outfile);
312  byte[] buffer = new byte[32768]; // 32K, probably too small
313  int len;
314 
315  while ((len = in.read(buffer)) > 0) {
316  out.write(buffer, 0, len);
317  }
318 
319  in.close();
320  out.close();
321  return false; // No Error
322  } catch (IOException e) {
323  e.printStackTrace();
324  return true; // Oops
325  }
326  }
327 
328  private Response processLoadExtensionsRequest(Properties parms) {
329  try {
330  JSONArray array = new JSONArray(parms.getProperty("extensions", "[]"));
331  List<String> extensionsToLoad = new ArrayList<String>();
332  for (int i = 0; i < array.length(); i++) {
333  String extensionName = array.optString(i);
334  if (extensionName != null) {
335  extensionsToLoad.add(extensionName);
336  } else {
337  return error("Invalid JSON content at index " + i);
338  }
339  }
340  try {
341  form.loadComponents(extensionsToLoad);
342  } catch (Exception e) {
343  return error(e);
344  }
345  return message("OK");
346  } catch (JSONException e) {
347  return error(e);
348  }
349  }
350 
358  private void adoptMainThreadClassLoader() {
359  ClassLoader mainClassLoader = Looper.getMainLooper().getThread().getContextClassLoader();
360  Thread myThread = Thread.currentThread();
361  if (myThread.getContextClassLoader() != mainClassLoader) {
362  myThread.setContextClassLoader(mainClassLoader);
363  }
364  }
365 
366  private Response message(String txt) {
367  return addHeaders(new Response(HTTP_OK, MIME_PLAINTEXT, txt));
368  }
369 
370  private Response json(String json) {
371  return addHeaders(new Response(HTTP_OK, MIME_JSON, json));
372  }
373 
374  private Response error(String msg) {
375  JSONObject result = new JSONObject();
376  try {
377  result.put("status", "BAD");
378  result.put("message", msg);
379  } catch(JSONException e) {
380  Log.wtf(LOG_TAG, "Unable to write basic JSON content", e);
381  }
382  return addHeaders(new Response(HTTP_OK, MIME_JSON, result.toString()));
383  }
384 
385  private Response error(Throwable t) {
386  return error(t.toString());
387  }
388 
389  private Response addHeaders(Response res) {
390  res.addHeader("Access-Control-Allow-Origin", "*");
391  res.addHeader("Access-Control-Allow-Headers", "origin, content-type");
392  res.addHeader("Access-Control-Allow-Methods", "POST,OPTIONS,GET,HEAD,PUT");
393  res.addHeader("Allow", "POST,OPTIONS,GET,HEAD,PUT");
394  return res;
395  }
396 
401  public static void setHmacKey(String inputKey) {
402  hmacKey = inputKey.getBytes();
403  seq = 1; // Initialize this now
404  }
405 
406  public void resetSeq() {
407  seq = 1;
408  }
409 
410 }
com.google.appinventor.components.runtime.ReplForm
Definition: ReplForm.java:62
com.google.appinventor.components.runtime.util.NanoHTTPD.MIME_PLAINTEXT
static final String MIME_PLAINTEXT
Definition: NanoHTTPD.java:224
com.google.appinventor.components.runtime.util.NanoHTTPD
Definition: NanoHTTPD.java:82
com.google.appinventor.components.runtime.util.ErrorMessages
Definition: ErrorMessages.java:17
com.google.appinventor.components.runtime.util.NanoHTTPD.Response
Definition: NanoHTTPD.java:137
com.google.appinventor.components
com.google.appinventor.components.runtime.util.AppInvHTTPD
Definition: AppInvHTTPD.java:41
com.google.appinventor.components.runtime.ReplForm.loadComponents
void loadComponents(List< String > extensionNames)
Definition: ReplForm.java:404
com.google.appinventor.components.runtime.util.RetValManager.appendReturnValue
static void appendReturnValue(String blockid, String ok, String item)
Definition: RetValManager.java:51
com.google.appinventor.components.runtime.util.SdkLevel.LEVEL_ECLAIR
static final int LEVEL_ECLAIR
Definition: SdkLevel.java:22
com.google.appinventor.components.runtime.util.RetValManager.fetch
static String fetch(boolean block)
Definition: RetValManager.java:210
com.google.appinventor.components.runtime.util.NanoHTTPD.Response.addHeader
void addHeader(String name, String value)
Definition: NanoHTTPD.java:178
com.google.appinventor.components.runtime.util.AppInvHTTPD.AppInvHTTPD
AppInvHTTPD(int port, File wwwroot, boolean secure, ReplForm form)
Definition: AppInvHTTPD.java:56
com.google.appinventor.components.runtime.util.SdkLevel
Definition: SdkLevel.java:19
com.google.appinventor.components.runtime.util.AppInvHTTPD.serve
Response serve(String uri, String method, Properties header, Properties parms, Properties files, Socket mySocket)
Definition: AppInvHTTPD.java:74
com.google.appinventor.components.runtime.util.SdkLevel.getLevel
static int getLevel()
Definition: SdkLevel.java:45
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.util.EclairUtil.getInstallerPackageName
static String getInstallerPackageName(String pname, Activity form)
Definition: EclairUtil.java:105
com.google.appinventor.components.runtime.util.NanoHTTPD.serveFile
Response serveFile(String uri, Properties header, File homeDir, boolean allowDirectoryListing)
Definition: NanoHTTPD.java:895
com.google.appinventor.components.runtime.util.AppInvHTTPD.setHmacKey
static void setHmacKey(String inputKey)
Definition: AppInvHTTPD.java:401
com.google.appinventor.components.runtime.Form.dispatchErrorOccurredEvent
void dispatchErrorOccurredEvent(final Component component, final String functionName, final int errorNumber, final Object... messageArgs)
Definition: Form.java:1011
com.google
com
com.google.appinventor.components.runtime.Form.clear
void clear()
Definition: Form.java:2397
com.google.appinventor.components.runtime.util.RetValManager
Definition: RetValManager.java:27
com.google.appinventor.components.runtime.util.AppInvHTTPD.resetSeq
void resetSeq()
Definition: AppInvHTTPD.java:406
com.google.appinventor.components.runtime.util.ErrorMessages.ERROR_REPL_SECURITY_ERROR
static final int ERROR_REPL_SECURITY_ERROR
Definition: ErrorMessages.java:168
com.google.appinventor.components.runtime.util.EclairUtil
Definition: EclairUtil.java:31
com.google.appinventor
com.google.appinventor.components.runtime.util.NanoHTTPD.HTTP_OK
static final String HTTP_OK
Definition: NanoHTTPD.java:209