Added server project for internet routing.
Also adjusted Log trait and visibility of library classes.
This commit is contained in:
parent
28cb4f15d9
commit
199b185861
49 changed files with 671 additions and 121 deletions
|
@ -37,8 +37,8 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.nutomic.ensichat"
|
applicationId "com.nutomic.ensichat"
|
||||||
targetSdkVersion 23
|
targetSdkVersion 23
|
||||||
versionCode 8
|
versionCode project.properties.versionCode.toInteger()
|
||||||
versionName "0.1.7"
|
versionName project.properties.versionName
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
|
testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="android.support.multidex.MultiDexApplication"
|
android:name=".App"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="true"
|
android:fullBackupContent="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
|
@ -60,6 +62,12 @@
|
||||||
|
|
||||||
<service android:name=".service.ChatService" />
|
<service android:name=".service.ChatService" />
|
||||||
|
|
||||||
|
<receiver android:name=".util.NetworkChangedReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
15
android/src/main/scala/com/nutomic/ensichat/App.scala
Normal file
15
android/src/main/scala/com/nutomic/ensichat/App.scala
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,8 +19,6 @@ import com.nutomic.ensichat.views.UsersAdapter
|
||||||
*/
|
*/
|
||||||
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
class ConnectionsActivity extends EnsichatActivity with OnItemClickListener {
|
||||||
|
|
||||||
private val Tag = "AddContactsActivity"
|
|
||||||
|
|
||||||
private lazy val database = new Database(this)
|
private lazy val database = new Database(this)
|
||||||
|
|
||||||
private lazy val adapter = new UsersAdapter(this)
|
private lazy val adapter = new UsersAdapter(this)
|
||||||
|
|
|
@ -11,8 +11,8 @@ import android.view.{KeyEvent, View}
|
||||||
import android.widget.TextView.OnEditorActionListener
|
import android.widget.TextView.OnEditorActionListener
|
||||||
import android.widget.{Button, EditText, TextView}
|
import android.widget.{Button, EditText, TextView}
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.core.interfaces.Settings
|
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||||
import com.nutomic.ensichat.core.interfaces.Settings._
|
import com.nutomic.ensichat.core.interfaces.SettingsInterface._
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shown on first start, lets the user enter their name.
|
* Shown on first start, lets the user enter their name.
|
||||||
|
@ -69,11 +69,11 @@ class FirstStartActivity extends AppCompatActivity with OnEditorActionListener w
|
||||||
preferences
|
preferences
|
||||||
.edit()
|
.edit()
|
||||||
.putBoolean(KeyIsFirstStart, false)
|
.putBoolean(KeyIsFirstStart, false)
|
||||||
.putString(Settings.KeyUserName, username.getText.toString.trim)
|
.putString(SettingsInterface.KeyUserName, username.getText.toString.trim)
|
||||||
.putString(Settings.KeyUserStatus, Settings.DefaultUserStatus)
|
.putString(SettingsInterface.KeyUserStatus, SettingsInterface.DefaultUserStatus)
|
||||||
.putBoolean(Settings.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
|
.putBoolean(SettingsInterface.KeyNotificationSoundsOn, DefaultNotificationSoundsOn)
|
||||||
.putString(Settings.KeyScanInterval, DefaultScanInterval.toString)
|
.putString(SettingsInterface.KeyScanInterval, DefaultScanInterval.toString)
|
||||||
.putString(Settings.KeyMaxConnections, DefaultMaxConnections.toString)
|
.putString(SettingsInterface.KeyMaxConnections, DefaultMaxConnections.toString)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
startMainActivity()
|
startMainActivity()
|
||||||
|
|
|
@ -8,7 +8,7 @@ import android.util.Log
|
||||||
/**
|
/**
|
||||||
* Attempts to connect to another device and calls [[onConnected]] on success.
|
* 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"
|
private val Tag = "ConnectThread"
|
||||||
|
|
|
@ -9,7 +9,7 @@ import android.preference.PreferenceManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.core.body.ConnectionInfo
|
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.core.{Address, ConnectionHandler, Message}
|
||||||
import com.nutomic.ensichat.service.ChatService
|
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 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
|
private var cancelDiscovery = false
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||||
* Starts discovery and listening.
|
* Starts discovery and listening.
|
||||||
*/
|
*/
|
||||||
private def startBluetoothConnections(): Unit = {
|
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()
|
listenThread.get.start()
|
||||||
cancelDiscovery = false
|
cancelDiscovery = false
|
||||||
discover()
|
discover()
|
||||||
|
@ -106,7 +106,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||||
|
|
||||||
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val scanInterval =
|
val scanInterval =
|
||||||
pm.getString(Settings.KeyScanInterval, Settings.DefaultScanInterval.toString).toInt * 1000
|
pm.getString(SettingsInterface.KeyScanInterval, SettingsInterface.DefaultScanInterval.toString).toInt * 1000
|
||||||
mainHandler.postDelayed(new Runnable {
|
mainHandler.postDelayed(new Runnable {
|
||||||
override def run(): Unit = discover()
|
override def run(): Unit = discover()
|
||||||
}, scanInterval)
|
}, scanInterval)
|
||||||
|
@ -128,7 +128,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||||
override def onReceive(context: Context, intent: Intent): Unit = {
|
override def onReceive(context: Context, intent: Intent): Unit = {
|
||||||
discovered.filterNot(d => connections.keySet.contains(d.id))
|
discovered.filterNot(d => connections.keySet.contains(d.id))
|
||||||
.foreach { d =>
|
.foreach { d =>
|
||||||
new ConnectThread(d, connectionOpened).start()
|
new BluetoothConnectThread(d, connectionOpened).start()
|
||||||
devices += (d.id -> d)
|
devices += (d.id -> d)
|
||||||
}
|
}
|
||||||
discovered = Set[Device]()
|
discovered = Set[Device]()
|
||||||
|
@ -162,7 +162,7 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||||
def connectionOpened(device: Device, socket: BluetoothSocket): Unit = {
|
def connectionOpened(device: Device, socket: BluetoothSocket): Unit = {
|
||||||
devices += (device.id -> device)
|
devices += (device.id -> device)
|
||||||
connections += (device.id ->
|
connections += (device.id ->
|
||||||
new TransferThread(context, device, socket, this, crypto, onReceiveMessage))
|
new BluetoothTransferThread(context, device, socket, this, crypto, onReceiveMessage))
|
||||||
connections(device.id).start()
|
connections(device.id).start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,13 +198,18 @@ class BluetoothInterface(context: Context, mainHandler: Handler,
|
||||||
/**
|
/**
|
||||||
* Sends the message to nextHop.
|
* Sends the message to nextHop.
|
||||||
*/
|
*/
|
||||||
override def send(nextHop: Address, msg: Message): Unit =
|
override def send(nextHop: Address, msg: Message): Unit = {
|
||||||
connections.get(addressDeviceMap(nextHop)).foreach(_.send(msg))
|
addressDeviceMap
|
||||||
|
.find(_._1 == nextHop)
|
||||||
|
.map(i => connections.get(i._2))
|
||||||
|
.getOrElse(None)
|
||||||
|
.foreach(_.send(msg))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all active Bluetooth connections.
|
* 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
|
connections.map(x => addressDeviceMap.find(_._2 == x._1).get._1).toSet
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import android.util.Log
|
||||||
*
|
*
|
||||||
* @param name Service name to broadcast.
|
* @param name Service name to broadcast.
|
||||||
*/
|
*/
|
||||||
class ListenThread(name: String, adapter: BluetoothAdapter,
|
class BluetoothListenThread(name: String, adapter: BluetoothAdapter,
|
||||||
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
onConnected: (Device, BluetoothSocket) => Unit) extends Thread {
|
||||||
|
|
||||||
private val Tag = "ListenThread"
|
private val Tag = "ListenThread"
|
|
@ -17,7 +17,7 @@ import com.nutomic.ensichat.core.{Address, Crypto, Message}
|
||||||
* @param socket An open socket to the given device.
|
* @param socket An open socket to the given device.
|
||||||
* @param onReceive Called when a message was received from the other 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 {
|
crypto: Crypto, onReceive: (Message, Device.ID) => Unit) extends Thread {
|
||||||
|
|
||||||
private val Tag = "TransferThread"
|
private val Tag = "TransferThread"
|
||||||
|
@ -30,6 +30,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.e(Tag, "Failed to open stream", e)
|
Log.e(Tag, "Failed to open stream", e)
|
||||||
|
close()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +40,7 @@ class TransferThread(context: Context, device: Device, socket: BluetoothSocket,
|
||||||
} catch {
|
} catch {
|
||||||
case e: IOException =>
|
case e: IOException =>
|
||||||
Log.e(Tag, "Failed to open stream", e)
|
Log.e(Tag, "Failed to open stream", e)
|
||||||
|
close()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import android.view._
|
||||||
import android.widget.{ListView, TextView}
|
import android.widget.{ListView, TextView}
|
||||||
import com.nutomic.ensichat.R
|
import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.activities.{ConnectionsActivity, EnsichatActivity, MainActivity, SettingsActivity}
|
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.service.{CallbackHandler, ChatService}
|
||||||
import com.nutomic.ensichat.util.Database
|
import com.nutomic.ensichat.util.Database
|
||||||
import com.nutomic.ensichat.views.UsersAdapter
|
import com.nutomic.ensichat.views.UsersAdapter
|
||||||
|
@ -102,7 +102,7 @@ class ContactsFragment extends ListFragment with OnClickListener {
|
||||||
bundle.putString(
|
bundle.putString(
|
||||||
IdenticonFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString)
|
IdenticonFragment.ExtraAddress, ChatService.newCrypto(getActivity).localAddress.toString)
|
||||||
bundle.putString(
|
bundle.putString(
|
||||||
IdenticonFragment.ExtraUserName, prefs.getString(Settings.KeyUserName, ""))
|
IdenticonFragment.ExtraUserName, prefs.getString(SettingsInterface.KeyUserName, ""))
|
||||||
fragment.setArguments(bundle)
|
fragment.setArguments(bundle)
|
||||||
fragment.show(getFragmentManager, "dialog")
|
fragment.show(getFragmentManager, "dialog")
|
||||||
true
|
true
|
||||||
|
|
|
@ -3,14 +3,13 @@ package com.nutomic.ensichat.fragments
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.Preference.OnPreferenceChangeListener
|
import android.preference.{PreferenceFragment, PreferenceManager}
|
||||||
import android.preference.{Preference, PreferenceFragment, PreferenceManager}
|
|
||||||
import com.nutomic.ensichat.{BuildConfig, R}
|
|
||||||
import com.nutomic.ensichat.activities.EnsichatActivity
|
import com.nutomic.ensichat.activities.EnsichatActivity
|
||||||
import com.nutomic.ensichat.core.body.UserInfo
|
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.fragments.SettingsFragment._
|
||||||
import com.nutomic.ensichat.util.Database
|
import com.nutomic.ensichat.util.Database
|
||||||
|
import com.nutomic.ensichat.{BuildConfig, R}
|
||||||
|
|
||||||
object SettingsFragment {
|
object SettingsFragment {
|
||||||
val Version = "version"
|
val Version = "version"
|
||||||
|
|
|
@ -2,8 +2,8 @@ package com.nutomic.ensichat.service
|
||||||
|
|
||||||
import android.content.{Context, Intent}
|
import android.content.{Context, Intent}
|
||||||
import android.support.v4.content.LocalBroadcastManager
|
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.interfaces.CallbackInterface
|
||||||
|
import com.nutomic.ensichat.core.{ConnectionHandler, Message}
|
||||||
import com.nutomic.ensichat.service.CallbackHandler._
|
import com.nutomic.ensichat.service.CallbackHandler._
|
||||||
|
|
||||||
object CallbackHandler {
|
object CallbackHandler {
|
||||||
|
|
|
@ -6,9 +6,8 @@ import android.app.Service
|
||||||
import android.content.{Context, Intent}
|
import android.content.{Context, Intent}
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
||||||
import com.nutomic.ensichat.core.interfaces.Log
|
|
||||||
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
|
import com.nutomic.ensichat.core.{ConnectionHandler, Crypto}
|
||||||
import com.nutomic.ensichat.util.{Database, PRNGFixes, SettingsWrapper}
|
import com.nutomic.ensichat.util.{Database, SettingsWrapper}
|
||||||
|
|
||||||
object ChatService {
|
object ChatService {
|
||||||
|
|
||||||
|
@ -17,6 +16,8 @@ object ChatService {
|
||||||
private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
|
private def keyFolder(context: Context) = new File(context.getFilesDir, "keys")
|
||||||
def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
|
def newCrypto(context: Context) = new Crypto(new SettingsWrapper(context), keyFolder(context))
|
||||||
|
|
||||||
|
val ActionNetworkChanged = "network_changed"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatService extends Service {
|
class ChatService extends Service {
|
||||||
|
@ -29,23 +30,26 @@ class ChatService extends Service {
|
||||||
|
|
||||||
private lazy val connectionHandler =
|
private lazy val connectionHandler =
|
||||||
new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
|
new ConnectionHandler(new SettingsWrapper(this), new Database(this), callbackHandler,
|
||||||
ChatService.keyFolder(this))
|
ChatService.newCrypto(this))
|
||||||
|
|
||||||
override def onBind(intent: Intent) = binder
|
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.
|
* Generates keys and starts Bluetooth interface.
|
||||||
*/
|
*/
|
||||||
override def onCreate(): Unit = {
|
override def onCreate(): Unit = {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
PRNGFixes.apply()
|
|
||||||
Log.setLogClass(classOf[android.util.Log])
|
|
||||||
notificationHandler.showPersistentNotification()
|
notificationHandler.showPersistentNotification()
|
||||||
|
connectionHandler.addTransmissionInterface(new BluetoothInterface(this, new Handler(),
|
||||||
|
connectionHandler))
|
||||||
connectionHandler.start()
|
connectionHandler.start()
|
||||||
connectionHandler.setTransmissionInterface(new BluetoothInterface(this, new Handler(),
|
|
||||||
connectionHandler))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override def onDestroy(): Unit = {
|
override def onDestroy(): Unit = {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import com.nutomic.ensichat.R
|
||||||
import com.nutomic.ensichat.activities.MainActivity
|
import com.nutomic.ensichat.activities.MainActivity
|
||||||
import com.nutomic.ensichat.core.Message
|
import com.nutomic.ensichat.core.Message
|
||||||
import com.nutomic.ensichat.core.body.Text
|
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._
|
import com.nutomic.ensichat.service.NotificationHandler._
|
||||||
|
|
||||||
object NotificationHandler {
|
object NotificationHandler {
|
||||||
|
@ -65,7 +65,7 @@ class NotificationHandler(context: Context) {
|
||||||
*/
|
*/
|
||||||
private def defaults(): Int = {
|
private def defaults(): Int = {
|
||||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
if (sp.getBoolean(Settings.KeyNotificationSoundsOn, Settings.DefaultNotificationSoundsOn))
|
if (sp.getBoolean(SettingsInterface.KeyNotificationSoundsOn, SettingsInterface.DefaultNotificationSoundsOn))
|
||||||
Notification.DEFAULT_ALL
|
Notification.DEFAULT_ALL
|
||||||
else
|
else
|
||||||
Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
|
Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ package com.nutomic.ensichat.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.preference.PreferenceManager
|
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)
|
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ object Address {
|
||||||
*
|
*
|
||||||
* @param bytes SHA-256 hash of the node's public key.
|
* @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 + ")")
|
require(bytes.length == Address.Length, "Invalid address length (was " + bytes.length + ")")
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Date
|
||||||
import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo}
|
import com.nutomic.ensichat.core.body.{ConnectionInfo, MessageBody, UserInfo}
|
||||||
import com.nutomic.ensichat.core.header.ContentHeader
|
import com.nutomic.ensichat.core.header.ContentHeader
|
||||||
import com.nutomic.ensichat.core.interfaces._
|
import com.nutomic.ensichat.core.interfaces._
|
||||||
|
import com.nutomic.ensichat.core.internet.InternetInterface
|
||||||
import com.nutomic.ensichat.core.util.FutureHelper
|
import com.nutomic.ensichat.core.util.FutureHelper
|
||||||
|
|
||||||
import scala.concurrent.ExecutionContext.Implicits.global
|
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.
|
* High-level handling of all message transfers and callbacks.
|
||||||
*/
|
*/
|
||||||
class ConnectionHandler(settings: Settings, database: DatabaseInterface,
|
final class ConnectionHandler(settings: SettingsInterface, database: DatabaseInterface,
|
||||||
callbacks: CallbackInterface, keyFolder: File) {
|
callbacks: CallbackInterface, crypto: Crypto) {
|
||||||
|
|
||||||
private val Tag = "ConnectionHandler"
|
private val Tag = "ConnectionHandler"
|
||||||
|
|
||||||
private lazy val crypto = new Crypto(settings, keyFolder)
|
private var transmissionInterfaces = Set[TransmissionInterface]()
|
||||||
|
|
||||||
private var transmissionInterface: TransmissionInterface = _
|
|
||||||
|
|
||||||
private lazy val router = new Router(connections, sendVia)
|
private lazy val router = new Router(connections, sendVia)
|
||||||
|
|
||||||
|
@ -40,16 +39,20 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
|
||||||
FutureHelper {
|
FutureHelper {
|
||||||
crypto.generateLocalKeys()
|
crypto.generateLocalKeys()
|
||||||
Log.i(Tag, "Service started, address is " + crypto.localAddress)
|
Log.i(Tag, "Service started, address is " + crypto.localAddress)
|
||||||
|
transmissionInterfaces += new InternetInterface(this, crypto)
|
||||||
|
transmissionInterfaces.foreach(_.create())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def stop(): Unit = {
|
def stop(): Unit = {
|
||||||
transmissionInterface.destroy()
|
transmissionInterfaces.foreach(_.destroy())
|
||||||
}
|
}
|
||||||
|
|
||||||
def setTransmissionInterface(interface: TransmissionInterface) = {
|
/**
|
||||||
transmissionInterface = interface
|
* NOTE: This *must* be called before [[start()]], or it will have no effect.
|
||||||
transmissionInterface.create()
|
*/
|
||||||
|
def addTransmissionInterface(interface: TransmissionInterface) = {
|
||||||
|
transmissionInterfaces += interface
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -70,7 +73,7 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
private def sendVia(nextHop: Address, msg: Message) =
|
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()]].
|
* 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 = {
|
def onMessageReceived(msg: Message): Unit = {
|
||||||
if (msg.header.target == crypto.localAddress) {
|
if (msg.header.target == crypto.localAddress) {
|
||||||
crypto.verifyAndDecrypt(msg) match {
|
crypto.verifyAndDecrypt(msg) match {
|
||||||
case Some(msg) => onNewMessage(msg)
|
case Some(m) => onNewMessage(m)
|
||||||
case None =>
|
case None => Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
|
||||||
Log.i(Tag, "Ignoring message with invalid signature from " + msg.header.origin)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
router.onReceive(msg)
|
router.onReceive(msg)
|
||||||
|
@ -118,7 +119,8 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
|
||||||
* @return True if the connection is valid
|
* @return True if the connection is valid
|
||||||
*/
|
*/
|
||||||
def onConnectionOpened(msg: Message): Boolean = {
|
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) {
|
if (connections().size == maxConnections) {
|
||||||
Log.i(Tag, "Maximum number of connections reached")
|
Log.i(Tag, "Maximum number of connections reached")
|
||||||
return false
|
return false
|
||||||
|
@ -144,17 +146,22 @@ class ConnectionHandler(settings: Settings, database: DatabaseInterface,
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i(Tag, "Node " + sender + " connected")
|
Log.i(Tag, "Node " + sender + " connected")
|
||||||
sendTo(sender, new UserInfo(settings.get(Settings.KeyUserName, ""),
|
sendTo(sender, new UserInfo(settings.get(SettingsInterface.KeyUserName, ""),
|
||||||
settings.get(Settings.KeyUserStatus, "")))
|
settings.get(SettingsInterface.KeyUserStatus, "")))
|
||||||
callbacks.onConnectionsChanged()
|
callbacks.onConnectionsChanged()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
def onConnectionClosed() = callbacks.onConnectionsChanged()
|
def onConnectionClosed() = callbacks.onConnectionsChanged()
|
||||||
|
|
||||||
def connections() = transmissionInterface.getConnections
|
def connections(): Set[Address] = transmissionInterfaces.flatMap(_.getConnections)
|
||||||
|
|
||||||
def getUser(address: Address) =
|
def getUser(address: Address) =
|
||||||
knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, ""))
|
knownUsers.find(_.address == address).getOrElse(new User(address, address.toString, ""))
|
||||||
|
|
||||||
|
def internetConnectionChanged(): Unit = {
|
||||||
|
transmissionInterfaces
|
||||||
|
.find(_.isInstanceOf[InternetInterface])
|
||||||
|
.foreach(_.asInstanceOf[InternetInterface].connectionChanged())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import javax.crypto.{Cipher, CipherOutputStream, KeyGenerator, SecretKey}
|
||||||
import com.nutomic.ensichat.core.Crypto._
|
import com.nutomic.ensichat.core.Crypto._
|
||||||
import com.nutomic.ensichat.core.body._
|
import com.nutomic.ensichat.core.body._
|
||||||
import com.nutomic.ensichat.core.header.ContentHeader
|
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 {
|
object Crypto {
|
||||||
|
|
||||||
|
@ -20,6 +20,11 @@ object Crypto {
|
||||||
*/
|
*/
|
||||||
val PublicKeyAlgorithm = "RSA"
|
val PublicKeyAlgorithm = "RSA"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Algorithm used to read public keys.
|
||||||
|
*/
|
||||||
|
val CipherAlgorithm = "RSA/ECB/PKCS1Padding"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Length of the local public/private keypair in bits.
|
* 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.
|
* 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]].
|
* Filename of the local private key in [[Crypto.keyFolder]].
|
||||||
|
@ -60,7 +65,7 @@ object Crypto {
|
||||||
/**
|
/**
|
||||||
* Filename of the local public key in [[Crypto.keyFolder]].
|
* 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.
|
* @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"
|
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.
|
* keys.
|
||||||
*
|
*
|
||||||
* Does nothing if the key pair already exists.
|
* Does nothing if the key pair already exists.
|
||||||
*/
|
*/
|
||||||
def generateLocalKeys(): Unit = {
|
private[core] def generateLocalKeys(): Unit = {
|
||||||
if (localKeysExist)
|
if (localKeysExist)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -92,7 +97,7 @@ class Crypto(settings: Settings, keyFolder: File) {
|
||||||
|
|
||||||
address = calculateAddress(keyPair.getPublic)
|
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)
|
} while(address == Address.Broadcast || address == Address.Null)
|
||||||
|
|
||||||
settings.put(LocalAddressKey, address.toString)
|
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.
|
* 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.
|
* 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 If the key does not exist.
|
||||||
*/
|
*/
|
||||||
@throws[RuntimeException]
|
@throws[RuntimeException]
|
||||||
def getPublicKey(address: Address): PublicKey = {
|
private[core] def getPublicKey(address: Address): PublicKey = {
|
||||||
loadKey(address.toString, classOf[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 If a key already exists for this address.
|
||||||
*/
|
*/
|
||||||
@throws[RuntimeException]
|
@throws[RuntimeException]
|
||||||
def addPublicKey(address: Address, key: PublicKey): Unit = {
|
private[core] def addPublicKey(address: Address, key: PublicKey): Unit = {
|
||||||
if (havePublicKey(address))
|
if (havePublicKey(address))
|
||||||
throw new RuntimeException("Already have key for " + address + ", not overwriting")
|
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)
|
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)
|
val sig = Signature.getInstance(SigningAlgorithm)
|
||||||
lazy val defaultKey = loadKey(msg.header.origin.toString, classOf[PublicKey])
|
lazy val defaultKey = loadKey(msg.header.origin.toString, classOf[PublicKey])
|
||||||
sig.initVerify(key.getOrElse(defaultKey))
|
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.
|
* 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.
|
* Returns the local public key.
|
||||||
|
@ -193,7 +198,7 @@ class Crypto(settings: Settings, keyFolder: File) {
|
||||||
* @return The key read from storage.
|
* @return The key read from storage.
|
||||||
* @throws RuntimeException If the key does not exist.
|
* @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)
|
val path = new File(keyFolder, alias)
|
||||||
if (!path.exists()) {
|
if (!path.exists()) {
|
||||||
throw new RuntimeException("The requested key with alias " + alias + " does not exist")
|
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))
|
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))
|
if (verify(msg, key))
|
||||||
Option(decrypt(msg))
|
Option(decrypt(msg))
|
||||||
else
|
else
|
||||||
|
@ -240,7 +245,7 @@ class Crypto(settings: Settings, keyFolder: File) {
|
||||||
val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.body.write))
|
val encrypted = new EncryptedBody(copyThroughCipher(symmetricCipher, msg.body.write))
|
||||||
|
|
||||||
// Asymmetric encryption of secret key
|
// 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])
|
lazy val defaultKey = loadKey(msg.header.target.toString, classOf[PublicKey])
|
||||||
asymmetricCipher.init(Cipher.WRAP_MODE, key.getOrElse(defaultKey))
|
asymmetricCipher.init(Cipher.WRAP_MODE, key.getOrElse(defaultKey))
|
||||||
|
|
||||||
|
@ -250,7 +255,7 @@ class Crypto(settings: Settings, keyFolder: File) {
|
||||||
|
|
||||||
private def decrypt(msg: Message): Message = {
|
private def decrypt(msg: Message): Message = {
|
||||||
// Asymmetric decryption of secret key
|
// Asymmetric decryption of secret key
|
||||||
val asymmetricCipher = Cipher.getInstance(PublicKeyAlgorithm)
|
val asymmetricCipher = Cipher.getInstance(CipherAlgorithm)
|
||||||
asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey]))
|
asymmetricCipher.init(Cipher.UNWRAP_MODE, loadKey(PrivateKeyAlias, classOf[PrivateKey]))
|
||||||
val key = asymmetricCipher.unwrap(msg.crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY)
|
val key = asymmetricCipher.unwrap(msg.crypto.key.get, SymmetricKeyAlgorithm, Cipher.SECRET_KEY)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import com.nutomic.ensichat.core.header.{ContentHeader, MessageHeader}
|
||||||
/**
|
/**
|
||||||
* Forwards messages to all connected devices.
|
* 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)]()
|
private var messageSeen = Set[(Address, Int)]()
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package com.nutomic.ensichat.core
|
package com.nutomic.ensichat.core
|
||||||
|
|
||||||
import com.nutomic.ensichat.core.header.ContentHeader
|
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.
|
* 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"
|
private val KeySequenceNumber = "sequence_number"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
package com.nutomic.ensichat.core
|
package com.nutomic.ensichat.core
|
||||||
|
|
||||||
case class User(address: Address, name: String, status: String)
|
final case class User(address: Address, name: String, status: String)
|
||||||
|
|
|
@ -32,7 +32,7 @@ object ConnectionInfo {
|
||||||
/**
|
/**
|
||||||
* Holds a node's public key.
|
* 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
|
override def protocolType = ConnectionInfo.Type
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ object CryptoData {
|
||||||
/**
|
/**
|
||||||
* Holds the signature and (optional) key that are stored in a message.
|
* 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 {
|
override def equals(a: Any): Boolean = a match {
|
||||||
case o: CryptoData => util.Arrays.equals(signature.orNull, o.signature.orNull) &&
|
case o: CryptoData => util.Arrays.equals(signature.orNull, o.signature.orNull) &&
|
||||||
|
|
|
@ -3,7 +3,7 @@ package com.nutomic.ensichat.core.body
|
||||||
/**
|
/**
|
||||||
* Represents the data in an encrypted message 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
|
override def protocolType = -1
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ object Text {
|
||||||
/**
|
/**
|
||||||
* Holds a plain text message.
|
* Holds a plain text message.
|
||||||
*/
|
*/
|
||||||
case class Text(text: String) extends MessageBody {
|
final case class Text(text: String) extends MessageBody {
|
||||||
|
|
||||||
override def protocolType = -1
|
override def protocolType = -1
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ object UserInfo {
|
||||||
/**
|
/**
|
||||||
* Holds display name and status of the sender.
|
* 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
|
override def protocolType = -1
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ object ContentHeader {
|
||||||
*
|
*
|
||||||
* This is [[AbstractHeader]] with messageId and time fields set.
|
* 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 target: Address,
|
||||||
override val seqNum: Int,
|
override val seqNum: Int,
|
||||||
contentType: Int,
|
contentType: Int,
|
||||||
|
|
|
@ -41,7 +41,7 @@ object MessageHeader {
|
||||||
*
|
*
|
||||||
* This is the same as [[AbstractHeader]].
|
* 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 origin: Address,
|
||||||
override val target: Address,
|
override val target: Address,
|
||||||
override val seqNum: Int,
|
override val seqNum: Int,
|
||||||
|
|
|
@ -7,4 +7,5 @@ trait CallbackInterface {
|
||||||
def onMessageReceived(msg: Message): Unit
|
def onMessageReceived(msg: Message): Unit
|
||||||
|
|
||||||
def onConnectionsChanged(): Unit
|
def onConnectionsChanged(): Unit
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,28 +2,24 @@ package com.nutomic.ensichat.core.interfaces
|
||||||
|
|
||||||
object Log {
|
object Log {
|
||||||
|
|
||||||
def setLogClass[T](logClass: Class[T]) = {
|
def setLogInstance(log: Log) = instance = Option(log)
|
||||||
this.logClass = Option(logClass)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 d(tag: String, message: String, tr: Throwable = null) = log("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 i(tag: String, message: String, tr: Throwable = null) = log("i", tag, message, tr)
|
def e(tag: String, message: String, tr: Throwable = null) = instance.foreach(_.e(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)
|
trait Log {
|
||||||
|
|
||||||
private def log(level: String, tag: String, message: String, throwable: Throwable) = logClass match {
|
def v(tag: String, message: String, tr: Throwable = null)
|
||||||
case Some(l) =>
|
def d(tag: String, message: String, tr: Throwable = null)
|
||||||
l.getMethod(level, classOf[String], classOf[String], classOf[Throwable])
|
def i(tag: String, message: String, tr: Throwable = null)
|
||||||
.invoke(null, tag, message, throwable)
|
def w(tag: String, message: String, tr: Throwable = null)
|
||||||
case None =>
|
def e(tag: String, message: String, tr: Throwable = null)
|
||||||
System.out.println(level + tag + message + throwable)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.nutomic.ensichat.core.interfaces
|
package com.nutomic.ensichat.core.interfaces
|
||||||
|
|
||||||
object Settings {
|
object SettingsInterface {
|
||||||
|
|
||||||
val KeyUserName = "user_name"
|
val KeyUserName = "user_name"
|
||||||
val KeyUserStatus = "user_status"
|
val KeyUserStatus = "user_status"
|
||||||
|
@ -26,9 +26,9 @@ object Settings {
|
||||||
/**
|
/**
|
||||||
* Interface for persistent storage of key value pairs.
|
* 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 put[T](key: String, value: T): Unit
|
||||||
def get[T](key: String, default: T): T
|
def get[T](key: String, default: T): T
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import java.nio.ByteBuffer
|
||||||
/**
|
/**
|
||||||
* Provides various helper methods for [[ByteBuffer]].
|
* Provides various helper methods for [[ByteBuffer]].
|
||||||
*/
|
*/
|
||||||
object BufferUtils {
|
private[core] object BufferUtils {
|
||||||
|
|
||||||
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
|
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ object FutureHelper {
|
||||||
// cross-platform way to execute on the foreground thread.
|
// cross-platform way to execute on the foreground thread.
|
||||||
// We use this to make sure exceptions are not hidden in the logs.
|
// We use this to make sure exceptions are not hidden in the logs.
|
||||||
Log.e(Tag, "Exception in Future", e)
|
Log.e(Tag, "Exception in Future", e)
|
||||||
System.exit(-1)
|
//System.exit(-1)
|
||||||
}
|
}
|
||||||
f
|
f
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ package com.nutomic.ensichat.core
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
import com.nutomic.ensichat.core.interfaces.Settings
|
import com.nutomic.ensichat.core.interfaces.SettingsInterface
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.junit.Assert._
|
import org.junit.Assert._
|
||||||
|
|
||||||
object CryptoTest {
|
object CryptoTest {
|
||||||
|
|
||||||
class TestSettings extends Settings {
|
class TestSettings extends SettingsInterface {
|
||||||
private var map = Map[String, Any]()
|
private var map = Map[String, Any]()
|
||||||
override def get[T](key: String, default: T): T = map.getOrElse(key, default).asInstanceOf[T]
|
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)
|
override def put[T](key: String, value: T): Unit = map += (key -> value)
|
||||||
|
|
|
@ -15,4 +15,7 @@
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# 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
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
# org.gradle.parallel=true
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
versionName=0.1.7
|
||||||
|
versionCode=8
|
||||||
|
|
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
23
server/build.gradle
Normal file
23
server/build.gradle
Normal file
|
@ -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+')
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.nutomic.ensichat.server
|
||||||
|
|
||||||
|
case class Config(name: Option[String] = None)
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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))
|
||||||
|
|
||||||
|
}
|
74
server/src/main/scala/com/nutomic/ensichat/server/Main.scala
Normal file
74
server/src/main/scala/com/nutomic/ensichat/server/Main.scala
Normal file
|
@ -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 = {}
|
||||||
|
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
include ':android', ':core'
|
include ':android', ':core', ':server'
|
||||||
|
|
Reference in a new issue