package org.sufficientlysecure.keychain.linked.resources; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.StringRes; import org.apache.http.client.methods.HttpGet; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType; import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog; import org.sufficientlysecure.keychain.linked.LinkedTokenResource; import org.sufficientlysecure.keychain.util.Log; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public class GithubResource extends LinkedTokenResource { final String mHandle; final String mGistId; GithubResource(Set flags, HashMap params, URI uri, String handle, String gistId) { super(flags, params, uri); mHandle = handle; mGistId = gistId; } public static String generate(Context context, byte[] fingerprint) { String token = LinkedTokenResource.generate(fingerprint); return String.format(context.getResources().getString(R.string.linked_id_github_text), token); } @SuppressWarnings("deprecation") // HttpGet is deprecated @Override protected String fetchResource (Context context, OperationLog log, int indent) throws HttpStatusException, IOException, JSONException { log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString()); indent += 1; HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + mGistId); String response = getResponseBody(context, httpGet); JSONObject obj = new JSONObject(response); JSONObject owner = obj.getJSONObject("owner"); if (!mHandle.equals(owner.getString("login"))) { log.add(LogType.MSG_LV_ERROR_GITHUB_HANDLE, indent); return null; } JSONObject files = obj.getJSONObject("files"); Iterator it = files.keys(); if (it.hasNext()) { // TODO can there be multiple candidates? JSONObject file = files.getJSONObject(it.next()); return file.getString("content"); } log.add(LogType.MSG_LV_ERROR_GITHUB_NOT_FOUND, indent); return null; } @Deprecated // not used for now, but could be used to pick up earlier posted gist if already present? @SuppressWarnings({ "deprecation", "unused" }) public static GithubResource searchInGithubStream( Context context, String screenName, String needle, OperationLog log) { // narrow the needle down to important part Matcher matcher = magicPattern.matcher(needle); if (!matcher.find()) { throw new AssertionError("Needle must contain token pattern! This is a programming error, please report."); } needle = matcher.group(); try { JSONArray array; { HttpGet httpGet = new HttpGet("https://api.github.com/users/" + screenName + "/gists"); httpGet.setHeader("Content-Type", "application/json"); httpGet.setHeader("User-Agent", "OpenKeychain"); String response = getResponseBody(context, httpGet); array = new JSONArray(response); } for (int i = 0, j = Math.min(array.length(), 5); i < j; i++) { JSONObject obj = array.getJSONObject(i); JSONObject files = obj.getJSONObject("files"); Iterator it = files.keys(); if (it.hasNext()) { JSONObject file = files.getJSONObject(it.next()); String type = file.getString("type"); if (!"text/plain".equals(type)) { continue; } String id = obj.getString("id"); HttpGet httpGet = new HttpGet("https://api.github.com/gists/" + id); httpGet.setHeader("User-Agent", "OpenKeychain"); JSONObject gistObj = new JSONObject(getResponseBody(context, httpGet)); JSONObject gistFiles = gistObj.getJSONObject("files"); Iterator gistIt = gistFiles.keys(); if (!gistIt.hasNext()) { continue; } // TODO can there be multiple candidates? JSONObject gistFile = gistFiles.getJSONObject(gistIt.next()); String content = gistFile.getString("content"); if (!content.contains(needle)) { continue; } URI uri = URI.create("https://gist.github.com/" + screenName + "/" + id); return create(uri); } } // update the results with the body of the response log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 2); return null; } catch (HttpStatusException e) { // log verbose output to logcat Log.e(Constants.TAG, "http error (" + e.getStatus() + "): " + e.getReason()); log.add(LogType.MSG_LV_FETCH_ERROR, 2, Integer.toString(e.getStatus())); } catch (MalformedURLException e) { log.add(LogType.MSG_LV_FETCH_ERROR_URL, 2); } catch (IOException e) { Log.e(Constants.TAG, "io error", e); log.add(LogType.MSG_LV_FETCH_ERROR_IO, 2); } catch (JSONException e) { Log.e(Constants.TAG, "json error", e); log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 2); } return null; } public static GithubResource create(URI uri) { return create(new HashSet(), new HashMap(), uri); } public static GithubResource create(Set flags, HashMap params, URI uri) { // no params or flags if (!flags.isEmpty() || !params.isEmpty()) { return null; } Pattern p = Pattern.compile("https://gist\\.github\\.com/([a-zA-Z0-9_]+)/([0-9a-f]+)"); Matcher match = p.matcher(uri.toString()); if (!match.matches()) { return null; } String handle = match.group(1); String gistId = match.group(2); return new GithubResource(flags, params, uri, handle, gistId); } @Override public @DrawableRes int getDisplayIcon() { return R.drawable.linked_github; } @Override public @StringRes int getVerifiedText(boolean isSecret) { return isSecret ? R.string.linked_verified_secret_github : R.string.linked_verified_github; } @Override public String getDisplayTitle(Context context) { return context.getString(R.string.linked_title_github); } @Override public String getDisplayComment(Context context) { return mHandle; } @Override public boolean isViewable() { return true; } @Override public Intent getViewIntent() { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(mSubUri.toString())); return intent; } }