466 lines
16 KiB
Kotlin
466 lines
16 KiB
Kotlin
/*
|
|
* Copyright 2015 Christian Basler
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package ch.dissem.bitmessage
|
|
|
|
import ch.dissem.bitmessage.BitmessageContext.Companion.version
|
|
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_EXTRA_BYTES
|
|
import ch.dissem.bitmessage.InternalContext.Companion.NETWORK_NONCE_TRIALS_PER_BYTE
|
|
import ch.dissem.bitmessage.entity.BitmessageAddress
|
|
import ch.dissem.bitmessage.entity.CustomMessage
|
|
import ch.dissem.bitmessage.entity.MessagePayload
|
|
import ch.dissem.bitmessage.entity.Plaintext
|
|
import ch.dissem.bitmessage.entity.Plaintext.Status.DRAFT
|
|
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
|
|
import ch.dissem.bitmessage.entity.Plaintext.Type.MSG
|
|
import ch.dissem.bitmessage.entity.payload.Broadcast
|
|
import ch.dissem.bitmessage.entity.payload.ObjectType
|
|
import ch.dissem.bitmessage.entity.payload.Pubkey.Feature
|
|
import ch.dissem.bitmessage.entity.valueobject.PrivateKey
|
|
import ch.dissem.bitmessage.exception.DecryptionFailedException
|
|
import ch.dissem.bitmessage.factory.Factory
|
|
import ch.dissem.bitmessage.ports.*
|
|
import ch.dissem.bitmessage.utils.Property
|
|
import ch.dissem.bitmessage.utils.UnixTime.MINUTE
|
|
import org.slf4j.LoggerFactory
|
|
import java.net.InetAddress
|
|
import java.util.concurrent.CancellationException
|
|
import java.util.concurrent.ExecutionException
|
|
import kotlin.properties.Delegates
|
|
|
|
/**
|
|
*
|
|
* Use this class if you want to create a Bitmessage client.
|
|
* You'll need the Builder to create a BitmessageContext, and set the following properties:
|
|
*
|
|
* * addressRepo
|
|
* * inventory
|
|
* * nodeRegistry
|
|
* * networkHandler
|
|
* * messageRepo
|
|
* * streams
|
|
*
|
|
*
|
|
* The default implementations in the different module builds can be used.
|
|
*
|
|
* The port defaults to 8444 (the default Bitmessage port)
|
|
*/
|
|
class BitmessageContext private constructor(builder: BitmessageContext.Builder) {
|
|
|
|
/**
|
|
* The [InternalContext] - normally you wouldn't need it,
|
|
* unless you are doing something crazy with the protocol.
|
|
*/
|
|
val internals: InternalContext
|
|
@JvmName("internals") get
|
|
|
|
val labeler: Labeler
|
|
@JvmName("labeler") get
|
|
|
|
val addresses: AddressRepository
|
|
@JvmName("addresses") get
|
|
|
|
val labels: LabelRepository
|
|
@JvmName("labels") get
|
|
|
|
val messages: MessageRepository
|
|
@JvmName("messages") get
|
|
|
|
fun createIdentity(shorter: Boolean, vararg features: Feature): BitmessageAddress {
|
|
val identity = BitmessageAddress(PrivateKey(
|
|
shorter,
|
|
internals.streams[0],
|
|
NETWORK_NONCE_TRIALS_PER_BYTE,
|
|
NETWORK_EXTRA_BYTES,
|
|
*features
|
|
))
|
|
internals.addressRepository.save(identity)
|
|
if (internals.preferences.sendPubkeyOnIdentityCreation) {
|
|
internals.sendPubkey(identity, identity.stream)
|
|
}
|
|
return identity
|
|
}
|
|
|
|
fun joinChan(passphrase: String, address: String): BitmessageAddress {
|
|
val chan = BitmessageAddress.chan(address, passphrase)
|
|
chan.alias = passphrase
|
|
internals.addressRepository.save(chan)
|
|
return chan
|
|
}
|
|
|
|
fun createChan(passphrase: String): BitmessageAddress {
|
|
// FIXME: hardcoded stream number
|
|
val chan = BitmessageAddress.chan(1, passphrase)
|
|
internals.addressRepository.save(chan)
|
|
return chan
|
|
}
|
|
|
|
fun createDeterministicAddresses(
|
|
passphrase: String, numberOfAddresses: Int, version: Long, stream: Long, shorter: Boolean): List<BitmessageAddress> {
|
|
val result = BitmessageAddress.deterministic(
|
|
passphrase, numberOfAddresses, version, stream, shorter)
|
|
for (i in result.indices) {
|
|
val address = result[i]
|
|
address.alias = "deterministic (" + (i + 1) + ")"
|
|
internals.addressRepository.save(address)
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun broadcast(from: BitmessageAddress, subject: String, message: String) {
|
|
send(Plaintext(
|
|
type = BROADCAST,
|
|
from = from,
|
|
subject = subject,
|
|
body = message,
|
|
status = DRAFT
|
|
))
|
|
}
|
|
|
|
fun send(from: BitmessageAddress, to: BitmessageAddress, subject: String, message: String) {
|
|
if (from.privateKey == null) {
|
|
throw IllegalArgumentException("'From' must be an identity, i.e. have a private key.")
|
|
}
|
|
send(Plaintext(
|
|
type = MSG,
|
|
from = from,
|
|
to = to,
|
|
subject = subject,
|
|
body = message
|
|
))
|
|
}
|
|
|
|
fun send(msg: Plaintext) {
|
|
if (msg.from.privateKey == null) {
|
|
throw IllegalArgumentException("'From' must be an identity, i.e. have a private key.")
|
|
}
|
|
labeler.markAsSending(msg)
|
|
val to = msg.to
|
|
if (to != null) {
|
|
if (to.pubkey == null) {
|
|
LOG.info("Public key is missing from recipient. Requesting.")
|
|
internals.requestPubkey(to)
|
|
}
|
|
if (to.pubkey == null) {
|
|
internals.messageRepository.save(msg)
|
|
}
|
|
}
|
|
if (to == null || to.pubkey != null) {
|
|
LOG.info("Sending message.")
|
|
internals.messageRepository.save(msg)
|
|
if (msg.type == MSG) {
|
|
internals.send(msg)
|
|
} else {
|
|
internals.send(
|
|
msg.from,
|
|
to,
|
|
Factory.getBroadcast(msg),
|
|
msg.ttl
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun startup() {
|
|
internals.networkHandler.start()
|
|
}
|
|
|
|
fun shutdown() {
|
|
internals.networkHandler.stop()
|
|
}
|
|
|
|
/**
|
|
* @param host a trusted node that must be reliable (it's used for every synchronization)
|
|
* @param port of the trusted host, default is 8444
|
|
* @param timeoutInSeconds synchronization should end no later than about 5 seconds after the timeout elapsed,
|
|
* even if not all objects were fetched
|
|
* @param wait waits for the synchronization thread to finish
|
|
*/
|
|
fun synchronize(host: InetAddress, port: Int, timeoutInSeconds: Long, wait: Boolean) {
|
|
val future = internals.networkHandler.synchronize(host, port, timeoutInSeconds)
|
|
if (wait) {
|
|
try {
|
|
future.get()
|
|
} catch (e: InterruptedException) {
|
|
LOG.info("Thread was interrupted. Trying to shut down synchronization and returning.")
|
|
future.cancel(true)
|
|
} catch (e: CancellationException) {
|
|
LOG.debug(e.message, e)
|
|
} catch (e: ExecutionException) {
|
|
LOG.debug(e.message, e)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a custom message to a specific node (that should implement handling for this message type) and returns
|
|
* the response, which in turn is expected to be a [CustomMessage].
|
|
*
|
|
* @param server the node's address
|
|
* @param port the node's port
|
|
* @param request the request
|
|
* @return the response
|
|
*/
|
|
fun send(server: InetAddress, port: Int, request: CustomMessage): CustomMessage =
|
|
internals.networkHandler.send(server, port, request)
|
|
|
|
/**
|
|
* Removes expired objects from the inventory. You should call this method regularly,
|
|
* e.g. daily and on each shutdown.
|
|
*/
|
|
fun cleanup() {
|
|
internals.inventory.cleanup()
|
|
internals.nodeRegistry.cleanup()
|
|
}
|
|
|
|
/**
|
|
* Sends messages again whose time to live expired without being acknowledged. (And whose
|
|
* recipient is expected to send acknowledgements.
|
|
*
|
|
* You should call this method regularly, but be aware of the following:
|
|
*
|
|
* * As messages might be sent, POW will be done. It is therefore not advised to
|
|
* call it on shutdown.
|
|
* * It shouldn't be called right after startup, as it's possible the missing
|
|
* acknowledgement was sent while the client was offline.
|
|
* * Other than that, the call isn't expensive as long as there is no message
|
|
* to send, so it might be a good idea to just call it every few minutes.
|
|
*/
|
|
fun resendUnacknowledgedMessages() {
|
|
internals.resendUnacknowledged()
|
|
}
|
|
|
|
fun isRunning() = internals.networkHandler.isRunning
|
|
|
|
fun addContact(contact: BitmessageAddress) {
|
|
internals.addressRepository.save(contact)
|
|
if (contact.pubkey == null) {
|
|
// If it already existed, the saved contact might have the public key
|
|
if (internals.addressRepository.getAddress(contact.address)!!.pubkey == null) {
|
|
internals.requestPubkey(contact)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun addSubscribtion(address: BitmessageAddress) {
|
|
address.isSubscribed = true
|
|
internals.addressRepository.save(address)
|
|
tryToFindBroadcastsForAddress(address)
|
|
}
|
|
|
|
private fun tryToFindBroadcastsForAddress(address: BitmessageAddress) {
|
|
for (objectMessage in internals.inventory.getObjects(address.stream, Broadcast.getVersion(address), ObjectType.BROADCAST)) {
|
|
try {
|
|
val broadcast = objectMessage.payload as Broadcast
|
|
broadcast.decrypt(address)
|
|
// This decrypts it twice, but on the other hand it doesn't try to decrypt the objects with
|
|
// other subscriptions and the interface stays as simple as possible.
|
|
internals.networkListener.receive(objectMessage)
|
|
} catch (ignore: DecryptionFailedException) {
|
|
} catch (e: Exception) {
|
|
LOG.debug(e.message, e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun status(): Property {
|
|
return Property("status",
|
|
Property("user agent", internals.preferences.userAgent),
|
|
internals.networkHandler.getNetworkStatus(),
|
|
Property("unacknowledged", internals.messageRepository.findMessagesToResend().size)
|
|
)
|
|
}
|
|
|
|
interface Listener {
|
|
fun receive(plaintext: Plaintext)
|
|
|
|
/**
|
|
* A message listener that needs a [BitmessageContext], i.e. for implementing some sort of chat bot.
|
|
*/
|
|
interface WithContext : Listener {
|
|
fun setContext(ctx: BitmessageContext)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Kotlin users: you might want to use [BitmessageContext.build] instead.
|
|
*/
|
|
class Builder {
|
|
var inventory by Delegates.notNull<Inventory>()
|
|
var nodeRegistry by Delegates.notNull<NodeRegistry>()
|
|
var networkHandler by Delegates.notNull<NetworkHandler>()
|
|
var addressRepo by Delegates.notNull<AddressRepository>()
|
|
var labelRepo by Delegates.notNull<LabelRepository>()
|
|
var messageRepo by Delegates.notNull<MessageRepository>()
|
|
var proofOfWorkRepo by Delegates.notNull<ProofOfWorkRepository>()
|
|
var proofOfWorkEngine: ProofOfWorkEngine? = null
|
|
var cryptography by Delegates.notNull<Cryptography>()
|
|
var customCommandHandler: CustomCommandHandler? = null
|
|
var labeler: Labeler? = null
|
|
var listener by Delegates.notNull<Listener>()
|
|
val preferences = Preferences()
|
|
|
|
fun inventory(inventory: Inventory): Builder {
|
|
this.inventory = inventory
|
|
return this
|
|
}
|
|
|
|
fun nodeRegistry(nodeRegistry: NodeRegistry): Builder {
|
|
this.nodeRegistry = nodeRegistry
|
|
return this
|
|
}
|
|
|
|
fun networkHandler(networkHandler: NetworkHandler): Builder {
|
|
this.networkHandler = networkHandler
|
|
return this
|
|
}
|
|
|
|
fun addressRepo(addressRepo: AddressRepository): Builder {
|
|
this.addressRepo = addressRepo
|
|
return this
|
|
}
|
|
|
|
fun labelRepo(labelRepo: LabelRepository): Builder {
|
|
this.labelRepo = labelRepo
|
|
return this
|
|
}
|
|
|
|
fun messageRepo(messageRepo: MessageRepository): Builder {
|
|
this.messageRepo = messageRepo
|
|
return this
|
|
}
|
|
|
|
fun powRepo(proofOfWorkRepository: ProofOfWorkRepository): Builder {
|
|
this.proofOfWorkRepo = proofOfWorkRepository
|
|
return this
|
|
}
|
|
|
|
fun cryptography(cryptography: Cryptography): Builder {
|
|
this.cryptography = cryptography
|
|
return this
|
|
}
|
|
|
|
fun customCommandHandler(handler: CustomCommandHandler): Builder {
|
|
this.customCommandHandler = handler
|
|
return this
|
|
}
|
|
|
|
fun proofOfWorkEngine(proofOfWorkEngine: ProofOfWorkEngine): Builder {
|
|
this.proofOfWorkEngine = proofOfWorkEngine
|
|
return this
|
|
}
|
|
|
|
fun labeler(labeler: Labeler): Builder {
|
|
this.labeler = labeler
|
|
return this
|
|
}
|
|
|
|
fun listener(listener: Listener): Builder {
|
|
this.listener = listener
|
|
return this
|
|
}
|
|
|
|
@JvmSynthetic
|
|
fun listener(listener: (Plaintext) -> Unit): Builder {
|
|
this.listener = object : Listener {
|
|
override fun receive(plaintext: Plaintext) {
|
|
listener.invoke(plaintext)
|
|
}
|
|
}
|
|
return this
|
|
}
|
|
|
|
fun build() = BitmessageContext(this)
|
|
}
|
|
|
|
init {
|
|
this.labeler = builder.labeler ?: DefaultLabeler()
|
|
this.internals = InternalContext(
|
|
builder.cryptography,
|
|
builder.inventory,
|
|
builder.nodeRegistry,
|
|
builder.networkHandler,
|
|
builder.addressRepo,
|
|
builder.labelRepo,
|
|
builder.messageRepo,
|
|
builder.proofOfWorkRepo,
|
|
builder.proofOfWorkEngine ?: MultiThreadedPOWEngine(),
|
|
builder.customCommandHandler ?: object : CustomCommandHandler {
|
|
override fun handle(request: CustomMessage): MessagePayload? {
|
|
BitmessageContext.LOG.debug("Received custom request, but no custom command handler configured.")
|
|
return null
|
|
}
|
|
},
|
|
builder.listener,
|
|
labeler,
|
|
builder.preferences
|
|
)
|
|
this.addresses = builder.addressRepo
|
|
this.labels = builder.labelRepo
|
|
this.messages = builder.messageRepo
|
|
(builder.listener as? Listener.WithContext)?.setContext(this)
|
|
internals.proofOfWorkService.doMissingProofOfWork(builder.preferences.doMissingProofOfWorkDelayInSeconds * 1000L)
|
|
}
|
|
|
|
companion object {
|
|
@JvmField
|
|
val CURRENT_VERSION = 3
|
|
private val LOG = LoggerFactory.getLogger(BitmessageContext::class.java)
|
|
|
|
val version: String by lazy {
|
|
BitmessageContext::class.java.getResource("/version")?.readText() ?: "local build"
|
|
}
|
|
@JvmStatic get
|
|
|
|
@JvmSynthetic
|
|
inline fun build(block: Builder.() -> Unit): BitmessageContext {
|
|
val builder = Builder()
|
|
block(builder)
|
|
return builder.build()
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
class Preferences {
|
|
var port = 8444
|
|
/**
|
|
* Defaults to "/Jabit:<version>/", and whatever you set will be inserted into "/<your user agent>/Jabit:<version>/"
|
|
*/
|
|
var userAgent = "/Jabit:$version/"
|
|
set(value) {
|
|
field = "/$value/Jabit:$version/"
|
|
}
|
|
/**
|
|
* Time to live for any connection
|
|
*/
|
|
var connectionTTL = 30 * MINUTE
|
|
/**
|
|
* Maximum number of connections. Values below 8 would probably result in erratic behaviour, so you shouldn't do that.
|
|
*/
|
|
var connectionLimit = 150
|
|
/**
|
|
* By default a client will send the public key when an identity is being created. On weaker devices
|
|
* this behaviour might not be desirable.
|
|
*/
|
|
var sendPubkeyOnIdentityCreation = true
|
|
/**
|
|
* Delay in seconds before outstandinng proof of work is calculated.
|
|
*/
|
|
var doMissingProofOfWorkDelayInSeconds = 30
|
|
}
|