package com.jobmineplus.mobile.widgets; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.security.KeyStore; import java.util.ArrayList; import java.util.List; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import android.content.Context; import android.util.Log; import com.jobmineplus.mobile.R; import com.jobmineplus.mobile.exceptions.JbmnplsLoggedOutException; import com.jobmineplus.mobile.widgets.ssl.AdditionalKeyStoresSSLSocketFactory; public final class JbmnplsHttpClient { //================ // Static Links //================ static public final class GET_LINKS { public static final String DOCUMENTS = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_STUDDOCS"; public static final String PROFILE = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_STUDENT"; public static final String SKILLS = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_STUDENT?PAGE=UW_CO_STU_SKL_MTN"; public static final String SEARCH = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_JOBSRCH"; public static final String SHORTLIST = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_JOB_SLIST"; public static final String APPLICATIONS = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_APP_SUMMARY"; public static final String INTERVIEWS = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_STU_INTVS"; public static final String RANKINGS = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_RNK2"; public static final String DESCRIP_PRE = "https://jobmine.ccol.uwaterloo.ca/psc/SS/EMPLOYEE/WORK/c/UW_CO_STUDENTS.UW_CO_JOBDTLS?UW_CO_JOB_ID="; } static public final class POST_LINKS { public static final String LOGIN = "https://jobmine.ccol.uwaterloo.ca/psp/SS/?cmd=login&languageCd=ENG&sessionId="; public static final String LOGOUT = "https://jobmine.ccol.uwaterloo.ca/psp/SS/?cmd=login&languageCd=ENG&"; } //=========================== // Logged in or out states //=========================== static public enum LOGGED { IN, OUT, OFFLINE } //============= // Constants //============= private static final int AUTO_LOGOUT_TIME = 1000 * 60 * 20; //20 min private static final int BUFFER_READER_SIZE = 1024; // Login constants private static final String LOGIN_UNIQUE_STRING = "Signin HTML for JobMine."; private static final String LOGIN_OFFLINE_MESSAGE = "Invalid signon time for user"; private static final String DEFAULT_HTML_ENCODER = "UTF-8"; private static final String FAILED_URL = "Invalid URL - no Node found in"; private static final int LOGIN_READ_LENGTH = 400; private static final int LOGIN_ERROR_MSG_SKIP = 3200; private static final int MAX_LOGIN_ATTEMPTS = 3; //===================== // Private Variables //===================== private final Object requestLock = new Object(); private final Object timeStampLock = new Object(); HttpClient client = new DefaultHttpClient(); private long loginTimeStamp = 0; private String username = ""; private String password = ""; private HttpRequestBase currentRequest; private boolean canAbort = true; private boolean pendingAbort = false; private static KeyStore sTrustedStore = null; private static final Object sTrustedLock = new Object(); //========================= // Static Initialization //========================= public static void init(Context ctx) { if (sTrustedStore == null) { synchronized (sTrustedLock) { if (sTrustedStore == null) { try { KeyStore trusted = KeyStore.getInstance("BKS"); InputStream in = ctx.getResources().openRawResource(R.raw.jobmine_certificate); try { trusted.load(in, ctx.getString(R.string.ssl_certificate_password).toCharArray()); } finally { in.close(); } sTrustedStore = trusted; } catch (Exception e) { throw new AssertionError(e); } } } } } //========================= // Constructor //========================= public JbmnplsHttpClient() { reset(); } public JbmnplsHttpClient(String user, String pass) { username = user; password = pass; reset(); } //============== // Login Data //============== public void setLoginCredentials(String user, String pass) { synchronized(username) { synchronized (password) { username = user; password = pass; } } } public String getUsername() { synchronized(username) { if (username == "") { return null; } return username; } } public String getPassword() { synchronized(password) { if (password == "") { return null; } return password; } } public boolean isLoggedIn() { synchronized(timeStampLock) { long timeNow = System.currentTimeMillis(); return loginTimeStamp != 0 && (timeNow - loginTimeStamp) < AUTO_LOGOUT_TIME; } } public LOGGED login() { return login(username, password); } public boolean verifyLogin() { if (!isLoggedIn()) { for (int i = 0; i < MAX_LOGIN_ATTEMPTS; i++) { JbmnplsHttpClient.LOGGED result = login(); if (result == JbmnplsHttpClient.LOGGED.IN) { return true; } else if (result == JbmnplsHttpClient.LOGGED.OFFLINE) { return false; } } return false; } return true; } public LOGGED login(String user, String pass) { reset(); if (user.length() == 0 || pass.length() == 0) { Log.i("jbmnplsmbl", "Logged out no pass and user"); return LOGGED.OUT; } List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2); nameValuePairs.add(new BasicNameValuePair("httpPort", "")); nameValuePairs.add(new BasicNameValuePair("submit", "Submit")); nameValuePairs.add(new BasicNameValuePair("timezoneOffset", "480")); nameValuePairs.add(new BasicNameValuePair("pwd", pass)); nameValuePairs.add(new BasicNameValuePair("userid", user)); BufferedReader reader = null; try { StopWatch s = new StopWatch(true); HttpResponse response = internalPost(nameValuePairs, JbmnplsHttpClient.POST_LINKS.LOGIN); s.printElapsed("%s ms login post"); if (response == null || response.getStatusLine().getStatusCode() != 200) { return LOGGED.OUT; } reader = getReaderFromResponse(response); LOGGED result = validateLoginJobmine(reader); if (result != LOGGED.IN) { return result; } // Successful login setLoginCredentials(user, pass); updateTimestamp(); return LOGGED.IN; } catch (IOException e) { e.printStackTrace(); return LOGGED.OFFLINE; } finally { try { if (reader != null) { reader.close(); } } catch(IOException e) { e.printStackTrace(); return LOGGED.OUT; } finally { if (canAbort && pendingAbort) { pendingAbort = false; } } } } public void logout() { client = new DefaultHttpClient(); synchronized (timeStampLock) { loginTimeStamp = 0; } } //===================== // GET HTTP Requests //===================== public HttpResponse get(String url) { HttpResponse result = internalGet(url); if (canAbort && pendingAbort) { pendingAbort = false; } return result; } private HttpResponse internalGet(String url) { HttpResponse response = null; try { if (pendingAbort && canAbort) { return null; } HttpGet request = new HttpGet(url); StopWatch s = new StopWatch(true); response = client.execute(request); s.printElapsed("%s ms to get"); } catch (Exception e) { e.printStackTrace(); return null; } return response; } public String getJobmineHtml (String url) throws JbmnplsLoggedOutException, IOException{ synchronized (requestLock) { InputStream in = null; BufferedReader reader = null; try { // Attempt 3 times if logged out boolean loggedIn = false; for (int i = 0; i < MAX_LOGIN_ATTEMPTS; i++) { if (in != null) { in.close(); in = null; } HttpResponse response = internalGet(url); if (response != null) { in = response.getEntity().getContent(); reader = new BufferedReader(new InputStreamReader(in, DEFAULT_HTML_ENCODER), BUFFER_READER_SIZE); // Validates the html to make sure we logged in // If failed to login, try it again 2 more times LOGGED result = validateLoginJobmine(reader); if (result == LOGGED.IN) { loggedIn = true; break; } else if (result == LOGGED.OFFLINE) { throw new JbmnplsLoggedOutException(); } } else { return null; } if (login() == LOGGED.OFFLINE) { throw new JbmnplsLoggedOutException(); } } if (!loggedIn) { throw new JbmnplsLoggedOutException(); } // Successfully logged in StringBuilder str = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) { str.append(line); } updateTimestamp(); return str.toString(); } catch (IOException e) { throw e; } finally { if (canAbort && pendingAbort) { pendingAbort = false; } if (in != null) { try { in.close(); } catch(IOException e) {} } } } } //====================== // POST HTTP Requests //====================== public HttpResponse post(List<NameValuePair> postData, String url) { synchronized (requestLock) { HttpResponse result = internalPost(postData, url); if (canAbort && pendingAbort) { pendingAbort = false; } return result; } } public HttpResponse internalPost(List<NameValuePair> postData, String url) { if (canAbort && pendingAbort) { return null; } HttpResponse response = null; try { currentRequest = new HttpPost(url); ((HttpPost)currentRequest).setEntity(new UrlEncodedFormEntity(postData)); response = client.execute(currentRequest); currentRequest = null; return response; } catch (Exception e) { e.printStackTrace(); return null; } } public String postJobmineHtml (List<NameValuePair> postData, String url) throws JbmnplsLoggedOutException, IOException { synchronized (requestLock) { InputStream in = null; BufferedReader reader = null; try { // Attempt 3 times if logged out boolean loggedIn = false; for (int i = 0; i < MAX_LOGIN_ATTEMPTS; i++) { if (in != null) { in.close(); in = null; } HttpResponse response = internalPost(postData, url); if (response != null) { in = response.getEntity().getContent(); reader = new BufferedReader(new InputStreamReader(in, DEFAULT_HTML_ENCODER), BUFFER_READER_SIZE); reader.mark(1); // Validates the html to make sure we logged in // If failed to login, try it again 2 more times LOGGED result = validateLoginJobmine(reader); if (result == LOGGED.IN) { loggedIn = true; break; } else if (result == LOGGED.OFFLINE) { throw new JbmnplsLoggedOutException(); } } else { return null; } if (login() == LOGGED.OFFLINE) { throw new JbmnplsLoggedOutException(); } } if (!loggedIn) { throw new JbmnplsLoggedOutException(); } // Successfully logged in StringBuilder str = new StringBuilder(); String line = null; reader.reset(); while ((line = reader.readLine()) != null) { str.append(line); } updateTimestamp(); return str.toString(); } catch (IOException e) { throw e; } finally { if (canAbort && pendingAbort) { pendingAbort = false; } if (in != null) { try { in.close(); } catch(IOException e) {} } } } } //======================== // Abort/Cancel Methods //======================== public void abort() { pendingAbort = true; if (currentRequest != null && canAbort) { currentRequest.abort(); // ignore the warning. currentRequest = null; } } public void canAbort(boolean flag) { canAbort = flag; } public boolean isAbortPending() { return pendingAbort; } //=================== // Private Methods //=================== private synchronized void reset() { // Since JobMine's SSL implementation broke around Spring 2014, we need to implement // a custom trust certificate. This is not ideal but hey, it's Waterloo. // More info: http://stackoverflow.com/a/6378872/654628 AdditionalKeyStoresSSLSocketFactory fact = null; try { fact = new AdditionalKeyStoresSSLSocketFactory(sTrustedStore); } catch (Exception e) { throw new AssertionError(e); } final SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); schemeRegistry.register(new Scheme("https", fact, 443)); HttpParams params = new BasicHttpParams(); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setUserAgent(params, "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:16.0) Gecko/20100101 Firefox/16.0"); client = new DefaultHttpClient(new ThreadSafeClientConnManager(params, schemeRegistry), params); } private LOGGED validateLoginJobmine(BufferedReader reader) throws IOException { char[] buffer = new char[LOGIN_READ_LENGTH]; reader.read(buffer, 0, LOGIN_READ_LENGTH); String text = new String(buffer); if (text.contains(LOGIN_UNIQUE_STRING)) { // On login page reader.skip(LOGIN_ERROR_MSG_SKIP); reader.read(buffer, 0, LOGIN_READ_LENGTH); text = new String(buffer); // Check for offline error if (text.contains(LOGIN_OFFLINE_MESSAGE)) { return LOGGED.OFFLINE; } return LOGGED.OUT; } else if (text.contains(FAILED_URL)) { synchronized (timeStampLock) { loginTimeStamp = 0; } return LOGGED.OUT; } return LOGGED.IN; } private BufferedReader getReaderFromResponse(HttpResponse response) throws IllegalStateException, IOException { return getReaderFromResponse(response, DEFAULT_HTML_ENCODER); } private BufferedReader getReaderFromResponse(HttpResponse response, String encoder) throws IllegalStateException, IOException { InputStream in = response.getEntity().getContent(); return new BufferedReader(new InputStreamReader(in, encoder), BUFFER_READER_SIZE); } private void updateTimestamp() { synchronized(timeStampLock) { loginTimeStamp = System.currentTimeMillis(); } } }