linked-ids: code cleanup, handle all lint errors

This commit is contained in:
Vincent Breitmoser
2015-05-09 12:24:48 +02:00
parent 39382e978f
commit 4378f8f871
22 changed files with 292 additions and 321 deletions

View File

@@ -0,0 +1,282 @@
package org.sufficientlysecure.keychain.linked;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.json.JSONException;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.linked.resources.DnsResource;
import org.sufficientlysecure.keychain.linked.resources.GenericHttpsResource;
import org.sufficientlysecure.keychain.linked.resources.GithubResource;
import org.sufficientlysecure.keychain.linked.resources.TwitterResource;
import org.sufficientlysecure.keychain.operations.results.LinkedVerifyResult;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class LinkedCookieResource extends LinkedResource {
protected final URI mSubUri;
protected final Set<String> mFlags;
protected final HashMap<String,String> mParams;
public static Pattern magicPattern =
Pattern.compile("\\[Verifying my (?:Open)?PGP key: openpgp4fpr:([a-zA-Z0-9]+)]");
protected LinkedCookieResource(Set<String> flags, HashMap<String, String> params, URI uri) {
mFlags = flags;
mParams = params;
mSubUri = uri;
}
@SuppressWarnings("unused")
public URI getSubUri () {
return mSubUri;
}
public Set<String> getFlags () {
return new HashSet<>(mFlags);
}
public HashMap<String,String> getParams () {
return new HashMap<>(mParams);
}
public static String generate (byte[] fingerprint) {
return String.format("[Verifying my OpenPGP key: openpgp4fpr:%s]",
KeyFormattingUtils.convertFingerprintToHex(fingerprint));
}
protected static LinkedCookieResource fromUri (URI uri) {
if (!"openpgpid+cookie".equals(uri.getScheme())) {
Log.e(Constants.TAG, "unknown uri scheme in (suspected) linked id packet");
return null;
}
if (!uri.isOpaque()) {
Log.e(Constants.TAG, "non-opaque uri in (suspected) linked id packet");
return null;
}
String specific = uri.getSchemeSpecificPart();
if (!specific.contains("@")) {
Log.e(Constants.TAG, "unknown uri scheme in linked id packet");
return null;
}
String[] pieces = specific.split("@", 2);
URI subUri = URI.create(pieces[1]);
Set<String> flags = new HashSet<>();
HashMap<String,String> params = new HashMap<>();
if (!pieces[0].isEmpty()) {
String[] rawParams = pieces[0].split(";");
for (String param : rawParams) {
String[] p = param.split("=", 2);
if (p.length == 1) {
flags.add(param);
} else {
params.put(p[0], p[1]);
}
}
}
return findResourceType(flags, params, subUri);
}
protected static LinkedCookieResource findResourceType (Set<String> flags,
HashMap<String,String> params,
URI subUri) {
LinkedCookieResource res;
res = GenericHttpsResource.create(flags, params, subUri);
if (res != null) {
return res;
}
res = DnsResource.create(flags, params, subUri);
if (res != null) {
return res;
}
res = TwitterResource.create(flags, params, subUri);
if (res != null) {
return res;
}
res = GithubResource.create(flags, params, subUri);
if (res != null) {
return res;
}
return null;
}
public URI toUri () {
StringBuilder b = new StringBuilder();
b.append("openpgpid+cookie:");
// add flags
if (mFlags != null) {
boolean first = true;
for (String flag : mFlags) {
if (!first) {
b.append(";");
}
first = false;
b.append(flag);
}
}
// add parameters
if (mParams != null) {
boolean first = true;
for (Entry<String, String> stringStringEntry : mParams.entrySet()) {
if (!first) {
b.append(";");
}
first = false;
b.append(stringStringEntry.getKey()).append("=").append(stringStringEntry.getValue());
}
}
b.append("@");
b.append(mSubUri);
return URI.create(b.toString());
}
public LinkedVerifyResult verify(byte[] fingerprint) {
OperationLog log = new OperationLog();
log.add(LogType.MSG_LV, 0);
// Try to fetch resource. Logs for itself
String res = null;
try {
res = fetchResource(log, 1);
} 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);
}
if (res == null) {
// if this is null, an error was recorded in fetchResource above
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
Log.d(Constants.TAG, "Resource data: '" + res + "'");
return verifyString(log, 1, res, fingerprint);
}
protected abstract String fetchResource (OperationLog log, int indent) throws HttpStatusException, IOException,
JSONException;
protected Matcher matchResource (OperationLog log, int indent, String res) {
return magicPattern.matcher(res);
}
protected LinkedVerifyResult verifyString (OperationLog log, int indent,
String res,
byte[] fingerprint) {
log.add(LogType.MSG_LV_MATCH, indent);
Matcher match = matchResource(log, indent+1, res);
if (!match.find()) {
log.add(LogType.MSG_LV_MATCH_ERROR, 2);
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
String candidateFp = match.group(1).toLowerCase();
String fp = KeyFormattingUtils.convertFingerprintToHex(fingerprint);
if (!fp.equals(candidateFp)) {
log.add(LogType.MSG_LV_FP_ERROR, indent);
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_ERROR, log);
}
log.add(LogType.MSG_LV_FP_OK, indent);
return new LinkedVerifyResult(LinkedVerifyResult.RESULT_OK, log);
}
@SuppressWarnings("deprecation") // HttpRequestBase is deprecated
public static String getResponseBody(HttpRequestBase request) throws IOException, HttpStatusException {
StringBuilder sb = new StringBuilder();
request.setHeader("User-Agent", "Open Keychain");
DefaultHttpClient httpClient = new DefaultHttpClient(new BasicHttpParams());
HttpResponse response = httpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
String reason = response.getStatusLine().getReasonPhrase();
if (statusCode != 200) {
throw new HttpStatusException(statusCode, reason);
}
HttpEntity entity = response.getEntity();
InputStream inputStream = entity.getContent();
BufferedReader bReader = new BufferedReader(
new InputStreamReader(inputStream, "UTF-8"), 8);
String line;
while ((line = bReader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
public static class HttpStatusException extends Throwable {
private final int mStatusCode;
private final String mReason;
HttpStatusException(int statusCode, String reason) {
super("http status " + statusCode + ": " + reason);
mStatusCode = statusCode;
mReason = reason;
}
public int getStatus() {
return mStatusCode;
}
public String getReason() {
return mReason;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.sufficientlysecure.keychain.linked;
import java.net.URI;
import android.content.Context;
import android.support.annotation.DrawableRes;
public class LinkedIdentity extends RawLinkedIdentity {
public final LinkedResource mResource;
protected LinkedIdentity(URI uri, LinkedResource resource) {
super(uri);
if (resource == null) {
throw new AssertionError("resource must not be null in a LinkedIdentity!");
}
mResource = resource;
}
public @DrawableRes int getDisplayIcon() {
return mResource.getDisplayIcon();
}
public String getDisplayTitle(Context context) {
return mResource.getDisplayTitle(context);
}
public String getDisplayComment(Context context) {
return mResource.getDisplayComment(context);
}
}

View File

@@ -0,0 +1,25 @@
package org.sufficientlysecure.keychain.linked;
import java.net.URI;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
public abstract class LinkedResource {
public abstract URI toUri();
public abstract @DrawableRes int getDisplayIcon();
public abstract @StringRes int getVerifiedText(boolean isSecret);
public abstract String getDisplayTitle(Context context);
public abstract String getDisplayComment(Context context);
public boolean isViewable() {
return false;
}
public Intent getViewIntent() {
return null;
}
}

View File

@@ -0,0 +1,79 @@
package org.sufficientlysecure.keychain.linked;
import org.spongycastle.util.Strings;
import org.sufficientlysecure.keychain.Constants;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.pgp.WrappedUserAttribute;
import org.sufficientlysecure.keychain.util.Log;
import java.io.IOException;
import java.net.URI;
import android.content.Context;
import android.support.annotation.DrawableRes;
/** The RawLinkedIdentity contains raw parsed data from a Linked Identity subpacket. */
public class RawLinkedIdentity {
public final URI mUri;
protected RawLinkedIdentity(URI uri) {
mUri = uri;
}
public byte[] getEncoded() {
return Strings.toUTF8ByteArray(mUri.toASCIIString());
}
public static RawLinkedIdentity fromAttributeData(byte[] data) throws IOException {
WrappedUserAttribute att = WrappedUserAttribute.fromData(data);
byte[][] subpackets = att.getSubpackets();
if (subpackets.length >= 1) {
return fromSubpacketData(subpackets[0]);
}
throw new IOException("no subpacket data");
}
static RawLinkedIdentity fromSubpacketData(byte[] data) {
try {
String uriStr = Strings.fromUTF8ByteArray(data);
URI uri = URI.create(uriStr);
LinkedResource res = LinkedCookieResource.fromUri(uri);
if (res == null) {
return new RawLinkedIdentity(uri);
}
return new LinkedIdentity(uri, res);
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "error parsing uri in (suspected) linked id packet");
return null;
}
}
public static RawLinkedIdentity fromResource (LinkedCookieResource res) {
return new RawLinkedIdentity(res.toUri());
}
public WrappedUserAttribute toUserAttribute () {
return WrappedUserAttribute.fromSubpacket(WrappedUserAttribute.UAT_LINKED_ID, getEncoded());
}
public @DrawableRes int getDisplayIcon() {
return R.drawable.ic_warning_grey_24dp;
}
public String getDisplayTitle(Context context) {
return "unknown";
}
public String getDisplayComment(Context context) {
return null;
}
}

View File

@@ -0,0 +1,130 @@
package org.sufficientlysecure.keychain.linked.resources;
import android.content.Context;
import android.support.annotation.DrawableRes;
import android.support.annotation.StringRes;
import org.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedCookieResource;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Question;
import de.measite.minidns.Record;
import de.measite.minidns.Record.CLASS;
import de.measite.minidns.Record.TYPE;
import de.measite.minidns.record.TXT;
public class DnsResource extends LinkedCookieResource {
final static Pattern magicPattern =
Pattern.compile("openpgpid\\+cookie=([a-zA-Z0-9]+)(?:#|;)([a-zA-Z0-9]+)");
String mFqdn;
CLASS mClass;
TYPE mType;
DnsResource(Set<String> flags, HashMap<String, String> params, URI uri,
String fqdn, CLASS clazz, TYPE type) {
super(flags, params, uri);
mFqdn = fqdn;
mClass = clazz;
mType = type;
}
public static String generateText(byte[] fingerprint) {
return String.format("openpgpid+cookie=%s",
KeyFormattingUtils.convertFingerprintToHex(fingerprint));
}
public static DnsResource createNew (String domain) {
HashSet<String> flags = new HashSet<>();
HashMap<String,String> params = new HashMap<>();
URI uri = URI.create("dns:" + domain);
return create(flags, params, uri);
}
public static DnsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
if ( ! ("dns".equals(uri.getScheme())
&& (flags == null || flags.isEmpty())
&& (params == null || params.isEmpty()))) {
return null;
}
//
String spec = uri.getSchemeSpecificPart();
// If there are // at the beginning, this includes an authority - we don't support those!
if (spec.startsWith("//")) {
return null;
}
String[] pieces = spec.split("\\?", 2);
// In either case, part before a ? is the fqdn
String fqdn = pieces[0];
// There may be a query part
/*
if (pieces.length > 1) {
// parse CLASS and TYPE query paramters
}
*/
CLASS clazz = CLASS.IN;
TYPE type = TYPE.TXT;
return new DnsResource(flags, params, uri, fqdn, clazz, type);
}
@SuppressWarnings("unused")
public String getFqdn() {
return mFqdn;
}
@Override
protected String fetchResource (OperationLog log, int indent) {
Client c = new Client();
DNSMessage msg = c.query(new Question(mFqdn, mType, mClass));
Record aw = msg.getAnswers()[0];
TXT txt = (TXT) aw.getPayload();
return txt.getText().toLowerCase();
}
@Override
protected Matcher matchResource(OperationLog log, int indent, String res) {
return magicPattern.matcher(res);
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_dns : R.string.linked_verified_dns;
}
@Override
public @DrawableRes int getDisplayIcon() {
return R.drawable.linked_dns;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_dns);
}
@Override
public String getDisplayComment(Context context) {
return mFqdn;
}
}

View File

@@ -0,0 +1,94 @@
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.sufficientlysecure.keychain.R;
import org.sufficientlysecure.keychain.operations.results.OperationResult.LogType;
import org.sufficientlysecure.keychain.operations.results.OperationResult.OperationLog;
import org.sufficientlysecure.keychain.linked.LinkedCookieResource;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
public class GenericHttpsResource extends LinkedCookieResource {
GenericHttpsResource(Set<String> flags, HashMap<String,String> params, URI uri) {
super(flags, params, uri);
}
public static String generateText (Context context, byte[] fingerprint) {
String cookie = LinkedCookieResource.generate(fingerprint);
return String.format(context.getResources().getString(R.string.linked_id_generic_text),
cookie, "0x" + KeyFormattingUtils.convertFingerprintToHex(fingerprint).substring(24));
}
@SuppressWarnings("deprecation") // HttpGet is deprecated
@Override
protected String fetchResource (OperationLog log, int indent) throws HttpStatusException, IOException {
log.add(LogType.MSG_LV_FETCH, indent, mSubUri.toString());
HttpGet httpGet = new HttpGet(mSubUri);
return getResponseBody(httpGet);
}
public static GenericHttpsResource createNew (URI uri) {
HashSet<String> flags = new HashSet<>();
flags.add("generic");
HashMap<String,String> params = new HashMap<>();
return create(flags, params, uri);
}
public static GenericHttpsResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
if ( ! ("https".equals(uri.getScheme())
&& flags != null && flags.size() == 1 && flags.contains("generic")
&& (params == null || params.isEmpty()))) {
return null;
}
return new GenericHttpsResource(flags, params, uri);
}
@Override
public @DrawableRes
int getDisplayIcon() {
return R.drawable.linked_https;
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_https : R.string.linked_verified_https;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_https);
}
@Override
public String getDisplayComment(Context context) {
return mSubUri.toString();
}
@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;
}
}

View File

@@ -0,0 +1,217 @@
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.LinkedCookieResource;
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 LinkedCookieResource {
final String mHandle;
final String mGistId;
GithubResource(Set<String> flags, HashMap<String,String> params, URI uri,
String handle, String gistId) {
super(flags, params, uri);
mHandle = handle;
mGistId = gistId;
}
public static String generate(Context context, byte[] fingerprint) {
String cookie = LinkedCookieResource.generate(fingerprint);
return String.format(context.getResources().getString(R.string.linked_id_github_text), cookie);
}
@SuppressWarnings("deprecation") // HttpGet is deprecated
@Override
protected String fetchResource (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(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<String> 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;
}
@SuppressWarnings("deprecation")
public static GithubResource searchInGithubStream(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 cookie 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(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<String> 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(httpGet));
JSONObject gistFiles = gistObj.getJSONObject("files");
Iterator<String> 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<String>(), new HashMap<String,String>(), uri);
}
public static GithubResource create(Set<String> flags, HashMap<String,String> 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;
}
}

View File

@@ -0,0 +1,244 @@
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 android.util.Log;
import com.textuality.keybase.lib.JWalk;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
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.LinkedCookieResource;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TwitterResource extends LinkedCookieResource {
final String mHandle;
final String mTweetId;
TwitterResource(Set<String> flags, HashMap<String,String> params,
URI uri, String handle, String tweetId) {
super(flags, params, uri);
mHandle = handle;
mTweetId = tweetId;
}
public static TwitterResource create(URI uri) {
return create(new HashSet<String>(), new HashMap<String,String>(), uri);
}
public static TwitterResource create(Set<String> flags, HashMap<String,String> params, URI uri) {
// no params or flags
if (!flags.isEmpty() || !params.isEmpty()) {
return null;
}
Pattern p = Pattern.compile("https://twitter\\.com/([a-zA-Z0-9_]+)/status/([0-9]+)");
Matcher match = p.matcher(uri.toString());
if (!match.matches()) {
return null;
}
String handle = match.group(1);
String tweetId = match.group(2);
return new TwitterResource(flags, params, uri, handle, tweetId);
}
@SuppressWarnings("deprecation")
@Override
protected String fetchResource(OperationLog log, int indent) throws IOException, HttpStatusException,
JSONException {
String authToken;
try {
authToken = getAuthToken();
} catch (IOException | HttpStatusException | JSONException e) {
log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, indent);
return null;
}
HttpGet httpGet =
new HttpGet("https://api.twitter.com/1.1/statuses/show.json"
+ "?id=" + mTweetId
+ "&include_entities=false");
// construct a normal HTTPS request and include an Authorization
// header with the value of Bearer <>
httpGet.setHeader("Authorization", "Bearer " + authToken);
httpGet.setHeader("Content-Type", "application/json");
try {
String response = getResponseBody(httpGet);
JSONObject obj = new JSONObject(response);
JSONObject user = obj.getJSONObject("user");
if (!mHandle.equalsIgnoreCase(user.getString("screen_name"))) {
log.add(LogType.MSG_LV_ERROR_TWITTER_HANDLE, indent);
return null;
}
// update the results with the body of the response
return obj.getString("text");
} catch (JSONException e) {
log.add(LogType.MSG_LV_ERROR_TWITTER_RESPONSE, indent);
return null;
}
}
@Override
public @DrawableRes int getDisplayIcon() {
return R.drawable.linked_twitter;
}
@Override
public @StringRes
int getVerifiedText(boolean isSecret) {
return isSecret ? R.string.linked_verified_secret_twitter : R.string.linked_verified_twitter;
}
@Override
public String getDisplayTitle(Context context) {
return context.getString(R.string.linked_title_twitter);
}
@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;
}
@SuppressWarnings("deprecation")
public static TwitterResource searchInTwitterStream(
String screenName, String needle, OperationLog log) {
String authToken;
try {
authToken = getAuthToken();
} catch (IOException | HttpStatusException | JSONException e) {
log.add(LogType.MSG_LV_ERROR_TWITTER_AUTH, 1);
return null;
}
HttpGet httpGet =
new HttpGet("https://api.twitter.com/1.1/statuses/user_timeline.json"
+ "?screen_name=" + screenName
+ "&count=15"
+ "&include_rts=false"
+ "&trim_user=true"
+ "&exclude_replies=true");
// construct a normal HTTPS request and include an Authorization
// header with the value of Bearer <>
httpGet.setHeader("Authorization", "Bearer " + authToken);
httpGet.setHeader("Content-Type", "application/json");
try {
String response = getResponseBody(httpGet);
JSONArray array = new JSONArray(response);
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
String tweet = obj.getString("text");
if (tweet.contains(needle)) {
String id = obj.getString("id_str");
URI uri = URI.create("https://twitter.com/" + screenName + "/status/" + id);
return create(uri);
}
}
// update the results with the body of the response
log.add(LogType.MSG_LV_FETCH_ERROR_NOTHING, 1);
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, 1, Integer.toString(e.getStatus()));
} catch (MalformedURLException e) {
log.add(LogType.MSG_LV_FETCH_ERROR_URL, 1);
} catch (IOException e) {
Log.e(Constants.TAG, "io error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_IO, 1);
} catch (JSONException e) {
Log.e(Constants.TAG, "json error", e);
log.add(LogType.MSG_LV_FETCH_ERROR_FORMAT, 1);
}
return null;
}
private static String cachedAuthToken;
@SuppressWarnings("deprecation")
private static String getAuthToken() throws IOException, HttpStatusException, JSONException {
if (cachedAuthToken != null) {
return cachedAuthToken;
}
String base64Encoded = rot13("D293FQqanH0jH29KIaWJER5DomqSGRE2Ewc1LJACn3cbD1c"
+ "Fq1bmqSAQAz5MI2cIHKOuo3cPoRAQI1OyqmIVFJS6LHMXq2g6MRLkIj") + "==";
// Step 2: Obtain a bearer token
HttpPost httpPost = new HttpPost("https://api.twitter.com/oauth2/token");
httpPost.setHeader("Authorization", "Basic " + base64Encoded);
httpPost.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
httpPost.setEntity(new StringEntity("grant_type=client_credentials"));
JSONObject rawAuthorization = new JSONObject(getResponseBody(httpPost));
// Applications should verify that the value associated with the
// token_type key of the returned object is bearer
if (!"bearer".equals(JWalk.getString(rawAuthorization, "token_type"))) {
throw new JSONException("Expected bearer token in response!");
}
cachedAuthToken = rawAuthorization.getString("access_token");
return cachedAuthToken;
}
public static String rot13(String input) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (c >= 'a' && c <= 'm') c += 13;
else if (c >= 'A' && c <= 'M') c += 13;
else if (c >= 'n' && c <= 'z') c -= 13;
else if (c >= 'N' && c <= 'Z') c -= 13;
sb.append(c);
}
return sb.toString();
}
}