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 {
|
||||
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"
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<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-feature android:name="android.hardware.bluetooth" android:required="true" />
|
||||
|
||||
<application
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
|
@ -60,6 +62,12 @@
|
|||
|
||||
<service android:name=".service.ChatService" />
|
||||
|
||||
<receiver android:name=".util.NetworkChangedReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
</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 {
|
||||
|
||||
private val Tag = "AddContactsActivity"
|
||||
|
||||
private lazy val database = new Database(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.{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()
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.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)
|
||||
|
||||
|
|
|
@ -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 + ")")
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)]()
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) &&
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -7,4 +7,5 @@ trait CallbackInterface {
|
|||
def onMessageReceived(msg: Message): Unit
|
||||
|
||||
def onConnectionsChanged(): Unit
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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]].
|
||||
*/
|
||||
object BufferUtils {
|
||||
private[core] object BufferUtils {
|
||||
|
||||
def getUnsignedByte(bb: ByteBuffer): Short = (bb.get & 0xff).toShort
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
# 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