Java tutorial
/* SchoolPlanner4Untis - Android app to manage your Untis timetable Copyright (C) 2011 Mathias Kub <mail@makubi.at> Gerald Schreiber <mail@gerald-schreiber.at> Philip Woelfel <philip@woelfel.at> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package edu.htl3r.schoolplanner.backend.network; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.security.KeyStore; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocket; import org.apache.http.HttpResponse; import org.apache.http.HttpVersion; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ClientConnectionManager; 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.conn.ssl.SSLSocketFactory; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.apache.http.params.HttpProtocolParams; import org.apache.http.protocol.HTTP; import android.util.Log; import edu.htl3r.schoolplanner.R; import edu.htl3r.schoolplanner.SchoolplannerContext; import edu.htl3r.schoolplanner.backend.network.exceptions.SSLForcedButUnavailableException; import edu.htl3r.schoolplanner.backend.preferences.loginSets.LoginSet; /** * * Direkter Netzwerkzugriff ueber URL. * */ public class Network { private String oldServerUrl = ""; private String oldSchool = ""; private HttpClient client; private LoginSet loginCredentials; private URI serverUrl; private URI httpsServerUrl; private URI usedUrl; private String jsessionid; // Prioritized private SSLSocketFactory[] sslSocketFactories = new SSLSocketFactory[2]; private Scheme[] sslSchemes = new Scheme[2]; boolean sslAvailable = true; public Network() { initSSLSocketFactories(); HttpParams params = new BasicHttpParams(); // TODO: Timeouts sind statisch HttpConnectionParams.setConnectionTimeout(params, 20000); HttpConnectionParams.setSoTimeout(params, 10000); HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); HttpProtocolParams.setContentCharset(params, HTTP.UTF_8); HttpProtocolParams.setUseExpectContinue(params, false); SchemeRegistry registry = new SchemeRegistry(); ClientConnectionManager connman = new ThreadSafeClientConnManager(params, registry); client = new DefaultHttpClient(connman, params); } /** * Initialisiert die {@link SSLSocketFactory}s, die benoetigte CAs enthalten. * Zur Zeit sind das die Standard-CAs, CACert und StartSSL. */ private void initSSLSocketFactories() { sslSocketFactories[0] = SSLSocketFactory.getSocketFactory(); sslSocketFactories[1] = additionalCASSLSocketFactory(); } /** * Initialisiert alle {@link Scheme}s fuer HTTPS. */ private void initSSLSchemes() { sslSchemes[0] = new Scheme("https", sslSocketFactories[0], httpsServerUrl.getPort() != -1 ? httpsServerUrl.getPort() : 443); sslSchemes[1] = new Scheme("https", sslSocketFactories[1], httpsServerUrl.getPort() != -1 ? httpsServerUrl.getPort() : 443); } /** * Liefert ein {@link SSLSocket}, wenn eine Verbindung via SSL zum Server aufgebaut werden konnte oder 'null', wenn SSL nicht verfuegbar ist. * @param sa Die Adresse des Sockets, zum dem die Verbindung aufgebaut werden soll * @param set Ein Set mit {@link SSLSocket}s, mithilfe derer versucht werden soll, eine Verbindung aufzubauen * @return Das erste SSLSocket aus dem Set, mit dem eine problemlos Verbindung zum Server aufgebaut werden konnte oder 'null', wenn dies mit keinem moeglich war */ private SSLSocket getWorkingSSLSocket(SocketAddress sa, Set<SSLSocket> set) { final int sslSocketTimeout = 2000; for (SSLSocket sslSocket : set) { try { sslSocket.connect(sa, sslSocketTimeout); sslSocket.setSoTimeout(sslSocketTimeout); sslSocket.setReuseAddress(true); sslSocket.startHandshake(); return sslSocket; } catch (IOException e) { } finally { try { sslSocket.close(); } catch (IOException e) { } } } return null; } /** * Ueberprueft, ob der Server SSL unterstuetzt oder nicht und registriert die passenden {@link Scheme}s fuer die spaetere Verwendung. * @throws IOException Wenn die URL nicht zur Uebertragung verwendet werden kann */ private void checkServerCapability() throws IOException { final SocketAddress sa; try { sa = new InetSocketAddress(httpsServerUrl.getHost(), httpsServerUrl.getPort() != -1 ? httpsServerUrl.getPort() : 443); } catch (IllegalArgumentException e) { // Thrown, if server url can not be parsed throw new IOException("Unable to parse URL"); } try { Map<SSLSocket, Scheme> checkSocketsToSchemeMapping = new HashMap<SSLSocket, Scheme>(); checkSocketsToSchemeMapping.put((SSLSocket) sslSocketFactories[0].createSocket(), sslSchemes[0]); checkSocketsToSchemeMapping.put((SSLSocket) sslSocketFactories[1].createSocket(), sslSchemes[1]); SSLSocket availableSocket = getWorkingSSLSocket(sa, checkSocketsToSchemeMapping.keySet()); sslAvailable = availableSocket != null; registerSchemes(checkSocketsToSchemeMapping.get(availableSocket)); } catch (IOException e) { sslAvailable = false; registerSchemes(); } Log.i("Network", "SSL available: " + sslAvailable); } /** * Registriert nur eine {@link Scheme} fuer plain HTTP. */ private void registerSchemes() { registerSchemes(null); } /** * Registriert eine {@link Scheme} fuer plain HTTP und falls uebergeben, eine fuer HTTPS. * @param scheme Scheme fuer HTTPS, die registriert werden soll */ private void registerSchemes(Scheme scheme) { client.getConnectionManager().getSchemeRegistry() .register(new Scheme("http", PlainSocketFactory.getSocketFactory(), serverUrl != null && serverUrl.getPort() != -1 ? serverUrl.getPort() : 80)); if (scheme != null) { client.getConnectionManager().getSchemeRegistry().register(scheme); } } public String getResponse(String request) throws IOException { return executeRequest(request); } /** * Versucht mittels HTTP(S), eine Verbindung mit dem Server herzustellen, der sich hinter der gesetzten URL befindet, sendet die uebergeben Anfrage an ihn und gibt die Antwort als String zurueck. * Wenn eine JSESSIONID bekannt ist, wird diese an den HTTP-Header angehaengt. * Das verwendete Character-Set ist UTF-8. * @param request Anfrage, die gesendet werden soll * @return Antwort auf die Anfrage * @throws IOException Wenn waehrend der Uebertragung ein Fehler auftritt */ private String executeRequest(String request) throws IOException { checkPreferenceChange(); HttpPost httpRequest = new HttpPost(usedUrl); if (jsessionid != null) { httpRequest.addHeader("Cookie", "JSESSIONID=" + jsessionid); Log.d("Network", "Added header: " + httpRequest.getHeaders("Cookie")[0].toString()); } StringEntity entity = new StringEntity(request, "UTF-8"); httpRequest.setEntity(entity); HttpResponse httpResponse = client.execute(httpRequest); Log.d("Network", "Sent: " + request); ByteArrayOutputStream body = new ByteArrayOutputStream(); httpResponse.getEntity().writeTo(body); String response = body.toString(); Log.d("Network", "Got status: " + httpResponse.getStatusLine()); Log.d("Network", "Got body: " + response); return response; } private boolean preferencesChanged() { return !(loginCredentials.getServerUrl().equals(oldServerUrl) && loginCredentials.getSchool().equals(oldSchool)); } private void checkPreferenceChange() throws IOException { if (preferencesChanged()) { String serverUrl = loginCredentials.getServerUrl(); String school = loginCredentials.getSchool(); try { jsessionid = null; setServerUrl(serverUrl); initSSLSchemes(); checkServerCapability(); setSchool(school); } catch (URISyntaxException e) { // Thrown, if server url can not be parsed throw new IOException("Unable to parse URL: " + serverUrl); } oldServerUrl = new String(serverUrl); oldSchool = new String(school); } } /** * Setzt den Namen der Schule, die als GET-Parameter in der Request-URL verwendet werden soll . * @param school Name der zu verwendenden Schule * @throws UnsupportedEncodingException Wenn die Kodierung nicht unterstuetzt wird * @throws URISyntaxException Wenn die URL nicht gesetzt werden konnte */ private void setSchool(String school) throws UnsupportedEncodingException, URISyntaxException, SSLException { // Encode school as UTF-8 string String encodedSchool = URLEncoder.encode(school, "UTF-8"); if (loginCredentials.isSslOnly() && !sslAvailable) { throw new SSLForcedButUnavailableException( httpsServerUrl.toString() + ":" + httpsServerUrl.getPort() + " does not have SSL enabled"); } else { usedUrl = sslAvailable ? new URI(httpsServerUrl + "?school=" + encodedSchool) : new URI(serverUrl + "?school=" + encodedSchool); } Log.d("Network", "Setting url: " + usedUrl.toString()); } public void setJsessionid(String jsessionid) { this.jsessionid = jsessionid; } /** * Setzt die URL des Servers (inklusive Port). * @param serverUrl URL des Servers inklusive Port * @throws URISyntaxException Wenn die URL nicht geparst werden konnte */ private void setServerUrl(String serverUrl) throws URISyntaxException { this.serverUrl = new URI("http://" + serverUrl + "/WebUntis/jsonrpc.do"); this.httpsServerUrl = new URI("https://" + serverUrl + "/WebUntis/jsonrpc.do"); } private SSLSocketFactory additionalCASSLSocketFactory() { try { KeyStore trusted = KeyStore.getInstance("BKS"); InputStream in = SchoolplannerContext.context.getResources().openRawResource(R.raw.additional_cas); try { final String keystorePassword = "additionalCAs"; trusted.load(in, keystorePassword.toCharArray()); } finally { in.close(); } return new SSLSocketFactory(trusted); } catch (Exception e) { throw new AssertionError(e); } } public void setLoginCredentials(LoginSet loginSet) { this.loginCredentials = loginSet; } }