diff --git a/android/build.gradle b/android/build.gradle index c808709..45d4503 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -37,8 +37,8 @@ android { defaultConfig { applicationId "com.nutomic.ensichat" targetSdkVersion 23 - versionCode 8 - versionName "0.1.7" + versionCode project.properties.versionCode.toInteger() + versionName project.properties.versionName multiDexEnabled true testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner" } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 3c8e103..dd10ed1 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -6,12 +6,14 @@ + + + + + + + + diff --git a/android/src/main/scala/com/nutomic/ensichat/App.scala b/android/src/main/scala/com/nutomic/ensichat/App.scala new file mode 100644 index 0000000..f133d60 --- /dev/null +++ b/android/src/main/scala/com/nutomic/ensichat/App.scala @@ -0,0 +1,15 @@ +package com.nutomic.ensichat + +import android.support.multidex.MultiDexApplication +import com.nutomic.ensichat.core.interfaces.Log +import com.nutomic.ensichat.util.{Logging, PRNGFixes} + +class App extends MultiDexApplication { + + override def onCreate(): Unit = { + super.onCreate() + Log.setLogInstance(new Logging()) + PRNGFixes.apply() + } + +} \ No newline at end of file diff --git a/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala index 936baf6..ff271ee 100644 --- a/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala +++ b/android/src/main/scala/com/nutomic/ensichat/activities/ConnectionsActivity.scala @@ -19,8 +19,6 @@ import com.nutomic.ensichat.views.UsersAdapter */ class ConnectionsActivity extends EnsichatActivity with OnItemClickListener { - private val Tag = "AddContactsActivity" - private lazy val database = new Database(this) private lazy val adapter = new UsersAdapter(this) diff --git a/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala b/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala index 86905a6..40d826b 100644 --- a/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala +++ b/android/src/main/scala/com/nutomic/ensichat/activities/FirstStartActivity.scala @@ -11,8 +11,8 @@ import android.view.{KeyEvent, View} import android.widget.TextView.OnEditorActionListener import android.widget.{Button, EditText, TextView} import com.nutomic.ensichat.R -import com.nutomic.ensichat.core.interfaces.Settings -import com.nutomic.ensichat.core.interfaces.Settings._ +import com.nutomic.ensichat.core.interfaces.SettingsInterface +import com.nutomic.ensichat.core.interfaces.SettingsInterface._ /** * Shown on first start, lets the user enter their name. @@ -69,11 +69,11 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w preferences .edit() .putBoolean(KeyIsFirstStart, false) - .putString(Settings.KeyUserName, username.getText.toString.trim) - .putString(Settings.KeyUserStatus, Settings.DefaultUserStatus) - .putBoolean(Settings.KeyNotificationSoundsOn, DefaultNotificationSoundsOn) - .putString(Settings.KeyScanInterval, DefaultScanInterval.toString) - .putString(Settings.KeyMaxConnections, DefaultMaxConnections.toString) + .putString(SettingsInterface.KeyUserName, username.getText.toString.trim) + .putString(SettingsInterface.KeyUserStatus, SettingsInterface.DefaultUserStatus) + .putBoolean(SettingsInterface.KeyNotificationSoundsOn, DefaultNotificationSoundsOn) + .putString(SettingsInterface.KeyScanInterval, DefaultScanInterval.toString) + .putString(SettingsInterface.KeyMaxConnections, DefaultMaxConnections.toString) .apply() startMainActivity() diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala similarity index 89% rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala index 639cfaf..100f1e8 100644 --- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ConnectThread.scala +++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothConnectThread.scala @@ -8,7 +8,7 @@ import android.util.Log /** * Attempts to connect to another device and calls [[onConnected]] on success. */ -class ConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread { +class BluetoothConnectThread(device: Device, onConnected: (Device, BluetoothSocket) => Unit) extends Thread { private val Tag = "ConnectThread" diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothDevice.scala similarity index 100% rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/Device.scala rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothDevice.scala diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala index f2a83c0..12b488d 100644 --- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala +++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothInterface.scala @@ -9,7 +9,7 @@ import android.preference.PreferenceManager import android.util.Log import com.nutomic.ensichat.R import com.nutomic.ensichat.core.body.ConnectionInfo -import com.nutomic.ensichat.core.interfaces.{Settings, TransmissionInterface} +import com.nutomic.ensichat.core.interfaces.{SettingsInterface, TransmissionInterface} import com.nutomic.ensichat.core.{Address, ConnectionHandler, Message} import com.nutomic.ensichat.service.ChatService @@ -38,9 +38,9 @@ class BluetoothInterface(context: Context, mainHandler: Handler, private var devices = new HashMap[Device.ID, Device]() - private var connections = new HashMap[Device.ID, TransferThread]() + private var connections = new HashMap[Device.ID, BluetoothTransferThread]() - private var listenThread: Option[ListenThread] = None + private var listenThread: Option[BluetoothListenThread] = None private var cancelDiscovery = false @@ -86,7 +86,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler, * Starts discovery and listening. */ private def startBluetoothConnections(): Unit = { - listenThread = Some(new ListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened)) + listenThread = Some(new BluetoothListenThread(context.getString(R.string.app_name), btAdapter, connectionOpened)) listenThread.get.start() cancelDiscovery = false discover() @@ -106,7 +106,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler, val pm = PreferenceManager.getDefaultSharedPreferences(context) val scanInterval = - pm.getString(Settings.KeyScanInterval, Settings.DefaultScanInterval.toString).toInt * 1000 + pm.getString(SettingsInterface.KeyScanInterval, SettingsInterface.DefaultScanInterval.toString).toInt * 1000 mainHandler.postDelayed(new Runnable { override def run(): Unit = discover() }, scanInterval) @@ -128,7 +128,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler, override def onReceive(context: Context, intent: Intent): Unit = { discovered.filterNot(d => connections.keySet.contains(d.id)) .foreach { d => - new ConnectThread(d, connectionOpened).start() + new BluetoothConnectThread(d, connectionOpened).start() devices += (d.id -> d) } discovered = Set[Device]() @@ -162,7 +162,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler, def connectionOpened(device: Device, socket: BluetoothSocket): Unit = { devices += (device.id -> device) connections += (device.id -> - new TransferThread(context, device, socket, this, crypto, onReceiveMessage)) + new BluetoothTransferThread(context, device, socket, this, crypto, onReceiveMessage)) connections(device.id).start() } @@ -198,13 +198,18 @@ class BluetoothInterface(context: Context, mainHandler: Handler, /** * Sends the message to nextHop. */ - override def send(nextHop: Address, msg: Message): Unit = - connections.get(addressDeviceMap(nextHop)).foreach(_.send(msg)) + override def send(nextHop: Address, msg: Message): Unit = { + addressDeviceMap + .find(_._1 == nextHop) + .map(i => connections.get(i._2)) + .getOrElse(None) + .foreach(_.send(msg)) + } /** * Returns all active Bluetooth connections. */ - def getConnections: Set[Address] = + override def getConnections: Set[Address] = connections.map(x => addressDeviceMap.find(_._2 == x._1).get._1).toSet } diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala similarity index 95% rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala index 7bc2734..accb172 100644 --- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/ListenThread.scala +++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothListenThread.scala @@ -10,7 +10,7 @@ import android.util.Log * * @param name Service name to broadcast. */ -class ListenThread(name: String, adapter: BluetoothAdapter, +class BluetoothListenThread(name: String, adapter: BluetoothAdapter, onConnected: (Device, BluetoothSocket) => Unit) extends Thread { private val Tag = "ListenThread" diff --git a/android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala similarity index 95% rename from android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala rename to android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala index e033743..406ec82 100644 --- a/android/src/main/scala/com/nutomic/ensichat/bluetooth/TransferThread.scala +++ b/android/src/main/scala/com/nutomic/ensichat/bluetooth/BluetoothTransferThread.scala @@ -17,7 +17,7 @@ import com.nutomic.ensichat.core.{Address, Crypto, Message} * @param socket An open socket to the given device. * @param onReceive Called when a message was received from the other device. */ -class TransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface, +class BluetoothTransferThread(context: Context, device: Device, socket: BluetoothSocket, handler: BluetoothInterface, crypto: Crypto, onReceive: (Message, Device.ID) => Unit) extends Thread { private val Tag = "TransferThread" @@ -30,6 +30,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket, } catch { case e: IOException => Log.e(Tag, "Failed to open stream", e) + close() null } @@ -39,6 +40,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket, } catch { case e: IOException => Log.e(Tag, "Failed to open stream", e) + close() null } diff --git a/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala index 09621e2..e375407 100644 --- a/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala +++ b/android/src/main/scala/com/nutomic/ensichat/fragments/ContactsFragment.scala @@ -15,7 +15,7 @@ import android.view._ import android.widget.{ListView, TextView} import com.nutomic.ensichat.R import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity} -import com.nutomic.ensichat.core.interfaces.Settings +import com.nutomic.ensichat.core.interfaces.SettingsInterface import com.nutomic.ensichat.service.{CallbackHandler, ChatService} import com.nutomic.ensichat.util.Database import com.nutomic.ensichat.views.UsersAdapter @@ -102,7 +102,7 @@ class ContactsFragment extends ListFragment with OnClickListener { bundle.putString( IdenticonFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString) bundle.putString( - IdenticonFragment.ExtraUserName, prefs.getString(Settings.KeyUserName, "")) + IdenticonFragment.ExtraUserName, prefs.getString(SettingsInterface.KeyUserName, "")) fragment.setArguments(bundle) fragment.show(getFragmentManager, "dialog") true diff --git a/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala b/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala index 7c5aed4..724629a 100644 --- a/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala +++ b/android/src/main/scala/com/nutomic/ensichat/fragments/SettingsFragment.scala @@ -3,14 +3,13 @@ package com.nutomic.ensichat.fragments import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Bundle -import android.preference.Preference.OnPreferenceChangeListener -import android.preference.{Preference, PreferenceFragment, PreferenceManager} -import com.nutomic.ensichat.{BuildConfig, R} +import android.preference.{PreferenceFragment, PreferenceManager} import com.nutomic.ensichat.activities.EnsichatActivity import com.nutomic.ensichat.core.body.UserInfo -import com.nutomic.ensichat.core.interfaces.Settings._ +import com.nutomic.ensichat.core.interfaces.SettingsInterface._ import com.nutomic.ensichat.fragments.SettingsFragment._ import com.nutomic.ensichat.util.Database +import com.nutomic.ensichat.{BuildConfig, R} object SettingsFragment { val Version = "version" diff --git a/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala b/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala index 8b570d7..0301bd7 100644 --- a/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala +++ b/android/src/main/scala/com/nutomic/ensichat/service/CallbackHandler.scala @@ -2,8 +2,8 @@ package com.nutomic.ensichat.service import android.content.{Context, Intent} import android.support.v4.content.LocalBroadcastManager -import com.nutomic.ensichat.core.{ConnectionHandler, Message} import com.nutomic.ensichat.core.interfaces.CallbackInterface +import com.nutomic.ensichat.core.{ConnectionHandler, Message} import com.nutomic.ensichat.service.CallbackHandler._ object CallbackHandler { diff --git a/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala b/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala index 92da36c..73d321c 100644 --- a/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala +++ b/android/src/main/scala/com/nutomic/ensichat/service/ChatService.scala @@ -6,9 +6,8 @@ import android.app.Service import android.content.{Context, Intent} import android.os.Handler import com.nutomic.ensichat.bluetooth.BluetoothInterface -import com.nutomic.ensichat.core.interfaces.Log import com.nutomic.ensichat.core.{ConnectionHandler, Crypto} -import com.nutomic.ensichat.util.{Database, PRNGFixes, SettingsWrapper} +import com.nutomic.ensichat.util.{Database, SettingsWrapper} object ChatService { @@ -17,6 +16,8 @@ object ChatService { private def keyFolder(context: Context) = new File(context.getFilesDir, "keys") def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context)) + val ActionNetworkChanged = "network_changed" + } class ChatService extends Service { @@ -29,23 +30,26 @@ class ChatService extends Service { private lazy val connectionHandler = new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler, - ChatService.keyFolder(this)) + ChatService.newCrypto(this)) override def onBind(intent: Intent) = binder - override def onStartCommand(intent: Intent, flags: Int, startId: Int) = Service.START_STICKY + override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = { + if (intent.getAction == ChatService.ActionNetworkChanged) + connectionHandler.internetConnectionChanged() + + Service.START_STICKY + } /** * Generates keys and starts Bluetooth interface. */ override def onCreate(): Unit = { super.onCreate() - PRNGFixes.apply() - Log.setLogClass(classOf[android.util.Log]) notificationHandler.showPersistentNotification() + connectionHandler.addTransmissionInterface(new BluetoothInterface(this, new Handler(), + connectionHandler)) connectionHandler.start() - connectionHandler.setTransmissionInterface(new BluetoothInterface(this, new Handler(), - connectionHandler)) } override def onDestroy(): Unit = { diff --git a/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala b/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala index 8210e06..ca8bf06 100644 --- a/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala +++ b/android/src/main/scala/com/nutomic/ensichat/service/NotificationHandler.scala @@ -8,7 +8,7 @@ import com.nutomic.ensichat.R import com.nutomic.ensichat.activities.MainActivity import com.nutomic.ensichat.core.Message import com.nutomic.ensichat.core.body.Text -import com.nutomic.ensichat.core.interfaces.Settings +import com.nutomic.ensichat.core.interfaces.SettingsInterface import com.nutomic.ensichat.service.NotificationHandler._ object NotificationHandler { @@ -65,7 +65,7 @@ class NotificationHandler(context: Context) { */ private def defaults(): Int = { val sp = PreferenceManager.getDefaultSharedPreferences(context) - if (sp.getBoolean(Settings.KeyNotificationSoundsOn, Settings.DefaultNotificationSoundsOn)) + if (sp.getBoolean(SettingsInterface.KeyNotificationSoundsOn, SettingsInterface.DefaultNotificationSoundsOn)) Notification.DEFAULT_ALL else Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS diff --git a/android/src/main/scala/com/nutomic/ensichat/util/Logging.scala b/android/src/main/scala/com/nutomic/ensichat/util/Logging.scala new file mode 100644 index 0000000..e49cabe --- /dev/null +++ b/android/src/main/scala/com/nutomic/ensichat/util/Logging.scala @@ -0,0 +1,13 @@ +package com.nutomic.ensichat.util + +import android.util +import com.nutomic.ensichat.core.interfaces.Log + +class Logging extends Log { + + def v(tag: String, message: String, tr: Throwable = null) = util.Log.v(tag, message, tr) + def d(tag: String, message: String, tr: Throwable = null) = util.Log.d(tag, message, tr) + def i(tag: String, message: String, tr: Throwable = null) = util.Log.i(tag, message, tr) + def w(tag: String, message: String, tr: Throwable = null) = util.Log.w(tag, message, tr) + def e(tag: String, message: String, tr: Throwable = null) = util.Log.e(tag, message, tr) +} \ No newline at end of file diff --git a/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala b/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala new file mode 100644 index 0000000..8760284 --- /dev/null +++ b/android/src/main/scala/com/nutomic/ensichat/util/NetworkChangedReceiver.scala @@ -0,0 +1,15 @@ +package com.nutomic.ensichat.util + +import android.content.{Intent, Context, BroadcastReceiver} +import android.net.ConnectivityManager +import com.nutomic.ensichat.service.ChatService + +class NetworkChangedReceiver extends BroadcastReceiver { + + override def onReceive(context: Context, intent: Intent): Unit = { + val intent = new Intent(context, classOf[ChatService]) + intent.setAction(ChatService.ActionNetworkChanged) + context.startService(intent) + } + +} diff --git a/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala b/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala index fc8bd8e..17c7ae8 100644 --- a/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala +++ b/android/src/main/scala/com/nutomic/ensichat/util/SettingsWrapper.scala @@ -2,9 +2,9 @@ package com.nutomic.ensichat.util import android.content.Context import android.preference.PreferenceManager -import com.nutomic.ensichat.core.interfaces.Settings +import com.nutomic.ensichat.core.interfaces.SettingsInterface -class SettingsWrapper(context: Context) extends Settings { +class SettingsWrapper(context: Context) extends SettingsInterface { private val prefs = PreferenceManager.getDefaultSharedPreferences(context) diff --git a/core/src/main/scala/com/nutomic/ensichat/core/Address.scala b/core/src/main/scala/com/nutomic/ensichat/core/Address.scala index 59ba756..cab8638 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/Address.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/Address.scala @@ -24,7 +24,7 @@ object Address { * * @param bytes SHA-256 hash of the node's public key. */ -case class Address(bytes: Array[Byte]) { +final case class Address(bytes: Array[Byte]) { require(bytes.length == Address.Length, "Invalid address length (was " + bytes.length + ")") diff --git a/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala b/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala index 90aa9f1..4332552 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/ConnectionHandler.scala @@ -6,6 +6,7 @@ import java.util.Date import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo} import com.nutomic.ensichat.core.header.ContentHeader import com.nutomic.ensichat.core.interfaces._ +import com.nutomic.ensichat.core.internet.InternetInterface import com.nutomic.ensichat.core.util.FutureHelper import scala.concurrent.ExecutionContext.Implicits.global @@ -13,14 +14,12 @@ import scala.concurrent.ExecutionContext.Implicits.global /** * High-level handling of all message transfers and callbacks. */ -class ConnectionHandler(settings: Settings, database: DatabaseInterface, - callbacks: CallbackInterface, keyFolder: File) { +final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInterface, + callbacks: CallbackInterface, crypto: Crypto) { private val Tag = "ConnectionHandler" - private lazy val crypto = new Crypto(settings, keyFolder) - - private var transmissionInterface: TransmissionInterface = _ + private var transmissionInterfaces = Set[TransmissionInterface]() private lazy val router = new Router(connections, sendVia) @@ -40,16 +39,20 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface, FutureHelper { crypto.generateLocalKeys() Log.i(Tag, "Service started, address is " + crypto.localAddress) + transmissionInterfaces += new InternetInterface(this, crypto) + transmissionInterfaces.foreach(_.create()) } } def stop(): Unit = { - transmissionInterface.destroy() + transmissionInterfaces.foreach(_.destroy()) } - def setTransmissionInterface(interface: TransmissionInterface) = { - transmissionInterface = interface - transmissionInterface.create() + /** + * NOTE: This *must* be called before [[start()]], or it will have no effect. + */ + def addTransmissionInterface(interface: TransmissionInterface) = { + transmissionInterfaces += interface } /** @@ -70,7 +73,7 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface, } private def sendVia(nextHop: Address, msg: Message) = - transmissionInterface.send(nextHop, msg) + transmissionInterfaces.foreach(_.send(nextHop, msg)) /** * Decrypts and verifies incoming messages, forwards valid ones to [[onNewMessage()]]. @@ -78,10 +81,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface, def onMessageReceived(msg: Message): Unit = { if (msg.header.target == crypto.localAddress) { crypto.verifyAndDecrypt(msg) match { - case Some(msg) => onNewMessage(msg) - case None => - Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin) - return + case Some(m) => onNewMessage(m) + case None => Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin) } } else { router.onReceive(msg) @@ -118,7 +119,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface, * @return True if the connection is valid */ def onConnectionOpened(msg: Message): Boolean = { - val maxConnections = settings.get(Settings.KeyMaxConnections, Settings.DefaultMaxConnections.toString).toInt + val maxConnections = settings.get(SettingsInterface.KeyMaxConnections, + SettingsInterface.DefaultMaxConnections.toString).toInt if (connections().size == maxConnections) { Log.i(Tag, "Maximum number of connections reached") return false @@ -144,17 +146,22 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface, } Log.i(Tag, "Node " + sender + " connected") - sendTo(sender, new UserInfo(settings.get(Settings.KeyUserName, ""), - settings.get(Settings.KeyUserStatus, ""))) + sendTo(sender, new UserInfo(settings.get(SettingsInterface.KeyUserName, ""), + settings.get(SettingsInterface.KeyUserStatus, ""))) callbacks.onConnectionsChanged() true } def onConnectionClosed() = callbacks.onConnectionsChanged() - def connections() = transmissionInterface.getConnections + def connections(): Set[Address] = transmissionInterfaces.flatMap(_.getConnections) def getUser(address: Address) = knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, "")) + def internetConnectionChanged(): Unit = { + transmissionInterfaces + .find(_.isInstanceOf[InternetInterface]) + .foreach(_.asInstanceOf[InternetInterface].connectionChanged()) + } } diff --git a/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala b/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala index 5e72d31..cc5cd81 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/Crypto.scala @@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey} import com.nutomic.ensichat.core.Crypto._ import com.nutomic.ensichat.core.body._ import com.nutomic.ensichat.core.header.ContentHeader -import com.nutomic.ensichat.core.interfaces.{Log, Settings} +import com.nutomic.ensichat.core.interfaces.{Log, SettingsInterface} object Crypto { @@ -20,6 +20,11 @@ object Crypto { */ val PublicKeyAlgorithm = "RSA" + /** + * Algorithm used to read public keys. + */ + val CipherAlgorithm = "RSA/ECB/PKCS1Padding" + /** * Length of the local public/private keypair in bits. */ @@ -50,7 +55,7 @@ object Crypto { /** * Name of the preference where the local address is stored. */ - private val LocalAddressKey = "local_address" + val LocalAddressKey = "local_address" /** * Filename of the local private key in [[Crypto.keyFolder]]. @@ -60,7 +65,7 @@ object Crypto { /** * Filename of the local public key in [[Crypto.keyFolder]]. */ - private val PublicKeyAlias = "local-public" + val PublicKeyAlias = "local-public" } @@ -69,17 +74,17 @@ object Crypto { * * @param keyFolder Folder where private and public keys are stored. */ -class Crypto(settings: Settings, keyFolder: File) { +class Crypto(settings: SettingsInterface, keyFolder: File) { private val Tag = "Crypto" /** - * Generates a new key pair using [[ [[Crypto.keyFolder]].]] with [[KeySize]] bits and stores the + * Generates a new key pair using [[keyFolder]] with [[PublicKeySize]] bits and stores the * keys. * * Does nothing if the key pair already exists. */ - def generateLocalKeys(): Unit = { + private[core] def generateLocalKeys(): Unit = { if (localKeysExist) return @@ -92,7 +97,7 @@ class Crypto(settings: Settings, keyFolder: File) { address = calculateAddress(keyPair.getPublic) - // The hash must have at least one bit set to not collide with the broadcast address. + // Never generate an invalid address. } while(address == Address.Broadcast || address == Address.Null) settings.put(LocalAddressKey, address.toString) @@ -105,7 +110,7 @@ class Crypto(settings: Settings, keyFolder: File) { /** * Returns true if we have a public key stored for the given device. */ - def havePublicKey(address: Address): Boolean = new File(keyFolder, address.toString).exists() + private[core] def havePublicKey(address: Address) = new File(keyFolder, address.toString).exists() /** * Returns the public key for the given device. @@ -113,7 +118,7 @@ class Crypto(settings: Settings, keyFolder: File) { * @throws RuntimeException If the key does not exist. */ @throws[RuntimeException] - def getPublicKey(address: Address): PublicKey = { + private[core] def getPublicKey(address: Address): PublicKey = { loadKey(address.toString, classOf[PublicKey]) } @@ -123,7 +128,7 @@ class Crypto(settings: Settings, keyFolder: File) { * @throws RuntimeException If a key already exists for this address. */ @throws[RuntimeException] - def addPublicKey(address: Address, key: PublicKey): Unit = { + private[core] def addPublicKey(address: Address, key: PublicKey): Unit = { if (havePublicKey(address)) throw new RuntimeException("Already have key for " + address + ", not overwriting") @@ -138,7 +143,7 @@ class Crypto(settings: Settings, keyFolder: File) { new Message(msg.header, new CryptoData(Option(sig.sign), msg.crypto.key), msg.body) } - def verify(msg: Message, key: Option[PublicKey] = None): Boolean = { + private[core] def verify(msg: Message, key: Option[PublicKey] = None): Boolean = { val sig = Signature.getInstance(SigningAlgorithm) lazy val defaultKey = loadKey(msg.header.origin.toString, classOf[PublicKey]) sig.initVerify(key.getOrElse(defaultKey)) @@ -149,7 +154,7 @@ class Crypto(settings: Settings, keyFolder: File) { /** * Returns true if the local private and public key exist. */ - def localKeysExist = new File(keyFolder, PublicKeyAlias).exists() + private[core] def localKeysExist = new File(keyFolder, PublicKeyAlias).exists() /** * Returns the local public key. @@ -193,7 +198,7 @@ class Crypto(settings: Settings, keyFolder: File) { * @return The key read from storage. * @throws RuntimeException If the key does not exist. */ - private def loadKey[T](alias: String, keyType: Class[T]): T = { + private[core] def loadKey[T](alias: String, keyType: Class[T]): T = { val path = new File(keyFolder, alias) if (!path.exists()) { throw new RuntimeException("The requested key with alias " + alias + " does not exist") @@ -221,11 +226,11 @@ class Crypto(settings: Settings, keyFolder: File) { } } - def encryptAndSign(msg: Message, key: Option[PublicKey] = None): Message = { + private[core] def encryptAndSign(msg: Message, key: Option[PublicKey] = None): Message = { sign(encrypt(msg, key)) } - def verifyAndDecrypt(msg: Message, key: Option[PublicKey] = None): Option[Message] = { + private[core] def verifyAndDecrypt(msg: Message, key: Option[PublicKey] = None): Option[Message] = { if (verify(msg, key)) Option(decrypt(msg)) else @@ -240,7 +245,7 @@ class Crypto(settings: Settings, keyFolder: File) { val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.body.write)) // Asymmetric encryption of secret key - val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm) + val asymmetricCipher = Cipher.getInstance(CipherAlgorithm) lazy val defaultKey = loadKey(msg.header.target.toString, classOf[PublicKey]) asymmetricCipher.init(Cipher.WRAP_MODE, key.getOrElse(defaultKey)) @@ -250,7 +255,7 @@ class Crypto(settings: Settings, keyFolder: File) { private def decrypt(msg: Message): Message = { // Asymmetric decryption of secret key - val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm) + val asymmetricCipher = Cipher.getInstance(CipherAlgorithm) asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey])) val key = asymmetricCipher.unwrap(msg.crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY) diff --git a/core/src/main/scala/com/nutomic/ensichat/core/Router.scala b/core/src/main/scala/com/nutomic/ensichat/core/Router.scala index d138922..9f6415d 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/Router.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/Router.scala @@ -5,7 +5,7 @@ import com.nutomic.ensichat.core.header.{ContentHeader, MessageHeader} /** * Forwards messages to all connected devices. */ -class Router(activeConnections: () => Set[Address], send: (Address, Message) => Unit) { +final private[core] class Router(activeConnections: () => Set[Address], send: (Address, Message) => Unit) { private var messageSeen = Set[(Address, Int)]() diff --git a/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala b/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala index 3674602..ca39c49 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/SeqNumGenerator.scala @@ -1,12 +1,12 @@ package com.nutomic.ensichat.core import com.nutomic.ensichat.core.header.ContentHeader -import com.nutomic.ensichat.core.interfaces.Settings +import com.nutomic.ensichat.core.interfaces.SettingsInterface /** * Generates sequence numbers according to protocol, which are stored persistently. */ -class SeqNumGenerator(preferences: Settings) { +final private[core] class SeqNumGenerator(preferences: SettingsInterface) { private val KeySequenceNumber = "sequence_number" diff --git a/core/src/main/scala/com/nutomic/ensichat/core/User.scala b/core/src/main/scala/com/nutomic/ensichat/core/User.scala index e82e073..412181c 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/User.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/User.scala @@ -1,3 +1,3 @@ package com.nutomic.ensichat.core -case class User(address: Address, name: String, status: String) +final case class User(address: Address, name: String, status: String) diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala index 550e2c2..092df0c 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/body/ConnectionInfo.scala @@ -32,7 +32,7 @@ object ConnectionInfo { /** * Holds a node's public key. */ -case class ConnectionInfo(key: PublicKey) extends MessageBody { +final case class ConnectionInfo(key: PublicKey) extends MessageBody { override def protocolType = ConnectionInfo.Type diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala index f0fdbe1..bd3a55b 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/body/CryptoData.scala @@ -35,7 +35,7 @@ object CryptoData { /** * Holds the signature and (optional) key that are stored in a message. */ -case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) { +final case class CryptoData(signature: Option[Array[Byte]], key: Option[Array[Byte]]) { override def equals(a: Any): Boolean = a match { case o: CryptoData => util.Arrays.equals(signature.orNull, o.signature.orNull) && diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala index 2447cbb..5c6c18d 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/body/EncryptedBody.scala @@ -3,7 +3,7 @@ package com.nutomic.ensichat.core.body /** * Represents the data in an encrypted message body. */ -case class EncryptedBody(data: Array[Byte]) extends MessageBody { +final case class EncryptedBody(data: Array[Byte]) extends MessageBody { override def protocolType = -1 diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala index eda34d1..d99bff6 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/body/Text.scala @@ -25,7 +25,7 @@ object Text { /** * Holds a plain text message. */ -case class Text(text: String) extends MessageBody { +final case class Text(text: String) extends MessageBody { override def protocolType = -1 diff --git a/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala b/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala index d8c9cbc..73a1fdb 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/body/UserInfo.scala @@ -29,7 +29,7 @@ object UserInfo { /** * Holds display name and status of the sender. */ -case class UserInfo(name: String, status: String) extends MessageBody { +final case class UserInfo(name: String, status: String) extends MessageBody { override def protocolType = -1 diff --git a/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala index 1eed421..dd9c7a7 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/header/ContentHeader.scala @@ -39,7 +39,7 @@ object ContentHeader { * * This is [[AbstractHeader]] with messageId and time fields set. */ -case class ContentHeader(override val origin: Address, +final case class ContentHeader(override val origin: Address, override val target: Address, override val seqNum: Int, contentType: Int, diff --git a/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala b/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala index 90c7b78..c5060b5 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/header/MessageHeader.scala @@ -41,7 +41,7 @@ object MessageHeader { * * This is the same as [[AbstractHeader]]. */ -case class MessageHeader(override val protocolType: Int, +final case class MessageHeader(override val protocolType: Int, override val origin: Address, override val target: Address, override val seqNum: Int, diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala index e828441..e06f316 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/CallbackInterface.scala @@ -7,4 +7,5 @@ trait CallbackInterface { def onMessageReceived(msg: Message): Unit def onConnectionsChanged(): Unit + } diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala index c565b43..d9a3c47 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Log.scala @@ -2,28 +2,24 @@ package com.nutomic.ensichat.core.interfaces object Log { - def setLogClass[T](logClass: Class[T]) = { - this.logClass = Option(logClass) - } + def setLogInstance(log: Log) = instance = Option(log) - private var logClass: Option[Class[_]] = None + private var instance: Option[Log] = None - def v(tag: String, message: String, tr: Throwable = null) = log("v", tag, message, tr) - - def d(tag: String, message: String, tr: Throwable = null) = log("d", tag, message, tr) - - def i(tag: String, message: String, tr: Throwable = null) = log("i", tag, message, tr) - - def w(tag: String, message: String, tr: Throwable = null) = log("w", tag, message, tr) - - def e(tag: String, message: String, tr: Throwable = null) = log("e", tag, message, tr) - - private def log(level: String, tag: String, message: String, throwable: Throwable) = logClass match { - case Some(l) => - l.getMethod(level, classOf[String], classOf[String], classOf[Throwable]) - .invoke(null, tag, message, throwable) - case None => - System.out.println(level + tag + message + throwable) - } + def v(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.v(tag, message, tr)) + def d(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.d(tag, message, tr)) + def i(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.i(tag, message, tr)) + def w(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.w(tag, message, tr)) + def e(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.e(tag, message, tr)) + +} + +trait Log { + + def v(tag: String, message: String, tr: Throwable = null) + def d(tag: String, message: String, tr: Throwable = null) + def i(tag: String, message: String, tr: Throwable = null) + def w(tag: String, message: String, tr: Throwable = null) + def e(tag: String, message: String, tr: Throwable = null) } diff --git a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala similarity index 87% rename from core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala rename to core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala index 93c0824..1965f85 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/interfaces/Settings.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/interfaces/SettingsInterface.scala @@ -1,6 +1,6 @@ package com.nutomic.ensichat.core.interfaces -object Settings { +object SettingsInterface { val KeyUserName = "user_name" val KeyUserStatus = "user_status" @@ -26,9 +26,9 @@ object Settings { /** * Interface for persistent storage of key value pairs. * - * Must support at least storage of strings and integers. + * Must support at least storage of String, Int, Long. */ -trait Settings { +trait SettingsInterface { def put[T](key: String, value: T): Unit def get[T](key: String, default: T): T diff --git a/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala new file mode 100644 index 0000000..be911cf --- /dev/null +++ b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetConnectionThread.scala @@ -0,0 +1,87 @@ +package com.nutomic.ensichat.core.internet + +import java.io.{IOException, InputStream, OutputStream} +import java.net.{InetAddress, Socket} + +import com.nutomic.ensichat.core.Message.ReadMessageException +import com.nutomic.ensichat.core.body.ConnectionInfo +import com.nutomic.ensichat.core.header.MessageHeader +import com.nutomic.ensichat.core.interfaces.Log +import com.nutomic.ensichat.core.{Address, Crypto, Message} + +/** + * Encapsulates an active connection to another node. + */ +class InternetConnectionThread(socket: Socket, crypto: Crypto, onDisconnected: (InternetConnectionThread) => Unit, + onReceive: (Message, InternetConnectionThread) => Unit) extends Thread { + + private val Tag = "InternetConnectionThread" + + private val inStream: InputStream = + try { + socket.getInputStream + } catch { + case e: IOException => + Log.e(Tag, "Failed to open stream", e) + close() + null + } + + private val outStream: OutputStream = + try { + socket.getOutputStream + } catch { + case e: IOException => + Log.e(Tag, "Failed to open stream", e) + close() + null + } + + def internetAddress(): InetAddress = { + socket.getInetAddress + } + + override def run(): Unit = { + Log.i(Tag, "Connection opened to " + socket.getInetAddress) + + send(crypto.sign(new Message(new MessageHeader(ConnectionInfo.Type, + Address.Null, Address.Null, 0), new ConnectionInfo(crypto.getLocalPublicKey)))) + + socket.setKeepAlive(true) + while (socket.isConnected) { + try { + if (inStream.available() > 0) { + val msg = Message.read(inStream) + + onReceive(msg, this) + } + } catch { + case e @ (_: ReadMessageException | _: IOException) => + Log.w(Tag, "Failed to read incoming message", e) + close() + return + } + } + close() + Log.d(Tag, "exited: " + socket.isConnected) + } + + def send(msg: Message): Unit = { + try { + outStream.write(msg.write) + } catch { + case e: IOException => Log.e(Tag, "Failed to write message", e) + } + } + + def close(): Unit = { + try { + socket.close() + } catch { + case e: IOException => Log.w(Tag, "Failed to close socket", e) + } + Log.d(Tag, "Connection to " + socket.getInetAddress + " closed") + onDisconnected(this) + } + +} diff --git a/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala new file mode 100644 index 0000000..9a36795 --- /dev/null +++ b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetInterface.scala @@ -0,0 +1,129 @@ +package com.nutomic.ensichat.core.internet + +import java.io.IOException +import java.net.{InetAddress, Socket} + +import com.nutomic.ensichat.core.body.ConnectionInfo +import com.nutomic.ensichat.core.interfaces.{Log, TransmissionInterface} +import com.nutomic.ensichat.core.util.FutureHelper +import com.nutomic.ensichat.core.{Address, ConnectionHandler, Crypto, Message} +import scala.concurrent.ExecutionContext.Implicits.global + +object InternetInterface { + + val Port = 26344 + + val BootstrapNodes = Set("192.168.1.104:26344", // T420 + "46.101.229.4:26344") // digital ocean + +} + +/** + * Handles all Internet connectivity. + * + * TODO: send message through internet node: android -> server -> android + * TODO: send test reply to incoming messages + * TODO: send a list of all connected nodes when we connect to a node + */ +class InternetInterface(connectionHandler: ConnectionHandler, crypto: Crypto) + extends TransmissionInterface { + + private val Tag = "InternetInterface" + + private lazy val serverThread = new InternetServerThread(crypto, onConnected, onDisconnected, onReceiveMessage) + + private var connections = Set[InternetConnectionThread]() + + private var addressDeviceMap = Map[Address, InternetConnectionThread]() + + /** + * Initializes and starts discovery and listening. + */ + override def create(): Unit = { + FutureHelper { + serverThread.start() + InternetInterface.BootstrapNodes.foreach(openConnection) + } + } + + /** + * Stops discovery and listening. + */ + override def destroy(): Unit = { + serverThread.cancel() + connections.foreach(_.close()) + } + + private def openConnection(addressPort: String): Unit = { + val split = addressPort.split(":") + openConnection(split(0), split(1).toInt) + } + + /** + * Opens connection to the specified IP address in client mode. + */ + private def openConnection(nodeAddress: String, port: Int): Unit = { + try { + val socket = new Socket(InetAddress.getByName(nodeAddress), port) + val ct = new InternetConnectionThread(socket, crypto, onDisconnected, onReceiveMessage) + connections += ct + ct.start() + } catch { + case e: IOException => + Log.w(Tag, "Failed to open connection to " + nodeAddress + ":" + port, e) + } + } + + private def onConnected(connectionThread: InternetConnectionThread): Unit = { + connections += connectionThread + } + + private def onDisconnected(connectionThread: InternetConnectionThread): Unit = { + addressDeviceMap.find(_._2 == connectionThread).foreach { ad => + Log.d(Tag, "Connection closed to " + ad._1) + connections -= connectionThread + addressDeviceMap -= ad._1 + connectionHandler.onConnectionClosed() + } + } + + private def onReceiveMessage(msg: Message, thread: InternetConnectionThread): Unit = msg.body match { + case info: ConnectionInfo => + val address = crypto.calculateAddress(info.key) + if (address == crypto.localAddress) { + Log.i(Tag, "Address " + address + " is me, not connecting to myself") + thread.close() + return + } + + // Service.onConnectionOpened sends message, so mapping already needs to be in place. + addressDeviceMap += (address -> thread) + if (!connectionHandler.onConnectionOpened(msg)) + addressDeviceMap -= address + case _ => + connectionHandler.onMessageReceived(msg) + } + + /** + * Sends the message to nextHop. + */ + override def send(nextHop: Address, msg: Message): Unit = { + addressDeviceMap + .find(_._1 == nextHop) + .foreach(_._2.send(msg)) + } + + /** + * Returns all active Internet connections. + */ + override def getConnections = addressDeviceMap.keySet + + def connectionChanged(): Unit = { + FutureHelper { + Log.i(Tag, "Network has changed. Close all connections and connect to bootstrap nodes again") + connections.foreach(_.close()) + InternetInterface.BootstrapNodes.foreach(openConnection) + } + } + +} diff --git a/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala new file mode 100644 index 0000000..d296a84 --- /dev/null +++ b/core/src/main/scala/com/nutomic/ensichat/core/internet/InternetServerThread.scala @@ -0,0 +1,44 @@ +package com.nutomic.ensichat.core.internet + +import java.io.{IOException, PrintStream} +import java.net.{Socket, ServerSocket} + +import com.nutomic.ensichat.core.{Message, Crypto} +import com.nutomic.ensichat.core.interfaces.Log + +import scala.io.BufferedSource + +class InternetServerThread(crypto: Crypto, onConnected: (InternetConnectionThread) => Unit, + onDisconnected: (InternetConnectionThread) => Unit, onReceive: (Message, InternetConnectionThread) => Unit) extends Thread { + + private val Tag = "InternetServerThread" + + private lazy val socket: Option[ServerSocket] = try { + Option(new ServerSocket(InternetInterface.Port)) + } catch { + case e: IOException => + Log.w(Tag, "Failed to create server socket", e) + None + } + + override def run(): Unit = { + try { + while (socket.get.isBound) { + val connection = new InternetConnectionThread(socket.get.accept(), crypto, onDisconnected, onReceive) + onConnected(connection) + connection.start() + } + } catch { + case e: IOException => Log.w(Tag, "Failed to accept connection", e) + } + } + + def cancel(): Unit = { + try { + socket.get.close() + } catch { + case e: IOException => Log.w(Tag, "Failed to close socket", e) + } + } + +} diff --git a/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala b/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala index 55ae730..4dba573 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/util/BufferUtils.scala @@ -5,7 +5,7 @@ import java.nio.ByteBuffer /** * Provides various helper methods for [[ByteBuffer]]. */ -object BufferUtils { +private[core] object BufferUtils { def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort diff --git a/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala b/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala index 17fd04a..76b3702 100644 --- a/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala +++ b/core/src/main/scala/com/nutomic/ensichat/core/util/FutureHelper.scala @@ -21,7 +21,7 @@ object FutureHelper { // cross-platform way to execute on the foreground thread. // We use this to make sure exceptions are not hidden in the logs. Log.e(Tag, "Exception in Future", e) - System.exit(-1) + //System.exit(-1) } f } diff --git a/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala b/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala index 7bad8be..d08a215 100644 --- a/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala +++ b/core/src/test/scala/com/nutomic/ensichat/core/CryptoTest.scala @@ -2,13 +2,13 @@ package com.nutomic.ensichat.core import java.io.File -import com.nutomic.ensichat.core.interfaces.Settings +import com.nutomic.ensichat.core.interfaces.SettingsInterface import junit.framework.TestCase import org.junit.Assert._ object CryptoTest { - class TestSettings extends Settings { + class TestSettings extends SettingsInterface { private var map = Map[String, Any]() override def get[T](key: String, default: T): T = map.getOrElse(key, default).asInstanceOf[T] override def put[T](key: String, value: T): Unit = map += (key -> value) diff --git a/gradle.properties b/gradle.properties index 5d08ba7..5f6cf19 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,7 @@ # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true \ No newline at end of file +# org.gradle.parallel=true + +versionName=0.1.7 +versionCode=8 diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +/build diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 0000000..43810ec --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'scala' +apply plugin: 'application' + +dependencies { + compile 'org.scala-lang:scala-library:2.11.7' + compile project(path: ':core') + compile 'com.github.scopt:scopt_2.10:3.3.0' +} + +mainClassName = 'com.nutomic.ensichat.server.Main' +version = properties.versionName +applicationName = 'ensichat' + +run { + // Use this to pass command line arguments via `gradle run`. + // Example: + // ``` + // ./gradlew server:run -Pargs="--help" + // ``` + if (project.hasProperty('args')) { + args project.args.split('\\s+') + } +} diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Config.scala b/server/src/main/scala/com/nutomic/ensichat/server/Config.scala new file mode 100644 index 0000000..8de29d6 --- /dev/null +++ b/server/src/main/scala/com/nutomic/ensichat/server/Config.scala @@ -0,0 +1,3 @@ +package com.nutomic.ensichat.server + +case class Config(name: Option[String] = None) diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Database.scala b/server/src/main/scala/com/nutomic/ensichat/server/Database.scala new file mode 100644 index 0000000..dc23fa7 --- /dev/null +++ b/server/src/main/scala/com/nutomic/ensichat/server/Database.scala @@ -0,0 +1,23 @@ +package com.nutomic.ensichat.server + +import com.nutomic.ensichat.core.interfaces.DatabaseInterface +import com.nutomic.ensichat.core.{Address, Message, User} + +class Database extends DatabaseInterface { + + private var contacts = Set[User]() + + def onMessageReceived(msg: Message): Unit = {} + + def getContacts: Set[User] = contacts + + def getContact(address: Address): Option[User] = contacts.find(_.address == address) + + def addContact(contact: User): Unit = contacts += contact + + def updateContact(contact: User): Unit = { + contacts = contacts.filterNot(_.address == contact.address) + contacts += contact + } + +} diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Logging.scala b/server/src/main/scala/com/nutomic/ensichat/server/Logging.scala new file mode 100644 index 0000000..e384c0e --- /dev/null +++ b/server/src/main/scala/com/nutomic/ensichat/server/Logging.scala @@ -0,0 +1,42 @@ +package com.nutomic.ensichat.server + +import java.io.{PrintWriter, StringWriter} +import java.text.DateFormat +import java.util.{Date, Locale} + +import com.nutomic.ensichat.core.interfaces.Log + +import scala.collection.mutable + +class Logging extends Log { + + private val logs = new mutable.Queue[String]() + + def dequeue(): Seq[String] = logs.dequeueAll((String) => true) + + private def enqueue(tag: String, message: String, tr: Option[Throwable]): Unit = { + val df = DateFormat.getTimeInstance(DateFormat.DEFAULT, Locale.UK) + val throwableString = tr.map { tr => + val sw = new StringWriter() + tr.printStackTrace(new PrintWriter(sw)) + "\n" + sw.toString + } + logs.enqueue(df.format(new Date()) + " " + tag + ": " + message + throwableString.getOrElse("")) + } + + def v(tag: String, message: String, tr: Throwable = null): Unit = + enqueue("V/" + tag, message, Option(tr)) + + def d(tag: String, message: String, tr: Throwable = null): Unit = + enqueue("D/" + tag, message, Option(tr)) + + def i(tag: String, message: String, tr: Throwable = null): Unit = + enqueue("I/" + tag, message, Option(tr)) + + def w(tag: String, message: String, tr: Throwable = null): Unit = + enqueue("W/" + tag, message, Option(tr)) + + def e(tag: String, message: String, tr: Throwable = null): Unit = + enqueue("E/" + tag, message, Option(tr)) + +} \ No newline at end of file diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Main.scala b/server/src/main/scala/com/nutomic/ensichat/server/Main.scala new file mode 100644 index 0000000..96c9fe9 --- /dev/null +++ b/server/src/main/scala/com/nutomic/ensichat/server/Main.scala @@ -0,0 +1,74 @@ +package com.nutomic.ensichat.server + +import java.io.File +import java.util.concurrent.TimeUnit + +import com.nutomic.ensichat.core.body.Text +import com.nutomic.ensichat.core.interfaces.{CallbackInterface, Log, SettingsInterface} +import com.nutomic.ensichat.core.{Message, ConnectionHandler, Crypto} +import scopt.OptionParser + +object Main extends App with CallbackInterface { + + private val Tag = "Main" + + private val ConfigFolder = new File(System.getProperty("user.home"), ".config/ensichat") + private val ConfigFile = new File(ConfigFolder, "config.properties") + private val KeyFolder = new File(ConfigFolder, "keys") + + private val LogInterval = TimeUnit.SECONDS.toMillis(1) + + private lazy val logInstance = new Logging() + private lazy val settings = new Settings(ConfigFile) + private lazy val crypto = new Crypto(settings, KeyFolder) + private lazy val connectionHandler = new ConnectionHandler(settings, new Database(), this, crypto) + + init() + + private def init(): Unit = { + ConfigFolder.mkdirs() + KeyFolder.mkdirs() + Log.setLogInstance(logInstance) + sys.addShutdownHook(connectionHandler.stop()) + val parser = new OptionParser[Config]("ensichat") { + head("ensichat") + opt[String]('n', "name") action { (x, c) => + c.copy(name = Option(x)) + } text "the username for this node (optional)" + help("help") text "prints this usage text" + } + + parser.parse(args, Config()).foreach { config => + config.name.foreach(settings.put(SettingsInterface.KeyUserName, _)) + run() + } + } + + private def run(): Unit = { + connectionHandler.start() + + // Keep alive and print logs + while (true) { + Thread.sleep(LogInterval) + logInstance.dequeue().foreach(System.out.println) + } + } + + def onMessageReceived(msg: Message): Unit = { + if (msg.header.target != crypto.localAddress) + return + + msg.body match { + case text: Text => + val address = msg.header.origin + val name = connectionHandler.getUser(address).name + connectionHandler.sendTo(address, new Text("Hello " + name)) + Log.i(Tag, "Received text: " + text.text) + case _ => + Log.i(Tag, "Received msg: " + msg.body) + } + } + + def onConnectionsChanged(): Unit = {} + +} diff --git a/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala b/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala new file mode 100644 index 0000000..39473b3 --- /dev/null +++ b/server/src/main/scala/com/nutomic/ensichat/server/Settings.scala @@ -0,0 +1,53 @@ +package com.nutomic.ensichat.server + +import java.io._ +import java.util.Properties + +import com.nutomic.ensichat.core.interfaces.{Log, SettingsInterface} + +import scala.collection.JavaConverters._ + +class Settings(file: File) extends SettingsInterface { + + private val Tag = "Settings" + + if (!file.exists()) { + file.createNewFile() + put(SettingsInterface.KeyUserName, "unknown user") + } + + private lazy val props: Properties = { + val p = new Properties() + try { + val fis = new InputStreamReader(new FileInputStream(file), "UTF-8") + p.load(fis) + fis.close() + } catch { + case e: IOException => Log.w(Tag, "Failed to load settings from " + file, e) + } + p + } + + def put[T](key: String, value: T): Unit = { + props.asScala.put(key, value.toString) + try { + val fos = new OutputStreamWriter(new FileOutputStream(file), "UTF-8") + props.store(fos, "") + fos.close() + } catch { + case e: IOException => Log.w(Tag, "Failed to write preference for key " + key, e) + } + } + + def get[T](key: String, default: T): T = { + val value = props.asScala.getOrElse[String](key, default.toString) + val cast = default match { + case _: Int => value.toInt + case _: Long => value.toLong + case _: String => value + } + // This has no effect due to type erasure, but is needed to avoid compiler error. + cast.asInstanceOf[T] + } + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 00e5526..65899c0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':android', ':core' +include ':android', ':core', ':server'