/* * 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.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.HOUR 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( cryptography: Cryptography, inventory: Inventory, nodeRegistry: NodeRegistry, networkHandler: NetworkHandler, addressRepository: AddressRepository, messageRepository: MessageRepository, proofOfWorkRepository: ProofOfWorkRepository, proofOfWorkEngine: ProofOfWorkEngine = MultiThreadedPOWEngine(), customCommandHandler: CustomCommandHandler = object : CustomCommandHandler { override fun handle(request: CustomMessage): MessagePayload? { BitmessageContext.LOG.debug("Received custom request, but no custom command handler configured.") return null } }, listener: Listener, labeler: Labeler = DefaultLabeler(), userAgent: String? = null, port: Int = 8444, connectionTTL: Long = 30 * MINUTE, connectionLimit: Int = 150, sendPubkeyOnIdentityCreation: Boolean = true, doMissingProofOfWorkDelayInSeconds: Int = 30 ) { private constructor(builder: BitmessageContext.Builder) : this( builder.cryptography, builder.inventory, builder.nodeRegistry, builder.networkHandler, builder.addressRepo, builder.messageRepo, builder.proofOfWorkRepository, 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, builder.labeler ?: DefaultLabeler(), builder.userAgent, builder.port, builder.connectionTTL, builder.connectionLimit, builder.sendPubkeyOnIdentityCreation, builder.doMissingProofOfWorkDelay ) private val sendPubkeyOnIdentityCreation: Boolean /** * 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 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 (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 { 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 { return 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() } /** * 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.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) } } class Builder { internal var port = 8444 internal var inventory by Delegates.notNull() internal var nodeRegistry by Delegates.notNull() internal var networkHandler by Delegates.notNull() internal var addressRepo by Delegates.notNull() internal var messageRepo by Delegates.notNull() internal var proofOfWorkRepository by Delegates.notNull() internal var proofOfWorkEngine: ProofOfWorkEngine? = null internal var cryptography by Delegates.notNull() internal var customCommandHandler: CustomCommandHandler? = null internal var labeler: Labeler? = null internal var userAgent: String? = null internal var listener by Delegates.notNull() internal var connectionLimit = 150 internal var connectionTTL = 30 * MINUTE internal var sendPubkeyOnIdentityCreation = true internal var doMissingProofOfWorkDelay = 30 fun port(port: Int): Builder { this.port = port return this } 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 messageRepo(messageRepo: MessageRepository): Builder { this.messageRepo = messageRepo return this } fun powRepo(proofOfWorkRepository: ProofOfWorkRepository): Builder { this.proofOfWorkRepository = 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 } @JvmName("kotlinListener") fun listener(listener: (Plaintext) -> Unit): Builder { this.listener = object : Listener { override fun receive(plaintext: Plaintext) { listener.invoke(plaintext) } } return this } fun connectionLimit(connectionLimit: Int): Builder { this.connectionLimit = connectionLimit return this } fun connectionTTL(hours: Int): Builder { this.connectionTTL = hours * HOUR return this } fun doMissingProofOfWorkDelay(seconds: Int) { this.doMissingProofOfWorkDelay = seconds } /** * By default a client will send the public key when an identity is being created. On weaker devices * this behaviour might not be desirable. */ fun doNotSendPubkeyOnIdentityCreation(): Builder { this.sendPubkeyOnIdentityCreation = false return this } fun build(): BitmessageContext { return BitmessageContext(this) } } init { this.labeler = labeler this.internals = InternalContext( cryptography, inventory, nodeRegistry, networkHandler, addressRepository, messageRepository, proofOfWorkRepository, proofOfWorkEngine, customCommandHandler, listener, labeler, userAgent?.let { "/$it/Jabit:$version/" } ?: "/Jabit:$version/", port, connectionTTL, connectionLimit ) this.addresses = addressRepository this.messages = messageRepository this.sendPubkeyOnIdentityCreation = sendPubkeyOnIdentityCreation (listener as? Listener.WithContext)?.setContext(this) internals.proofOfWorkService.doMissingProofOfWork(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 } }