diff options
Diffstat (limited to 'src/frontends')
58 files changed, 6703 insertions, 130 deletions
diff --git a/src/frontends/android/AndroidManifest.xml b/src/frontends/android/AndroidManifest.xml index 5b1d03d7c..747fe1df3 100644 --- a/src/frontends/android/AndroidManifest.xml +++ b/src/frontends/android/AndroidManifest.xml @@ -1,29 +1,71 @@ <?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.strongswan.android" android:versionCode="1" - android:versionName="1.0" > + android:versionName="1.0.0" > <uses-sdk android:minSdkVersion="14" /> + <uses-permission android:name="android.permission.INTERNET" /> <application android:icon="@drawable/ic_launcher" - android:label="@string/app_name" > + android:label="@string/app_name" + android:theme="@style/ApplicationTheme" > <activity - android:name=".strongSwanActivity" - android:label="@string/app_name" > + android:name=".ui.MainActivity" + android:label="@string/main_activity_name" + android:launchMode="singleTop" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <service android:name=".CharonVpnService" android:permission="android.permission.BIND_VPN_SERVICE"> + <activity + android:name=".ui.VpnProfileDetailActivity" > + </activity> + <activity + android:name=".ui.LogActivity" + android:label="@string/log_title" > + </activity> + + <service + android:name=".logic.VpnStateService" + android:exported="false" > + </service> + <service + android:name=".logic.CharonVpnService" + android:exported="false" + android:permission="android.permission.BIND_VPN_SERVICE" > <intent-filter> - <action android:name="android.net.VpnService"/> + <action android:name="org.strongswan.android.logic.CharonVpnService" /> </intent-filter> </service> + + <provider + android:name=".data.LogContentProvider" + android:authorities="org.strongswan.android.content.log" > + <!-- android:grantUriPermissions="true" combined with a custom permission does + not work (probably too many indirections with ACTION_SEND) so we secure + this provider with a custom ticketing system --> + </provider> </application> -</manifest>
\ No newline at end of file +</manifest> diff --git a/src/frontends/android/jni/libandroidbridge/Android.mk b/src/frontends/android/jni/libandroidbridge/Android.mk index 3b8b98b86..e1806f702 100644 --- a/src/frontends/android/jni/libandroidbridge/Android.mk +++ b/src/frontends/android/jni/libandroidbridge/Android.mk @@ -3,7 +3,14 @@ include $(CLEAR_VARS) # copy-n-paste from Makefile.am LOCAL_SRC_FILES := \ -charonservice.c +android_jni.c android_jni.h \ +backend/android_attr.c backend/android_attr.h \ +backend/android_creds.c backend/android_creds.h \ +backend/android_service.c backend/android_service.h \ +charonservice.c charonservice.h \ +kernel/android_ipsec.c kernel/android_ipsec.h \ +kernel/android_net.c kernel/android_net.h \ +vpnservice_builder.c vpnservice_builder.h # build libandroidbridge ------------------------------------------------------- diff --git a/src/frontends/android/jni/libandroidbridge/android_jni.c b/src/frontends/android/jni/libandroidbridge/android_jni.c new file mode 100644 index 000000000..e7cb14fb7 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/android_jni.c @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +#include "android_jni.h" + +#include <library.h> +#include <threading/thread_value.h> + +/** + * JVM + */ +static JavaVM *android_jvm; + +jclass *android_charonvpnservice_class; +jclass *android_charonvpnservice_builder_class; + +/** + * Thread-local variable. Only used because of the destructor + */ +static thread_value_t *androidjni_threadlocal; + +/** + * Thread-local destructor to ensure that a native thread is detached + * from the JVM even if androidjni_detach_thread() is not called. + */ +static void attached_thread_cleanup(void *arg) +{ + (*android_jvm)->DetachCurrentThread(android_jvm); +} + +/* + * Described in header + */ +void androidjni_attach_thread(JNIEnv **env) +{ + if ((*android_jvm)->GetEnv(android_jvm, (void**)env, + JNI_VERSION_1_6) == JNI_OK) + { /* already attached or even a Java thread */ + return; + } + (*android_jvm)->AttachCurrentThread(android_jvm, env, NULL); + /* use a thread-local value with a destructor that automatically detaches + * the thread from the JVM before it terminates, if not done manually */ + androidjni_threadlocal->set(androidjni_threadlocal, (void*)*env); +} + +/* + * Described in header + */ +void androidjni_detach_thread() +{ + if (androidjni_threadlocal->get(androidjni_threadlocal)) + { /* only do this if we actually attached this thread */ + androidjni_threadlocal->set(androidjni_threadlocal, NULL); + (*android_jvm)->DetachCurrentThread(android_jvm); + } +} + +/** + * Called when this library is loaded by the JVM + */ +jint JNI_OnLoad(JavaVM *vm, void *reserved) +{ + JNIEnv *env; + + android_jvm = vm; + + if ((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) + { + return -1; + } + + androidjni_threadlocal = thread_value_create(attached_thread_cleanup); + + android_charonvpnservice_class = + (*env)->NewGlobalRef(env, (*env)->FindClass(env, + JNI_PACKAGE_STRING "/CharonVpnService")); + android_charonvpnservice_builder_class = + (*env)->NewGlobalRef(env, (*env)->FindClass(env, + JNI_PACKAGE_STRING "/CharonVpnService$BuilderAdapter")); + + return JNI_VERSION_1_6; +} + +/** + * Called when this library is unloaded by the JVM (which never happens on + * Android) + */ +void JNI_OnUnload(JavaVM *vm, void *reserved) +{ + androidjni_threadlocal->destroy(androidjni_threadlocal); +} + diff --git a/src/frontends/android/jni/libandroidbridge/android_jni.h b/src/frontends/android/jni/libandroidbridge/android_jni.h new file mode 100644 index 000000000..774d37d7e --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/android_jni.h @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup android_jni android_jni + * @{ @ingroup libandroidbridge + */ + +#ifndef ANDROID_JNI_H_ +#define ANDROID_JNI_H_ + +#include <jni.h> +#include <library.h> + +#define JNI_PACKAGE org_strongswan_android_logic +#define JNI_PACKAGE_STRING "org/strongswan/android/logic" + +#define JNI_METHOD_PP(pack, klass, name, ret, ...) \ + ret Java_##pack##_##klass##_##name(JNIEnv *env, jobject this, ##__VA_ARGS__) + +#define JNI_METHOD_P(pack, klass, name, ret, ...) \ + JNI_METHOD_PP(pack, klass, name, ret, ##__VA_ARGS__) + +#define JNI_METHOD(klass, name, ret, ...) \ + JNI_METHOD_P(JNI_PACKAGE, klass, name, ret, ##__VA_ARGS__) + +/** + * Java classes + * Initialized in JNI_OnLoad() + */ +extern jclass *android_charonvpnservice_class; +extern jclass *android_charonvpnservice_builder_class; + +/** + * Attach the current thread to the JVM + * + * As local JNI references are not freed until the thread detaches + * androidjni_detach_thread() should be called as soon as possible. + * If it is not called a thread-local destructor ensures that the + * thread is at least detached as soon as it terminates. + * + * @param env JNIEnv + */ +void androidjni_attach_thread(JNIEnv **env); + +/** + * Detach the current thread from the JVM + * + * Call this as soon as possible to ensure that local JNI references are freed. + */ +void androidjni_detach_thread(); + +/** + * Handle exceptions thrown by a JNI call + * + * @param env JNIEnv + * @return TRUE if an exception was thrown + */ +static inline bool androidjni_exception_occurred(JNIEnv *env) +{ + if ((*env)->ExceptionOccurred(env)) + { /* clear any exception, otherwise the VM is terminated */ + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + return TRUE; + } + return FALSE; +} + +/** + * Convert a Java string to a C string. Memory is allocated. + * + * @param env JNIEnv + * @param jstr Java string + * @return native C string (allocated) + */ +static inline char *androidjni_convert_jstring(JNIEnv *env, jstring jstr) +{ + char *str; + jsize len; + + len = (*env)->GetStringUTFLength(env, jstr); + str = malloc(len + 1); + (*env)->GetStringUTFRegion(env, jstr, 0, len, str); + str[len] = '\0'; + return str; +} + +#endif /** ANDROID_JNI_H_ @}*/ diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_attr.c b/src/frontends/android/jni/libandroidbridge/backend/android_attr.c new file mode 100644 index 000000000..e8c506950 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/backend/android_attr.c @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +#include "android_attr.h" +#include "../charonservice.h" + +#include <hydra.h> +#include <debug.h> +#include <library.h> + +typedef struct private_android_attr_t private_android_attr_t; + +/** + * Private data of an android_attr_t object. + */ +struct private_android_attr_t { + + /** + * Public interface. + */ + android_attr_t public; +}; + +METHOD(attribute_handler_t, handle, bool, + private_android_attr_t *this, identification_t *server, + configuration_attribute_type_t type, chunk_t data) +{ + vpnservice_builder_t *builder; + host_t *dns; + + switch (type) + { + case INTERNAL_IP4_DNS: + dns = host_create_from_chunk(AF_INET, data, 0); + break; + default: + return FALSE; + } + + if (!dns || dns->is_anyaddr(dns)) + { + DESTROY_IF(dns); + return FALSE; + } + + builder = charonservice->get_vpnservice_builder(charonservice); + builder->add_dns(builder, dns); + dns->destroy(dns); + return TRUE; +} + +METHOD(attribute_handler_t, release, void, + private_android_attr_t *this, identification_t *server, + configuration_attribute_type_t type, chunk_t data) +{ + /* DNS servers cannot be removed from an existing TUN device */ +} + +METHOD(enumerator_t, enumerate_dns, bool, + enumerator_t *this, configuration_attribute_type_t *type, chunk_t *data) +{ + *type = INTERNAL_IP4_DNS; + *data = chunk_empty; + this->enumerate = (void*)return_false; + return TRUE; +} + +METHOD(attribute_handler_t, create_attribute_enumerator, enumerator_t*, + private_android_attr_t *this, identification_t *server, host_t *vip) +{ + enumerator_t *enumerator; + + INIT(enumerator, + .enumerate = (void*)_enumerate_dns, + .destroy = (void*)free, + ); + return enumerator; +} + +METHOD(android_attr_t, destroy, void, + private_android_attr_t *this) +{ + free(this); +} + +/** + * Described in header + */ +android_attr_t *android_attr_create() +{ + private_android_attr_t *this; + + INIT(this, + .public = { + .handler = { + .handle = _handle, + .release = _release, + .create_attribute_enumerator = _create_attribute_enumerator, + }, + .destroy = _destroy, + }, + ); + + return &this->public; +} + diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_attr.h b/src/frontends/android/jni/libandroidbridge/backend/android_attr.h new file mode 100644 index 000000000..56b02e1ce --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/backend/android_attr.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup android_attr android_attr + * @{ @ingroup android_backend + */ + +#ifndef ANDROID_ATTR_H_ +#define ANDROID_ATTR_H_ + +#include <library.h> +#include <attributes/attribute_handler.h> + +typedef struct android_attr_t android_attr_t; + +/** + * Handler for DNS configuration + */ +struct android_attr_t { + + /** + * implements the attribute_handler_t interface + */ + attribute_handler_t handler; + + /** + * Destroy a android_attr_t + */ + void (*destroy)(android_attr_t *this); +}; + +/** + * Create a android_attr_t instance. + */ +android_attr_t *android_attr_create(void); + +#endif /** ANDROID_ATTR_H_ @}*/ + diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_creds.c b/src/frontends/android/jni/libandroidbridge/backend/android_creds.c new file mode 100644 index 000000000..27023d721 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/backend/android_creds.c @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +#include "android_creds.h" +#include "../charonservice.h" + +#include <daemon.h> +#include <library.h> +#include <credentials/sets/mem_cred.h> +#include <threading/rwlock.h> + +typedef struct private_android_creds_t private_android_creds_t; + +/** + * Private data of an android_creds_t object + */ +struct private_android_creds_t { + + /** + * Public interface + */ + android_creds_t public; + + /** + * Credential set storing trusted certificates and user credentials + */ + mem_cred_t *creds; + + /** + * read/write lock to make sure certificates are only loaded once + */ + rwlock_t *lock; + + /** + * TRUE if certificates have been loaded via JNI + */ + bool loaded; +}; + +/** + * Load trusted certificates via charonservice (JNI). + */ +static void load_trusted_certificates(private_android_creds_t *this) +{ + linked_list_t *certs; + certificate_t *cert; + chunk_t *current; + + certs = charonservice->get_trusted_certificates(charonservice); + if (certs) + { + while (certs->remove_first(certs, (void**)¤t) == SUCCESS) + { + cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509, + BUILD_BLOB_ASN1_DER, *current, BUILD_END); + if (cert) + { + DBG2(DBG_CFG, "loaded CA certificate '%Y'", + cert->get_subject(cert)); + this->creds->add_cert(this->creds, TRUE, cert); + } + chunk_free(current); + free(current); + } + certs->destroy(certs); + } +} + +METHOD(credential_set_t, create_cert_enumerator, enumerator_t*, + private_android_creds_t *this, certificate_type_t cert, key_type_t key, + identification_t *id, bool trusted) +{ + enumerator_t *enumerator; + + if (!trusted || (cert != CERT_ANY && cert != CERT_X509)) + { + return NULL; + } + this->lock->read_lock(this->lock); + if (!this->loaded) + { + this->lock->unlock(this->lock); + this->lock->write_lock(this->lock); + /* check again after acquiring the write lock */ + if (!this->loaded) + { + load_trusted_certificates(this); + this->loaded = TRUE; + } + this->lock->unlock(this->lock); + this->lock->read_lock(this->lock); + } + enumerator = this->creds->set.create_cert_enumerator(&this->creds->set, + cert, key, id, trusted); + return enumerator_create_cleaner(enumerator, (void*)this->lock->unlock, + this->lock); +} + +METHOD(android_creds_t, add_username_password, void, + private_android_creds_t *this, char *username, char *password) +{ + shared_key_t *shared_key; + identification_t *id; + chunk_t secret; + + secret = chunk_create(password, strlen(password)); + shared_key = shared_key_create(SHARED_EAP, chunk_clone(secret)); + id = identification_create_from_string(username); + + this->creds->add_shared(this->creds, shared_key, id, NULL); +} + +METHOD(credential_set_t, create_shared_enumerator, enumerator_t*, + private_android_creds_t *this, shared_key_type_t type, + identification_t *me, identification_t *other) +{ + return this->creds->set.create_shared_enumerator(&this->creds->set, + type, me, other); +} + +METHOD(android_creds_t, clear, void, + private_android_creds_t *this) +{ + this->lock->write_lock(this->lock); + this->creds->clear(this->creds); + this->loaded = FALSE; + this->lock->unlock(this->lock); +} + +METHOD(android_creds_t, destroy, void, + private_android_creds_t *this) +{ + clear(this); + this->creds->destroy(this->creds); + this->lock->destroy(this->lock); + free(this); +} + +/** + * Described in header. + */ +android_creds_t *android_creds_create() +{ + private_android_creds_t *this; + + INIT(this, + .public = { + .set = { + .create_cert_enumerator = _create_cert_enumerator, + .create_shared_enumerator = _create_shared_enumerator, + .create_private_enumerator = (void*)return_null, + .create_cdp_enumerator = (void*)return_null, + .cache_cert = (void*)nop, + }, + .add_username_password = _add_username_password, + .clear = _clear, + .destroy = _destroy, + }, + .creds = mem_cred_create(), + .lock = rwlock_create(RWLOCK_TYPE_DEFAULT), + ); + + return &this->public; +} diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_creds.h b/src/frontends/android/jni/libandroidbridge/backend/android_creds.h new file mode 100644 index 000000000..33de838c1 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/backend/android_creds.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup android_creds android_creds + * @{ @ingroup android_backend + */ + +#ifndef ANDROID_CREDS_H_ +#define ANDROID_CREDS_H_ + +#include <library.h> +#include <credentials/credential_set.h> + +typedef struct android_creds_t android_creds_t; + +/** + * Android credential set that provides CA certificates via JNI and supplied + * user credentials. + */ +struct android_creds_t { + + /** + * Implements credential_set_t + */ + credential_set_t set; + + /** + * Add user name and password for EAP authentication + * + * @param username user name + * @param password password + */ + void (*add_username_password)(android_creds_t *this, char *username, + char *password); + + /** + * Clear the cached certificates and stored credentials. + */ + void (*clear)(android_creds_t *this); + + /** + * Destroy a android_creds instance. + */ + void (*destroy)(android_creds_t *this); + +}; + +/** + * Create an android_creds instance. + */ +android_creds_t *android_creds_create(); + +#endif /** ANDROID_CREDS_H_ @}*/ + diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_service.c b/src/frontends/android/jni/libandroidbridge/backend/android_service.c new file mode 100644 index 000000000..dfc0d2342 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/backend/android_service.c @@ -0,0 +1,533 @@ +/* + * Copyright (C) 2010-2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +#include <errno.h> +#include <unistd.h> + +#include "android_service.h" +#include "../charonservice.h" +#include "../vpnservice_builder.h" + +#include <daemon.h> +#include <library.h> +#include <ipsec.h> +#include <processing/jobs/callback_job.h> +#include <threading/rwlock.h> +#include <threading/thread.h> + +typedef struct private_android_service_t private_android_service_t; + +#define TUN_DEFAULT_MTU 1400 + +/** + * private data of Android service + */ +struct private_android_service_t { + + /** + * public interface + */ + android_service_t public; + + /** + * current IKE_SA + */ + ike_sa_t *ike_sa; + + /** + * local ipv4 address + */ + char *local_address; + + /** + * gateway + */ + char *gateway; + + /** + * username + */ + char *username; + + /** + * lock to safely access the TUN device fd + */ + rwlock_t *lock; + + /** + * TUN device file descriptor + */ + int tunfd; + +}; + +/** + * Outbound callback + */ +static void send_esp(void *data, esp_packet_t *packet) +{ + charon->sender->send_no_marker(charon->sender, (packet_t*)packet); +} + +/** + * Inbound callback + */ +static void deliver_plain(private_android_service_t *this, + ip_packet_t *packet) +{ + chunk_t encoding; + ssize_t len; + + encoding = packet->get_encoding(packet); + + this->lock->read_lock(this->lock); + if (this->tunfd < 0) + { /* the TUN device is already closed */ + this->lock->unlock(this->lock); + packet->destroy(packet); + return; + } + len = write(this->tunfd, encoding.ptr, encoding.len); + this->lock->unlock(this->lock); + + if (len < 0 || len != encoding.len) + { + DBG1(DBG_DMN, "failed to write packet to TUN device: %s", + strerror(errno)); + } + packet->destroy(packet); +} + +/** + * Receiver callback + */ +static void receiver_esp_cb(void *data, packet_t *packet) +{ + esp_packet_t *esp_packet; + + esp_packet = esp_packet_create_from_packet(packet); + ipsec->processor->queue_inbound(ipsec->processor, esp_packet); +} + +/** + * Job handling outbound plaintext packets + */ +static job_requeue_t handle_plain(private_android_service_t *this) +{ + ip_packet_t *packet; + chunk_t raw; + fd_set set; + ssize_t len; + int tunfd; + bool old; + + FD_ZERO(&set); + + this->lock->read_lock(this->lock); + if (this->tunfd < 0) + { /* the TUN device is already closed */ + this->lock->unlock(this->lock); + return JOB_REQUEUE_NONE; + } + tunfd = this->tunfd; + FD_SET(tunfd, &set); + this->lock->unlock(this->lock); + + old = thread_cancelability(TRUE); + len = select(tunfd + 1, &set, NULL, NULL, NULL); + thread_cancelability(old); + + if (len < 0) + { + DBG1(DBG_DMN, "select on TUN device failed: %s", strerror(errno)); + return JOB_REQUEUE_NONE; + } + + raw = chunk_alloc(TUN_DEFAULT_MTU); + len = read(tunfd, raw.ptr, raw.len); + if (len < 0) + { + DBG1(DBG_DMN, "reading from TUN device failed: %s", strerror(errno)); + chunk_free(&raw); + return JOB_REQUEUE_FAIR; + } + raw.len = len; + + packet = ip_packet_create(raw); + if (packet) + { + ipsec->processor->queue_outbound(ipsec->processor, packet); + } + else + { + DBG1(DBG_DMN, "invalid IP packet read from TUN device"); + } + return JOB_REQUEUE_DIRECT; +} + +/** + * Add a route to the TUN device builder + */ +static bool add_route(vpnservice_builder_t *builder, host_t *net, + u_int8_t prefix) +{ + /* if route is 0.0.0.0/0, split it into two routes 0.0.0.0/1 and + * 128.0.0.0/1 because otherwise it would conflict with the current default + * route */ + if (net->is_anyaddr(net) && prefix == 0) + { + bool success; + + success = add_route(builder, net, 1); + net = host_create_from_string("128.0.0.0", 0); + success = success && add_route(builder, net, 1); + net->destroy(net); + return success; + } + return builder->add_route(builder, net, prefix); +} + +/** + * Generate and set routes from installed IPsec policies + */ +static bool add_routes(vpnservice_builder_t *builder, child_sa_t *child_sa) +{ + traffic_selector_t *src_ts, *dst_ts; + enumerator_t *enumerator; + bool success = TRUE; + + enumerator = child_sa->create_policy_enumerator(child_sa); + while (success && enumerator->enumerate(enumerator, &src_ts, &dst_ts)) + { + host_t *net; + u_int8_t prefix; + + dst_ts->to_subnet(dst_ts, &net, &prefix); + success = add_route(builder, net, prefix); + net->destroy(net); + } + enumerator->destroy(enumerator); + return success; +} + +/** + * Setup a new TUN device for the supplied SAs, also queues a job that + * reads packets from this device. + * Additional information such as DNS servers are gathered in appropriate + * listeners asynchronously. To be sure every required bit of information is + * available this should be called after the CHILD_SA has been established. + */ +static bool setup_tun_device(private_android_service_t *this, + ike_sa_t *ike_sa, child_sa_t *child_sa) +{ + vpnservice_builder_t *builder; + host_t *vip; + int tunfd; + + DBG1(DBG_DMN, "setting up TUN device for CHILD_SA %s{%u}", + child_sa->get_name(child_sa), child_sa->get_reqid(child_sa)); + vip = ike_sa->get_virtual_ip(ike_sa, TRUE); + if (!vip || vip->is_anyaddr(vip)) + { + DBG1(DBG_DMN, "setting up TUN device failed, no virtual IP found"); + return FALSE; + } + + builder = charonservice->get_vpnservice_builder(charonservice); + if (!builder->add_address(builder, vip) || + !add_routes(builder, child_sa) || + !builder->set_mtu(builder, TUN_DEFAULT_MTU)) + { + return FALSE; + } + + tunfd = builder->establish(builder); + if (tunfd == -1) + { + return FALSE; + } + + this->lock->write_lock(this->lock); + this->tunfd = tunfd; + this->lock->unlock(this->lock); + + DBG1(DBG_DMN, "successfully created TUN device"); + + charon->receiver->add_esp_cb(charon->receiver, + (receiver_esp_cb_t)receiver_esp_cb, NULL); + ipsec->processor->register_inbound(ipsec->processor, + (ipsec_inbound_cb_t)deliver_plain, this); + ipsec->processor->register_outbound(ipsec->processor, + (ipsec_outbound_cb_t)send_esp, NULL); + + lib->processor->queue_job(lib->processor, + (job_t*)callback_job_create((callback_job_cb_t)handle_plain, this, + NULL, (callback_job_cancel_t)return_false)); + return TRUE; +} + +/** + * Close the current tun device + */ +static void close_tun_device(private_android_service_t *this) +{ + int tunfd; + + this->lock->write_lock(this->lock); + if (this->tunfd < 0) + { /* already closed (or never created) */ + this->lock->unlock(this->lock); + return; + } + tunfd = this->tunfd; + this->tunfd = -1; + this->lock->unlock(this->lock); + + ipsec->processor->unregister_outbound(ipsec->processor, + (ipsec_outbound_cb_t)send_esp); + ipsec->processor->unregister_inbound(ipsec->processor, + (ipsec_inbound_cb_t)deliver_plain); + charon->receiver->del_esp_cb(charon->receiver, + (receiver_esp_cb_t)receiver_esp_cb); + close(tunfd); +} + +METHOD(listener_t, child_updown, bool, + private_android_service_t *this, ike_sa_t *ike_sa, child_sa_t *child_sa, + bool up) +{ + if (this->ike_sa == ike_sa) + { + if (up) + { + /* disable the hooks registered to catch initiation failures */ + this->public.listener.ike_updown = NULL; + this->public.listener.ike_state_change = NULL; + if (!setup_tun_device(this, ike_sa, child_sa)) + { + DBG1(DBG_DMN, "failed to setup TUN device"); + charonservice->update_status(charonservice, + CHARONSERVICE_GENERIC_ERROR); + return FALSE; + + } + charonservice->update_status(charonservice, + CHARONSERVICE_CHILD_STATE_UP); + } + else + { + close_tun_device(this); + charonservice->update_status(charonservice, + CHARONSERVICE_CHILD_STATE_DOWN); + return FALSE; + } + } + return TRUE; +} + +METHOD(listener_t, ike_updown, bool, + private_android_service_t *this, ike_sa_t *ike_sa, bool up) +{ + /* this callback is only registered during initiation, so if the IKE_SA + * goes down we assume an authentication error */ + if (this->ike_sa == ike_sa && !up) + { + charonservice->update_status(charonservice, + CHARONSERVICE_AUTH_ERROR); + return FALSE; + } + return TRUE; +} + +METHOD(listener_t, ike_state_change, bool, + private_android_service_t *this, ike_sa_t *ike_sa, ike_sa_state_t state) +{ + /* this call back is only registered during initiation */ + if (this->ike_sa == ike_sa && state == IKE_DESTROYING) + { + charonservice->update_status(charonservice, + CHARONSERVICE_UNREACHABLE_ERROR); + return FALSE; + } + return TRUE; +} + +METHOD(listener_t, alert, bool, + private_android_service_t *this, ike_sa_t *ike_sa, alert_t alert, + va_list args) +{ + if (this->ike_sa == ike_sa) + { + switch (alert) + { + case ALERT_PEER_ADDR_FAILED: + charonservice->update_status(charonservice, + CHARONSERVICE_LOOKUP_ERROR); + break; + case ALERT_PEER_AUTH_FAILED: + charonservice->update_status(charonservice, + CHARONSERVICE_PEER_AUTH_ERROR); + break; + default: + break; + } + } + return TRUE; +} + +METHOD(listener_t, ike_rekey, bool, + private_android_service_t *this, ike_sa_t *old, ike_sa_t *new) +{ + if (this->ike_sa == old) + { + this->ike_sa = new; + } + return TRUE; +} + +static job_requeue_t initiate(private_android_service_t *this) +{ + identification_t *gateway, *user; + ike_cfg_t *ike_cfg; + peer_cfg_t *peer_cfg; + child_cfg_t *child_cfg; + traffic_selector_t *ts; + ike_sa_t *ike_sa; + auth_cfg_t *auth; + lifetime_cfg_t lifetime = { + .time = { + .life = 10800, /* 3h */ + .rekey = 10200, /* 2h50min */ + .jitter = 300 /* 5min */ + } + }; + + ike_cfg = ike_cfg_create(TRUE, TRUE, this->local_address, FALSE, + charon->socket->get_port(charon->socket, FALSE), + this->gateway, FALSE, IKEV2_UDP_PORT); + ike_cfg->add_proposal(ike_cfg, proposal_create_default(PROTO_IKE)); + + peer_cfg = peer_cfg_create("android", IKEV2, ike_cfg, CERT_SEND_IF_ASKED, + UNIQUE_REPLACE, 1, /* keyingtries */ + 36000, 0, /* rekey 10h, reauth none */ + 600, 600, /* jitter, over 10min */ + TRUE, FALSE, /* mobike, aggressive */ + 0, 0, /* DPD delay, timeout */ + host_create_from_string("0.0.0.0", 0) /* virt */, + NULL, FALSE, NULL, NULL); /* pool, mediation */ + + + auth = auth_cfg_create(); + auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_EAP); + user = identification_create_from_string(this->username); + auth->add(auth, AUTH_RULE_IDENTITY, user); + peer_cfg->add_auth_cfg(peer_cfg, auth, TRUE); + auth = auth_cfg_create(); + auth->add(auth, AUTH_RULE_AUTH_CLASS, AUTH_CLASS_PUBKEY); + gateway = identification_create_from_string(this->gateway); + auth->add(auth, AUTH_RULE_IDENTITY, gateway); + peer_cfg->add_auth_cfg(peer_cfg, auth, FALSE); + + child_cfg = child_cfg_create("android", &lifetime, NULL, TRUE, MODE_TUNNEL, + ACTION_NONE, ACTION_NONE, ACTION_NONE, FALSE, + 0, 0, NULL, NULL, 0); + child_cfg->add_proposal(child_cfg, proposal_create_default(PROTO_ESP)); + ts = traffic_selector_create_dynamic(0, 0, 65535); + child_cfg->add_traffic_selector(child_cfg, TRUE, ts); + ts = traffic_selector_create_from_string(0, TS_IPV4_ADDR_RANGE, "0.0.0.0", + 0, "255.255.255.255", 65535); + child_cfg->add_traffic_selector(child_cfg, FALSE, ts); + peer_cfg->add_child_cfg(peer_cfg, child_cfg); + + /* get us an IKE_SA */ + ike_sa = charon->ike_sa_manager->checkout_by_config(charon->ike_sa_manager, + peer_cfg); + if (!ike_sa) + { + peer_cfg->destroy(peer_cfg); + charonservice->update_status(charonservice, + CHARONSERVICE_GENERIC_ERROR); + return JOB_REQUEUE_NONE; + } + if (!ike_sa->get_peer_cfg(ike_sa)) + { + ike_sa->set_peer_cfg(ike_sa, peer_cfg); + } + peer_cfg->destroy(peer_cfg); + + /* store the IKE_SA so we can track its progress */ + this->ike_sa = ike_sa; + + /* get an additional reference because initiate consumes one */ + child_cfg->get_ref(child_cfg); + if (ike_sa->initiate(ike_sa, child_cfg, 0, NULL, NULL) != SUCCESS) + { + DBG1(DBG_CFG, "failed to initiate tunnel"); + charon->ike_sa_manager->checkin_and_destroy(charon->ike_sa_manager, + ike_sa); + return JOB_REQUEUE_NONE; + } + charon->ike_sa_manager->checkin(charon->ike_sa_manager, ike_sa); + return JOB_REQUEUE_NONE; +} + +METHOD(android_service_t, destroy, void, + private_android_service_t *this) +{ + charon->bus->remove_listener(charon->bus, &this->public.listener); + /* make sure the tun device is actually closed */ + close_tun_device(this); + this->lock->destroy(this->lock); + free(this->local_address); + free(this->username); + free(this->gateway); + free(this); +} + +/** + * See header + */ +android_service_t *android_service_create(char *local_address, char *gateway, + char *username) +{ + private_android_service_t *this; + + INIT(this, + .public = { + .listener = { + .ike_rekey = _ike_rekey, + .ike_updown = _ike_updown, + .ike_state_change = _ike_state_change, + .child_updown = _child_updown, + .alert = _alert, + }, + .destroy = _destroy, + }, + .lock = rwlock_create(RWLOCK_TYPE_DEFAULT), + .local_address = local_address, + .username = username, + .gateway = gateway, + .tunfd = -1, + ); + + charon->bus->add_listener(charon->bus, &this->public.listener); + + lib->processor->queue_job(lib->processor, + (job_t*)callback_job_create((callback_job_cb_t)initiate, this, + NULL, NULL)); + return &this->public; +} diff --git a/src/frontends/android/jni/libandroidbridge/backend/android_service.h b/src/frontends/android/jni/libandroidbridge/backend/android_service.h new file mode 100644 index 000000000..a7bd8b059 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/backend/android_service.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010-2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup android_service android_service + * @{ @ingroup android_backend + */ + +#ifndef ANDROID_SERVICE_H_ +#define ANDROID_SERVICE_H_ + +#include "android_creds.h" + +#include <library.h> +#include <bus/listeners/listener.h> + +typedef struct android_service_t android_service_t; + +/** + * Service that sets up an IKE_SA/CHILD_SA and handles events + */ +struct android_service_t { + + /** + * Implements listener_t. + */ + listener_t listener; + + /** + * Destroy a android_service_t. + */ + void (*destroy)(android_service_t *this); + +}; + +/** + * Create an Android service instance. Queues a job that starts initiation of a + * new IKE SA. + * + * @param local_address local ip address + * @param gateway gateway address + * @param username user name (local identity) + */ +android_service_t *android_service_create(char *local_address, char *gateway, + char *username); + +#endif /** ANDROID_SERVICE_H_ @}*/ diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.c b/src/frontends/android/jni/libandroidbridge/charonservice.c index 424d50d24..fab99ac10 100644 --- a/src/frontends/android/jni/libandroidbridge/charonservice.c +++ b/src/frontends/android/jni/libandroidbridge/charonservice.c @@ -1,4 +1,6 @@ /* + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager * Copyright (C) 2012 Tobias Brunner * Hochschule fuer Technik Rapperswil * @@ -13,30 +15,78 @@ * for more details. */ +#include <signal.h> #include <string.h> +#include <sys/utsname.h> #include <android/log.h> -#include <jni.h> +#include <errno.h> +#include "charonservice.h" +#include "android_jni.h" +#include "backend/android_attr.h" +#include "backend/android_creds.h" +#include "backend/android_service.h" +#include "kernel/android_ipsec.h" +#include "kernel/android_net.h" + +#include <daemon.h> #include <hydra.h> #include <ipsec.h> -#include <daemon.h> #include <library.h> +#include <threading/thread.h> + +#define ANDROID_DEBUG_LEVEL 1 +#define ANDROID_RETRASNMIT_TRIES 3 +#define ANDROID_RETRANSMIT_TIMEOUT 3.0 +#define ANDROID_RETRANSMIT_BASE 1.4 + +typedef struct private_charonservice_t private_charonservice_t; + +/** + * private data of charonservice + */ +struct private_charonservice_t { -#define JNI_PACKAGE org_strongswan_android + /** + * public interface + */ + charonservice_t public; -#define JNI_METHOD_PP(pack, klass, name, ret, ...) \ - ret Java_##pack##_##klass##_##name(JNIEnv *env, jobject this, ##__VA_ARGS__) + /** + * android_attr instance + */ + android_attr_t *attr; -#define JNI_METHOD_P(pack, klass, name, ret, ...) \ - JNI_METHOD_PP(pack, klass, name, ret, ##__VA_ARGS__) + /** + * android_creds instance + */ + android_creds_t *creds; -#define JNI_METHOD(klass, name, ret, ...) \ - JNI_METHOD_P(JNI_PACKAGE, klass, name, ret, ##__VA_ARGS__) + /** + * android_service instance + */ + android_service_t *service; + + /** + * VpnService builder (accessed via JNI) + */ + vpnservice_builder_t *builder; + + /** + * CharonVpnService reference + */ + jobject vpn_service; +}; + +/** + * Single instance of charonservice_t. + */ +charonservice_t *charonservice; /** * hook in library for debugging messages */ -extern void (*dbg) (debug_t group, level_t level, char *fmt, ...); +extern void (*dbg)(debug_t group, level_t level, char *fmt, ...); /** * Logging hook for library logs, using android specific logging @@ -45,10 +95,11 @@ static void dbg_android(debug_t group, level_t level, char *fmt, ...) { va_list args; - if (level <= 4) + if (level <= ANDROID_DEBUG_LEVEL) { char sgroup[16], buffer[8192]; char *current = buffer, *next; + snprintf(sgroup, sizeof(sgroup), "%N", debug_names, group); va_start(args, fmt); vsnprintf(buffer, sizeof(buffer), fmt, args); @@ -68,10 +119,276 @@ static void dbg_android(debug_t group, level_t level, char *fmt, ...) } /** + * Initialize file logger + */ +static void initialize_logger(char *logfile) +{ + file_logger_t *file_logger; + debug_t group; + FILE *file; + + /* truncate an existing file */ + file = fopen(logfile, "w"); + if (!file) + { + DBG1(DBG_DMN, "opening file %s for logging failed: %s", + logfile, strerror(errno)); + return; + } + /* flush each line */ + setlinebuf(file); + + file_logger = file_logger_create(file, "%b %e %T", FALSE); + for (group = 0; group < DBG_MAX; group++) + { + file_logger->set_level(file_logger, group, ANDROID_DEBUG_LEVEL); + } + charon->file_loggers->insert_last(charon->file_loggers, file_logger); + charon->bus->add_logger(charon->bus, &file_logger->logger); +} + +METHOD(charonservice_t, update_status, bool, + private_charonservice_t *this, android_vpn_state_t code) +{ + JNIEnv *env; + jmethodID method_id; + bool success = FALSE; + + androidjni_attach_thread(&env); + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_class, + "updateStatus", "(I)V"); + if (!method_id) + { + goto failed; + } + (*env)->CallVoidMethod(env, this->vpn_service, method_id, (jint)code); + success = !androidjni_exception_occurred(env); + +failed: + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return success; +} + +METHOD(charonservice_t, bypass_socket, bool, + private_charonservice_t *this, int fd, int family) +{ + JNIEnv *env; + jmethodID method_id; + + androidjni_attach_thread(&env); + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_class, + "protect", "(I)Z"); + if (!method_id) + { + goto failed; + } + if (!(*env)->CallBooleanMethod(env, this->vpn_service, method_id, fd)) + { + DBG1(DBG_CFG, "VpnService.protect() failed"); + goto failed; + } + androidjni_detach_thread(); + return TRUE; + +failed: + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return FALSE; +} + +METHOD(charonservice_t, get_trusted_certificates, linked_list_t*, + private_charonservice_t *this) +{ + JNIEnv *env; + jmethodID method_id; + jobjectArray jcerts; + linked_list_t *list; + jsize i; + + androidjni_attach_thread(&env); + + method_id = (*env)->GetMethodID(env, + android_charonvpnservice_class, + "getTrustedCertificates", "(Ljava/lang/String;)[[B"); + if (!method_id) + { + goto failed; + } + jcerts = (*env)->CallObjectMethod(env, this->vpn_service, method_id, NULL); + if (!jcerts) + { + goto failed; + } + list = linked_list_create(); + for (i = 0; i < (*env)->GetArrayLength(env, jcerts); ++i) + { + chunk_t *ca_cert; + jbyteArray jcert; + + ca_cert = malloc_thing(chunk_t); + list->insert_last(list, ca_cert); + + jcert = (*env)->GetObjectArrayElement(env, jcerts, i); + *ca_cert = chunk_alloc((*env)->GetArrayLength(env, jcert)); + (*env)->GetByteArrayRegion(env, jcert, 0, ca_cert->len, ca_cert->ptr); + (*env)->DeleteLocalRef(env, jcert); + } + (*env)->DeleteLocalRef(env, jcerts); + androidjni_detach_thread(); + return list; + +failed: + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return NULL; +} + +METHOD(charonservice_t, get_vpnservice_builder, vpnservice_builder_t*, + private_charonservice_t *this) +{ + return this->builder; +} + +/** + * Initiate a new connection + * + * @param local local ip address (gets owned) + * @param gateway gateway address (gets owned) + * @param username username (gets owned) + * @param password password (gets owned) + */ +static void initiate(char *local, char *gateway, char *username, char *password) +{ + private_charonservice_t *this = (private_charonservice_t*)charonservice; + + this->creds->clear(this->creds); + this->creds->add_username_password(this->creds, username, password); + memwipe(password, strlen(password)); + free(password); + + DESTROY_IF(this->service); + this->service = android_service_create(local, gateway, username); +} + +/** + * Initialize/deinitialize Android backend + */ +static bool charonservice_register(void *plugin, plugin_feature_t *feature, + bool reg, void *data) +{ + private_charonservice_t *this = (private_charonservice_t*)charonservice; + if (reg) + { + lib->credmgr->add_set(lib->credmgr, &this->creds->set); + hydra->attributes->add_handler(hydra->attributes, + &this->attr->handler); + } + else + { + lib->credmgr->remove_set(lib->credmgr, &this->creds->set); + hydra->attributes->remove_handler(hydra->attributes, + &this->attr->handler); + if (this->service) + { + this->service->destroy(this->service); + this->service = NULL; + } + } + return TRUE; +} + +/** + * Initialize the charonservice object + */ +static void charonservice_init(JNIEnv *env, jobject service, jobject builder) +{ + private_charonservice_t *this; + static plugin_feature_t features[] = { + PLUGIN_CALLBACK(kernel_net_register, kernel_android_net_create), + PLUGIN_PROVIDE(CUSTOM, "kernel-net"), + PLUGIN_CALLBACK(kernel_ipsec_register, kernel_android_ipsec_create), + PLUGIN_PROVIDE(CUSTOM, "kernel-ipsec"), + PLUGIN_CALLBACK((plugin_feature_callback_t)charonservice_register, NULL), + PLUGIN_PROVIDE(CUSTOM, "Android backend"), + PLUGIN_DEPENDS(CUSTOM, "libcharon"), + }; + + INIT(this, + .public = { + .update_status = _update_status, + .bypass_socket = _bypass_socket, + .get_trusted_certificates = _get_trusted_certificates, + .get_vpnservice_builder = _get_vpnservice_builder, + }, + .attr = android_attr_create(), + .creds = android_creds_create(), + .builder = vpnservice_builder_create(builder), + .vpn_service = (*env)->NewGlobalRef(env, service), + ); + charonservice = &this->public; + + lib->plugins->add_static_features(lib->plugins, "androidbridge", features, + countof(features), TRUE); + + lib->settings->set_int(lib->settings, + "charon.plugins.android_log.loglevel", ANDROID_DEBUG_LEVEL); + lib->settings->set_int(lib->settings, + "charon.retransmit_tries", ANDROID_RETRASNMIT_TRIES); + lib->settings->set_double(lib->settings, + "charon.retransmit_timeout", ANDROID_RETRANSMIT_TIMEOUT); + lib->settings->set_double(lib->settings, + "charon.retransmit_base", ANDROID_RETRANSMIT_BASE); + lib->settings->set_bool(lib->settings, + "charon.close_ike_on_child_failure", TRUE); + /* setting the source address breaks the VpnService.protect() function which + * uses SO_BINDTODEVICE internally. the addresses provided to the kernel as + * auxiliary data have precedence over this option causing a routing loop if + * the gateway is contained in the VPN routes. alternatively, providing an + * explicit device (in addition or instead of the source address) in the + * auxiliary data would also work, but we currently don't have that + * information */ + lib->settings->set_bool(lib->settings, + "charon.plugins.socket-default.set_source", FALSE); +} + +/** + * Deinitialize the charonservice object + */ +static void charonservice_deinit(JNIEnv *env) +{ + private_charonservice_t *this = (private_charonservice_t*)charonservice; + + this->builder->destroy(this->builder); + this->creds->destroy(this->creds); + this->attr->destroy(this->attr); + (*env)->DeleteGlobalRef(env, this->vpn_service); + free(this); + charonservice = NULL; +} + +/** + * Handle SIGSEGV/SIGILL signals raised by threads + */ +static void segv_handler(int signal) +{ + dbg_android(DBG_DMN, 1, "thread %u received %d", thread_current_id(), + signal); + exit(1); +} + +/** * Initialize charon and the libraries via JNI */ -JNI_METHOD(CharonVpnService, initializeCharon, void) +JNI_METHOD(CharonVpnService, initializeCharon, void, + jobject builder, jstring jlogfile) { + struct sigaction action; + struct utsname utsname; + char *logfile; + /* logging for library during initialization, as we have no bus yet */ dbg = dbg_android; @@ -97,28 +414,78 @@ JNI_METHOD(CharonVpnService, initializeCharon, void) return; } - if (!libcharon_init("charon") || - !charon->initialize(charon, PLUGINS)) + if (!libcharon_init("charon")) + { + libcharon_deinit(); + libipsec_deinit(); + libhydra_deinit(); + library_deinit(); + return; + } + + logfile = androidjni_convert_jstring(env, jlogfile); + initialize_logger(logfile); + free(logfile); + + charonservice_init(env, this, builder); + + if (uname(&utsname) != 0) + { + memset(&utsname, 0, sizeof(utsname)); + } + DBG1(DBG_DMN, "Starting IKE charon daemon (strongSwan "VERSION", %s %s, %s)", + utsname.sysname, utsname.release, utsname.machine); + + if (!charon->initialize(charon, PLUGINS)) { libcharon_deinit(); + charonservice_deinit(env); libipsec_deinit(); libhydra_deinit(); library_deinit(); return; } + /* add handler for SEGV and ILL etc. */ + action.sa_handler = segv_handler; + action.sa_flags = 0; + sigemptyset(&action.sa_mask); + sigaction(SIGSEGV, &action, NULL); + sigaction(SIGILL, &action, NULL); + sigaction(SIGBUS, &action, NULL); + action.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &action, NULL); + /* start daemon (i.e. the threads in the thread-pool) */ charon->start(charon); } /** - * Initialize charon and the libraries via JNI + * Deinitialize charon and all libraries */ JNI_METHOD(CharonVpnService, deinitializeCharon, void) { + /* deinitialize charon before we destroy our own objects */ libcharon_deinit(); + charonservice_deinit(env); libipsec_deinit(); libhydra_deinit(); library_deinit(); } +/** + * Initiate SA + */ +JNI_METHOD(CharonVpnService, initiate, void, + jstring jlocal_address, jstring jgateway, jstring jusername, + jstring jpassword) +{ + char *local_address, *gateway, *username, *password; + + local_address = androidjni_convert_jstring(env, jlocal_address); + gateway = androidjni_convert_jstring(env, jgateway); + username = androidjni_convert_jstring(env, jusername); + password = androidjni_convert_jstring(env, jpassword); + + initiate(local_address, gateway, username, password); +} diff --git a/src/frontends/android/jni/libandroidbridge/charonservice.h b/src/frontends/android/jni/libandroidbridge/charonservice.h new file mode 100644 index 000000000..706eaa220 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/charonservice.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup libandroidbridge libandroidbridge + * + * @defgroup android_backend backend + * @ingroup libandroidbridge + * + * @defgroup android_kernel kernel + * @ingroup libandroidbridge + * + * @defgroup charonservice charonservice + * @{ @ingroup libandroidbridge + */ + +#ifndef CHARONSERVICE_H_ +#define CHARONSERVICE_H_ + +#include "vpnservice_builder.h" + +#include <library.h> +#include <utils/linked_list.h> + +typedef enum android_vpn_state_t android_vpn_state_t; +typedef struct charonservice_t charonservice_t; + +/** + * VPN status codes. As defined in CharonVpnService.java + */ +enum android_vpn_state_t { + CHARONSERVICE_CHILD_STATE_UP = 1, + CHARONSERVICE_CHILD_STATE_DOWN, + CHARONSERVICE_AUTH_ERROR, + CHARONSERVICE_PEER_AUTH_ERROR, + CHARONSERVICE_LOOKUP_ERROR, + CHARONSERVICE_UNREACHABLE_ERROR, + CHARONSERVICE_GENERIC_ERROR, +}; + +/** + * Public interface of charonservice. + * + * Used to communicate with CharonVpnService via JNI + */ +struct charonservice_t { + + /** + * Update the status in the Java domain (UI) + * + * @param code status code + * @return TRUE on success + */ + bool (*update_status)(charonservice_t *this, android_vpn_state_t code); + + /** + * Install a bypass policy for the given socket using the protect() Method + * of the Android VpnService interface + * + * @param fd socket file descriptor + * @param family socket protocol family + * @return TRUE if operation successful + */ + bool (*bypass_socket)(charonservice_t *this, int fd, int family); + + /** + * Get a list of trusted certificates via JNI + * + * @return list of DER encoded certificates (as chunk_t*), + * NULL on failure + */ + linked_list_t *(*get_trusted_certificates)(charonservice_t *this); + + /** + * Get the current vpnservice_builder_t object + * + * @return VpnService.Builder instance + */ + vpnservice_builder_t *(*get_vpnservice_builder)(charonservice_t *this); + +}; + +/** + * The single instance of charonservice_t. + * + * Set between JNI calls to initializeCharon() and deinitializeCharon(). + */ +extern charonservice_t *charonservice; + +#endif /** CHARONSERVICE_H_ @}*/ diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c new file mode 100644 index 000000000..08cc61610 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.c @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. * + * 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. + */ + +#include "android_ipsec.h" +#include "../charonservice.h" + +#include <debug.h> +#include <library.h> +#include <hydra.h> +#include <ipsec.h> + +typedef struct private_kernel_android_ipsec_t private_kernel_android_ipsec_t; + +struct private_kernel_android_ipsec_t { + + /** + * Public kernel interface + */ + kernel_android_ipsec_t public; + + /** + * Listener for lifetime expire events + */ + ipsec_event_listener_t ipsec_listener; +}; + +/** + * Callback registrered with libipsec. + */ +void expire(u_int32_t reqid, u_int8_t protocol, u_int32_t spi, bool hard) +{ + hydra->kernel_interface->expire(hydra->kernel_interface, reqid, protocol, + spi, hard); +} + +METHOD(kernel_ipsec_t, get_spi, status_t, + private_kernel_android_ipsec_t *this, host_t *src, host_t *dst, + u_int8_t protocol, u_int32_t reqid, u_int32_t *spi) +{ + return ipsec->sas->get_spi(ipsec->sas, src, dst, protocol, reqid, spi); +} + +METHOD(kernel_ipsec_t, get_cpi, status_t, + private_kernel_android_ipsec_t *this, host_t *src, host_t *dst, + u_int32_t reqid, u_int16_t *cpi) +{ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, add_sa, status_t, + private_kernel_android_ipsec_t *this, host_t *src, host_t *dst, + u_int32_t spi, u_int8_t protocol, u_int32_t reqid, mark_t mark, + u_int32_t tfc, lifetime_cfg_t *lifetime, u_int16_t enc_alg, chunk_t enc_key, + u_int16_t int_alg, chunk_t int_key, ipsec_mode_t mode, u_int16_t ipcomp, + u_int16_t cpi, bool encap, bool esn, bool inbound, + traffic_selector_t *src_ts, traffic_selector_t *dst_ts) +{ + return ipsec->sas->add_sa(ipsec->sas, src, dst, spi, protocol, reqid, mark, + tfc, lifetime, enc_alg, enc_key, int_alg, int_key, + mode, ipcomp, cpi, encap, esn, inbound, src_ts, + dst_ts); +} + +METHOD(kernel_ipsec_t, update_sa, status_t, + private_kernel_android_ipsec_t *this, u_int32_t spi, u_int8_t protocol, + u_int16_t cpi, host_t *src, host_t *dst, host_t *new_src, host_t *new_dst, + bool encap, bool new_encap, mark_t mark) +{ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, query_sa, status_t, + private_kernel_android_ipsec_t *this, host_t *src, host_t *dst, + u_int32_t spi, u_int8_t protocol, mark_t mark, u_int64_t *bytes) +{ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, del_sa, status_t, + private_kernel_android_ipsec_t *this, host_t *src, host_t *dst, + u_int32_t spi, u_int8_t protocol, u_int16_t cpi, mark_t mark) +{ + return ipsec->sas->del_sa(ipsec->sas, src, dst, spi, protocol, cpi, mark); +} + +METHOD(kernel_ipsec_t, flush_sas, status_t, + private_kernel_android_ipsec_t *this) +{ + return ipsec->sas->flush_sas(ipsec->sas); +} + +METHOD(kernel_ipsec_t, add_policy, status_t, + private_kernel_android_ipsec_t *this, host_t *src, host_t *dst, + traffic_selector_t *src_ts, traffic_selector_t *dst_ts, + policy_dir_t direction, policy_type_t type, ipsec_sa_cfg_t *sa, mark_t mark, + policy_priority_t priority) +{ + return ipsec->policies->add_policy(ipsec->policies, src, dst, src_ts, + dst_ts, direction, type, sa, mark, + priority); +} + +METHOD(kernel_ipsec_t, query_policy, status_t, + private_kernel_android_ipsec_t *this, traffic_selector_t *src_ts, + traffic_selector_t *dst_ts, policy_dir_t direction, mark_t mark, + u_int32_t *use_time) +{ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, del_policy, status_t, + private_kernel_android_ipsec_t *this, traffic_selector_t *src_ts, + traffic_selector_t *dst_ts, policy_dir_t direction, u_int32_t reqid, + mark_t mark, policy_priority_t priority) +{ + return ipsec->policies->del_policy(ipsec->policies, src_ts, dst_ts, + direction, reqid, mark, priority); +} + +METHOD(kernel_ipsec_t, flush_policies, status_t, + private_kernel_android_ipsec_t *this) +{ + ipsec->policies->flush_policies(ipsec->policies); + return SUCCESS; +} + +METHOD(kernel_ipsec_t, bypass_socket, bool, + private_kernel_android_ipsec_t *this, int fd, int family) +{ + return charonservice->bypass_socket(charonservice, fd, family); +} + +METHOD(kernel_ipsec_t, enable_udp_decap, bool, + private_kernel_android_ipsec_t *this, int fd, int family, u_int16_t port) +{ + return NOT_SUPPORTED; +} + +METHOD(kernel_ipsec_t, destroy, void, + private_kernel_android_ipsec_t *this) +{ + ipsec->events->unregister_listener(ipsec->events, &this->ipsec_listener); + free(this); +} + +/* + * Described in header. + */ +kernel_android_ipsec_t *kernel_android_ipsec_create() +{ + private_kernel_android_ipsec_t *this; + + INIT(this, + .public = { + .interface = { + .get_spi = _get_spi, + .get_cpi = _get_cpi, + .add_sa = _add_sa, + .update_sa = _update_sa, + .query_sa = _query_sa, + .del_sa = _del_sa, + .flush_sas = _flush_sas, + .add_policy = _add_policy, + .query_policy = _query_policy, + .del_policy = _del_policy, + .flush_policies = _flush_policies, + .bypass_socket = _bypass_socket, + .enable_udp_decap = _enable_udp_decap, + .destroy = _destroy, + }, + }, + .ipsec_listener = { + .expire = expire, + }, + ); + + ipsec->events->register_listener(ipsec->events, &this->ipsec_listener); + + return &this->public; +} diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h new file mode 100644 index 000000000..3a2e8343f --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/kernel/android_ipsec.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup kernel_android_ipsec kernel_android_ipsec + * @{ @ingroup kernel_android + */ + +#ifndef KERNEL_ANDROID_IPSEC_H_ +#define KERNEL_ANDROID_IPSEC_H_ + +#include <library.h> +#include <kernel/kernel_ipsec.h> + +typedef struct kernel_android_ipsec_t kernel_android_ipsec_t; + +/** + * Implementation of the ipsec interface using libipsec on Android + */ +struct kernel_android_ipsec_t { + + /** + * Implements kernel_ipsec_t interface + */ + kernel_ipsec_t interface; +}; + +/** + * Create a android ipsec interface instance. + * + * @return kernel_android_ipsec_t instance + */ +kernel_android_ipsec_t *kernel_android_ipsec_create(); + +#endif /** KERNEL_ANDROID_IPSEC_H_ @}*/ diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_net.c b/src/frontends/android/jni/libandroidbridge/kernel/android_net.c new file mode 100644 index 000000000..e29f95510 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/kernel/android_net.c @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. * + * 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. + */ + +#include "android_net.h" + +typedef struct private_kernel_android_net_t private_kernel_android_net_t; + +struct private_kernel_android_net_t { + + /** + * Public kernel interface + */ + kernel_android_net_t public; +}; + +METHOD(kernel_net_t, add_ip, status_t, + private_kernel_android_net_t *this, host_t *virtual_ip, host_t *iface_ip) +{ + /* we get the IP from the IKE_SA once the CHILD_SA is established */ + return SUCCESS; +} + +METHOD(kernel_net_t, destroy, void, + private_kernel_android_net_t *this) +{ + free(this); +} + +/* + * Described in header. + */ +kernel_android_net_t *kernel_android_net_create() +{ + private_kernel_android_net_t *this; + + INIT(this, + .public = { + .interface = { + .get_source_addr = (void*)return_null, + .get_nexthop = (void*)return_null, + .get_interface = (void*)return_null, + .create_address_enumerator = (void*)enumerator_create_empty, + .add_ip = _add_ip, + .del_ip = (void*)return_failed, + .add_route = (void*)return_failed, + .del_route = (void*)return_failed, + .destroy = _destroy, + }, + }, + ); + + return &this->public; +}; diff --git a/src/frontends/android/jni/libandroidbridge/kernel/android_net.h b/src/frontends/android/jni/libandroidbridge/kernel/android_net.h new file mode 100644 index 000000000..470029fad --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/kernel/android_net.h @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup kernel_android_net kernel_android_net + * @{ @ingroup kernel_android + */ + +#ifndef KERNEL_ANDROID_NET_H_ +#define KERNEL_ANDROID_NET_H_ + +#include <library.h> +#include <kernel/kernel_net.h> + +typedef struct kernel_android_net_t kernel_android_net_t; + +/** + * Implementation of the kernel-net interface. This currently consists of only + * noops because a kernel_net_t implementation is required and we can't use + * kernel_netlink_net_t at the moment. + */ +struct kernel_android_net_t { + + /** + * Implements kernel_net_t interface + */ + kernel_net_t interface; +}; + +/** + * Create a android net interface instance. + * + * @return kernel_android_net_t instance + */ +kernel_android_net_t *kernel_android_net_create(); + +#endif /** KERNEL_ANDROID_NET_H_ @}*/ diff --git a/src/frontends/android/jni/libandroidbridge/vpnservice_builder.c b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.c new file mode 100644 index 000000000..6ff732520 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.c @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +#include "vpnservice_builder.h" +#include "android_jni.h" + +#include <debug.h> +#include <library.h> + +typedef struct private_vpnservice_builder_t private_vpnservice_builder_t; + +/** + * private data of vpnservice_builder + */ +struct private_vpnservice_builder_t { + + /** + * public interface + */ + vpnservice_builder_t public; + + /** + * Java object + */ + jobject builder; +}; + +METHOD(vpnservice_builder_t, add_address, bool, + private_vpnservice_builder_t *this, host_t *addr) +{ + JNIEnv *env; + jmethodID method_id; + jstring str; + char buf[INET_ADDRSTRLEN]; + + androidjni_attach_thread(&env); + + DBG2(DBG_LIB, "builder: adding interface address %H", addr); + + if (addr->get_family(addr) != AF_INET) + { + goto failed; + } + if (snprintf(buf, sizeof(buf), "%H", addr) >= sizeof(buf)) + { + goto failed; + } + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class, + "addAddress", "(Ljava/lang/String;I)Z"); + if (!method_id) + { + goto failed; + } + str = (*env)->NewStringUTF(env, buf); + if (!str) + { + goto failed; + } + if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str, 32)) + { + goto failed; + } + androidjni_detach_thread(); + return TRUE; + +failed: + DBG1(DBG_LIB, "builder: failed to add address"); + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return FALSE; +} + +METHOD(vpnservice_builder_t, set_mtu, bool, + private_vpnservice_builder_t *this, int mtu) +{ + JNIEnv *env; + jmethodID method_id; + + androidjni_attach_thread(&env); + + DBG2(DBG_LIB, "builder: setting MTU to %d", mtu); + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class, + "setMtu", "(I)Z"); + if (!method_id) + { + goto failed; + } + if (!(*env)->CallBooleanMethod(env, this->builder, method_id, mtu)) + { + goto failed; + } + androidjni_detach_thread(); + return TRUE; + +failed: + DBG1(DBG_LIB, "builder: failed to set MTU"); + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return FALSE; +} + +METHOD(vpnservice_builder_t, add_route, bool, + private_vpnservice_builder_t *this, host_t *net, int prefix) +{ + JNIEnv *env; + jmethodID method_id; + jstring str; + char buf[INET_ADDRSTRLEN]; + + androidjni_attach_thread(&env); + + DBG2(DBG_LIB, "builder: adding route %+H/%d", net, prefix); + + if (net->get_family(net) != AF_INET) + { + goto failed; + } + if (snprintf(buf, sizeof(buf), "%+H", net) >= sizeof(buf)) + { + goto failed; + } + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class, + "addRoute", "(Ljava/lang/String;I)Z"); + if (!method_id) + { + goto failed; + } + str = (*env)->NewStringUTF(env, buf); + if (!str) + { + goto failed; + } + if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str, prefix)) + { + goto failed; + } + androidjni_detach_thread(); + return TRUE; + +failed: + DBG1(DBG_LIB, "builder: failed to add route"); + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return FALSE; +} + +METHOD(vpnservice_builder_t, add_dns, bool, + private_vpnservice_builder_t *this, host_t *dns) +{ + JNIEnv *env; + jmethodID method_id; + jstring str; + char buf[INET_ADDRSTRLEN]; + + androidjni_attach_thread(&env); + + DBG2(DBG_LIB, "builder: adding DNS server %H", dns); + + if (dns->get_family(dns) != AF_INET) + { + goto failed; + } + if (snprintf(buf, sizeof(buf), "%H", dns) >= sizeof(buf)) + { + goto failed; + } + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class, + "addDnsServer", "(Ljava/lang/String;)Z"); + if (!method_id) + { + goto failed; + } + str = (*env)->NewStringUTF(env, buf); + if (!str) + { + goto failed; + } + if (!(*env)->CallBooleanMethod(env, this->builder, method_id, str)) + { + goto failed; + } + androidjni_detach_thread(); + return TRUE; + +failed: + DBG1(DBG_LIB, "builder: failed to add DNS server"); + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return FALSE; +} + +METHOD(vpnservice_builder_t, establish, int, + private_vpnservice_builder_t *this) +{ + JNIEnv *env; + jmethodID method_id; + int fd; + + androidjni_attach_thread(&env); + + DBG2(DBG_LIB, "builder: building TUN device"); + + method_id = (*env)->GetMethodID(env, android_charonvpnservice_builder_class, + "establish", "()I"); + if (!method_id) + { + goto failed; + } + fd = (*env)->CallIntMethod(env, this->builder, method_id); + if (fd == -1) + { + goto failed; + } + androidjni_detach_thread(); + return fd; + +failed: + DBG1(DBG_LIB, "builder: failed to build TUN device"); + androidjni_exception_occurred(env); + androidjni_detach_thread(); + return -1; +} + +METHOD(vpnservice_builder_t, destroy, void, + private_vpnservice_builder_t *this) +{ + JNIEnv *env; + + androidjni_attach_thread(&env); + (*env)->DeleteGlobalRef(env, this->builder); + androidjni_detach_thread(); + free(this); +} + +vpnservice_builder_t *vpnservice_builder_create(jobject builder) +{ + JNIEnv *env; + private_vpnservice_builder_t *this; + + INIT(this, + .public = { + .add_address = _add_address, + .add_route = _add_route, + .add_dns = _add_dns, + .set_mtu = _set_mtu, + .establish = _establish, + .destroy = _destroy, + }, + ); + + androidjni_attach_thread(&env); + this->builder = (*env)->NewGlobalRef(env, builder); + androidjni_detach_thread(); + + return &this->public; +} diff --git a/src/frontends/android/jni/libandroidbridge/vpnservice_builder.h b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.h new file mode 100644 index 000000000..82efd05f7 --- /dev/null +++ b/src/frontends/android/jni/libandroidbridge/vpnservice_builder.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +/** + * @defgroup vpnservice_builder vpnservice_builder + * @{ @ingroup libandroidbridge + */ + +#ifndef VPNSERVICE_BUILDER_H_ +#define VPNSERVICE_BUILDER_H_ + +#include <jni.h> + +#include <library.h> +#include <utils/host.h> + +typedef struct vpnservice_builder_t vpnservice_builder_t; + +/** + * VpnService.Builder, used to build a TUN device. + * + * Communicates with CharonVpnService.BuilderAdapter via JNI + */ +struct vpnservice_builder_t { + + /** + * Add an interface address + * + * @param addr the desired interface address + * @return TRUE on success + */ + bool (*add_address)(vpnservice_builder_t *this, host_t *addr); + + /** + * Add a route + * + * @param net the network address + * @param prefix_length the prefix length + * @return TRUE on success + */ + bool (*add_route)(vpnservice_builder_t *this, host_t *net, int prefix); + + /** + * Add a DNS server + * + * @param dns the address of the DNS server + * @return TRUE on success + */ + bool (*add_dns)(vpnservice_builder_t *this, host_t *dns); + + /** + * Set the MTU for the TUN device + * + * @param mtu the MTU to set + * @return TRUE on success + */ + bool (*set_mtu)(vpnservice_builder_t *this, int mtu); + + /** + * Build the TUN device + * + * @return the TUN file descriptor, -1 if failed + */ + int (*establish)(vpnservice_builder_t *this); + + /** + * Destroy a vpnservice_builder + */ + void (*destroy)(vpnservice_builder_t *this); + +}; + +/** + * Create a vpnservice_builder instance + * + * @param builder CharonVpnService.BuilderAdapter object + * @return vpnservice_builder_t instance + */ +vpnservice_builder_t *vpnservice_builder_create(jobject builder); + +#endif /** VPNSERVICE_BUILDER_H_ @}*/ diff --git a/src/frontends/android/res/drawable-hdpi/ic_launcher.png b/src/frontends/android/res/drawable-hdpi/ic_launcher.png Binary files differindex 8074c4c57..7cd1df4ee 100644 --- a/src/frontends/android/res/drawable-hdpi/ic_launcher.png +++ b/src/frontends/android/res/drawable-hdpi/ic_launcher.png diff --git a/src/frontends/android/res/drawable-ldpi/ic_launcher.png b/src/frontends/android/res/drawable-ldpi/ic_launcher.png Binary files differdeleted file mode 100644 index 1095584ec..000000000 --- a/src/frontends/android/res/drawable-ldpi/ic_launcher.png +++ /dev/null diff --git a/src/frontends/android/res/drawable-mdpi/ic_launcher.png b/src/frontends/android/res/drawable-mdpi/ic_launcher.png Binary files differindex a07c69fa5..200ee9677 100644 --- a/src/frontends/android/res/drawable-mdpi/ic_launcher.png +++ b/src/frontends/android/res/drawable-mdpi/ic_launcher.png diff --git a/src/frontends/android/res/drawable-xhdpi/ic_launcher.png b/src/frontends/android/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..2eb6db1b6 --- /dev/null +++ b/src/frontends/android/res/drawable-xhdpi/ic_launcher.png diff --git a/src/frontends/android/res/drawable/vpn_state_background.xml b/src/frontends/android/res/drawable/vpn_state_background.xml new file mode 100644 index 000000000..24f469add --- /dev/null +++ b/src/frontends/android/res/drawable/vpn_state_background.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + + <solid + android:color="#333" /> + +</shape>
\ No newline at end of file diff --git a/src/frontends/android/res/layout/log_activity.xml b/src/frontends/android/res/layout/log_activity.xml new file mode 100644 index 000000000..80fee09fb --- /dev/null +++ b/src/frontends/android/res/layout/log_activity.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <fragment + class="org.strongswan.android.ui.LogFragment" + android:id="@+id/log_frag" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</FrameLayout> diff --git a/src/frontends/android/res/layout/log_fragment.xml b/src/frontends/android/res/layout/log_fragment.xml new file mode 100644 index 000000000..c2e187a66 --- /dev/null +++ b/src/frontends/android/res/layout/log_fragment.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <org.strongswan.android.ui.LogScrollView + android:id="@+id/scroll_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="10dp" + android:scrollbarFadeDuration="0" + android:scrollbarAlwaysDrawVerticalTrack="true" > + + <TextView + android:id="@+id/log_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="9sp" + android:typeface="monospace" > + </TextView> + + </org.strongswan.android.ui.LogScrollView> + +</LinearLayout> diff --git a/src/frontends/android/res/layout/login_dialog.xml b/src/frontends/android/res/layout/login_dialog.xml new file mode 100644 index 000000000..0262af0a3 --- /dev/null +++ b/src/frontends/android/res/layout/login_dialog.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<LinearLayout + android:layout_height="match_parent" + android:layout_width="match_parent" + android:orientation="vertical" + android:padding="5dp" + xmlns:android="http://schemas.android.com/apk/res/android"> + + <TextView + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:text="@string/profile_username_label" + android:textStyle="bold" /> + + <EditText + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:id="@+id/username" + android:enabled="false" + android:inputType="none" /> + + <TextView + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:text="@string/profile_password_label" + android:textStyle="bold" /> + + <EditText + android:layout_height="wrap_content" + android:layout_width="match_parent" + android:id="@+id/password" + android:inputType="textPassword|textNoSuggestions" + android:singleLine="true" /> + +</LinearLayout> diff --git a/src/frontends/android/res/layout/main.xml b/src/frontends/android/res/layout/main.xml index bc12cd823..1c7973e20 100644 --- a/src/frontends/android/res/layout/main.xml +++ b/src/frontends/android/res/layout/main.xml @@ -1,12 +1,34 @@ <?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="fill_parent" + android:layout_width="match_parent" + android:layout_height="match_parent" android:orientation="vertical" > - <TextView - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:text="@string/hello" /> + <fragment + class="org.strongswan.android.ui.VpnStateFragment" + android:id="@+id/vpn_state_frag" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <fragment + class="org.strongswan.android.ui.VpnProfileListFragment" + android:id="@+id/profile_list_frag" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> -</LinearLayout>
\ No newline at end of file +</LinearLayout> diff --git a/src/frontends/android/res/layout/profile_detail_view.xml b/src/frontends/android/res/layout/profile_detail_view.xml new file mode 100644 index 000000000..4952ebaa5 --- /dev/null +++ b/src/frontends/android/res/layout/profile_detail_view.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="10dp" > + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="@string/profile_name_label" /> + + <EditText + android:id="@+id/name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:inputType="textNoSuggestions" + android:hint="@string/profile_name_hint" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="@string/profile_gateway_label" /> + + <EditText + android:id="@+id/gateway" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:inputType="textNoSuggestions" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="@string/profile_username_label" /> + + <EditText + android:id="@+id/username" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:inputType="textNoSuggestions" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="@string/profile_password_label" /> + + <EditText + android:id="@+id/password" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:inputType="textPassword|textNoSuggestions" + android:hint="@string/profile_password_hint" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:text="@string/profile_ca_label" /> + + <CheckBox + android:id="@+id/ca_auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/profile_ca_auto_label" /> + + <CheckBox + android:id="@+id/ca_show_all" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:text="@string/profile_ca_show_all" /> + + <Spinner + android:id="@+id/ca_spinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="5dp" /> + + </LinearLayout> + +</ScrollView>
\ No newline at end of file diff --git a/src/frontends/android/res/layout/profile_list_fragment.xml b/src/frontends/android/res/layout/profile_list_fragment.xml new file mode 100644 index 000000000..50d628bfa --- /dev/null +++ b/src/frontends/android/res/layout/profile_list_fragment.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="10dp" + android:paddingTop="10dp" + android:paddingLeft="5dp" + android:paddingRight="5dp" > + + <ListView + android:id="@+id/profile_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:dividerHeight="1dp" + android:divider="?android:attr/listDivider" + android:scrollbarAlwaysDrawVerticalTrack="true" /> + + <TextView android:id="@+id/profile_list_empty" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginLeft="15dp" + android:text="@string/no_profiles"/> + +</FrameLayout> diff --git a/src/frontends/android/res/layout/profile_list_item.xml b/src/frontends/android/res/layout/profile_list_item.xml new file mode 100644 index 000000000..f55c8357a --- /dev/null +++ b/src/frontends/android/res/layout/profile_list_item.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingBottom="6dip" + android:paddingTop="4dip" + android:background="?android:attr/activatedBackgroundIndicator" > + + <TextView + android:id="@+id/profile_item_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="15dp" + android:textAppearance="?android:attr/textAppearanceMedium" /> + + <TextView + android:id="@+id/profile_item_gateway" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:textColorSecondary" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_marginLeft="15dp" /> + + <TextView + android:id="@+id/profile_item_username" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:textColorSecondary" + android:textAppearance="?android:attr/textAppearanceSmall" + android:layout_marginLeft="15dp" /> + +</LinearLayout> diff --git a/src/frontends/android/res/layout/trusted_certificates_item.xml b/src/frontends/android/res/layout/trusted_certificates_item.xml new file mode 100644 index 000000000..48d77757d --- /dev/null +++ b/src/frontends/android/res/layout/trusted_certificates_item.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<TableLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <TextView + android:id="@+id/certificate_name" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:textSize="18sp" /> + +</TableLayout>
\ No newline at end of file diff --git a/src/frontends/android/res/layout/vpn_state_fragment.xml b/src/frontends/android/res/layout/vpn_state_fragment.xml new file mode 100644 index 000000000..6353f3289 --- /dev/null +++ b/src/frontends/android/res/layout/vpn_state_fragment.xml @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="5dp" + android:background="@drawable/vpn_state_background" + android:orientation="vertical" > + + <GridLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" + android:layout_marginTop="10dp" + android:columnCount="2" + android:rowCount="2" > + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="5dp" + android:gravity="top" + android:text="@string/state_label" + android:textColor="?android:textColorPrimary" + android:textSize="20sp" /> + + <TextView + android:id="@+id/vpn_state" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="top" + android:text="@string/state_disabled" + android:textColor="?android:textColorSecondary" + android:textSize="20sp" /> + + <TextView + android:id="@+id/vpn_profile_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="5dp" + android:gravity="top" + android:text="@string/profile_label" + android:textColor="?android:textColorPrimary" + android:textSize="20sp" + android:visibility="gone" > + </TextView> + + <TextView + android:id="@+id/vpn_profile_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="top" + android:textSize="20sp" + android:visibility="gone" > + </TextView> + </GridLayout> + + <Button + android:id="@+id/action" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:layout_marginLeft="20dp" + android:layout_marginRight="20dp" + android:text="@string/disconnect" + style="?android:attr/borderlessButtonStyle" > + </Button> + + <View + android:layout_width="match_parent" + android:layout_height="2dp" + android:background="?android:attr/listDivider" /> + +</LinearLayout> diff --git a/src/frontends/android/res/menu/log.xml b/src/frontends/android/res/menu/log.xml new file mode 100644 index 000000000..1af5bd397 --- /dev/null +++ b/src/frontends/android/res/menu/log.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <item + android:id="@+id/menu_send_log" + android:title="@string/send_log" + android:showAsAction="ifRoom|withText" /> + +</menu> diff --git a/src/frontends/android/res/menu/main.xml b/src/frontends/android/res/menu/main.xml new file mode 100644 index 000000000..4063110da --- /dev/null +++ b/src/frontends/android/res/menu/main.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/menu_reload_certs" + android:title="@string/reload_trusted_certs" + android:showAsAction="withText" /> + + <item + android:id="@+id/menu_show_log" + android:title="@string/show_log" + android:showAsAction="withText" /> + +</menu>
\ No newline at end of file diff --git a/src/frontends/android/res/menu/profile_edit.xml b/src/frontends/android/res/menu/profile_edit.xml new file mode 100644 index 000000000..e69020ed0 --- /dev/null +++ b/src/frontends/android/res/menu/profile_edit.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/menu_accept" + android:title="@string/profile_edit_save" + android:showAsAction="always|withText" /> + + <item + android:id="@+id/menu_cancel" + android:title="@string/profile_edit_cancel" + android:showAsAction="ifRoom" /> + +</menu> diff --git a/src/frontends/android/res/menu/profile_list.xml b/src/frontends/android/res/menu/profile_list.xml new file mode 100644 index 000000000..57c9a86a4 --- /dev/null +++ b/src/frontends/android/res/menu/profile_list.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item android:id="@+id/add_profile" + android:title="@string/add_profile" + android:showAsAction="always|withText" /> + +</menu> diff --git a/src/frontends/android/res/menu/profile_list_context.xml b/src/frontends/android/res/menu/profile_list_context.xml new file mode 100644 index 000000000..e674ae856 --- /dev/null +++ b/src/frontends/android/res/menu/profile_list_context.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item android:id="@+id/edit_profile" + android:title="@string/edit_profile" ></item> + + <item android:id="@+id/delete_profile" + android:title="@string/delete_profile" ></item> + +</menu>
\ No newline at end of file diff --git a/src/frontends/android/res/values-de/strings.xml b/src/frontends/android/res/values-de/strings.xml new file mode 100644 index 000000000..0e0ddd583 --- /dev/null +++ b/src/frontends/android/res/values-de/strings.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<resources> + + <!-- Application --> + <string name="app_name">strongSwan VPN Client</string> + <string name="main_activity_name">strongSwan</string> + <string name="reload_trusted_certs">CA-Zertifikate neu laden</string> + <string name="show_log">Log anzeigen</string> + + <!-- Log view --> + <string name="log_title">Log</string> + <string name="send_log">Logdatei senden</string> + <string name="empty_log">Logdatei ist leer</string> + <string name="log_mail_subject">strongSwan %1$s Logdatei</string> + + <!-- VPN profile list --> + <string name="no_profiles">Keine VPN Profile vorhanden.</string> + <string name="add_profile">Profil hinzufügen</string> + <string name="edit_profile">Bearbeiten</string> + <string name="delete_profile">Löschen</string> + <string name="select_profiles">Profile auswählen</string> + <string name="profiles_deleted">Ausgewählte Profile gelöscht</string> + <string name="no_profile_selected">Kein Profil ausgewählt</string> + <string name="one_profile_selected">Ein Profil ausgewählt</string> + <string name="x_profiles_selected">%1$d Profile ausgewählt</string> + + <!-- VPN profile details --> + <string name="profile_edit_save">Speichern</string> + <string name="profile_edit_cancel">Abbrechen</string> + <string name="profile_name_label">Profilname:</string> + <string name="profile_name_hint">(Gateway-Adresse verwenden)</string> + <string name="profile_gateway_label">Gateway:</string> + <string name="profile_username_label">Benutzername:</string> + <string name="profile_password_label">Passwort:</string> + <string name="profile_password_hint">(anfordern wenn benötigt)</string> + <string name="profile_ca_label">CA-Zertifikat:</string> + <string name="profile_ca_auto_label">Automatisch wählen</string> + <string name="profile_ca_show_all">Alle Zertifikate anzeigen</string> + <!-- Warnings/Notifications in the details view --> + <string name="alert_text_no_input_gateway">Bitte geben Sie hier die Gateway-Adresse ein</string> + <string name="alert_text_no_input_username">Bitte geben Sie hier Ihren Benutzernamen ein</string> + <string name="alert_text_nocertfound_title">Kein CA-Zertifikat ausgewählt</string> + <string name="alert_text_nocertfound">Bitte wählen Sie eines aus oder aktivieren Sie <i>Automatisch wählen</i></string> + + <!-- VPN state fragment --> + <string name="state_label">Status:</string> + <string name="profile_label">Profil:</string> + <string name="disconnect">Trennen</string> + <string name="state_connecting">Verbinden…</string> + <string name="state_connected">Verbunden</string> + <string name="state_disconnecting">Trennen…</string> + <string name="state_disabled">Kein aktives Profil</string> + <string name="state_error">Fehler</string> + + <!-- Dialogs --> + <string name="login_title">Passwort eingeben um zu verbinden</string> + <string name="login_confirm">Verbinden</string> + <string name="error_introduction">Fehler beim Aufsetzen des VPN:</string> + <string name="error_lookup_failed">Gateway-Adresse konnte nicht aufgelöst werden.</string> + <string name="error_unreachable">Gateway ist nicht erreichbar.</string> + <string name="error_peer_auth_failed">Authentifizierung des Gateway ist fehlgeschlagen.</string> + <string name="error_auth_failed">Benutzerauthentifizierung ist fehlgeschlagen.</string> + <string name="error_generic">Unbekannter Fehler während des Verbindens.</string> + <string name="connecting_title">Verbinden: %1$s</string> + <string name="connecting_message">Verbinde mit \""%1$s\".</string> + +</resources> diff --git a/src/frontends/android/res/values/colors.xml b/src/frontends/android/res/values/colors.xml new file mode 100644 index 000000000..be64d5d5a --- /dev/null +++ b/src/frontends/android/res/values/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<resources> + + <color + name="error_text">#D9192C</color> + + <color + name="success_text">#99CC00</color> + +</resources> diff --git a/src/frontends/android/res/values/strings.xml b/src/frontends/android/res/values/strings.xml index f4df7613e..a83e219a7 100644 --- a/src/frontends/android/res/values/strings.xml +++ b/src/frontends/android/res/values/strings.xml @@ -1,7 +1,83 @@ <?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Copyright (C) 2012 Giuliano Grassi + Copyright (C) 2012 Ralf Sager + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> <resources> - <string name="hello">Hello World, strongSwanActivity!</string> - <string name="app_name">strongSwan</string> + <!-- Application --> + <string name="app_name">strongSwan VPN Client</string> + <string name="main_activity_name">strongSwan</string> + <string name="reload_trusted_certs">Reload CA certificates</string> + <string name="show_log">View log</string> + + <!-- Log view --> + <string name="log_title">Log</string> + <string name="send_log">Send log file</string> + <string name="empty_log">Log file is empty</string> + <string name="log_mail_subject">strongSwan %1$s Log File</string> + + <!-- VPN profile list --> + <string name="no_profiles">No VPN profiles.</string> + <string name="add_profile">Add VPN profile</string> + <string name="edit_profile">Edit</string> + <string name="delete_profile">Delete</string> + <string name="select_profiles">Select profiles</string> + <string name="profiles_deleted">Selected profiles deleted</string> + <string name="no_profile_selected">No profile selected</string> + <string name="one_profile_selected">One profile selected</string> + <string name="x_profiles_selected">%1$d profiles selected"</string> + + <!-- VPN profile details --> + <string name="profile_edit_save">Save</string> + <string name="profile_edit_cancel">Cancel</string> + <string name="profile_name_label">Profile Name:</string> + <string name="profile_name_hint">(use gateway address)</string> + <string name="profile_gateway_label">Gateway:</string> + <string name="profile_username_label">Username:</string> + <string name="profile_password_label">Password:</string> + <string name="profile_password_hint">(prompt when needed)</string> + <string name="profile_ca_label">CA certificate:</string> + <string name="profile_ca_auto_label">Select automatically</string> + <string name="profile_ca_show_all">Show all certificates</string> + <!-- Warnings/Notifications in the details view --> + <string name="alert_text_no_input_gateway">Please enter the gateway address here</string> + <string name="alert_text_no_input_username">Please enter your username here</string> + <string name="alert_text_nocertfound_title">No CA certificate selected</string> + <string name="alert_text_nocertfound">Please select one or activate <i>Select automatically</i></string> + + <!-- VPN state fragment --> + <string name="state_label">Status:</string> + <string name="profile_label">Profile:</string> + <string name="disconnect">Disconnect</string> + <string name="state_connecting">Connecting…</string> + <string name="state_connected">Connected</string> + <string name="state_disconnecting">Disconnecting…</string> + <string name="state_disabled">No active VPN</string> + <string name="state_error">Error</string> + + <!-- Dialogs --> + <string name="login_title">Enter password to connect</string> + <string name="login_confirm">Connect</string> + <string name="error_introduction">Failed to establish VPN:</string> + <string name="error_lookup_failed">Gateway address lookup failed.</string> + <string name="error_unreachable">Gateway is unreachable.</string> + <string name="error_peer_auth_failed">Verifying gateway authentication failed.</string> + <string name="error_auth_failed">User authentication failed.</string> + <string name="error_generic">Unspecified failure while connecting.</string> + <string name="connecting_title">Connecting: %1$s</string> + <string name="connecting_message">Establishing VPN with \""%1$s\".</string> -</resources>
\ No newline at end of file +</resources> diff --git a/src/frontends/android/res/values/styles.xml b/src/frontends/android/res/values/styles.xml new file mode 100644 index 000000000..739ba7000 --- /dev/null +++ b/src/frontends/android/res/values/styles.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2012 Tobias Brunner + Hochschule fuer Technik Rapperswil + + 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 2 of the License, or (at your + option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + + 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. +--> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="ApplicationTheme" parent="@android:style/Theme.Holo"> + </style> + +</resources> diff --git a/src/frontends/android/src/org/strongswan/android/CharonVpnService.java b/src/frontends/android/src/org/strongswan/android/CharonVpnService.java deleted file mode 100644 index d917d3eae..000000000 --- a/src/frontends/android/src/org/strongswan/android/CharonVpnService.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.strongswan.android; - -import android.content.Intent; -import android.net.VpnService; - -public class CharonVpnService extends VpnService -{ - - @Override - public int onStartCommand(Intent intent, int flags, int startId) - { - // called whenever the service is started with startService - // create our own thread because we are running in the calling processes - // main thread - return super.onStartCommand(intent, flags, startId); - } - - @Override - public void onCreate() - { - // onCreate is only called once - initializeCharon(); - super.onCreate(); - } - - @Override - public void onDestroy() - { - // called once the service is to be destroyed - deinitializeCharon(); - super.onDestroy(); - } - - /** - * Initialization of charon, provided by libandroidbridge.so - */ - public native void initializeCharon(); - - /** - * Deinitialize charon, provided by libandroidbridge.so - */ - public native void deinitializeCharon(); - - /* - * The libraries are extracted to /data/data/org.strongswan.android/... - * during installation. - */ - static - { - System.loadLibrary("crypto"); - System.loadLibrary("strongswan"); - System.loadLibrary("hydra"); - System.loadLibrary("charon"); - System.loadLibrary("ipsec"); - System.loadLibrary("androidbridge"); - } -} diff --git a/src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java b/src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java new file mode 100644 index 000000000..370a8d5e4 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/data/LogContentProvider.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.data; + +import java.io.File; +import java.io.FileNotFoundException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.concurrent.ConcurrentHashMap; + +import org.strongswan.android.logic.CharonVpnService; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.provider.OpenableColumns; + +public class LogContentProvider extends ContentProvider +{ + private static final String AUTHORITY = "org.strongswan.android.content.log"; + /* an Uri is valid for 30 minutes */ + private static final long URI_VALIDITY = 30 * 60 * 1000; + private static ConcurrentHashMap<Uri, Long> mUris = new ConcurrentHashMap<Uri, Long>(); + private File mLogFile; + + public LogContentProvider() + { + } + + @Override + public boolean onCreate() + { + mLogFile = new File(getContext().getFilesDir(), CharonVpnService.LOG_FILE); + return true; + } + + /** + * The log file can only be accessed by Uris created with this method + * @return null if failed to create the Uri + */ + public static Uri createContentUri() + { + SecureRandom random; + try + { + random = SecureRandom.getInstance("SHA1PRNG"); + } + catch (NoSuchAlgorithmException e) + { + return null; + } + Uri uri = Uri.parse("content://" + AUTHORITY + "/" + random.nextLong()); + mUris.put(uri, SystemClock.uptimeMillis()); + return uri; + } + + @Override + public String getType(Uri uri) + { + /* MIME type for our log file */ + return "text/plain"; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) + { + /* this is called by apps to find out the name and size of the file. + * since we only provide a single file this is simple to implement */ + if (projection == null || projection.length < 1) + { + return null; + } + Long timestamp = mUris.get(uri); + if (timestamp == null) + { /* don't check the validity as this information is not really private */ + return null; + } + MatrixCursor cursor = new MatrixCursor(projection, 1); + if (OpenableColumns.DISPLAY_NAME.equals(cursor.getColumnName(0))) + { + cursor.newRow().add(CharonVpnService.LOG_FILE); + } + else if (OpenableColumns.SIZE.equals(cursor.getColumnName(0))) + { + cursor.newRow().add(mLogFile.length()); + } + else + { + return null; + } + return cursor; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException + { + Long timestamp = mUris.get(uri); + if (timestamp != null) + { + long elapsed = SystemClock.uptimeMillis() - timestamp; + if (elapsed > 0 && elapsed < URI_VALIDITY) + { /* we fail if clock wrapped, should happen rarely though */ + return ParcelFileDescriptor.open(mLogFile, ParcelFileDescriptor.MODE_CREATE | ParcelFileDescriptor.MODE_READ_ONLY); + } + mUris.remove(uri); + } + return super.openFile(uri, mode); + } + + @Override + public Uri insert(Uri uri, ContentValues values) + { + /* not supported */ + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) + { + /* not supported */ + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) + { + /* not supported */ + return 0; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java new file mode 100644 index 000000000..053f91555 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/data/VpnProfile.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.data; + +public class VpnProfile implements Cloneable +{ + private String mName, mGateway, mUsername, mPassword, mCertificate; + private long mId = -1; + + public long getId() + { + return mId; + } + + public void setId(long id) + { + this.mId = id; + } + + public String getName() + { + return mName; + } + + public void setName(String name) + { + this.mName = name; + } + + public String getGateway() + { + return mGateway; + } + + public void setGateway(String gateway) + { + this.mGateway = gateway; + } + + public String getUsername() + { + return mUsername; + } + + public void setUsername(String username) + { + this.mUsername = username; + } + + public String getPassword() + { + return mPassword; + } + + public void setPassword(String password) + { + this.mPassword = password; + } + + public String getCertificateAlias() + { + return mCertificate; + } + + public void setCertificateAlias(String certificate) + { + this.mCertificate = certificate; + } + + @Override + public String toString() + { + return mName; + } + + @Override + public boolean equals(Object o) + { + if (o != null && o instanceof VpnProfile) + { + return this.mId == ((VpnProfile)o).getId(); + } + return false; + } + + @Override + public VpnProfile clone() + { + try + { + return (VpnProfile)super.clone(); + } + catch (CloneNotSupportedException e) + { + throw new AssertionError(); + } + } +} diff --git a/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java new file mode 100644 index 000000000..18632ad6f --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/data/VpnProfileDataSource.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.data; + +import java.util.ArrayList; +import java.util.List; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +public class VpnProfileDataSource +{ + private static final String TAG = VpnProfileDataSource.class.getSimpleName(); + public static final String KEY_ID = "_id"; + public static final String KEY_NAME = "name"; + public static final String KEY_GATEWAY = "gateway"; + public static final String KEY_USERNAME = "username"; + public static final String KEY_PASSWORD = "password"; + public static final String KEY_CERTIFICATE = "certificate"; + + private DatabaseHelper mDbHelper; + private SQLiteDatabase mDatabase; + private final Context mContext; + + private static final String DATABASE_NAME = "strongswan.db"; + private static final String TABLE_VPNPROFILE = "vpnprofile"; + + private static final int DATABASE_VERSION = 1; + + public static final String DATABASE_CREATE = + "CREATE TABLE " + TABLE_VPNPROFILE + " (" + + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + KEY_NAME + " TEXT NOT NULL," + + KEY_GATEWAY + " TEXT NOT NULL," + + KEY_USERNAME + " TEXT NOT NULL," + + KEY_PASSWORD + " TEXT," + + KEY_CERTIFICATE + " TEXT" + + ");"; + private final String[] ALL_COLUMNS = new String[] { + KEY_ID, + KEY_NAME, + KEY_GATEWAY, + KEY_USERNAME, + KEY_PASSWORD, + KEY_CERTIFICATE + }; + + private static class DatabaseHelper extends SQLiteOpenHelper + { + public DatabaseHelper(Context context) + { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase database) + { + database.execSQL(DATABASE_CREATE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + Log.w(TAG, "Upgrading database from version " + oldVersion + + " to " + newVersion + ", which will destroy all old data"); + db.execSQL("DROP TABLE IF EXISTS " + TABLE_VPNPROFILE); + onCreate(db); + } + } + + /** + * Construct a new VPN profile data source. The context is used to + * open/create the database. + * @param context context used to access the database + */ + public VpnProfileDataSource(Context context) + { + this.mContext = context; + } + + /** + * Open the VPN profile data source. The database is automatically created + * if it does not yet exist. If that fails an exception is thrown. + * @return itself (allows to chain initialization calls) + * @throws SQLException if the database could not be opened or created + */ + public VpnProfileDataSource open() throws SQLException + { + if (mDbHelper == null) + { + mDbHelper = new DatabaseHelper(mContext); + mDatabase = mDbHelper.getWritableDatabase(); + } + return this; + } + + /** + * Close the data source. + */ + public void close() + { + if (mDbHelper != null) + { + mDbHelper.close(); + mDbHelper = null; + } + } + + /** + * Insert the given VPN profile into the database. On success the Id of + * the object is updated and the object returned. + * + * @param profile the profile to add + * @return the added VPN profile or null, if failed + */ + public VpnProfile insertProfile(VpnProfile profile) + { + ContentValues values = ContentValuesFromVpnProfile(profile); + long insertId = mDatabase.insert(TABLE_VPNPROFILE, null, values); + if (insertId == -1) + { + return null; + } + profile.setId(insertId); + return profile; + } + + /** + * Updates the given VPN profile in the database. + * @param profile the profile to update + * @return true if update succeeded, false otherwise + */ + public boolean updateVpnProfile(VpnProfile profile) + { + long id = profile.getId(); + ContentValues values = ContentValuesFromVpnProfile(profile); + return mDatabase.update(TABLE_VPNPROFILE, values, KEY_ID + " = " + id, null) > 0; + } + + /** + * Delete the given VPN profile from the database. + * @param profile the profile to delete + * @return true if deleted, false otherwise + */ + public boolean deleteVpnProfile(VpnProfile profile) + { + long id = profile.getId(); + return mDatabase.delete(TABLE_VPNPROFILE, KEY_ID + " = " + id, null) > 0; + } + + /** + * Get a single VPN profile from the database. + * @param id the ID of the VPN profile + * @return the profile or null, if not found + */ + public VpnProfile getVpnProfile(long id) + { + VpnProfile profile = null; + Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, + KEY_ID + "=" + id, null, null, null, null); + if (cursor.moveToFirst()) + { + profile = VpnProfileFromCursor(cursor); + } + cursor.close(); + return profile; + } + + /** + * Get a list of all VPN profiles stored in the database. + * @return list of VPN profiles + */ + public List<VpnProfile> getAllVpnProfiles() + { + List<VpnProfile> vpnProfiles = new ArrayList<VpnProfile>(); + + Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, null, null, null, null, null); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) + { + VpnProfile vpnProfile = VpnProfileFromCursor(cursor); + vpnProfiles.add(vpnProfile); + cursor.moveToNext(); + } + cursor.close(); + return vpnProfiles; + } + + private VpnProfile VpnProfileFromCursor(Cursor cursor) + { + VpnProfile profile = new VpnProfile(); + profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID))); + profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME))); + profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY))); + profile.setUsername(cursor.getString(cursor.getColumnIndex(KEY_USERNAME))); + profile.setPassword(cursor.getString(cursor.getColumnIndex(KEY_PASSWORD))); + profile.setCertificateAlias(cursor.getString(cursor.getColumnIndex(KEY_CERTIFICATE))); + return profile; + } + + private ContentValues ContentValuesFromVpnProfile(VpnProfile profile) + { + ContentValues values = new ContentValues(); + values.put(KEY_NAME, profile.getName()); + values.put(KEY_GATEWAY, profile.getGateway()); + values.put(KEY_USERNAME, profile.getUsername()); + values.put(KEY_PASSWORD, profile.getPassword()); + values.put(KEY_CERTIFICATE, profile.getCertificateAlias()); + return values; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java new file mode 100644 index 000000000..c9c1ad02a --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/logic/CharonVpnService.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.logic; + +import java.io.File; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Enumeration; + +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.logic.VpnStateService.ErrorState; +import org.strongswan.android.logic.VpnStateService.State; +import org.strongswan.android.ui.MainActivity; + +import android.app.PendingIntent; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.net.VpnService; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +public class CharonVpnService extends VpnService implements Runnable +{ + private static final String TAG = CharonVpnService.class.getSimpleName(); + public static final String LOG_FILE = "charon.log"; + + private String mLogFile; + private VpnProfileDataSource mDataSource; + private Thread mConnectionHandler; + private VpnProfile mCurrentProfile; + private volatile String mCurrentCertificateAlias; + private VpnProfile mNextProfile; + private volatile boolean mProfileUpdated; + private volatile boolean mTerminate; + private volatile boolean mIsDisconnecting; + private VpnStateService mService; + private final Object mServiceLock = new Object(); + private final ServiceConnection mServiceConnection = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) + { /* since the service is local this is theoretically only called when the process is terminated */ + synchronized (mServiceLock) + { + mService = null; + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + synchronized (mServiceLock) + { + mService = ((VpnStateService.LocalBinder)service).getService(); + } + /* we are now ready to start the handler thread */ + mConnectionHandler.start(); + } + }; + + /** + * as defined in charonservice.h + */ + static final int STATE_CHILD_SA_UP = 1; + static final int STATE_CHILD_SA_DOWN = 2; + static final int STATE_AUTH_ERROR = 3; + static final int STATE_PEER_AUTH_ERROR = 4; + static final int STATE_LOOKUP_ERROR = 5; + static final int STATE_UNREACHABLE_ERROR = 6; + static final int STATE_GENERIC_ERROR = 7; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + if (intent != null) + { + Bundle bundle = intent.getExtras(); + VpnProfile profile = null; + if (bundle != null) + { + profile = mDataSource.getVpnProfile(bundle.getLong(VpnProfileDataSource.KEY_ID)); + if (profile != null) + { + String password = bundle.getString(VpnProfileDataSource.KEY_PASSWORD); + profile.setPassword(password); + } + } + setNextProfile(profile); + } + return START_NOT_STICKY; + } + + @Override + public void onCreate() + { + mLogFile = getFilesDir().getAbsolutePath() + File.separator + LOG_FILE; + + mDataSource = new VpnProfileDataSource(this); + mDataSource.open(); + /* use a separate thread as main thread for charon */ + mConnectionHandler = new Thread(this); + /* the thread is started when the service is bound */ + bindService(new Intent(this, VpnStateService.class), + mServiceConnection, Service.BIND_AUTO_CREATE); + } + + @Override + public void onRevoke() + { /* the system revoked the rights grated with the initial prepare() call. + * called when the user clicks disconnect in the system's VPN dialog */ + setNextProfile(null); + } + + @Override + public void onDestroy() + { + mTerminate = true; + setNextProfile(null); + try + { + mConnectionHandler.join(); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + if (mService != null) + { + unbindService(mServiceConnection); + } + mDataSource.close(); + } + + /** + * Set the profile that is to be initiated next. Notify the handler thread. + * + * @param profile the profile to initiate + */ + private void setNextProfile(VpnProfile profile) + { + synchronized (this) + { + this.mNextProfile = profile; + mProfileUpdated = true; + notifyAll(); + } + } + + @Override + public void run() + { + while (true) + { + synchronized (this) + { + try + { + while (!mProfileUpdated) + { + wait(); + } + + mProfileUpdated = false; + stopCurrentConnection(); + if (mNextProfile == null) + { + setProfile(null); + setState(State.DISABLED); + if (mTerminate) + { + break; + } + } + else + { + mCurrentProfile = mNextProfile; + mNextProfile = null; + + /* store this in a separate (volatile) variable to avoid + * a possible deadlock during deinitialization */ + mCurrentCertificateAlias = mCurrentProfile.getCertificateAlias(); + + setProfile(mCurrentProfile); + setError(ErrorState.NO_ERROR); + setState(State.CONNECTING); + mIsDisconnecting = false; + + BuilderAdapter builder = new BuilderAdapter(mCurrentProfile.getName()); + initializeCharon(builder, mLogFile); + Log.i(TAG, "charon started"); + + String local_address = getLocalIPv4Address(); + initiate(local_address != null ? local_address : "0.0.0.0", + mCurrentProfile.getGateway(), mCurrentProfile.getUsername(), + mCurrentProfile.getPassword()); + } + } + catch (InterruptedException ex) + { + stopCurrentConnection(); + setState(State.DISABLED); + } + } + } + } + + /** + * Stop any existing connection by deinitializing charon. + */ + private void stopCurrentConnection() + { + synchronized (this) + { + if (mCurrentProfile != null) + { + setState(State.DISCONNECTING); + mIsDisconnecting = true; + deinitializeCharon(); + Log.i(TAG, "charon stopped"); + mCurrentProfile = null; + } + } + } + + /** + * Update the VPN profile on the state service. Called by the handler thread. + * + * @param profile currently active VPN profile + */ + private void setProfile(VpnProfile profile) + { + synchronized (mServiceLock) + { + if (mService != null) + { + mService.setProfile(profile); + } + } + } + + /** + * Update the current VPN state on the state service. Called by the handler + * thread and any of charon's threads. + * + * @param state current state + */ + private void setState(State state) + { + synchronized (mServiceLock) + { + if (mService != null) + { + mService.setState(state); + } + } + } + + /** + * Set an error on the state service. Called by the handler thread and any + * of charon's threads. + * + * @param error error state + */ + private void setError(ErrorState error) + { + synchronized (mServiceLock) + { + if (mService != null) + { + mService.setError(error); + } + } + } + + /** + * Set an error on the state service and disconnect the current connection. + * This is not done by calling stopCurrentConnection() above, but instead + * is done asynchronously via state service. + * + * @param error error state + */ + private void setErrorDisconnect(ErrorState error) + { + synchronized (mServiceLock) + { + if (mService != null) + { + mService.setError(error); + if (!mIsDisconnecting) + { + mService.disconnect(); + } + } + } + } + + /** + * Updates the state of the current connection. + * Called via JNI by different threads (but not concurrently). + * + * @param status new state + */ + public void updateStatus(int status) + { + switch (status) + { + case STATE_CHILD_SA_DOWN: + synchronized (mServiceLock) + { + /* if we are not actively disconnecting we assume the remote terminated + * the connection and call disconnect() to deinitialize charon properly */ + if (mService != null && !mIsDisconnecting) + { + mService.disconnect(); + } + } + break; + case STATE_CHILD_SA_UP: + setState(State.CONNECTED); + break; + case STATE_AUTH_ERROR: + setErrorDisconnect(ErrorState.AUTH_FAILED); + break; + case STATE_PEER_AUTH_ERROR: + setErrorDisconnect(ErrorState.PEER_AUTH_FAILED); + break; + case STATE_LOOKUP_ERROR: + setErrorDisconnect(ErrorState.LOOKUP_FAILED); + break; + case STATE_UNREACHABLE_ERROR: + setErrorDisconnect(ErrorState.UNREACHABLE); + break; + case STATE_GENERIC_ERROR: + setErrorDisconnect(ErrorState.GENERIC_ERROR); + break; + default: + Log.e(TAG, "Unknown status code received"); + break; + } + } + + /** + * Function called via JNI to generate a list of DER encoded CA certificates + * as byte array. + * + * @param hash optional alias (only hash part), if given matching certificates are returned + * @return a list of DER encoded CA certificates + */ + private byte[][] getTrustedCertificates(String hash) + { + ArrayList<byte[]> certs = new ArrayList<byte[]>(); + TrustedCertificateManager certman = TrustedCertificateManager.getInstance(); + try + { + if (hash != null) + { + String alias = "user:" + hash + ".0"; + X509Certificate cert = certman.getCACertificateFromAlias(alias); + if (cert == null) + { + alias = "system:" + hash + ".0"; + cert = certman.getCACertificateFromAlias(alias); + } + if (cert == null) + { + return null; + } + certs.add(cert.getEncoded()); + } + else + { + String alias = this.mCurrentCertificateAlias; + if (alias != null) + { + X509Certificate cert = certman.getCACertificateFromAlias(alias); + if (cert == null) + { + return null; + } + certs.add(cert.getEncoded()); + } + else + { + for (X509Certificate cert : certman.getAllCACertificates().values()) + { + certs.add(cert.getEncoded()); + } + } + } + } + catch (CertificateEncodingException e) + { + e.printStackTrace(); + return null; + } + return certs.toArray(new byte[certs.size()][]); + } + + /** + * Initialization of charon, provided by libandroidbridge.so + * + * @param builder BuilderAdapter for this connection + * @param logfile absolute path to the logfile + */ + public native void initializeCharon(BuilderAdapter builder, String logfile); + + /** + * Deinitialize charon, provided by libandroidbridge.so + */ + public native void deinitializeCharon(); + + /** + * Initiate VPN, provided by libandroidbridge.so + */ + public native void initiate(String local_address, String gateway, + String username, String password); + + /** + * Helper function that retrieves a local IPv4 address. + * + * @return string representation of an IPv4 address, or null if none found + */ + private static String getLocalIPv4Address() + { + try + { + Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); + while (en.hasMoreElements()) + { + NetworkInterface intf = en.nextElement(); + + Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); + while (enumIpAddr.hasMoreElements()) + { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (!inetAddress.isLoopbackAddress() && inetAddress.getAddress().length == 4) + { + return inetAddress.getHostAddress().toString(); + } + } + } + } + catch (SocketException ex) + { + ex.printStackTrace(); + return null; + } + return null; + } + + /** + * Adapter for VpnService.Builder which is used to access it safely via JNI. + * There is a corresponding C object to access it from native code. + */ + public class BuilderAdapter + { + VpnService.Builder builder; + + public BuilderAdapter(String name) + { + builder = new CharonVpnService.Builder(); + builder.setSession(name); + + /* even though the option displayed in the system dialog says "Configure" + * we just use our main Activity */ + Context context = getApplicationContext(); + Intent intent = new Intent(context, MainActivity.class); + PendingIntent pending = PendingIntent.getActivity(context, 0, intent, + Intent.FLAG_ACTIVITY_NEW_TASK); + builder.setConfigureIntent(pending); + } + + public synchronized boolean addAddress(String address, int prefixLength) + { + try + { + builder.addAddress(address, prefixLength); + } + catch (IllegalArgumentException ex) + { + return false; + } + return true; + } + + public synchronized boolean addDnsServer(String address) + { + try + { + builder.addDnsServer(address); + } + catch (IllegalArgumentException ex) + { + return false; + } + return true; + } + + public synchronized boolean addRoute(String address, int prefixLength) + { + try + { + builder.addRoute(address, prefixLength); + } + catch (IllegalArgumentException ex) + { + return false; + } + return true; + } + + public synchronized boolean addSearchDomain(String domain) + { + try + { + builder.addSearchDomain(domain); + } + catch (IllegalArgumentException ex) + { + return false; + } + return true; + } + + public synchronized boolean setMtu(int mtu) + { + try + { + builder.setMtu(mtu); + } + catch (IllegalArgumentException ex) + { + return false; + } + return true; + } + + public synchronized int establish() + { + ParcelFileDescriptor fd; + try + { + fd = builder.establish(); + } + catch (Exception ex) + { + ex.printStackTrace(); + return -1; + } + if (fd == null) + { + return -1; + } + return fd.detachFd(); + } + } + + /* + * The libraries are extracted to /data/data/org.strongswan.android/... + * during installation. + */ + static + { + System.loadLibrary("crypto"); + System.loadLibrary("strongswan"); + System.loadLibrary("hydra"); + System.loadLibrary("charon"); + System.loadLibrary("ipsec"); + System.loadLibrary("androidbridge"); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java b/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java new file mode 100644 index 000000000..74868dc44 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/logic/TrustedCertificateManager.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.logic; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import android.util.Log; + +public class TrustedCertificateManager +{ + private static final String TAG = TrustedCertificateManager.class.getSimpleName(); + private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); + private Hashtable<String, X509Certificate> mCACerts = new Hashtable<String, X509Certificate>(); + private boolean mLoaded; + + /** + * Private constructor to prevent instantiation from other classes. + */ + private TrustedCertificateManager() + { + } + + /** + * This is not instantiated until the first call to getInstance() + */ + private static class Singleton { + public static final TrustedCertificateManager mInstance = new TrustedCertificateManager(); + } + + /** + * Get the single instance of the CA certificate manager. + * @return CA certificate manager + */ + public static TrustedCertificateManager getInstance() + { + return Singleton.mInstance; + } + + /** + * Forces a load/reload of the cached CA certificates. + * As this takes a while it should be called asynchronously. + * @return reference to itself + */ + public TrustedCertificateManager reload() + { + Log.d(TAG, "Force reload of cached CA certificates"); + this.mLock.writeLock().lock(); + loadCertificates(); + this.mLock.writeLock().unlock(); + return this; + } + + /** + * Ensures that the certificates are loaded but does not force a reload. + * As this takes a while if the certificates are not loaded yet it should + * be called asynchronously. + * @return reference to itself + */ + public TrustedCertificateManager load() + { + Log.d(TAG, "Ensure cached CA certificates are loaded"); + this.mLock.writeLock().lock(); + if (!this.mLoaded) + { + loadCertificates(); + } + this.mLock.writeLock().unlock(); + return this; + } + + /** + * Opens the CA certificate KeyStore and loads the cached certificates. + * The lock must be locked when calling this method. + */ + private void loadCertificates() + { + Log.d(TAG, "Load cached CA certificates"); + try + { + KeyStore store = KeyStore.getInstance("AndroidCAStore"); + store.load(null, null); + this.mCACerts = fetchCertificates(store); + this.mLoaded = true; + Log.d(TAG, "Cached CA certificates loaded"); + } + catch (Exception ex) + { + ex.printStackTrace(); + this.mCACerts = new Hashtable<String, X509Certificate>(); + } + } + + /** + * Load all X.509 certificates from the given KeyStore. + * @param store KeyStore to load certificates from + * @return Hashtable mapping aliases to certificates + */ + private Hashtable<String, X509Certificate> fetchCertificates(KeyStore store) + { + Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>(); + try + { + Enumeration<String> aliases = store.aliases(); + while (aliases.hasMoreElements()) + { + String alias = aliases.nextElement(); + Certificate cert; + cert = store.getCertificate(alias); + if (cert != null && cert instanceof X509Certificate) + { + certs.put(alias, (X509Certificate)cert); + } + } + } + catch (KeyStoreException ex) + { + ex.printStackTrace(); + } + return certs; + } + + /** + * Retrieve the CA certificate with the given alias. + * @param alias alias of the certificate to get + * @return the certificate, null if not found + */ + public X509Certificate getCACertificateFromAlias(String alias) + { + X509Certificate certificate = null; + + if (this.mLock.readLock().tryLock()) + { + certificate = this.mCACerts.get(alias); + this.mLock.readLock().unlock(); + } + else + { /* if we cannot get the lock load it directly from the KeyStore, + * should be fast for a single certificate */ + try + { + KeyStore store = KeyStore.getInstance("AndroidCAStore"); + store.load(null, null); + Certificate cert = store.getCertificate(alias); + if (cert != null && cert instanceof X509Certificate) + { + certificate = (X509Certificate)cert; + } + } + catch (Exception e) + { + e.printStackTrace(); + } + + } + return certificate; + } + + /** + * Get all CA certificates (from the system and user keystore). + * @return Hashtable mapping aliases to certificates + */ + @SuppressWarnings("unchecked") + public Hashtable<String, X509Certificate> getAllCACertificates() + { + Hashtable<String, X509Certificate> certs; + this.mLock.readLock().lock(); + certs = (Hashtable<String, X509Certificate>)this.mCACerts.clone(); + this.mLock.readLock().unlock(); + return certs; + } + + /** + * Get only the CA certificates installed by the user. + * @return Hashtable mapping aliases to certificates + */ + public Hashtable<String, X509Certificate> getUserCACertificates() + { + Hashtable<String, X509Certificate> certs = new Hashtable<String, X509Certificate>(); + this.mLock.readLock().lock(); + for (String alias : this.mCACerts.keySet()) + { + if (alias.startsWith("user:")) + { + certs.put(alias, this.mCACerts.get(alias)); + } + } + this.mLock.readLock().unlock(); + return certs; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java b/src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java new file mode 100644 index 000000000..1c14cb601 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/logic/VpnStateService.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.logic; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +import org.strongswan.android.data.VpnProfile; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; + +public class VpnStateService extends Service +{ + private final List<VpnStateListener> mListeners = new ArrayList<VpnStateListener>(); + private final IBinder mBinder = new LocalBinder(); + private Handler mHandler; + private VpnProfile mProfile; + private State mState = State.DISABLED; + private ErrorState mError = ErrorState.NO_ERROR; + + public enum State + { + DISABLED, + CONNECTING, + CONNECTED, + DISCONNECTING, + } + + public enum ErrorState + { + NO_ERROR, + AUTH_FAILED, + PEER_AUTH_FAILED, + LOOKUP_FAILED, + UNREACHABLE, + GENERIC_ERROR, + } + + /** + * Listener interface for bound clients that are interested in changes to + * this Service. + */ + public interface VpnStateListener + { + public void stateChanged(); + } + + /** + * Simple Binder that allows to directly access this Service class itself + * after binding to it. + */ + public class LocalBinder extends Binder + { + public VpnStateService getService() + { + return VpnStateService.this; + } + } + + @Override + public void onCreate() + { + /* this handler allows us to notify listeners from the UI thread and + * not from the threads that actually report any state changes */ + mHandler = new Handler(); + } + + @Override + public IBinder onBind(Intent intent) + { + return mBinder; + } + + @Override + public void onDestroy() + { + } + + /** + * Register a listener with this Service. We assume this is called from + * the main thread so no synchronization is happening. + * + * @param listener listener to register + */ + public void registerListener(VpnStateListener listener) + { + mListeners.add(listener); + } + + /** + * Unregister a listener from this Service. + * + * @param listener listener to unregister + */ + public void unregisterListener(VpnStateListener listener) + { + mListeners.remove(listener); + } + + /** + * Get the current VPN profile. + * + * @return profile + */ + public VpnProfile getProfile() + { /* only updated from the main thread so no synchronization needed */ + return mProfile; + } + + /** + * Get the current state. + * + * @return state + */ + public State getState() + { /* only updated from the main thread so no synchronization needed */ + return mState; + } + + /** + * Get the current error, if any. + * + * @return error + */ + public ErrorState getErrorState() + { /* only updated from the main thread so no synchronization needed */ + return mError; + } + + /** + * Disconnect any existing connection and shutdown the daemon, the + * VpnService is not stopped but it is reset so new connections can be + * started. + */ + public void disconnect() + { + /* as soon as the TUN device is created by calling establish() on the + * VpnService.Builder object the system binds to the service and keeps + * bound until the file descriptor of the TUN device is closed. thus + * calling stopService() here would not stop (destroy) the service yet, + * instead we call startService() with an empty Intent which shuts down + * the daemon (and closes the TUN device, if any) */ + Context context = getApplicationContext(); + Intent intent = new Intent(context, CharonVpnService.class); + context.startService(intent); + } + + /** + * Update state and notify all listeners about the change. By using a Handler + * this is done from the main UI thread and not the initial reporter thread. + * Also, in doing the actual state change from the main thread, listeners + * see all changes and none are skipped. + * + * @param change the state update to perform before notifying listeners, returns true if state changed + */ + private void notifyListeners(final Callable<Boolean> change) + { + mHandler.post(new Runnable() { + @Override + public void run() + { + try + { + if (change.call()) + { /* otherwise there is no need to notify the listeners */ + for (VpnStateListener listener : mListeners) + { + listener.stateChanged(); + } + } + } + catch (Exception e) + { + e.printStackTrace(); + } + } + }); + } + + /** + * Set the VPN profile currently active. Listeners are not notified. + * + * May be called from threads other than the main thread. + * + * @param profile current profile + */ + public void setProfile(final VpnProfile profile) + { + /* even though we don't notify the listeners the update is done from the + * same handler so updates are predictable for listeners */ + mHandler.post(new Runnable() { + @Override + public void run() + { + VpnStateService.this.mProfile = profile; + } + }); + } + + /** + * Update the state and notify all listeners, if changed. + * + * May be called from threads other than the main thread. + * + * @param state new state + */ + public void setState(final State state) + { + notifyListeners(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception + { + if (VpnStateService.this.mState != state) + { + VpnStateService.this.mState = state; + return true; + } + return false; + } + }); + } + + /** + * Set the current error state and notify all listeners, if changed. + * + * May be called from threads other than the main thread. + * + * @param error error state + */ + public void setError(final ErrorState error) + { + notifyListeners(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception + { + if (VpnStateService.this.mError != error) + { + VpnStateService.this.mError = error; + return true; + } + return false; + } + }); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/strongSwanActivity.java b/src/frontends/android/src/org/strongswan/android/strongSwanActivity.java deleted file mode 100644 index fabf71897..000000000 --- a/src/frontends/android/src/org/strongswan/android/strongSwanActivity.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.strongswan.android; - -import android.app.Activity; -import android.content.Intent; -import android.net.VpnService; -import android.os.Bundle; - -public class strongSwanActivity extends Activity -{ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.main); - startVpnService(); - } - - private void startVpnService() - { - Intent intent = VpnService.prepare(this); - if (intent != null) - { - startActivityForResult(intent, 0); - } - else - { - onActivityResult(0, RESULT_OK, null); - } - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) - { - if (resultCode == RESULT_OK) - { - Intent intent = new Intent(this, CharonVpnService.class); - startService(intent); - } - } -} diff --git a/src/frontends/android/src/org/strongswan/android/ui/LogActivity.java b/src/frontends/android/src/org/strongswan/android/ui/LogActivity.java new file mode 100644 index 000000000..a5efecc09 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/LogActivity.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import java.io.File; + +import org.strongswan.android.R; +import org.strongswan.android.data.LogContentProvider; +import org.strongswan.android.logic.CharonVpnService; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.Toast; + +public class LogActivity extends Activity +{ + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.log_activity); + + getActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + getMenuInflater().inflate(R.menu.log, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case android.R.id.home: + finish(); + return true; + case R.id.menu_send_log: + File logfile = new File(getFilesDir(), CharonVpnService.LOG_FILE); + if (!logfile.exists() || logfile.length() == 0) + { + Toast.makeText(this, getString(R.string.empty_log), Toast.LENGTH_SHORT).show(); + return true; + } + + String version = ""; + try + { + version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName; + } + catch (NameNotFoundException e) + { + e.printStackTrace(); + } + + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_EMAIL, new String[] { MainActivity.CONTACT_EMAIL }); + intent.putExtra(Intent.EXTRA_SUBJECT, String.format(getString(R.string.log_mail_subject), version)); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_STREAM, LogContentProvider.createContentUri()); + startActivity(Intent.createChooser(intent, getString(R.string.send_log))); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/LogFragment.java b/src/frontends/android/src/org/strongswan/android/ui/LogFragment.java new file mode 100644 index 000000000..8740e0c46 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/LogFragment.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.StringReader; + +import org.strongswan.android.R; +import org.strongswan.android.logic.CharonVpnService; + +import android.app.Fragment; +import android.os.Bundle; +import android.os.FileObserver; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class LogFragment extends Fragment implements Runnable +{ + private String mLogFilePath; + private Handler mLogHandler; + private TextView mLogView; + private LogScrollView mScrollView; + private BufferedReader mReader; + private Thread mThread; + private volatile boolean mRunning; + private FileObserver mDirectoryObserver; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + mLogFilePath = getActivity().getFilesDir() + File.separator + CharonVpnService.LOG_FILE; + /* use a handler to update the log view */ + mLogHandler = new Handler(); + + mDirectoryObserver = new LogDirectoryObserver(getActivity().getFilesDir().getAbsolutePath()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.log_fragment, null); + mLogView = (TextView)view.findViewById(R.id.log_view); + mScrollView = (LogScrollView)view.findViewById(R.id.scroll_view); + return view; + } + + @Override + public void onStart() + { + super.onStart(); + startLogReader(); + mDirectoryObserver.startWatching(); + } + + @Override + public void onStop() + { + super.onStop(); + mDirectoryObserver.stopWatching(); + stopLogReader(); + } + + /** + * Start reading from the log file + */ + private void startLogReader() + { + try + { + mReader = new BufferedReader(new FileReader(mLogFilePath)); + } + catch (FileNotFoundException e) + { + mReader = new BufferedReader(new StringReader("")); + } + + mLogView.setText(""); + mRunning = true; + mThread = new Thread(this); + mThread.start(); + } + + /** + * Stop reading from the log file + */ + private void stopLogReader() + { + try + { + mRunning = false; + mThread.interrupt(); + mThread.join(); + } + catch (InterruptedException e) + { + } + } + + /** + * Write the given log line to the TextView. We strip the prefix off to save + * some space (it is not that helpful for regular users anyway). + * + * @param line log line to log + */ + public void logLine(final String line) + { + mLogHandler.post(new Runnable() { + @Override + public void run() + { + /* strip off prefix (month=3, day=2, time=8, thread=2, spaces=3) */ + mLogView.append((line.length() > 18 ? line.substring(18) : line) + '\n'); + /* calling autoScroll() directly does not work, probably because content + * is not yet updated, so we post this to be done later */ + mScrollView.post(new Runnable() { + @Override + public void run() + { + mScrollView.autoScroll(); + } + }); + } + }); + } + + @Override + public void run() + { + while (mRunning) + { + try + { /* this works as long as the file is not truncated */ + String line = mReader.readLine(); + if (line == null) + { /* wait until there is more to log */ + Thread.sleep(1000); + } + else + { + logLine(line); + } + } + catch (Exception e) + { + break; + } + } + } + + /** + * FileObserver that checks for changes regarding the log file. Since charon + * truncates it (for which there is no explicit event) we check for any modification + * to the file, keep track of the file size and reopen it if it got smaller. + */ + private class LogDirectoryObserver extends FileObserver + { + private final File mFile; + private long mSize; + + public LogDirectoryObserver(String path) + { + super(path, FileObserver.CREATE | FileObserver.MODIFY | FileObserver.DELETE); + mFile = new File(mLogFilePath); + mSize = mFile.length(); + } + + @Override + public void onEvent(int event, String path) + { + if (path == null || !path.equals(CharonVpnService.LOG_FILE)) + { + return; + } + switch (event) + { /* even though we only subscribed for these we check them, + * as strange events are sometimes received */ + case FileObserver.CREATE: + case FileObserver.DELETE: + restartLogReader(); + break; + case FileObserver.MODIFY: + /* if the size got smaller reopen the log file, as it was probably truncated */ + long size = mFile.length(); + if (size < mSize) + { + restartLogReader(); + } + mSize = size; + break; + } + } + + private void restartLogReader() + { + /* we are called from a separate thread, so we use the handler */ + mLogHandler.post(new Runnable() { + @Override + public void run() + { + stopLogReader(); + startLogReader(); + } + }); + } + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java b/src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java new file mode 100644 index 000000000..7eee820ce --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/LogScrollView.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ScrollView; + +public class LogScrollView extends ScrollView +{ + private boolean mAutoScroll = true; + + public LogScrollView(Context context) + { + super(context); + } + + public LogScrollView(Context context, AttributeSet attrs) + { + super(context, attrs); + } + + public LogScrollView(Context context, AttributeSet attrs, int defStyle) + { + super(context, attrs, defStyle); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) + { + /* disable auto-scrolling when the user starts scrolling around */ + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) + { + mAutoScroll = false; + } + return super.onTouchEvent(ev); + } + + /** + * Call this to move newly added content into view by scrolling to the bottom. + * Nothing happens if auto-scrolling is disabled. + */ + public void autoScroll() + { + if (mAutoScroll) + { + fullScroll(View.FOCUS_DOWN); + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) + { + super.onScrollChanged(l, t, oldl, oldt); + /* if the user scrolls to the bottom we enable auto-scrolling again */ + if (t == getChildAt(getChildCount() - 1).getHeight() - getHeight()) + { + mAutoScroll = true; + } + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java new file mode 100644 index 000000000..80f1a27b3 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/MainActivity.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.logic.CharonVpnService; +import org.strongswan.android.logic.TrustedCertificateManager; +import org.strongswan.android.ui.VpnProfileListFragment.OnVpnProfileSelectedListener; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.VpnService; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.EditText; + +public class MainActivity extends Activity implements OnVpnProfileSelectedListener +{ + public static final String CONTACT_EMAIL = "android@strongswan.org"; + private static final int PREPARE_VPN_SERVICE = 0; + private VpnProfile activeProfile; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setContentView(R.layout.main); + + ActionBar bar = getActionBar(); + bar.setDisplayShowTitleEnabled(false); + + /* load CA certificates in a background task */ + new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, false); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case R.id.menu_reload_certs: + new CertificateLoadTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, true); + return true; + case R.id.menu_show_log: + Intent logIntent = new Intent(this, LogActivity.class); + startActivity(logIntent); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Prepare the VpnService. If this succeeds the current VPN profile is + * started. + */ + protected void prepareVpnService() + { + Intent intent = VpnService.prepare(this); + if (intent != null) + { + startActivityForResult(intent, PREPARE_VPN_SERVICE); + } + else + { + onActivityResult(PREPARE_VPN_SERVICE, RESULT_OK, null); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + switch (requestCode) + { + case PREPARE_VPN_SERVICE: + if (resultCode == RESULT_OK && activeProfile != null) + { + Intent intent = new Intent(this, CharonVpnService.class); + intent.putExtra(VpnProfileDataSource.KEY_ID, activeProfile.getId()); + /* submit the password as the profile might not store one */ + intent.putExtra(VpnProfileDataSource.KEY_PASSWORD, activeProfile.getPassword()); + this.startService(intent); + } + break; + default: + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onVpnProfileSelected(VpnProfile profile) + { + activeProfile = profile; + if (activeProfile.getPassword() == null) + { + new LoginDialog().show(getFragmentManager(), "LoginDialog"); + } + else + { + prepareVpnService(); + } + } + + /** + * Class that loads or reloads the cached CA certificates. + */ + private class CertificateLoadTask extends AsyncTask<Boolean, Void, TrustedCertificateManager> + { + @Override + protected void onPreExecute() + { + setProgressBarIndeterminateVisibility(true); + } + @Override + protected TrustedCertificateManager doInBackground(Boolean... params) + { + if (params.length > 0 && params[0]) + { /* force a reload of the certificates */ + return TrustedCertificateManager.getInstance().reload(); + } + return TrustedCertificateManager.getInstance().load(); + } + @Override + protected void onPostExecute(TrustedCertificateManager result) + { + setProgressBarIndeterminateVisibility(false); + } + } + + private class LoginDialog extends DialogFragment + { + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) + { + LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.login_dialog, null); + EditText username = (EditText)view.findViewById(R.id.username); + username.setText(activeProfile.getUsername()); + final EditText password = (EditText)view.findViewById(R.id.password); + + Builder adb = new AlertDialog.Builder(MainActivity.this); + adb.setView(view); + adb.setTitle(getString(R.string.login_title)); + adb.setPositiveButton(R.string.login_confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) + { + /* let's work on a clone of the profile when updating the password */ + activeProfile = activeProfile.clone(); + activeProfile.setPassword(password.getText().toString().trim()); + prepareVpnService(); + } + }); + adb.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + dismiss(); + } + }); + return adb.create(); + } + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java new file mode 100644 index 000000000..05ba5e8b3 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import java.security.cert.X509Certificate; +import java.util.Hashtable; + +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.logic.TrustedCertificateManager; +import org.strongswan.android.ui.adapter.TrustedCertificateAdapter; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.EditText; +import android.widget.Spinner; + +public class VpnProfileDetailActivity extends Activity +{ + private VpnProfileDataSource mDataSource; + private Long mId; + private VpnProfile mProfile; + private boolean mCertsLoaded; + private String mCertAlias; + private Spinner mCertSpinner; + private TrustedCertificateAdapter.CertEntry mSelectedCert; + private EditText mName; + private EditText mGateway; + private EditText mUsername; + private EditText mPassword; + private CheckBox mCheckAll; + private CheckBox mCheckAuto; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + /* the title is set when we load the profile, if any */ + getActionBar().setDisplayHomeAsUpEnabled(true); + + mDataSource = new VpnProfileDataSource(this); + mDataSource.open(); + + setContentView(R.layout.profile_detail_view); + + mName = (EditText)findViewById(R.id.name); + mPassword = (EditText)findViewById(R.id.password); + mGateway = (EditText)findViewById(R.id.gateway); + mUsername = (EditText)findViewById(R.id.username); + + mCheckAll = (CheckBox)findViewById(R.id.ca_show_all); + mCheckAuto = (CheckBox)findViewById(R.id.ca_auto); + mCertSpinner = (Spinner)findViewById(R.id.ca_spinner); + + mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) + { + updateCertSpinner(); + } + }); + + mCheckAll.setOnCheckedChangeListener(new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) + { + Hashtable<String, X509Certificate> certs; + certs = isChecked ? TrustedCertificateManager.getInstance().getAllCACertificates() + : TrustedCertificateManager.getInstance().getUserCACertificates(); + mCertSpinner.setAdapter(new TrustedCertificateAdapter(VpnProfileDetailActivity.this, certs)); + mSelectedCert = (TrustedCertificateAdapter.CertEntry)mCertSpinner.getSelectedItem(); + } + }); + + mCertSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parent, View view, + int pos, long id) + { + mSelectedCert = (TrustedCertificateAdapter.CertEntry)parent.getSelectedItem(); + } + + @Override + public void onNothingSelected(AdapterView<?> arg0) + { + mSelectedCert = null; + } + }); + + mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID); + if (mId == null) + { + Bundle extras = getIntent().getExtras(); + mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID); + } + + loadProfileData(); + + new CertificateLoadTask().execute(); + } + + @Override + protected void onDestroy() + { + super.onDestroy(); + mDataSource.close(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + outState.putLong(VpnProfileDataSource.KEY_ID, mId); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.profile_edit, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case android.R.id.home: + case R.id.menu_cancel: + finish(); + return true; + case R.id.menu_accept: + saveProfile(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Show an alert in case the previously selected certificate is not found anymore + * or the user did not select a certificate in the spinner. + */ + private void showCertificateAlert() + { + AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this); + adb.setTitle(R.string.alert_text_nocertfound_title); + adb.setMessage(R.string.alert_text_nocertfound); + adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) + { + dialog.cancel(); + } + }); + adb.show(); + } + + /** + * Asynchronously executed task which confirms that the certificates are loaded. + * They are loaded from the main Activity already but might not be ready yet, or + * unloaded again. + * + * Once loaded the CA certificate spinner and checkboxes are updated + * accordingly. + */ + private class CertificateLoadTask extends AsyncTask<Void, Void, TrustedCertificateManager> + { + @Override + protected void onPreExecute() + { + setProgressBarIndeterminateVisibility(true); + } + + @Override + protected TrustedCertificateManager doInBackground(Void... params) + { + return TrustedCertificateManager.getInstance().load(); + } + + @Override + protected void onPostExecute(TrustedCertificateManager result) + { + TrustedCertificateAdapter adapter; + if (mCertAlias != null && mCertAlias.startsWith("system:")) + { + mCheckAll.setChecked(true); + adapter = new TrustedCertificateAdapter(VpnProfileDetailActivity.this, + result.getAllCACertificates()); + } + else + { + mCheckAll.setChecked(false); + adapter = new TrustedCertificateAdapter(VpnProfileDetailActivity.this, + result.getUserCACertificates()); + } + mCertSpinner.setAdapter(adapter); + + if (mCertAlias != null) + { + int position = adapter.getItemPosition(mCertAlias); + if (position == -1) + { /* previously selected certificate is not here anymore */ + showCertificateAlert(); + } + else + { + mCertSpinner.setSelection(position); + } + } + + mSelectedCert = (TrustedCertificateAdapter.CertEntry)mCertSpinner.getSelectedItem(); + + setProgressBarIndeterminateVisibility(false); + mCertsLoaded = true; + updateCertSpinner(); + } + } + + /** + * Update the CA certificate selection UI depending on whether the + * certificate should be automatically selected or not. + */ + private void updateCertSpinner() + { + if (!mCheckAuto.isChecked()) + { + if (mCertsLoaded) + { + mCertSpinner.setEnabled(true); + mCertSpinner.setVisibility(View.VISIBLE); + mCheckAll.setEnabled(true); + mCheckAll.setVisibility(View.VISIBLE); + } + } + else + { + mCertSpinner.setEnabled(false); + mCertSpinner.setVisibility(View.GONE); + mCheckAll.setEnabled(false); + mCheckAll.setVisibility(View.GONE); + } + } + + /** + * Save or update the profile depending on whether we actually have a + * profile object or not (this was created in updateProfileData) + */ + private void saveProfile() + { + if (verifyInput()) + { + if (mProfile != null) + { + updateProfileData(); + mDataSource.updateVpnProfile(mProfile); + } + else + { + mProfile = new VpnProfile(); + updateProfileData(); + mDataSource.insertProfile(mProfile); + } + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId())); + finish(); + } + } + + /** + * Verify the user input and display error messages. + * @return true if the input is valid + */ + private boolean verifyInput() + { + boolean valid = true; + if (mGateway.getText().toString().trim().isEmpty()) + { + mGateway.setError(getString(R.string.alert_text_no_input_gateway)); + valid = false; + } + if (mUsername.getText().toString().trim().isEmpty()) + { + mUsername.setError(getString(R.string.alert_text_no_input_username)); + valid = false; + } + if (!mCheckAuto.isChecked() && mSelectedCert == null) + { + showCertificateAlert(); + valid = false; + } + return valid; + } + + /** + * Update the profile object with the data entered by the user + */ + private void updateProfileData() + { + /* the name is optional, we default to the gateway if none is given */ + String name = mName.getText().toString().trim(); + String gateway = mGateway.getText().toString().trim(); + mProfile.setName(name.isEmpty() ? gateway : name); + mProfile.setGateway(gateway); + mProfile.setUsername(mUsername.getText().toString().trim()); + String password = mPassword.getText().toString().trim(); + password = password.isEmpty() ? null : password; + mProfile.setPassword(password); + String certAlias = mCheckAuto.isChecked() ? null : mSelectedCert.mAlias; + mProfile.setCertificateAlias(certAlias); + } + + /** + * Load an existing profile if we got an ID + */ + private void loadProfileData() + { + getActionBar().setTitle(R.string.add_profile); + if (mId != null) + { + mProfile = mDataSource.getVpnProfile(mId); + if (mProfile != null) + { + mName.setText(mProfile.getName()); + mGateway.setText(mProfile.getGateway()); + mUsername.setText(mProfile.getUsername()); + mPassword.setText(mProfile.getPassword()); + mCertAlias = mProfile.getCertificateAlias(); + getActionBar().setTitle(mProfile.getName()); + } + else + { + Log.e(VpnProfileDetailActivity.class.getSimpleName(), + "VPN profile with id " + mId + " not found"); + finish(); + } + } + mCheckAll.setChecked(false); + mCheckAuto.setChecked(mCertAlias == null); + updateCertSpinner(); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java new file mode 100644 index 000000000..1052558f2 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/VpnProfileListFragment.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.ui.adapter.VpnProfileAdapter; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView.MultiChoiceModeListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.Toast; + +public class VpnProfileListFragment extends Fragment +{ + private static final int ADD_REQUEST = 1; + private static final int EDIT_REQUEST = 2; + + private List<VpnProfile> mVpnProfiles; + private VpnProfileDataSource mDataSource; + private VpnProfileAdapter mListAdapter; + private ListView mListView; + private OnVpnProfileSelectedListener mListener; + + /** + * The activity containing this fragment should implement this interface + */ + public interface OnVpnProfileSelectedListener { + public void onVpnProfileSelected(VpnProfile profile); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.profile_list_fragment, null); + + mListView = (ListView)view.findViewById(R.id.profile_list); + mListView.setEmptyView(view.findViewById(R.id.profile_list_empty)); + mListView.setOnItemClickListener(mVpnProfileClicked); + mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + mListView.setMultiChoiceModeListener(mVpnProfileSelected); + mListView.setAdapter(mListAdapter); + + return view; + } + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + Context context = getActivity().getApplicationContext(); + + mDataSource = new VpnProfileDataSource(this.getActivity()); + mDataSource.open(); + + /* cached list of profiles used as backend for the ListView */ + mVpnProfiles = mDataSource.getAllVpnProfiles(); + + mListAdapter = new VpnProfileAdapter(context, R.layout.profile_list_item, mVpnProfiles); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + mDataSource.close(); + } + + @Override + public void onAttach(Activity activity) + { + super.onAttach(activity); + + if (activity instanceof OnVpnProfileSelectedListener) + { + mListener = (OnVpnProfileSelectedListener)activity; + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) + { + inflater.inflate(R.menu.profile_list, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + switch (item.getItemId()) + { + case R.id.add_profile: + Intent connectionIntent = new Intent(getActivity(), + VpnProfileDetailActivity.class); + startActivityForResult(connectionIntent, ADD_REQUEST); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) + { + switch (requestCode) + { + case ADD_REQUEST: + case EDIT_REQUEST: + if (resultCode != Activity.RESULT_OK) + { + return; + } + long id = data.getLongExtra(VpnProfileDataSource.KEY_ID, 0); + VpnProfile profile = mDataSource.getVpnProfile(id); + if (profile != null) + { /* in case this was an edit, we remove it first */ + mVpnProfiles.remove(profile); + mVpnProfiles.add(profile); + mListAdapter.notifyDataSetChanged(); + } + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> a, View v, int position, long id) + { + if (mListener != null) + { + mListener.onVpnProfileSelected((VpnProfile)a.getItemAtPosition(position)); + } + } + }; + + private final MultiChoiceModeListener mVpnProfileSelected = new MultiChoiceModeListener() { + private HashSet<Integer> mSelected; + private MenuItem mEditProfile; + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) + { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) + { + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) + { + MenuInflater inflater = mode.getMenuInflater(); + inflater.inflate(R.menu.profile_list_context, menu); + mEditProfile = menu.findItem(R.id.edit_profile); + mSelected = new HashSet<Integer>(); + mode.setTitle(R.string.select_profiles); + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) + { + switch (item.getItemId()) + { + case R.id.edit_profile: + { + int position = mSelected.iterator().next(); + VpnProfile profile = (VpnProfile)mListView.getItemAtPosition(position); + Intent connectionIntent = new Intent(getActivity(), VpnProfileDetailActivity.class); + connectionIntent.putExtra(VpnProfileDataSource.KEY_ID, profile.getId()); + startActivityForResult(connectionIntent, EDIT_REQUEST); + break; + } + case R.id.delete_profile: + { + ArrayList<VpnProfile> profiles = new ArrayList<VpnProfile>(); + for (int position : mSelected) + { + profiles.add((VpnProfile)mListView.getItemAtPosition(position)); + } + for (VpnProfile profile : profiles) + { + mDataSource.deleteVpnProfile(profile); + mVpnProfiles.remove(profile); + } + mListAdapter.notifyDataSetChanged(); + Toast.makeText(VpnProfileListFragment.this.getActivity(), + R.string.profiles_deleted, Toast.LENGTH_SHORT).show(); + break; + } + default: + return false; + } + mode.finish(); + return true; + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, + long id, boolean checked) + { + if (checked) + { + mSelected.add(position); + } + else + { + mSelected.remove(position); + } + final int checkedCount = mSelected.size(); + mEditProfile.setEnabled(checkedCount == 1); + switch (checkedCount) + { + case 0: + mode.setSubtitle(R.string.no_profile_selected); + break; + case 1: + mode.setSubtitle(R.string.one_profile_selected); + break; + default: + mode.setSubtitle(String.format(getString(R.string.x_profiles_selected), checkedCount)); + break; + } + } + }; +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java b/src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java new file mode 100644 index 000000000..738ed111f --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/VpnStateFragment.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui; + +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; +import org.strongswan.android.logic.VpnStateService; +import org.strongswan.android.logic.VpnStateService.ErrorState; +import org.strongswan.android.logic.VpnStateService.State; +import org.strongswan.android.logic.VpnStateService.VpnStateListener; + +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.ProgressDialog; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +public class VpnStateFragment extends Fragment implements VpnStateListener +{ + private static final String KEY_ERROR = "error"; + private static final String KEY_NAME = "name"; + + private TextView mProfileNameView; + private TextView mProfileView; + private TextView mStateView; + private int stateBaseColor; + private Button mActionButton; + private ProgressDialog mProgressDialog; + private State mState; + private AlertDialog mErrorDialog; + private ErrorState mError; + private String mErrorProfileName; + private VpnStateService mService; + private final ServiceConnection mServiceConnection = new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) + { + mService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) + { + mService = ((VpnStateService.LocalBinder)service).getService(); + mService.registerListener(VpnStateFragment.this); + updateView(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + /* bind to the service only seems to work from the ApplicationContext */ + Context context = getActivity().getApplicationContext(); + context.bindService(new Intent(context, VpnStateService.class), + mServiceConnection, Service.BIND_AUTO_CREATE); + + mError = ErrorState.NO_ERROR; + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_ERROR)) + { + mError = (ErrorState)savedInstanceState.getSerializable(KEY_ERROR); + mErrorProfileName = savedInstanceState.getString(KEY_NAME); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) + { + super.onSaveInstanceState(outState); + + outState.putSerializable(KEY_ERROR, mError); + outState.putString(KEY_NAME, mErrorProfileName); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.vpn_state_fragment, null); + + mActionButton = (Button)view.findViewById(R.id.action); + mActionButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) + { + if (mService != null) + { + mService.disconnect(); + } + } + }); + enableActionButton(false); + + mStateView = (TextView)view.findViewById(R.id.vpn_state); + stateBaseColor = mStateView.getCurrentTextColor(); + mProfileView = (TextView)view.findViewById(R.id.vpn_profile_label); + mProfileNameView = (TextView)view.findViewById(R.id.vpn_profile_name); + + return view; + } + + @Override + public void onStart() + { + super.onStart(); + if (mService != null) + { + updateView(); + } + } + + @Override + public void onStop() + { + super.onStop(); + hideErrorDialog(); + hideProgressDialog(); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + if (mService != null) + { + mService.unregisterListener(this); + getActivity().getApplicationContext().unbindService(mServiceConnection); + } + } + + @Override + public void stateChanged() + { + updateView(); + } + + public void updateView() + { + State state = mService.getState(); + ErrorState error = ErrorState.NO_ERROR; + String name = "", gateway = ""; + + if (state != State.DISABLED) + { + VpnProfile profile = mService.getProfile(); + if (profile != null) + { + name = profile.getName(); + gateway = profile.getGateway(); + } + error = mService.getErrorState(); + } + + if (reportError(name, state, error)) + { + return; + } + + if (state == mState) + { /* avoid unnecessary updates */ + return; + } + + hideProgressDialog(); + enableActionButton(false); + mProfileNameView.setText(name); + mState = state; + + switch (state) + { + case DISABLED: + showProfile(false); + mStateView.setText(R.string.state_disabled); + mStateView.setTextColor(stateBaseColor); + break; + case CONNECTING: + showProfile(true); + showConnectDialog(name, gateway); + mStateView.setText(R.string.state_connecting); + mStateView.setTextColor(stateBaseColor); + break; + case CONNECTED: + showProfile(true); + enableActionButton(true); + mStateView.setText(R.string.state_connected); + mStateView.setTextColor(getResources().getColor(R.color.success_text)); + break; + case DISCONNECTING: + showProfile(true); + showDisconnectDialog(name); + mStateView.setText(R.string.state_disconnecting); + mStateView.setTextColor(stateBaseColor); + break; + } + } + + private boolean reportError(String name, State state, ErrorState error) + { + if (mError != ErrorState.NO_ERROR) + { /* we are currently reporting an error which was not yet dismissed */ + error = mError; + name = mErrorProfileName; + } + else if (error != ErrorState.NO_ERROR && (state == State.CONNECTING || state == State.CONNECTED)) + { /* while initiating we report errors */ + mError = error; + mErrorProfileName = name; + } + else + { /* ignore all other errors */ + error = ErrorState.NO_ERROR; + } + if (error == ErrorState.NO_ERROR) + { + hideErrorDialog(); + return false; + } + else if (mErrorDialog != null) + { /* we already show the dialog */ + return true; + } + hideProgressDialog(); + mProfileNameView.setText(name); + showProfile(true); + enableActionButton(false); + mStateView.setText(R.string.state_error); + mStateView.setTextColor(getResources().getColor(R.color.error_text)); + switch (error) + { + case AUTH_FAILED: + showErrorDialog(R.string.error_auth_failed); + break; + case PEER_AUTH_FAILED: + showErrorDialog(R.string.error_peer_auth_failed); + break; + case LOOKUP_FAILED: + showErrorDialog(R.string.error_lookup_failed); + break; + case UNREACHABLE: + showErrorDialog(R.string.error_unreachable); + break; + default: + showErrorDialog(R.string.error_generic); + break; + } + return true; + } + + private void showProfile(boolean show) + { + mProfileView.setVisibility(show ? View.VISIBLE : View.GONE); + mProfileNameView.setVisibility(show ? View.VISIBLE : View.GONE); + } + + private void enableActionButton(boolean enable) + { + mActionButton.setEnabled(enable); + mActionButton.setVisibility(enable ? View.VISIBLE : View.GONE); + } + + private void hideProgressDialog() + { + if (mProgressDialog != null) + { + mProgressDialog.dismiss(); + mProgressDialog = null; + } + } + + private void hideErrorDialog() + { + if (mErrorDialog != null) + { + mErrorDialog.dismiss(); + mErrorDialog = null; + } + } + + private void showConnectDialog(String profile, String gateway) + { + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setTitle(String.format(getString(R.string.connecting_title), profile)); + mProgressDialog.setMessage(String.format(getString(R.string.connecting_message), gateway)); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setCancelable(false); + mProgressDialog.setButton(getString(android.R.string.cancel), + new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + if (mService != null) + { + mService.disconnect(); + } + } + }); + mProgressDialog.show(); + } + + private void showDisconnectDialog(String profile) + { + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setMessage(getString(R.string.state_disconnecting)); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setCancelable(false); + mProgressDialog.show(); + } + + private void showErrorDialog(int textid) + { + mErrorDialog = new AlertDialog.Builder(getActivity()) + .setMessage(getString(R.string.error_introduction) + " " + getString(textid)) + .setCancelable(false) + .setNeutralButton(R.string.show_log, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) + { + dialog.dismiss(); + Intent logIntent = new Intent(getActivity(), LogActivity.class); + startActivity(logIntent); + } + }) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int id) + { + dialog.dismiss(); + } + }).create(); + mErrorDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) + { /* clear the error */ + mError = ErrorState.NO_ERROR; + mErrorDialog = null; + updateView(); + } + }); + mErrorDialog.show(); + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java b/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java new file mode 100644 index 000000000..ae94adc52 --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/adapter/TrustedCertificateAdapter.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui.adapter; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Map.Entry; + +import org.strongswan.android.R; + +import android.content.Context; +import android.net.http.SslCertificate; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +public class TrustedCertificateAdapter extends BaseAdapter +{ + private final ArrayList<CertEntry> mContent; + private final Context mContext; + + public class CertEntry implements Comparable<CertEntry> + { + public X509Certificate mCert; + public String mAlias; + public String mDisplayName; + + public CertEntry(String alias, X509Certificate cert) + { + mCert = cert; + mAlias = alias; + } + + public String getDisplayText() + { + if (mDisplayName == null) + { + SslCertificate cert = new SslCertificate(mCert); + String o = cert.getIssuedTo().getOName(); + String ou = cert.getIssuedTo().getUName(); + String cn = cert.getIssuedTo().getCName(); + if (!o.isEmpty()) + { + mDisplayName = o; + if (!cn.isEmpty()) + { + mDisplayName = mDisplayName + ", " + cn; + } + else if (!ou.isEmpty()) + { + mDisplayName = mDisplayName + ", " + ou; + } + } + else if (!cn.isEmpty()) + { + mDisplayName = cn; + } + else + { + mDisplayName = cert.getIssuedTo().getDName(); + } + } + return mDisplayName; + } + + @Override + public int compareTo(CertEntry another) + { + return getDisplayText().compareToIgnoreCase(another.getDisplayText()); + } + } + + public TrustedCertificateAdapter(Context context, + Hashtable<String, X509Certificate> content) + { + mContext = context; + mContent = new ArrayList<TrustedCertificateAdapter.CertEntry>(); + for (Entry<String, X509Certificate> entry : content.entrySet()) + { + mContent.add(new CertEntry(entry.getKey(), entry.getValue())); + } + Collections.sort(mContent); + } + + @Override + public int getCount() + { + return mContent.size(); + } + + @Override + public Object getItem(int position) + { + return mContent.get(position); + } + + /** + * Returns the position (index) of the entry with the given alias. + * + * @param alias alias of the item to find + * @return the position (index) in the list + */ + public int getItemPosition(String alias) + { + for (int i = 0; i < mContent.size(); i++) + { + if (mContent.get(i).mAlias.equals(alias)) + { + return i; + } + } + return -1; + } + + @Override + public long getItemId(int position) + { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + LayoutInflater inflater = LayoutInflater.from(mContext); + final View certView = inflater.inflate(R.layout.trusted_certificates_item, null); + final TextView certText = (TextView)certView.findViewById(R.id.certificate_name); + certText.setText(mContent.get(position).getDisplayText()); + return certView; + } +} diff --git a/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java b/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java new file mode 100644 index 000000000..39e3e586a --- /dev/null +++ b/src/frontends/android/src/org/strongswan/android/ui/adapter/VpnProfileAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * Hochschule fuer Technik Rapperswil + * + * 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 2 of the License, or (at your + * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>. + * + * 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. + */ + +package org.strongswan.android.ui.adapter; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.strongswan.android.R; +import org.strongswan.android.data.VpnProfile; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +public class VpnProfileAdapter extends ArrayAdapter<VpnProfile> +{ + private final int resource; + private final List<VpnProfile> items; + + public VpnProfileAdapter(Context context, int resource, + List<VpnProfile> items) + { + super(context, resource, items); + this.resource = resource; + this.items = items; + sortItems(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + View vpnProfileView; + if (convertView != null) + { + vpnProfileView = convertView; + } + else + { + LayoutInflater inflater = LayoutInflater.from(getContext()); + vpnProfileView = inflater.inflate(resource, null); + } + VpnProfile profile = getItem(position); + TextView tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_name); + tv.setText(profile.getName()); + tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_gateway); + tv.setText(getContext().getString(R.string.profile_gateway_label) + " " + profile.getGateway()); + tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_username); + tv.setText(getContext().getString(R.string.profile_username_label) + " " + profile.getUsername()); + return vpnProfileView; + } + + @Override + public void notifyDataSetChanged() + { + sortItems(); + super.notifyDataSetChanged(); + } + + private void sortItems() + { + Collections.sort(this.items, new Comparator<VpnProfile>() { + @Override + public int compare(VpnProfile lhs, VpnProfile rhs) + { + return lhs.getName().compareToIgnoreCase(rhs.getName()); + } + }); + } +} |