├── relay ├── build.gradle └── src │ └── main │ └── kotlin │ └── net │ └── syncthing │ └── java │ └── client │ └── protocol │ └── rp │ ├── beans │ └── SessionInvitation.kt │ └── RelayClient.kt ├── http-relay ├── build.gradle └── src │ └── main │ ├── proto │ └── httpRelayProtos.proto │ └── kotlin │ └── net │ └── syncthing │ └── java │ └── httprelay │ ├── HttpRelayClient.kt │ └── HttpRelayConnection.kt ├── docs ├── http_relay_protocol.png └── http_relay_protocol.graphml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── client ├── build.gradle └── src │ └── main │ └── kotlin │ └── net │ └── syncthing │ └── java │ └── client │ └── SyncthingClient.kt ├── bep ├── build.gradle └── src │ └── main │ ├── proto │ ├── blockExchangeExtraProtos.proto │ └── blockExchangeProtos.proto │ └── kotlin │ └── net │ └── syncthing │ └── java │ └── bep │ ├── ClusterConfigFolderInfo.kt │ ├── FolderBrowser.kt │ ├── BlockPuller.kt │ ├── IndexBrowser.kt │ └── BlockPusher.kt ├── repository ├── build.gradle └── pom.xml ├── .gitignore ├── discovery ├── src │ └── main │ │ ├── proto │ │ └── localDiscoveryProtos.proto │ │ └── kotlin │ │ └── net │ │ └── syncthing │ │ └── java │ │ └── discovery │ │ ├── DeviceAddressSupplier.kt │ │ ├── utils │ │ └── AddressRanker.kt │ │ ├── protocol │ │ ├── GlobalDiscoveryHandler.kt │ │ └── LocalDiscoveryHandler.kt │ │ ├── Main.kt │ │ └── DiscoveryHandler.kt └── build.gradle ├── settings.gradle ├── client-cli ├── build.gradle └── src │ └── main │ └── kotlin │ └── net │ └── syncthing │ └── java │ └── client │ └── cli │ └── Main.kt ├── core ├── src │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── kotlin │ │ └── net │ │ └── syncthing │ │ └── java │ │ └── core │ │ ├── utils │ │ ├── NetworkUtils.kt │ │ ├── BlockUtils.kt │ │ ├── PathUtils.kt │ │ └── ExecutorUtils.kt │ │ ├── beans │ │ ├── BlockInfo.kt │ │ ├── DeviceInfo.kt │ │ ├── FolderInfo.kt │ │ ├── FileBlocks.kt │ │ ├── DeviceId.kt │ │ ├── FolderStats.kt │ │ ├── IndexInfo.kt │ │ ├── FileInfo.kt │ │ └── DeviceAddress.kt │ │ ├── interfaces │ │ ├── Sequencer.kt │ │ ├── RelayConnection.kt │ │ ├── TempRepository.kt │ │ └── IndexRepository.kt │ │ ├── events │ │ ├── DeviceAddressActiveEvent.kt │ │ └── DeviceAddressReceivedEvent.kt │ │ ├── configuration │ │ └── Configuration.kt │ │ └── security │ │ └── KeystoreHandler.kt └── build.gradle ├── .travis.yml ├── TODO.txt ├── prepare-release.bash ├── README.md ├── gradlew.bat └── gradlew /relay/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile project(':core') 3 | } 4 | -------------------------------------------------------------------------------- /http-relay/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile project(':relay') 3 | } 4 | -------------------------------------------------------------------------------- /docs/http_relay_protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncthing/syncthing-java/HEAD/docs/http_relay_protocol.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/syncthing/syncthing-java/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /client/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile project(':core') 3 | compile project(':bep') 4 | compile project(':discovery') 5 | } 6 | -------------------------------------------------------------------------------- /bep/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile project(':core') 3 | compile project(':relay') 4 | compile project(':http-relay') 5 | compile "net.jpountz.lz4:lz4:1.3.0" 6 | } 7 | -------------------------------------------------------------------------------- /repository/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile project(':bep') 3 | compile project(':core') 4 | compile 'com.h2database:h2:1.4.196' 5 | compile 'com.zaxxer:HikariCP-java7:2.4.13' 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle/ 3 | build/ 4 | */build/ 5 | */out/ 6 | 7 | # Intellij 8 | .idea/ 9 | *.iml 10 | 11 | # Visual Studio Code 12 | *.classpath 13 | *.project 14 | *.settings 15 | */bin/ 16 | -------------------------------------------------------------------------------- /bep/src/main/proto/blockExchangeExtraProtos.proto: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.bep; 2 | 3 | option optimize_for = LITE_RUNTIME; 4 | 5 | import "blockExchangeProtos.proto"; 6 | 7 | message Blocks { 8 | repeated BlockInfo blocks = 1; 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.3.1-bin.zip 6 | -------------------------------------------------------------------------------- /discovery/src/main/proto/localDiscoveryProtos.proto: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.discovery.protocol; 2 | 3 | option optimize_for = LITE_RUNTIME; 4 | 5 | message Announce { 6 | optional bytes id = 1; 7 | repeated string addresses = 2; 8 | optional int64 instance_id = 3; 9 | } 10 | -------------------------------------------------------------------------------- /discovery/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | mainClassName = 'net.syncthing.java.discovery.Main' 3 | 4 | dependencies { 5 | compile project(':core') 6 | compile "commons-cli:commons-cli:1.4" 7 | } 8 | 9 | run { 10 | if (project.hasProperty('args')) { 11 | args project.args.split('\\s+') 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'syncthing-java' 2 | include ':core' 3 | include ':relay' 4 | include ':http-relay' 5 | include ':bep' 6 | include ':client' 7 | include ':repository' 8 | include ':discovery' 9 | include ':client-cli' 10 | 11 | // Also call this syncthing-java so it can be easily used from syncthing-lite. 12 | project(':client').name = 'syncthing-java' 13 | -------------------------------------------------------------------------------- /client-cli/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'application' 2 | mainClassName = 'net.syncthing.java.client.cli.Main' 3 | 4 | dependencies { 5 | compile project(':syncthing-java') 6 | compile project(':repository') 7 | compile "commons-cli:commons-cli:1.4" 8 | } 9 | 10 | run { 11 | if (project.hasProperty('args')) { 12 | args project.args.split('\\s+') 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} %-5level %logger{0} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/utils/NetworkUtils.kt: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.core.utils 2 | 3 | import java.io.IOException 4 | 5 | object NetworkUtils { 6 | 7 | @Throws(IOException::class) 8 | fun assertProtocol(value: Boolean, lazyMessage: (() -> String)? = null) { 9 | if (!value) { 10 | if (lazyMessage != null) 11 | throw IOException(lazyMessage()) 12 | else 13 | throw IOException() 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | compile "org.apache.commons:commons-lang3:3.7" 3 | compile 'commons-codec:commons-codec:1.11' 4 | // Can't upgrade to 2.6 because it crashes on Android 5 | compile "commons-io:commons-io:2.5" 6 | compile "org.slf4j:slf4j-api:1.7.25" 7 | compile "ch.qos.logback:logback-classic:1.2.3" 8 | compile "com.google.code.gson:gson:2.8.2" 9 | compile "org.apache.httpcomponents:httpclient:4.5.4" 10 | compile "org.bouncycastle:bcmail-jdk15on:1.59" 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: oraclejdk8 3 | dist: trusty 4 | 5 | before_install: 6 | - sudo apt-get update 7 | - sudo apt-get install -y protobuf-compiler 8 | 9 | # Cache gradle dependencies 10 | # https://docs.travis-ci.com/user/languages/android/#Caching 11 | before_cache: 12 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 13 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 14 | cache: 15 | directories: 16 | - $HOME/.gradle/caches/ 17 | - $HOME/.gradle/wrapper/ 18 | 19 | script: 20 | - ./gradlew check 21 | - ./gradlew assemble 22 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | 2 | - read peer addresses from cluster config 3 | - update local index on push 4 | - delta index 5 | - index store version/discard unsupported versions 6 | - rename all to anyplace-sync-client 7 | 8 | - retry operation if first BEP connection fail/close 9 | - multi folder mode (change folder at runtime, multi-folder index handler) 10 | 11 | - use absolute path syntax "/path" in db/app 12 | - canonicalize all path on db/app (uri/url?) 13 | 14 | - block cache cleanup, block cache shutdown 15 | - connection wrapper, return to pool on close 16 | 17 | - config file lock (pid) 18 | -------------------------------------------------------------------------------- /prepare-release.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | NEW_VERSION_NAME=$1 6 | OLD_VERSION_NAME=$(grep " version =" "build.gradle" | awk '{print $3}' | tr -d "\"") 7 | if [[ -z ${NEW_VERSION_NAME} ]] 8 | then 9 | echo "New version name is empty. Please set a new version. Current version: $OLD_VERSION_NAME" 10 | exit 11 | fi 12 | 13 | echo " 14 | 15 | Updating Version 16 | ----------------------------- 17 | " 18 | sed -i "s/version = \"$OLD_VERSION_NAME\"/version = \"$NEW_VERSION_NAME\"/" "build.gradle" 19 | 20 | git add "build.gradle" 21 | git commit -m "Version $NEW_VERSION_NAME" 22 | git tag ${NEW_VERSION_NAME} 23 | 24 | echo " 25 | Update ready. 26 | " 27 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/BlockInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | data class BlockInfo(val offset: Long, val size: Int, val hash: String) 17 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/interfaces/Sequencer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.interfaces 15 | 16 | interface Sequencer { 17 | 18 | fun indexId(): Long 19 | 20 | fun nextSequence(): Long 21 | 22 | fun currentSequence(): Long 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/interfaces/RelayConnection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.interfaces 15 | 16 | import java.net.Socket 17 | 18 | interface RelayConnection { 19 | 20 | fun getSocket(): Socket 21 | 22 | fun isServerSocket(): Boolean 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/events/DeviceAddressActiveEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Davide Imbriaco . 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.events 15 | 16 | import net.syncthing.java.core.beans.DeviceAddress 17 | 18 | interface DeviceAddressActiveEvent { 19 | 20 | fun getDeviceAddress(): DeviceAddress 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/interfaces/TempRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.interfaces 15 | 16 | import java.io.Closeable 17 | 18 | interface TempRepository: Closeable { 19 | 20 | fun pushTempData(data: ByteArray): String 21 | 22 | fun popTempData(key: String): ByteArray 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/events/DeviceAddressReceivedEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Davide Imbriaco . 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.events 15 | 16 | import net.syncthing.java.core.beans.DeviceAddress 17 | 18 | interface DeviceAddressReceivedEvent { 19 | 20 | fun getDeviceAddresses(): List 21 | } 22 | -------------------------------------------------------------------------------- /bep/src/main/kotlin/net/syncthing/java/bep/ClusterConfigFolderInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.bep 15 | 16 | internal data class ClusterConfigFolderInfo(val folderId: String, var label: String = folderId, 17 | var isAnnounced: Boolean = false, var isShared: Boolean = false) { 18 | 19 | init { 20 | assert(folderId.isNotEmpty()) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/DeviceInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Davide Imbriaco . 3 | * 4 | * This Source Code Form is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | data class DeviceInfo(val deviceId: DeviceId, val name: String, val isConnected: Boolean? = null) { 17 | 18 | constructor(deviceId: DeviceId, name: String?) : 19 | this(deviceId, if (name != null && !name.isBlank()) name else deviceId.shortId, null) 20 | } 21 | -------------------------------------------------------------------------------- /http-relay/src/main/proto/httpRelayProtos.proto: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.httprelay; 2 | 3 | option optimize_for = LITE_RUNTIME; 4 | 5 | message HttpRelayPeerMessage{ 6 | optional HttpRelayPeerMessageType message_type = 1; 7 | optional string session_id = 2; 8 | optional string device_id = 3; 9 | optional int64 sequence = 4; 10 | optional bytes data = 5; 11 | } 12 | 13 | message HttpRelayServerMessage{ 14 | optional HttpRelayServerMessageType message_type = 1; 15 | optional string session_id = 2; 16 | optional bool is_server_socket = 3; 17 | optional int64 sequence = 4; 18 | optional bytes data = 5; 19 | } 20 | 21 | enum HttpRelayPeerMessageType { 22 | CONNECT = 0; 23 | PEER_TO_RELAY = 1; 24 | WAIT_FOR_DATA = 2; 25 | PEER_CLOSING = 3; 26 | } 27 | 28 | enum HttpRelayServerMessageType { 29 | PEER_CONNECTED = 0; 30 | DATA_ACCEPTED = 1; 31 | RELAY_TO_PEER = 2; 32 | SERVER_CLOSING = 3; 33 | ERROR = 4; 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/FolderInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | open class FolderInfo(val folderId: String, label: String? = null) { 17 | val label: String 18 | 19 | init { 20 | assert(!folderId.isEmpty()) 21 | this.label = if (label != null && !label.isEmpty()) label else folderId 22 | } 23 | 24 | override fun toString(): String { 25 | return "FolderInfo(folderId=$folderId, label=$label)" 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/utils/BlockUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.utils 15 | 16 | import net.syncthing.java.core.beans.BlockInfo 17 | import org.bouncycastle.util.encoders.Hex 18 | import java.security.MessageDigest 19 | 20 | object BlockUtils { 21 | 22 | fun hashBlocks(blocks: List): String { 23 | val string = blocks.joinToString(",") { it.hash }.toByteArray() 24 | val hash = MessageDigest.getInstance("SHA-256").digest(string) 25 | return Hex.toHexString(hash) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /repository/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | net.syncthing.java 6 | a-sync-parent 7 | 1.3 8 | ../a-sync-parent 9 | 10 | a-sync-repository 11 | jar 12 | 13 | 14 | ${project.groupId} 15 | a-sync-core 16 | ${project.version} 17 | 18 | 19 | com.h2database 20 | h2 21 | 1.4.193 22 | 23 | 24 | com.zaxxer 25 | HikariCP-java7 26 | 2.4.9 27 | 28 | 29 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/FileBlocks.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | import net.syncthing.java.core.utils.BlockUtils 17 | 18 | class FileBlocks(val folder: String, val path: String, blocks: List) { 19 | 20 | val blocks: List 21 | val hash: String 22 | val size: Long 23 | 24 | init { 25 | assert(!folder.isEmpty()) 26 | assert(!path.isEmpty()) 27 | this.blocks = blocks.toList() 28 | var num: Long = 0 29 | for (block in blocks) { 30 | num += block.size.toLong() 31 | } 32 | this.size = num 33 | this.hash = BlockUtils.hashBlocks(this.blocks) 34 | } 35 | 36 | override fun toString(): String { 37 | return "FileBlocks(" + "blocks=" + blocks.size + ", hash=" + hash + ", folder=" + folder + ", path=" + path + ", size=" + size + ")" 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /http-relay/src/main/kotlin/net/syncthing/java/httprelay/HttpRelayClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.httprelay 15 | 16 | import net.syncthing.java.core.beans.DeviceAddress 17 | import net.syncthing.java.core.beans.DeviceAddress.AddressType 18 | import org.slf4j.LoggerFactory 19 | 20 | class HttpRelayClient { 21 | 22 | private val logger = LoggerFactory.getLogger(javaClass) 23 | 24 | fun openRelayConnection(deviceAddress: DeviceAddress): HttpRelayConnection { 25 | assert(setOf(AddressType.HTTP_RELAY, AddressType.HTTPS_RELAY).contains(deviceAddress.getType())) 26 | val httpRelayServerUrl = deviceAddress.address.replaceFirst("^relay-".toRegex(), "") 27 | val deviceId = deviceAddress.deviceId 28 | logger.info("open http relay connection, relay url = {}, target device id = {}", httpRelayServerUrl, deviceId) 29 | return HttpRelayConnection(httpRelayServerUrl, deviceId) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/utils/PathUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.utils 15 | 16 | import org.apache.commons.io.FilenameUtils 17 | 18 | object PathUtils { 19 | 20 | val ROOT_PATH = "" 21 | val PATH_SEPARATOR = "/" 22 | val PARENT_PATH = ".." 23 | 24 | private fun normalizePath(path: String): String { 25 | return FilenameUtils.normalizeNoEndSeparator(path, true).replaceFirst(("^" + PATH_SEPARATOR).toRegex(), "") 26 | } 27 | 28 | fun isRoot(path: String): Boolean { 29 | return path.isEmpty() 30 | } 31 | 32 | fun isParent(path: String): Boolean { 33 | return path == PARENT_PATH 34 | } 35 | 36 | fun getParentPath(path: String): String { 37 | assert(!isRoot(path), {"cannot get parent of root path"}) 38 | return normalizePath(path + PATH_SEPARATOR + PARENT_PATH) 39 | } 40 | 41 | fun getFileName(path: String): String { 42 | return FilenameUtils.getName(path) 43 | } 44 | 45 | fun buildPath(dir: String, file: String): String { 46 | return normalizePath(dir + PATH_SEPARATOR + file) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/utils/ExecutorUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.utils 15 | 16 | import org.slf4j.LoggerFactory 17 | import java.util.concurrent.ExecutorService 18 | import java.util.concurrent.Future 19 | import java.util.concurrent.TimeUnit 20 | 21 | private val logger = LoggerFactory.getLogger(ExecutorService::class.java) 22 | 23 | fun ExecutorService.awaitTerminationSafe() { 24 | try { 25 | awaitTermination(2, TimeUnit.SECONDS) 26 | } catch (ex: InterruptedException) { 27 | logger.warn("", ex) 28 | } 29 | } 30 | 31 | fun ExecutorService.submitLogging(runnable: Runnable) = submitLogging { runnable.run() } 32 | 33 | /** 34 | * Wrapper method for [[ExecutorService.submit]], which silently swallows exceptions. If an exception is thrown in 35 | * [[runnable]], logs the exception and force crashes 36 | */ 37 | fun ExecutorService.submitLogging(runnable: () -> T): Future { 38 | return submit({ 39 | try { 40 | runnable() 41 | } catch (e: Exception) { 42 | logger.error("", e) 43 | System.exit(1) 44 | null 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This project was merged into [syncthing-lite](https://github.com/syncthing/syncthing-lite). The repository only exists for historic reasons. 2 | 3 | [![MPLv2 License](https://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](https://www.mozilla.org/MPL/2.0/) 4 | 5 | This project is an java client implementation of [Syncthing][1] protocols. It is made of several modules, providing: 6 | 7 | 1. a command line utility for accessing a Syncthing network (ie a network of devices that 8 | speak Syncthing protocols)' 9 | 10 | 2. a service implementation for the 'http-relay' protocol (that proxies 'relay' protocol 11 | over an http connection); 12 | 13 | 3. a client library for Syncthing protocol, that can be used by third-party applications 14 | (such as mobile apps) to integrate with Syncthing. 15 | 16 | Care is taken to make sure the client library is compatible with android (min sdk 19), so it 17 | can be used for the [syncthing-lite][2] project. 18 | 19 | This is a client-oriented implementation, designed to work online by downloading and 20 | uploading files from an active device on the network (instead of synchronizing a local copy 21 | of the entire repository). This is quite different from the way the [syncthing-android][3] app 22 | works, and its useful from those implementations that cannot or wish not to download the 23 | entire repository (for example, mobile devices with limited storage available, wishing to 24 | access a syncthing share). 25 | 26 | ## Building 27 | 28 | The project uses a standard gradle build. After installing gradle, simply run `gradle assemble` to compile, or 29 | `gradle install` to add it to your local maven repository. 30 | 31 | ## Running 32 | 33 | To use the command line client, run `gradle run -Pargs="-h"`. 34 | 35 | ## License 36 | 37 | All code is licensed under the [MPLv2 License][3]. 38 | 39 | [1]: https://syncthing.net/ 40 | [2]: https://github.com/Nutomic/syncthing-lite 41 | [3]: https://github.com/syncthing/syncthing-android 42 | [4]: LICENSE 43 | 44 | 45 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/interfaces/IndexRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.interfaces 15 | 16 | import net.syncthing.java.core.beans.* 17 | import java.io.Closeable 18 | import java.util.* 19 | 20 | interface IndexRepository: Closeable { 21 | 22 | fun setOnFolderStatsUpdatedListener(listener: ((IndexRepository.FolderStatsUpdatedEvent) -> Unit)?) 23 | 24 | fun getSequencer(): Sequencer 25 | 26 | fun updateIndexInfo(indexInfo: IndexInfo) 27 | 28 | fun findIndexInfoByDeviceAndFolder(deviceId: DeviceId, folder: String): IndexInfo? 29 | 30 | fun findFileInfo(folder: String, path: String): FileInfo? 31 | 32 | fun findFileInfoLastModified(folder: String, path: String): Date? 33 | 34 | fun findNotDeletedFileInfo(folder: String, path: String): FileInfo? 35 | 36 | fun findFileBlocks(folder: String, path: String): FileBlocks? 37 | 38 | fun updateFileInfo(fileInfo: FileInfo, fileBlocks: FileBlocks?) 39 | 40 | fun findNotDeletedFilesByFolderAndParent(folder: String, parentPath: String): List 41 | 42 | fun clearIndex() 43 | 44 | fun findFolderStats(folder: String): FolderStats? 45 | 46 | fun findAllFolderStats(): List 47 | 48 | fun findFileInfoBySearchTerm(query: String): List 49 | 50 | fun countFileInfoBySearchTerm(query: String): Long 51 | 52 | abstract class FolderStatsUpdatedEvent { 53 | 54 | abstract fun getFolderStats(): List 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /relay/src/main/kotlin/net/syncthing/java/client/protocol/rp/beans/SessionInvitation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.client.protocol.rp.beans 15 | 16 | import java.net.InetAddress 17 | 18 | class SessionInvitation private constructor(val from: String, val key: String, val address: InetAddress, val port: Int, val isServerSocket: Boolean) { 19 | 20 | init { 21 | assert(!from.isEmpty()) 22 | assert(!key.isEmpty()) 23 | } 24 | 25 | class Builder { 26 | 27 | private var from: String? = null 28 | private var key: String? = null 29 | private var address: InetAddress? = null 30 | private var port: Int = 0 31 | private var isServerSocket: Boolean = false 32 | 33 | fun getFrom() = from 34 | fun getKey() = key 35 | fun getAddress() = address 36 | fun getPort() = port 37 | fun isServerSocket() = isServerSocket 38 | 39 | fun setFrom(from: String): Builder { 40 | this.from = from 41 | return this 42 | } 43 | 44 | fun setKey(key: String): Builder { 45 | this.key = key 46 | return this 47 | } 48 | 49 | fun setAddress(address: InetAddress): Builder { 50 | this.address = address 51 | return this 52 | } 53 | 54 | fun setPort(port: Int): Builder { 55 | this.port = port 56 | return this 57 | } 58 | 59 | fun setServerSocket(isServerSocket: Boolean): Builder { 60 | this.isServerSocket = isServerSocket 61 | return this 62 | } 63 | 64 | fun build(): SessionInvitation { 65 | return SessionInvitation(from!!, key!!, address!!, port, isServerSocket) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /bep/src/main/kotlin/net/syncthing/java/bep/FolderBrowser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.bep 15 | 16 | import net.syncthing.java.core.beans.FolderInfo 17 | import net.syncthing.java.core.beans.FolderStats 18 | import net.syncthing.java.core.interfaces.IndexRepository 19 | import java.io.Closeable 20 | 21 | class FolderBrowser internal constructor(private val indexHandler: IndexHandler) : Closeable { 22 | private val folderStatsCache = mutableMapOf() 23 | private val indexRepositoryEventListener = { event: IndexRepository.FolderStatsUpdatedEvent -> 24 | addFolderStats(event.getFolderStats()) 25 | } 26 | 27 | fun folderInfoAndStatsList(): List> = 28 | indexHandler.folderInfoList() 29 | .map { folderInfo -> Pair(folderInfo, getFolderStats(folderInfo.folderId)) } 30 | .sortedBy { it.first.label } 31 | 32 | init { 33 | indexHandler.indexRepository.setOnFolderStatsUpdatedListener(indexRepositoryEventListener) 34 | addFolderStats(indexHandler.indexRepository.findAllFolderStats()) 35 | } 36 | 37 | private fun addFolderStats(folderStatsList: List) { 38 | for (folderStats in folderStatsList) { 39 | folderStatsCache.put(folderStats.folderId, folderStats) 40 | } 41 | } 42 | 43 | fun getFolderStats(folder: String): FolderStats { 44 | return folderStatsCache[folder] ?: let { 45 | FolderStats.Builder() 46 | .setFolder(folder) 47 | .build() 48 | } 49 | } 50 | 51 | fun getFolderInfo(folder: String): FolderInfo? { 52 | return indexHandler.getFolderInfo(folder) 53 | } 54 | 55 | override fun close() { 56 | indexHandler.indexRepository.setOnFolderStatsUpdatedListener(null) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/DeviceId.kt: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.core.beans 2 | 3 | import net.syncthing.java.core.utils.NetworkUtils 4 | import org.apache.commons.codec.binary.Base32 5 | import org.slf4j.LoggerFactory 6 | import java.io.IOException 7 | 8 | data class DeviceId @Throws(IOException::class) constructor(val deviceId: String) { 9 | 10 | init { 11 | val withoutDashes = this.deviceId.replace("-", "") 12 | NetworkUtils.assertProtocol(DeviceId.fromHashDataToString(toHashData()) == withoutDashes) 13 | } 14 | 15 | val shortId 16 | get() = deviceId.substring(0, 7) 17 | 18 | fun toHashData(): ByteArray { 19 | NetworkUtils.assertProtocol(deviceId.matches("^[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}$".toRegex()), {"device id syntax error for deviceId = $deviceId"}) 20 | val base32data = deviceId.replaceFirst("(.{7})-(.{6}).-(.{7})-(.{6}).-(.{7})-(.{6}).-(.{7})-(.{6}).".toRegex(), "$1$2$3$4$5$6$7$8") + "===" 21 | val binaryData = Base32().decode(base32data) 22 | NetworkUtils.assertProtocol(binaryData.size == SHA256_BYTES) 23 | return binaryData 24 | } 25 | 26 | companion object { 27 | 28 | private const val SHA256_BYTES = 256 / 8 29 | 30 | private fun fromHashDataToString(hashData: ByteArray): String { 31 | NetworkUtils.assertProtocol(hashData.size == SHA256_BYTES) 32 | val string = Base32().encodeAsString(hashData).replace("=", "") 33 | return string.chunked(13).joinToString("") { part -> part + generateLuhn32Checksum(part) } 34 | } 35 | 36 | fun fromHashData(hashData: ByteArray): DeviceId { 37 | return DeviceId(fromHashDataToString(hashData).chunked(7).joinToString("-")) 38 | } 39 | 40 | private fun generateLuhn32Checksum(string: String): Char { 41 | val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" 42 | var factor = 1 43 | var sum = 0 44 | val n = alphabet.length 45 | for (character in string.toCharArray()) { 46 | val index = alphabet.indexOf(character) 47 | NetworkUtils.assertProtocol(index >= 0) 48 | var add = factor * index 49 | factor = if (factor == 2) 1 else 2 50 | add = add / n + add % n 51 | sum += add 52 | } 53 | val remainder = sum % n 54 | val check = (n - remainder) % n 55 | return alphabet[check] 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /discovery/src/main/kotlin/net/syncthing/java/discovery/DeviceAddressSupplier.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.discovery 15 | 16 | import net.syncthing.java.core.beans.DeviceAddress 17 | import org.slf4j.LoggerFactory 18 | import java.util.* 19 | 20 | class DeviceAddressSupplier(private val discoveryHandler: DiscoveryHandler) : Iterable { 21 | 22 | private val logger = LoggerFactory.getLogger(javaClass) 23 | private val deviceAddressQueue = PriorityQueue(11, compareBy { it.score }) 24 | private val queueLock = Object() 25 | 26 | private fun getDeviceAddress(): DeviceAddress? { 27 | synchronized(queueLock) { 28 | return deviceAddressQueue.poll() 29 | } 30 | } 31 | 32 | internal fun onNewDeviceAddressAcquired(address: DeviceAddress) { 33 | if (address.isWorking()) { 34 | synchronized(queueLock) { 35 | deviceAddressQueue.add(address) 36 | queueLock.notify() 37 | } 38 | } 39 | } 40 | 41 | @Throws(InterruptedException::class) 42 | fun getDeviceAddressOrWait(): DeviceAddress? = getDeviceAddressOrWait(5000) 43 | 44 | init { 45 | synchronized(queueLock) { 46 | deviceAddressQueue.addAll(discoveryHandler.getAllWorkingDeviceAddresses())// note: slight risk of duplicate address loading 47 | } 48 | } 49 | 50 | @Throws(InterruptedException::class) 51 | private fun getDeviceAddressOrWait(timeout: Long): DeviceAddress? { 52 | synchronized(queueLock) { 53 | if (deviceAddressQueue.isEmpty()) { 54 | queueLock.wait(timeout) 55 | } 56 | return getDeviceAddress() 57 | } 58 | } 59 | 60 | override fun iterator(): Iterator { 61 | return object : Iterator { 62 | 63 | private var hasNext: Boolean? = null 64 | private var next: DeviceAddress? = null 65 | 66 | override fun hasNext(): Boolean { 67 | if (hasNext == null) { 68 | try { 69 | next = getDeviceAddressOrWait() 70 | } catch (ex: InterruptedException) { 71 | logger.warn("", ex) 72 | } 73 | 74 | hasNext = next != null 75 | } 76 | return hasNext!! 77 | } 78 | 79 | override fun next(): DeviceAddress? { 80 | assert(hasNext()) 81 | val res = next 82 | hasNext = null 83 | next = null 84 | return res 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/FolderStats.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | import org.apache.commons.io.FileUtils 17 | 18 | import java.util.Date 19 | 20 | class FolderStats private constructor(val fileCount: Long, val dirCount: Long, val size: Long, val lastUpdate: Date, folder: String, label: String?) : FolderInfo(folder, label) { 21 | 22 | fun getRecordCount(): Long = dirCount + fileCount 23 | 24 | fun describeSize(): String = FileUtils.byteCountToDisplaySize(size) 25 | 26 | fun dumpInfo(): String { 27 | return ("folder " + label + " (" + folderId + ") file count = " + fileCount 28 | + " dir count = " + dirCount + " folder size = " + describeSize() + " last update = " + lastUpdate) 29 | } 30 | 31 | override fun toString(): String { 32 | return "FolderStats{folder=$folderId, fileCount=$fileCount, dirCount=$dirCount, size=$size, lastUpdate=$lastUpdate}" 33 | } 34 | 35 | fun copyBuilder(): Builder = Builder(fileCount, dirCount, size, folderId, label) 36 | 37 | class Builder { 38 | 39 | private var fileCount: Long = 0 40 | private var dirCount: Long = 0 41 | private var size: Long = 0 42 | private var lastUpdate = Date(0) 43 | private var folder: String? = null 44 | private var label: String? = null 45 | 46 | constructor() 47 | 48 | constructor(fileCount: Long, dirCount: Long, size: Long, folder: String, label: String) { 49 | this.fileCount = fileCount 50 | this.dirCount = dirCount 51 | this.size = size 52 | this.folder = folder 53 | this.label = label 54 | } 55 | 56 | fun getFileCount(): Long = fileCount 57 | 58 | fun setFileCount(fileCount: Long): Builder { 59 | this.fileCount = fileCount 60 | return this 61 | } 62 | 63 | fun getDirCount(): Long = dirCount 64 | 65 | fun setDirCount(dirCount: Long): Builder { 66 | this.dirCount = dirCount 67 | return this 68 | } 69 | 70 | fun getSize(): Long = size 71 | 72 | fun setSize(size: Long): Builder { 73 | this.size = size 74 | return this 75 | } 76 | 77 | fun getLastUpdate(): Date = lastUpdate 78 | 79 | fun setLastUpdate(lastUpdate: Date): Builder { 80 | this.lastUpdate = lastUpdate 81 | return this 82 | } 83 | 84 | fun getFolder(): String? = folder 85 | 86 | fun setFolder(folder: String): Builder { 87 | this.folder = folder 88 | return this 89 | } 90 | 91 | fun getLabel(): String? = label 92 | 93 | fun setLabel(label: String): Builder { 94 | this.label = label 95 | return this 96 | } 97 | 98 | fun build(): FolderStats { 99 | return FolderStats(fileCount, dirCount, size, lastUpdate, folder!!, label) 100 | } 101 | 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/IndexInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | 17 | class IndexInfo private constructor(folder: String, val deviceId: String, val indexId: Long, val localSequence: Long, val maxSequence: Long) : FolderInfo(folder) { 18 | 19 | fun getCompleted(): Double = if (maxSequence > 0) localSequence.toDouble() / maxSequence else 0.0 20 | 21 | init { 22 | assert(!deviceId.isEmpty()) 23 | } 24 | 25 | fun copyBuilder(): Builder { 26 | return Builder(folderId, indexId, deviceId, localSequence, maxSequence) 27 | } 28 | 29 | override fun toString(): String { 30 | return "FolderIndexInfo{indexId=$indexId, folder=$folderId, deviceId=$deviceId, localSequence=$localSequence, maxSequence=$maxSequence}" 31 | } 32 | 33 | class Builder { 34 | 35 | private var indexId: Long = 0 36 | private var deviceId: String? = null 37 | private var folder: String? = null 38 | private var localSequence: Long = 0 39 | private var maxSequence: Long = 0 40 | 41 | internal constructor() 42 | 43 | internal constructor(folder: String, indexId: Long, deviceId: String, localSequence: Long, maxSequence: Long) { 44 | assert(!folder.isEmpty()) 45 | assert(!deviceId.isEmpty()) 46 | this.folder = folder 47 | this.indexId = indexId 48 | this.deviceId = deviceId 49 | this.localSequence = localSequence 50 | this.maxSequence = maxSequence 51 | } 52 | 53 | fun getIndexId(): Long { 54 | return indexId 55 | } 56 | 57 | fun getDeviceId(): String? { 58 | return deviceId 59 | } 60 | 61 | fun getFolder(): String? { 62 | return folder 63 | } 64 | 65 | fun getLocalSequence(): Long { 66 | return localSequence 67 | } 68 | 69 | fun getMaxSequence(): Long { 70 | return maxSequence 71 | } 72 | 73 | fun setIndexId(indexId: Long): Builder { 74 | this.indexId = indexId 75 | return this 76 | } 77 | 78 | fun setDeviceId(deviceId: String): Builder { 79 | this.deviceId = deviceId 80 | return this 81 | } 82 | 83 | fun setFolder(folder: String): Builder { 84 | this.folder = folder 85 | return this 86 | } 87 | 88 | fun setLocalSequence(localSequence: Long): Builder { 89 | this.localSequence = localSequence 90 | return this 91 | } 92 | 93 | fun setMaxSequence(maxSequence: Long): Builder { 94 | this.maxSequence = maxSequence 95 | return this 96 | } 97 | 98 | fun build(): IndexInfo { 99 | return IndexInfo(folder!!, deviceId!!, indexId, localSequence, maxSequence) 100 | } 101 | 102 | } 103 | 104 | companion object { 105 | 106 | fun newBuilder(): Builder { 107 | return Builder() 108 | } 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /discovery/src/main/kotlin/net/syncthing/java/discovery/utils/AddressRanker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.discovery.utils 15 | 16 | import net.syncthing.java.core.beans.DeviceAddress 17 | import net.syncthing.java.core.beans.DeviceAddress.AddressType 18 | import net.syncthing.java.core.utils.submitLogging 19 | import org.slf4j.LoggerFactory 20 | import java.io.Closeable 21 | import java.io.IOException 22 | import java.net.Socket 23 | import java.util.concurrent.* 24 | 25 | internal class AddressRanker private constructor(private val sourceAddresses: List) : Closeable { 26 | 27 | companion object { 28 | 29 | private const val TCP_CONNECTION_TIMEOUT = 5000 30 | private val BASE_SCORE_MAP = mapOf( 31 | AddressType.TCP to 0, 32 | AddressType.RELAY to 2000, 33 | AddressType.HTTP_RELAY to 1000 * 2000, 34 | AddressType.HTTPS_RELAY to 1000 * 2000) 35 | private val ACCEPTED_ADDRESS_TYPES = BASE_SCORE_MAP.keys 36 | 37 | fun pingAddresses(list: List): List { 38 | AddressRanker(list).use { addressRanker -> 39 | return addressRanker.testAndRankAndWait() 40 | } 41 | } 42 | } 43 | 44 | private val logger = LoggerFactory.getLogger(javaClass) 45 | private val executorService = Executors.newCachedThreadPool() 46 | 47 | private fun addHttpRelays(list: List): List { 48 | val httpRelays = list 49 | .filter { address -> 50 | address.getType() == AddressType.RELAY && address.containsUriParamValue("httpUrl") 51 | } 52 | .map { address -> 53 | val httpUrl = address.getUriParam("httpUrl") 54 | address.copyBuilder().setAddress("relay-" + httpUrl!!).build() 55 | } 56 | return httpRelays + list 57 | } 58 | 59 | private fun testAndRankAndWait(): List { 60 | return addHttpRelays(sourceAddresses) 61 | .filter { ACCEPTED_ADDRESS_TYPES.contains(it.getType()) } 62 | .map { executorService.submitLogging { pingAddresses(it) } } 63 | .mapNotNull { future -> 64 | try { 65 | future.get((TCP_CONNECTION_TIMEOUT * 2).toLong(), TimeUnit.MILLISECONDS) 66 | } catch (e: ExecutionException) { 67 | logger.warn("Failed to ping device", e) 68 | null 69 | } catch (e: InterruptedException) { 70 | logger.warn("Failed to ping device", e) 71 | null 72 | } 73 | } 74 | .sortedBy { it.score } 75 | } 76 | 77 | private fun pingAddresses(deviceAddress: DeviceAddress): DeviceAddress? { 78 | val startTime = System.currentTimeMillis() 79 | try { 80 | Socket().use { socket -> 81 | socket.soTimeout = TCP_CONNECTION_TIMEOUT 82 | socket.connect(deviceAddress.getSocketAddress(), TCP_CONNECTION_TIMEOUT) 83 | } 84 | } catch (ex: IOException) { 85 | logger.debug("address unreacheable = $deviceAddress, ${ex.message}") 86 | return null 87 | } 88 | val ping = (System.currentTimeMillis() - startTime).toInt() 89 | 90 | val baseScore = BASE_SCORE_MAP[deviceAddress.getType()] ?: 0 91 | return deviceAddress.copyBuilder().setScore(ping + baseScore).build() 92 | } 93 | 94 | override fun close() { 95 | executorService.shutdown() 96 | try { 97 | executorService.awaitTermination(2, TimeUnit.SECONDS) 98 | } catch (ex: InterruptedException) { 99 | logger.warn("", ex) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /discovery/src/main/kotlin/net/syncthing/java/discovery/protocol/GlobalDiscoveryHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.discovery.protocol 15 | 16 | import com.google.gson.Gson 17 | import net.syncthing.java.core.beans.DeviceAddress 18 | import net.syncthing.java.core.beans.DeviceId 19 | import net.syncthing.java.core.configuration.Configuration 20 | import net.syncthing.java.discovery.utils.AddressRanker 21 | import org.apache.http.HttpStatus 22 | import org.apache.http.client.methods.HttpGet 23 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory 24 | import org.apache.http.conn.ssl.SSLContextBuilder 25 | import org.apache.http.conn.ssl.TrustSelfSignedStrategy 26 | import org.apache.http.impl.client.HttpClients 27 | import org.apache.http.util.EntityUtils 28 | import org.slf4j.LoggerFactory 29 | import java.io.Closeable 30 | import java.io.IOException 31 | import java.security.KeyManagementException 32 | import java.security.KeyStoreException 33 | import java.security.NoSuchAlgorithmException 34 | 35 | internal class GlobalDiscoveryHandler(private val configuration: Configuration) : Closeable { 36 | 37 | private val logger = LoggerFactory.getLogger(javaClass) 38 | 39 | fun query(deviceId: DeviceId, callback: (List) -> Unit) { 40 | val addresses = pickAnnounceServers() 41 | .map { 42 | try { 43 | queryAnnounceServer(it, deviceId) 44 | } catch (e: IOException) { 45 | logger.warn("Failed to query $it", e) 46 | listOf() 47 | } 48 | } 49 | .flatten() 50 | callback(addresses) 51 | } 52 | 53 | private fun pickAnnounceServers(): List { 54 | val list = AddressRanker 55 | .pingAddresses(configuration.discoveryServers.map { DeviceAddress(it, "tcp://$it:443") }) 56 | return list.map { it.deviceId } 57 | } 58 | 59 | @Throws(IOException::class) 60 | private fun queryAnnounceServer(server: String, deviceId: DeviceId): List { 61 | try { 62 | logger.debug("querying server {} for device id {}", server, deviceId) 63 | val httpClient = HttpClients.custom() 64 | .setSSLSocketFactory(SSLConnectionSocketFactory(SSLContextBuilder().loadTrustMaterial(null, TrustSelfSignedStrategy()).build(), SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)) 65 | .build() 66 | val httpGet = HttpGet("https://$server/v2/?device=${deviceId.deviceId}") 67 | return httpClient.execute>(httpGet) { response -> 68 | when (response.statusLine.statusCode) { 69 | HttpStatus.SC_NOT_FOUND -> { 70 | logger.debug("device not found: {}", deviceId) 71 | return@execute emptyList() 72 | } 73 | HttpStatus.SC_OK -> { 74 | val announcementMessage = Gson().fromJson(EntityUtils.toString(response.entity), AnnouncementMessage::class.java) 75 | return@execute (announcementMessage?.addresses ?: emptyList()) 76 | .map { DeviceAddress(deviceId.deviceId, it) } 77 | } 78 | else -> { 79 | throw IOException("http error ${response.statusLine}, response ${EntityUtils.toString(response.entity)}") 80 | } 81 | } 82 | } 83 | } catch (e: Exception) { 84 | when (e) { 85 | is IOException, is NoSuchAlgorithmException, is KeyStoreException, is KeyManagementException -> 86 | throw IOException(e) 87 | else -> throw e 88 | } 89 | } 90 | } 91 | 92 | override fun close() {} 93 | 94 | private data class AnnouncementMessage(val addresses: List) 95 | } 96 | -------------------------------------------------------------------------------- /bep/src/main/proto/blockExchangeProtos.proto: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.bep; 2 | 3 | option optimize_for = LITE_RUNTIME; 4 | 5 | message Hello { 6 | optional string device_name = 1; 7 | optional string client_name = 2; 8 | optional string client_version = 3; 9 | } 10 | 11 | message Header { 12 | optional MessageType type = 1; 13 | optional MessageCompression compression = 2; 14 | } 15 | 16 | enum MessageType { 17 | CLUSTER_CONFIG = 0; 18 | INDEX = 1; 19 | INDEX_UPDATE = 2; 20 | REQUEST = 3; 21 | RESPONSE = 4; 22 | DOWNLOAD_PROGRESS = 5; 23 | PING = 6; 24 | CLOSE = 7; 25 | } 26 | 27 | enum MessageCompression { 28 | NONE = 0; 29 | LZ4 = 1; 30 | } 31 | 32 | message ClusterConfig { 33 | repeated Folder folders = 1; 34 | } 35 | 36 | message Folder { 37 | optional string id = 1; 38 | optional string label = 2; 39 | optional bool read_only = 3; 40 | optional bool ignore_permissions = 4; 41 | optional bool ignore_delete = 5; 42 | optional bool disable_temp_indexes = 6; 43 | 44 | repeated Device devices = 16; 45 | } 46 | 47 | message Device { 48 | optional bytes id = 1; 49 | optional string name = 2; 50 | repeated string addresses = 3; 51 | optional Compression compression = 4; 52 | optional string cert_name = 5; 53 | optional int64 max_sequence = 6; 54 | optional bool introducer = 7; 55 | optional uint64 index_id = 8; 56 | } 57 | 58 | enum Compression { 59 | METADATA = 0; 60 | NEVER = 1; 61 | ALWAYS = 2; 62 | } 63 | 64 | message Index { 65 | optional string folder = 1; 66 | repeated FileInfo files = 2; 67 | } 68 | 69 | message IndexUpdate { 70 | optional string folder = 1; 71 | repeated FileInfo files = 2; 72 | } 73 | 74 | message FileInfo { 75 | optional string name = 1; 76 | optional FileInfoType type = 2; 77 | optional int64 size = 3; 78 | optional uint32 permissions = 4; 79 | optional int64 modified_s = 5; 80 | optional int32 modified_ns = 11; 81 | optional uint64 modified_by = 12; 82 | optional bool deleted = 6; 83 | optional bool invalid = 7; 84 | optional bool no_permissions = 8; 85 | optional Vector version = 9; 86 | optional int64 sequence = 10; 87 | 88 | repeated BlockInfo Blocks = 16; 89 | optional string symlink_target = 17; 90 | } 91 | 92 | enum FileInfoType { 93 | FILE = 0; 94 | DIRECTORY = 1; 95 | SYMLINK_FILE = 2; 96 | SYMLINK_DIRECTORY = 3; 97 | SYMLINK = 4; 98 | } 99 | 100 | message BlockInfo { 101 | optional int64 offset = 1; 102 | optional int32 size = 2; 103 | optional bytes hash = 3; 104 | optional uint32 weak_hash = 4; 105 | } 106 | 107 | message Vector { 108 | repeated Counter counters = 1; 109 | } 110 | 111 | message Counter { 112 | optional uint64 id = 1; 113 | optional uint64 value = 2; 114 | } 115 | 116 | message Request { 117 | optional int32 id = 1; 118 | optional string folder = 2; 119 | optional string name = 3; 120 | optional int64 offset = 4; 121 | optional int32 size = 5; 122 | optional bytes hash = 6; 123 | optional bool from_temporary = 7; 124 | } 125 | 126 | message Response { 127 | optional int32 id = 1; 128 | optional bytes data = 2; 129 | optional ErrorCode code = 3; 130 | } 131 | 132 | enum ErrorCode { 133 | NO_ERROR = 0; 134 | GENERIC = 1; 135 | NO_SUCH_FILE = 2; 136 | INVALID_FILE = 3; 137 | } 138 | 139 | message DownloadProgress { 140 | optional string folder = 1; 141 | repeated FileDownloadProgressUpdate updates = 2; 142 | } 143 | 144 | message FileDownloadProgressUpdate { 145 | optional FileDownloadProgressUpdateType update_type = 1; 146 | optional string name = 2; 147 | optional Vector version = 3; 148 | repeated int32 block_indexes = 4; 149 | } 150 | 151 | enum FileDownloadProgressUpdateType { 152 | APPEND = 0; 153 | FORGET = 1; 154 | } 155 | 156 | message Ping { 157 | } 158 | 159 | message Close { 160 | optional string reason = 1; 161 | } 162 | -------------------------------------------------------------------------------- /discovery/src/main/kotlin/net/syncthing/java/discovery/Main.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.discovery 15 | 16 | import net.syncthing.java.core.beans.DeviceAddress 17 | import net.syncthing.java.core.beans.DeviceId 18 | import net.syncthing.java.core.configuration.Configuration 19 | import net.syncthing.java.core.security.KeystoreHandler 20 | import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler 21 | import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler 22 | import org.apache.commons.cli.DefaultParser 23 | import org.apache.commons.cli.HelpFormatter 24 | import org.apache.commons.cli.Option 25 | import org.apache.commons.cli.Options 26 | import org.apache.commons.io.FileUtils 27 | import java.io.File 28 | import java.util.concurrent.CountDownLatch 29 | 30 | class Main { 31 | 32 | companion object { 33 | 34 | private const val MAX_WAIT = 60 * 1000 35 | 36 | @JvmStatic 37 | fun main(args: Array) { 38 | val options = generateOptions() 39 | val parser = DefaultParser() 40 | val cmd = parser.parse(options, args) 41 | if (cmd.hasOption("h")) { 42 | val formatter = HelpFormatter() 43 | formatter.printHelp("s-client", options) 44 | return 45 | } 46 | val configuration = if (cmd.hasOption("C")) Configuration(File(cmd.getOptionValue("C"))) 47 | else Configuration() 48 | 49 | val main = Main() 50 | cmd.options.forEach { main.handleOption(it, configuration) } 51 | } 52 | 53 | private fun generateOptions(): Options { 54 | val options = Options() 55 | options.addOption("C", "set-config", true, "set config file for s-client") 56 | options.addOption("q", "query", true, "query directory server for device id") 57 | options.addOption("d", "discovery", true, "discovery local network for device id") 58 | options.addOption("h", "help", false, "print help") 59 | return options 60 | } 61 | } 62 | 63 | private fun handleOption(option: Option, configuration: Configuration) { 64 | when (option.opt) { 65 | "q" -> { 66 | val deviceId = DeviceId(option.value) 67 | System.out.println("query device id = $deviceId") 68 | val latch = CountDownLatch(1) 69 | GlobalDiscoveryHandler(configuration).query(deviceId, { it -> 70 | val addresses = it.map { it.address }.fold("", { l, r -> "$l\n$r"}) 71 | System.out.println("server response: $addresses") 72 | latch.countDown() 73 | }) 74 | latch.await() 75 | } 76 | "d" -> { 77 | val deviceId = DeviceId(option.value) 78 | System.out.println("discovery device id = $deviceId") 79 | val deviceAddresses = queryLocalDiscovery(configuration, deviceId) 80 | System.out.println("local response = $deviceAddresses") 81 | } 82 | } 83 | } 84 | 85 | private fun queryLocalDiscovery(configuration: Configuration, deviceId: DeviceId): Collection { 86 | val lock = Object() 87 | val discoveredAddresses = mutableListOf() 88 | val handler = LocalDiscoveryHandler(configuration, { discoveredDeviceId, deviceAddresses -> 89 | synchronized(lock) { 90 | if (discoveredDeviceId == deviceId) { 91 | discoveredAddresses.addAll(deviceAddresses) 92 | lock.notify() 93 | } 94 | } 95 | }) 96 | handler.startListener() 97 | handler.sendAnnounceMessage() 98 | synchronized(lock) { 99 | try { 100 | lock.wait(MAX_WAIT.toLong()) 101 | } catch (ex: InterruptedException) { 102 | System.out.println(ex) 103 | } 104 | handler.close() 105 | return discoveredAddresses 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /discovery/src/main/kotlin/net/syncthing/java/discovery/DiscoveryHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * Copyright (C) 2018 Jonas Lochmann 4 | * 5 | * This Java file is subject to the terms of the Mozilla Public 6 | * License, v. 2.0. If a copy of the MPL was not distributed with this 7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package net.syncthing.java.discovery 16 | 17 | import net.syncthing.java.core.beans.DeviceAddress 18 | import net.syncthing.java.core.beans.DeviceId 19 | import net.syncthing.java.core.configuration.Configuration 20 | import net.syncthing.java.core.utils.awaitTerminationSafe 21 | import net.syncthing.java.core.utils.submitLogging 22 | import net.syncthing.java.discovery.protocol.GlobalDiscoveryHandler 23 | import net.syncthing.java.discovery.protocol.LocalDiscoveryHandler 24 | import net.syncthing.java.discovery.utils.AddressRanker 25 | import org.apache.commons.lang3.tuple.Pair 26 | import org.slf4j.LoggerFactory 27 | import java.io.Closeable 28 | import java.util.* 29 | import java.util.concurrent.Executors 30 | 31 | class DiscoveryHandler(private val configuration: Configuration) : Closeable { 32 | 33 | private val logger = LoggerFactory.getLogger(javaClass) 34 | private val globalDiscoveryHandler = GlobalDiscoveryHandler(configuration) 35 | private val localDiscoveryHandler = LocalDiscoveryHandler(configuration, { _, deviceAddresses -> 36 | logger.info("received device address list from local discovery") 37 | processDeviceAddressBg(deviceAddresses) 38 | }, { deviceId -> 39 | onMessageFromUnknownDeviceListeners.forEach { listener -> listener(deviceId) } 40 | }) 41 | private val executorService = Executors.newCachedThreadPool() 42 | private val deviceAddressMap = Collections.synchronizedMap(hashMapOf, DeviceAddress>()) 43 | private val deviceAddressSupplier = DeviceAddressSupplier(this) 44 | private var isClosed = false 45 | private val onMessageFromUnknownDeviceListeners = Collections.synchronizedSet(HashSet<(DeviceId) -> Unit>()) 46 | 47 | private var shouldLoadFromGlobal = true 48 | private var shouldStartLocalDiscovery = true 49 | 50 | fun getAllWorkingDeviceAddresses() = deviceAddressMap.values.filter { it.isWorking() } 51 | 52 | private fun updateAddressesBg() { 53 | if (shouldStartLocalDiscovery) { 54 | shouldStartLocalDiscovery = false 55 | localDiscoveryHandler.startListener() 56 | localDiscoveryHandler.sendAnnounceMessage() 57 | } 58 | if (shouldLoadFromGlobal) { 59 | shouldLoadFromGlobal = false //TODO timeout for reload 60 | executorService.submitLogging { 61 | for (deviceId in configuration.peerIds) { 62 | globalDiscoveryHandler.query(deviceId, this::processDeviceAddressBg) 63 | } 64 | } 65 | } 66 | } 67 | 68 | private fun processDeviceAddressBg(deviceAddresses: Iterable) { 69 | if (isClosed) { 70 | logger.debug("discarding device addresses, discovery handler already closed") 71 | } else { 72 | executorService.submitLogging { 73 | val list = deviceAddresses.toList() 74 | val peers = configuration.peerIds 75 | //do not process address already processed 76 | list.filter { deviceAddress -> 77 | !peers.contains(deviceAddress.deviceId()) || deviceAddressMap.containsKey(Pair.of(DeviceId(deviceAddress.deviceId), deviceAddress.address)) 78 | } 79 | AddressRanker.pingAddresses(list) 80 | .forEach { putDeviceAddress(it) } 81 | } 82 | } 83 | } 84 | 85 | private fun putDeviceAddress(deviceAddress: DeviceAddress) { 86 | deviceAddressMap[Pair.of(DeviceId(deviceAddress.deviceId), deviceAddress.address)] = deviceAddress 87 | deviceAddressSupplier.onNewDeviceAddressAcquired(deviceAddress) 88 | } 89 | 90 | fun newDeviceAddressSupplier(): DeviceAddressSupplier { 91 | updateAddressesBg() 92 | return deviceAddressSupplier 93 | } 94 | 95 | override fun close() { 96 | if (!isClosed) { 97 | isClosed = true 98 | localDiscoveryHandler.close() 99 | globalDiscoveryHandler.close() 100 | executorService.shutdown() 101 | executorService.awaitTerminationSafe() 102 | } 103 | } 104 | 105 | fun registerMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) { 106 | onMessageFromUnknownDeviceListeners.add(listener) 107 | } 108 | 109 | fun unregisterMessageFromUnknownDeviceListener(listener: (DeviceId) -> Unit) { 110 | onMessageFromUnknownDeviceListeners.remove(listener) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/FileInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | import net.syncthing.java.core.utils.PathUtils 17 | import org.apache.commons.io.FileUtils 18 | import java.util.* 19 | 20 | class FileInfo(val folder: String, val type: FileType, val path: String, size: Long? = null, 21 | lastModified: Date? = Date(0), hash: String? = null, versionList: List? = null, 22 | val isDeleted: Boolean = false) { 23 | val fileName: String 24 | val parent: String 25 | val hash: String? 26 | val size: Long? 27 | val lastModified: Date 28 | val versionList: List 29 | 30 | fun isDirectory(): Boolean = type == FileType.DIRECTORY 31 | 32 | fun isFile(): Boolean = type == FileType.FILE 33 | 34 | init { 35 | assert(!folder.isEmpty()) 36 | if (PathUtils.isParent(path)) { 37 | this.fileName = PathUtils.PARENT_PATH 38 | this.parent = PathUtils.ROOT_PATH 39 | } else { 40 | this.fileName = PathUtils.getFileName(path) 41 | this.parent = if (PathUtils.isRoot(path)) PathUtils.ROOT_PATH else PathUtils.getParentPath(path) 42 | } 43 | this.lastModified = lastModified ?: Date(0) 44 | if (type == FileType.DIRECTORY) { 45 | this.size = null 46 | this.hash = null 47 | } else { 48 | assert(size != null) 49 | assert(!hash.isNullOrEmpty()) 50 | this.size = size 51 | this.hash = hash 52 | } 53 | this.versionList = versionList ?: emptyList() 54 | } 55 | 56 | enum class FileType { 57 | FILE, DIRECTORY 58 | } 59 | 60 | fun describeSize(): String = if (isFile()) FileUtils.byteCountToDisplaySize(size!!) else "" 61 | 62 | override fun toString(): String { 63 | return "FileRecord{" + "folder=" + folder + ", path=" + path + ", size=" + size + ", lastModified=" + lastModified + ", type=" + type + ", last version = " + versionList.lastOrNull() + '}' 64 | } 65 | 66 | class Version(val id: Long, val value: Long) { 67 | 68 | override fun toString(): String { 69 | return "Version{id=$id, value=$value}" 70 | } 71 | 72 | } 73 | 74 | class Builder { 75 | 76 | private var folder: String? = null 77 | private var path: String? = null 78 | private var hash: String? = null 79 | private var size: Long? = null 80 | private var lastModified = Date(0) 81 | private var type: FileType? = null 82 | var versionList: List? = null 83 | private set 84 | private var deleted = false 85 | 86 | fun getFolder(): String? { 87 | return folder 88 | } 89 | 90 | fun setFolder(folder: String): Builder { 91 | this.folder = folder 92 | return this 93 | } 94 | 95 | fun getPath(): String? { 96 | return path 97 | } 98 | 99 | fun setPath(path: String): Builder { 100 | this.path = path 101 | return this 102 | } 103 | 104 | fun getSize(): Long? { 105 | return size 106 | } 107 | 108 | fun setSize(size: Long?): Builder { 109 | this.size = size 110 | return this 111 | } 112 | 113 | fun getLastModified(): Date { 114 | return lastModified 115 | } 116 | 117 | fun setLastModified(lastModified: Date): Builder { 118 | this.lastModified = lastModified 119 | return this 120 | } 121 | 122 | fun getType(): FileType? { 123 | return type 124 | } 125 | 126 | fun setType(type: FileType): Builder { 127 | this.type = type 128 | return this 129 | } 130 | 131 | fun setTypeFile(): Builder { 132 | return setType(FileType.FILE) 133 | } 134 | 135 | fun setTypeDir(): Builder { 136 | return setType(FileType.DIRECTORY) 137 | } 138 | 139 | fun setVersionList(versionList: Iterable?): Builder { 140 | this.versionList = versionList?.toList() 141 | return this 142 | } 143 | 144 | fun isDeleted(): Boolean { 145 | return deleted 146 | } 147 | 148 | fun setDeleted(deleted: Boolean): Builder { 149 | this.deleted = deleted 150 | return this 151 | } 152 | 153 | fun getHash(): String? { 154 | return hash 155 | } 156 | 157 | fun setHash(hash: String): Builder { 158 | this.hash = hash 159 | return this 160 | } 161 | 162 | fun build(): FileInfo { 163 | return FileInfo(folder!!, type!!, path!!, size, lastModified, hash, versionList, deleted) 164 | } 165 | 166 | } 167 | 168 | companion object { 169 | 170 | fun checkBlocks(fileInfo: FileInfo, fileBlocks: FileBlocks) { 171 | assert(fileBlocks.folder == fileInfo.folder, {"file info folder not match file block folder"}) 172 | assert(fileBlocks.path == fileInfo.path, {"file info path does not match file block path"}) 173 | assert(fileInfo.isFile(), {"file info must be of type 'FILE' to have blocks"}) 174 | assert(fileBlocks.size == fileInfo.size, {"file info size does not match file block size"}) 175 | assert(fileBlocks.hash == fileInfo.hash, {"file info hash does not match file block hash"}) 176 | } 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/configuration/Configuration.kt: -------------------------------------------------------------------------------- 1 | package net.syncthing.java.core.configuration 2 | 3 | import com.google.gson.GsonBuilder 4 | import net.syncthing.java.core.beans.DeviceId 5 | import net.syncthing.java.core.beans.DeviceInfo 6 | import net.syncthing.java.core.beans.FolderInfo 7 | import net.syncthing.java.core.security.KeystoreHandler 8 | import org.bouncycastle.util.encoders.Base64 9 | import org.slf4j.LoggerFactory 10 | import java.io.File 11 | import java.net.InetAddress 12 | import java.util.* 13 | 14 | class Configuration(configFolder: File = DefaultConfigFolder) { 15 | 16 | private val logger = LoggerFactory.getLogger(javaClass) 17 | 18 | private data class Config( 19 | val peers: Set, 20 | val folders: Set, 21 | val localDeviceName: String, 22 | val localDeviceId: String, 23 | val discoveryServers: Set, 24 | val keystoreAlgorithm: String, 25 | val keystoreData: String) { 26 | // Exclude keystoreData from toString() 27 | override fun toString() = "Config(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " + 28 | "localDeviceId=$localDeviceId, discoveryServers=$discoveryServers, keystoreAlgorithm=$keystoreAlgorithm)" 29 | } 30 | 31 | private val configFile = File(configFolder, ConfigFileName) 32 | val databaseFolder = File(configFolder, DatabaseFolderName) 33 | 34 | private var isSaved = true 35 | private var config: Config 36 | 37 | init { 38 | configFolder.mkdirs() 39 | databaseFolder.mkdirs() 40 | assert(configFolder.isDirectory && configFile.canWrite(), { "Invalid config folder $configFolder" }) 41 | 42 | if (!configFile.exists()) { 43 | var localDeviceName = InetAddress.getLocalHost().hostName 44 | if (localDeviceName.isEmpty() || localDeviceName == "localhost") { 45 | localDeviceName = "syncthing-lite" 46 | } 47 | val keystoreData = KeystoreHandler.Loader().generateKeystore() 48 | isSaved = false 49 | config = Config(peers = setOf(), folders = setOf(), 50 | localDeviceName = localDeviceName, 51 | discoveryServers = Companion.DiscoveryServers, 52 | localDeviceId = keystoreData.first.deviceId, 53 | keystoreData = Base64.toBase64String(keystoreData.second), 54 | keystoreAlgorithm = keystoreData.third) 55 | persistNow() 56 | } else { 57 | config = Gson.fromJson(configFile.readText(), Config::class.java) 58 | 59 | // automatic migration if the old config was used 60 | if (config.discoveryServers == OldDiscoveryServers) { 61 | config = Config( 62 | peers = config.peers, 63 | folders = config.folders, 64 | localDeviceName = config.localDeviceName, 65 | localDeviceId = config.localDeviceId, 66 | discoveryServers = Companion.DiscoveryServers, 67 | keystoreAlgorithm = config.keystoreAlgorithm, 68 | keystoreData = config.keystoreData 69 | ) 70 | } 71 | } 72 | logger.debug("Loaded config = $config") 73 | } 74 | 75 | companion object { 76 | private val DefaultConfigFolder = File(System.getProperty("user.home"), ".config/syncthing-java/") 77 | private const val ConfigFileName = "config.json" 78 | private const val DatabaseFolderName = "database" 79 | private val DiscoveryServers = setOf( 80 | "discovery.syncthing.net", "discovery-v4.syncthing.net", "discovery-v6.syncthing.net") 81 | private val OldDiscoveryServers = setOf( 82 | "discovery-v4-1.syncthing.net", "discovery-v4-2.syncthing.net", "discovery-v4-3.syncthing.net", 83 | "discovery-v6-1.syncthing.net", "discovery-v6-2.syncthing.net", "discovery-v6-3.syncthing.net") 84 | private val Gson = GsonBuilder().setPrettyPrinting().create() 85 | } 86 | 87 | val instanceId = Math.abs(Random().nextLong()) 88 | 89 | val localDeviceId: DeviceId 90 | get() = DeviceId(config.localDeviceId) 91 | 92 | val discoveryServers: Set 93 | get() = config.discoveryServers 94 | 95 | val keystoreData: ByteArray 96 | get() = Base64.decode(config.keystoreData) 97 | 98 | val keystoreAlgorithm: String 99 | get() = config.keystoreAlgorithm 100 | 101 | val clientName = "syncthing-java" 102 | 103 | val clientVersion = javaClass.`package`.implementationVersion ?: "0.0.0" 104 | 105 | val peerIds: Set 106 | get() = config.peers.map { it.deviceId }.toSet() 107 | 108 | var localDeviceName: String 109 | get() = config.localDeviceName 110 | set(localDeviceName) { 111 | config = config.copy(localDeviceName = localDeviceName) 112 | isSaved = false 113 | } 114 | 115 | var folders: Set 116 | get() = config.folders 117 | set(folders) { 118 | config = config.copy(folders = folders) 119 | isSaved = false 120 | } 121 | 122 | var peers: Set 123 | get() = config.peers 124 | set(peers) { 125 | config = config.copy(peers = peers) 126 | isSaved = false 127 | } 128 | 129 | fun persistNow() { 130 | persist() 131 | } 132 | 133 | fun persistLater() { 134 | Thread { persist() }.start() 135 | } 136 | 137 | private fun persist() { 138 | if (isSaved) 139 | return 140 | 141 | config.let { 142 | System.out.println("writing config to $configFile") 143 | configFile.writeText(Gson.toJson(config)) 144 | isSaved = true 145 | } 146 | } 147 | 148 | override fun toString() = "Configuration(peers=$peers, folders=$folders, localDeviceName=$localDeviceName, " + 149 | "localDeviceId=${localDeviceId.deviceId}, discoveryServers=$discoveryServers, instanceId=$instanceId, " + 150 | "configFile=$configFile, databaseFolder=$databaseFolder)" 151 | } 152 | -------------------------------------------------------------------------------- /bep/src/main/kotlin/net/syncthing/java/bep/BlockPuller.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.bep 15 | 16 | import com.google.protobuf.ByteString 17 | import net.syncthing.java.bep.BlockExchangeProtos.ErrorCode 18 | import net.syncthing.java.bep.BlockExchangeProtos.Request 19 | import net.syncthing.java.core.beans.FileInfo 20 | import net.syncthing.java.core.utils.NetworkUtils 21 | import org.apache.commons.io.FileUtils 22 | import org.bouncycastle.util.encoders.Hex 23 | import org.slf4j.LoggerFactory 24 | import java.io.* 25 | import java.security.MessageDigest 26 | import java.util.* 27 | import java.util.concurrent.ConcurrentHashMap 28 | import java.util.concurrent.atomic.AtomicReference 29 | 30 | class BlockPuller internal constructor(private val connectionHandler: ConnectionHandler, 31 | private val indexHandler: IndexHandler) { 32 | 33 | private val logger = LoggerFactory.getLogger(javaClass) 34 | private val blocksByHash = ConcurrentHashMap() 35 | private val hashList = mutableListOf() 36 | private val missingHashes: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) 37 | private val requestIds: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) 38 | private val lock = Object() 39 | 40 | fun pullFile(fileInfo: FileInfo): FileDownloadObserver { 41 | val fileBlocks = indexHandler.waitForRemoteIndexAcquired(connectionHandler) 42 | .getFileInfoAndBlocksByPath(fileInfo.folder, fileInfo.path) 43 | ?.value 44 | ?: throw IOException("file not found in local index for folder = ${fileInfo.folder} path = ${fileInfo.path}") 45 | logger.info("pulling file = {}", fileBlocks) 46 | NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileBlocks.folder), {"supplied connection handler $connectionHandler will not share folder ${fileBlocks.folder}"}) 47 | val error = AtomicReference() 48 | val fileDownloadObserver = object : FileDownloadObserver() { 49 | 50 | private fun receivedData() = (blocksByHash.size * BlockPusher.BLOCK_SIZE).toLong() 51 | 52 | private fun totalData() = ((blocksByHash.size + missingHashes.size) * BlockPusher.BLOCK_SIZE).toLong() 53 | 54 | override fun progress() = if (isCompleted()) 1.0 else receivedData() / totalData().toDouble() 55 | 56 | override fun progressMessage() = (Math.round(progress() * 1000.0) / 10.0).toString() + "% " + 57 | FileUtils.byteCountToDisplaySize(receivedData()) + " / " + FileUtils.byteCountToDisplaySize(totalData()) 58 | 59 | override fun isCompleted() = missingHashes.isEmpty() 60 | 61 | override fun inputStream(): InputStream { 62 | NetworkUtils.assertProtocol(missingHashes.isEmpty(), {"pull failed, some blocks are still missing"}) 63 | val blockList = hashList.map { blocksByHash[it] }.toList() 64 | return SequenceInputStream(Collections.enumeration(blockList.map { ByteArrayInputStream(it) })) 65 | } 66 | 67 | override fun checkError() { 68 | if (error.get() != null) { 69 | throw IOException(error.get()) 70 | } 71 | } 72 | 73 | @Throws(InterruptedException::class) 74 | override fun waitForProgressUpdate(): Double { 75 | if (!isCompleted()) { 76 | synchronized(lock) { 77 | checkError() 78 | lock.wait() 79 | checkError() 80 | } 81 | } 82 | return progress() 83 | } 84 | 85 | override fun close() { 86 | missingHashes.clear() 87 | hashList.clear() 88 | blocksByHash.clear() 89 | } 90 | } 91 | synchronized(lock) { 92 | hashList.addAll(fileBlocks.blocks.map { it.hash }) 93 | missingHashes.addAll(hashList) 94 | for (block in fileBlocks.blocks) { 95 | if (missingHashes.contains(block.hash)) { 96 | val requestId = Math.abs(Random().nextInt()) 97 | requestIds.add(requestId) 98 | connectionHandler.sendMessage(Request.newBuilder() 99 | .setId(requestId) 100 | .setFolder(fileBlocks.folder) 101 | .setName(fileBlocks.path) 102 | .setOffset(block.offset) 103 | .setSize(block.size) 104 | .setHash(ByteString.copyFrom(Hex.decode(block.hash))) 105 | .build()) 106 | logger.debug("sent request for block, hash = {}", block.hash) 107 | } 108 | } 109 | return fileDownloadObserver 110 | } 111 | } 112 | 113 | fun onResponseMessageReceived(response: BlockExchangeProtos.Response) { 114 | synchronized(lock) { 115 | if (!requestIds.contains(response.id)) { 116 | return 117 | } 118 | NetworkUtils.assertProtocol(response.code == ErrorCode.NO_ERROR, {"received error response, code = ${response.code}"}) 119 | val data = response.data.toByteArray() 120 | val hash = Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(data)) 121 | if (missingHashes.remove(hash)) { 122 | blocksByHash.put(hash, data) 123 | logger.debug("aquired block, hash = {}", hash) 124 | lock.notify() 125 | } else { 126 | logger.warn("received not-needed block, hash = {}", hash) 127 | } 128 | } 129 | } 130 | 131 | abstract inner class FileDownloadObserver : Closeable { 132 | 133 | abstract fun progress(): Double 134 | 135 | abstract fun progressMessage(): String 136 | 137 | abstract fun isCompleted(): Boolean 138 | 139 | abstract fun inputStream(): InputStream 140 | 141 | abstract fun checkError() 142 | 143 | @Throws(InterruptedException::class) 144 | abstract fun waitForProgressUpdate(): Double 145 | 146 | @Throws(InterruptedException::class) 147 | fun waitForComplete(): FileDownloadObserver { 148 | while (!isCompleted()) { 149 | waitForProgressUpdate() 150 | } 151 | return this 152 | } 153 | 154 | abstract override fun close() 155 | 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/beans/DeviceAddress.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.beans 15 | 16 | import org.slf4j.LoggerFactory 17 | import java.net.InetAddress 18 | import java.net.InetSocketAddress 19 | import java.net.UnknownHostException 20 | import java.util.* 21 | 22 | /** 23 | * 24 | * TODO: this class cant use [[DeviceId]] because [[GlobalDiscoveryHandler.pickAnnounceServers]] uses that field for discovery server URLs. 25 | */ 26 | class DeviceAddress private constructor(val deviceId: String, private val instanceId: Long?, val address: String, producer: AddressProducer?, score: Int?, lastModified: Date?) { 27 | private val producer = producer ?: AddressProducer.UNKNOWN 28 | val score = score ?: Integer.MAX_VALUE 29 | private val lastModified = lastModified ?: Date() 30 | 31 | fun deviceId() = DeviceId(deviceId) 32 | 33 | @Throws(UnknownHostException::class) 34 | private fun getInetAddress(): InetAddress = InetAddress.getByName(address.replaceFirst("^[^:]+://".toRegex(), "").replaceFirst("(:[0-9]+)?(/.*)?$".toRegex(), "")) 35 | 36 | private fun getPort(): Int = if (address.matches("^[a-z]+://[^:]+:([0-9]+).*".toRegex())) { 37 | Integer.parseInt(address.replaceFirst("^[a-z]+://[^:]+:([0-9]+).*".toRegex(), "$1")) 38 | } else { 39 | DEFAULT_PORT_BY_PROTOCOL[getType()]!! 40 | } 41 | 42 | fun getType(): AddressType = when { 43 | address.isEmpty() -> AddressType.NULL 44 | address.startsWith("tcp://") -> AddressType.TCP 45 | address.startsWith("relay://") -> AddressType.RELAY 46 | address.startsWith("relay-http://") -> AddressType.HTTP_RELAY 47 | address.startsWith("relay-https://") -> AddressType.HTTPS_RELAY 48 | else -> AddressType.OTHER 49 | } 50 | 51 | @Throws(UnknownHostException::class) 52 | fun getSocketAddress(): InetSocketAddress = InetSocketAddress(getInetAddress(), getPort()) 53 | 54 | fun isWorking(): Boolean = score < Integer.MAX_VALUE 55 | 56 | constructor(deviceId: String, address: String) : this(deviceId, null, address, null, null, null) 57 | 58 | fun containsUriParamValue(key: String): Boolean { 59 | return !getUriParam(key).isNullOrEmpty() 60 | } 61 | 62 | /** 63 | * Returns value for the specified URL parameter key. 64 | * 65 | * We need to parse the URL manually, as it is not URL encoded and may contain invalid key/values 66 | * like "key=a b" (with an unencoded space). 67 | */ 68 | fun getUriParam(key: String): String? { 69 | assert(!key.isEmpty()) 70 | return address 71 | .split("?", limit = 2).first() 72 | .splitToSequence("&") 73 | .map { it.split("=", limit = 2) } 74 | .map { it[0] to (it.getOrNull(1) ?: "") } 75 | .find { it.first == key } 76 | ?.second 77 | } 78 | 79 | enum class AddressType { 80 | TCP, RELAY, OTHER, NULL, HTTP_RELAY, HTTPS_RELAY 81 | } 82 | 83 | enum class AddressProducer { 84 | LOCAL_DISCOVERY, GLOBAL_DISCOVERY, UNKNOWN 85 | } 86 | 87 | override fun toString(): String { 88 | return "DeviceAddress(deviceId=$deviceId, instanceId=$instanceId, address=$address, producer=$producer, score=$score, lastModified=$lastModified)" 89 | } 90 | 91 | override fun hashCode(): Int { 92 | var hash = 3 93 | hash = 29 * hash + Objects.hashCode(this.deviceId) 94 | hash = 29 * hash + Objects.hashCode(this.address) 95 | return hash 96 | } 97 | 98 | override fun equals(obj: Any?): Boolean { 99 | if (this === obj) { 100 | return true 101 | } 102 | if (obj == null) { 103 | return false 104 | } 105 | if (javaClass != obj.javaClass) { 106 | return false 107 | } 108 | val other = obj as DeviceAddress? 109 | if (this.deviceId != other!!.deviceId) { 110 | return false 111 | } 112 | return this.address == other.address 113 | } 114 | 115 | fun copyBuilder(): Builder { 116 | return Builder(deviceId, instanceId, address, producer, score, lastModified) 117 | } 118 | 119 | class Builder { 120 | 121 | private var deviceId: String? = null 122 | private var instanceId: Long? = null 123 | private var address: String? = null 124 | private var producer: AddressProducer? = null 125 | private var score: Int? = null 126 | private var lastModified: Date? = null 127 | 128 | constructor() 129 | 130 | internal constructor(deviceId: String, instanceId: Long?, address: String, producer: AddressProducer, score: Int?, lastModified: Date) { 131 | this.deviceId = deviceId 132 | this.instanceId = instanceId 133 | this.address = address 134 | this.producer = producer 135 | this.score = score 136 | this.lastModified = lastModified 137 | } 138 | 139 | fun getLastModified(): Date? { 140 | return lastModified 141 | } 142 | 143 | fun setLastModified(lastModified: Date): Builder { 144 | this.lastModified = lastModified 145 | return this 146 | } 147 | 148 | fun getDeviceId(): String? { 149 | return deviceId 150 | } 151 | 152 | fun setDeviceId(deviceId: String): Builder { 153 | this.deviceId = deviceId 154 | return this 155 | } 156 | 157 | fun getInstanceId(): Long? { 158 | return instanceId 159 | } 160 | 161 | fun setInstanceId(instanceId: Long?): Builder { 162 | this.instanceId = instanceId 163 | return this 164 | } 165 | 166 | fun getAddress(): String? { 167 | return address 168 | } 169 | 170 | fun setAddress(address: String): Builder { 171 | this.address = address 172 | return this 173 | } 174 | 175 | fun getProducer(): AddressProducer? { 176 | return producer 177 | } 178 | 179 | fun setProducer(producer: AddressProducer): Builder { 180 | this.producer = producer 181 | return this 182 | } 183 | 184 | fun getScore(): Int? { 185 | return score 186 | } 187 | 188 | fun setScore(score: Int?): Builder { 189 | this.score = score 190 | return this 191 | } 192 | 193 | fun build(): DeviceAddress { 194 | return DeviceAddress(deviceId!!, instanceId, address!!, producer, score, lastModified) 195 | } 196 | } 197 | 198 | companion object { 199 | private val DEFAULT_PORT_BY_PROTOCOL = mapOf( 200 | AddressType.TCP to 22000, 201 | AddressType.RELAY to 22067, 202 | AddressType.HTTP_RELAY to 80, 203 | AddressType.HTTPS_RELAY to 443) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /discovery/src/main/kotlin/net/syncthing/java/discovery/protocol/LocalDiscoveryHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * Copyright (C) 2018 Jonas Lochmann 4 | * 5 | * This Java file is subject to the terms of the Mozilla Public 6 | * License, v. 2.0. If a copy of the MPL was not distributed with this 7 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package net.syncthing.java.discovery.protocol 16 | 17 | import com.google.protobuf.ByteString 18 | import com.google.protobuf.InvalidProtocolBufferException 19 | import net.syncthing.java.core.beans.DeviceAddress 20 | import net.syncthing.java.core.beans.DeviceId 21 | import net.syncthing.java.core.configuration.Configuration 22 | import net.syncthing.java.core.events.DeviceAddressReceivedEvent 23 | import net.syncthing.java.core.utils.NetworkUtils 24 | import net.syncthing.java.core.utils.submitLogging 25 | import net.syncthing.java.discovery.protocol.LocalDiscoveryProtos.Announce 26 | import org.apache.commons.io.IOUtils 27 | import org.slf4j.LoggerFactory 28 | import java.io.ByteArrayOutputStream 29 | import java.io.Closeable 30 | import java.io.DataOutputStream 31 | import java.io.IOException 32 | import java.net.DatagramPacket 33 | import java.net.DatagramSocket 34 | import java.net.InetAddress 35 | import java.net.NetworkInterface 36 | import java.nio.ByteBuffer 37 | import java.util.concurrent.Executors 38 | 39 | internal class LocalDiscoveryHandler(private val configuration: Configuration, 40 | private val onMessageReceivedListener: (DeviceId, List) -> Unit, 41 | private val onMessageFromUnknownDeviceListener: (DeviceId) -> Unit = {}) : Closeable { 42 | 43 | companion object { 44 | private const val MAGIC = 0x2EA7D90B 45 | private const val LISTENING_PORT = 21027 46 | private const val INCOMING_BUFFER_SIZE = 1024 47 | } 48 | 49 | private val logger = LoggerFactory.getLogger(javaClass) 50 | private val listeningExecutor = Executors.newSingleThreadScheduledExecutor() 51 | private val processingExecutor = Executors.newCachedThreadPool() 52 | 53 | private var datagramSocket: DatagramSocket? = null 54 | 55 | fun sendAnnounceMessage() { 56 | processingExecutor.submitLogging { 57 | try { 58 | val out = ByteArrayOutputStream() 59 | DataOutputStream(out).writeInt(MAGIC) 60 | Announce.newBuilder() 61 | .setId(ByteString.copyFrom(configuration.localDeviceId.toHashData())) 62 | .setInstanceId(configuration.instanceId) 63 | .build().writeTo(out) 64 | val data = out.toByteArray() 65 | val networkInterfaces = NetworkInterface.getNetworkInterfaces() 66 | while (networkInterfaces.hasMoreElements()) { 67 | val networkInterface = networkInterfaces.nextElement() 68 | for (interfaceAddress in networkInterface.interfaceAddresses) { 69 | val broadcastAddress = interfaceAddress.broadcast 70 | logger.trace("interface = {} address = {} broadcast = {}", networkInterface, interfaceAddress, broadcastAddress) 71 | if (broadcastAddress != null) { 72 | logger.debug("sending broadcast announce on {}", broadcastAddress) 73 | DatagramSocket().use { broadcastSocket -> 74 | broadcastSocket.broadcast = true 75 | val datagramPacket = DatagramPacket( 76 | data, data.size, broadcastAddress, LISTENING_PORT) 77 | broadcastSocket.send(datagramPacket) 78 | } 79 | } 80 | } 81 | } 82 | } catch (e: IOException) { 83 | logger.warn("Failed to send local announce message", e) 84 | } 85 | } 86 | } 87 | 88 | fun startListener() { 89 | if (datagramSocket == null || datagramSocket!!.isClosed) { 90 | try { 91 | datagramSocket = DatagramSocket(LISTENING_PORT, InetAddress.getByName("0.0.0.0")) 92 | logger.info("Opened udp socket {}", datagramSocket!!.localSocketAddress) 93 | } catch (e: IOException) { 94 | logger.warn("Failed to open listening socket on port $LISTENING_PORT, ${e.message}") 95 | return 96 | } 97 | 98 | } 99 | 100 | listeningExecutor.submitLogging(object : Runnable { 101 | override fun run() { 102 | try { 103 | val datagramPacket = DatagramPacket(ByteArray(INCOMING_BUFFER_SIZE), INCOMING_BUFFER_SIZE) 104 | logger.trace("waiting for message on socket addr = {}", datagramSocket!!.localSocketAddress) 105 | datagramSocket!!.receive(datagramPacket) 106 | processingExecutor.submitLogging { handleReceivedDatagram(datagramPacket) } 107 | listeningExecutor.submitLogging(this) 108 | } catch (e: IOException) { 109 | if (e.message == "Socket closed") { 110 | // Ignore exception on socket close. 111 | return 112 | } 113 | logger.warn("Error receiving datagram", e) 114 | close() 115 | } 116 | 117 | } 118 | }) 119 | } 120 | 121 | private fun handleReceivedDatagram(datagramPacket: DatagramPacket) { 122 | try { 123 | val sourceAddress = datagramPacket.address.hostAddress 124 | val byteBuffer = ByteBuffer.wrap( 125 | datagramPacket.data, datagramPacket.offset, datagramPacket.length) 126 | val magic = byteBuffer.int 127 | NetworkUtils.assertProtocol(magic == MAGIC, {"magic mismatch, expected $MAGIC, got $magic"}) 128 | val announce = Announce.parseFrom(ByteString.copyFrom(byteBuffer)) 129 | val deviceId = DeviceId.fromHashData(announce.id.toByteArray()) 130 | 131 | // Ignore announcement received from ourselves. 132 | if (deviceId == configuration.localDeviceId) 133 | return 134 | 135 | if (!configuration.peerIds.contains(deviceId)) { 136 | logger.trace("Received local announce from $deviceId which is not a peer, ignoring") 137 | 138 | onMessageFromUnknownDeviceListener(deviceId) 139 | 140 | return 141 | } 142 | 143 | logger.debug("received local announce from device id = {}", deviceId) 144 | val addressesList = announce.addressesList ?: listOf() 145 | val deviceAddresses = addressesList.map { address -> 146 | // When interpreting addresses with an unspecified address, e.g., 147 | // tcp://0.0.0.0:22000 or tcp://:42424, the source address of the 148 | // discovery announcement is to be used. 149 | DeviceAddress.Builder() 150 | .setAddress(address.replaceFirst("tcp://(0.0.0.0|):".toRegex(), "tcp://$sourceAddress:")) 151 | .setDeviceId(deviceId.deviceId) 152 | .setInstanceId(announce.instanceId) 153 | .setProducer(DeviceAddress.AddressProducer.LOCAL_DISCOVERY) 154 | .build() 155 | } 156 | onMessageReceivedListener(deviceId, deviceAddresses) 157 | } catch (ex: InvalidProtocolBufferException) { 158 | logger.warn("error processing datagram", ex) 159 | } 160 | 161 | } 162 | 163 | override fun close() { 164 | processingExecutor.shutdown() 165 | listeningExecutor.shutdown() 166 | if (datagramSocket != null) { 167 | IOUtils.closeQuietly(datagramSocket) 168 | } 169 | } 170 | 171 | abstract inner class MessageReceivedEvent : DeviceAddressReceivedEvent { 172 | 173 | abstract fun deviceId(): DeviceId 174 | 175 | abstract override fun getDeviceAddresses(): List 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /bep/src/main/kotlin/net/syncthing/java/bep/IndexBrowser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.bep 15 | 16 | import net.syncthing.java.core.beans.FileInfo 17 | import net.syncthing.java.core.interfaces.IndexRepository 18 | import net.syncthing.java.core.utils.PathUtils 19 | import net.syncthing.java.core.utils.awaitTerminationSafe 20 | import net.syncthing.java.core.utils.submitLogging 21 | import org.apache.commons.lang3.StringUtils 22 | import org.slf4j.LoggerFactory 23 | import java.io.Closeable 24 | import java.util.* 25 | import java.util.concurrent.Executors 26 | 27 | class IndexBrowser internal constructor(private val indexRepository: IndexRepository, private val indexHandler: IndexHandler, 28 | val folder: String, private val includeParentInList: Boolean = false, 29 | private val allowParentInRoot: Boolean = false, ordering: Comparator?) : Closeable { 30 | 31 | private fun isParent(fileInfo: FileInfo) = PathUtils.isParent(fileInfo.path) 32 | 33 | val ALPHA_ASC_DIR_FIRST: Comparator = 34 | compareBy({!isParent(it)}, {!it.isDirectory()}) 35 | .thenBy { it.fileName.toLowerCase() } 36 | val LAST_MOD_DESC: Comparator = 37 | compareBy({!isParent(it)}, {it.lastModified}) 38 | .thenBy { it.fileName.toLowerCase() } 39 | 40 | private val ordering = ordering ?: ALPHA_ASC_DIR_FIRST 41 | private val logger = LoggerFactory.getLogger(javaClass) 42 | 43 | var currentPath: String = PathUtils.ROOT_PATH 44 | private set 45 | private val PARENT_FILE_INFO: FileInfo 46 | private val ROOT_FILE_INFO: FileInfo 47 | private val executorService = Executors.newSingleThreadScheduledExecutor() 48 | private val preloadJobs = mutableSetOf() 49 | private val preloadJobsLock = Any() 50 | private var mOnPathChangedListener: (() -> Unit)? = null 51 | 52 | private fun isCacheReady(): Boolean { 53 | synchronized(preloadJobsLock) { 54 | return preloadJobs.isEmpty() 55 | } 56 | } 57 | 58 | internal fun onIndexChangedevent(folder: String, newRecord: FileInfo) { 59 | if (folder == this.folder) { 60 | preloadFileInfoForCurrentPath() 61 | } 62 | } 63 | 64 | fun currentPathInfo(): FileInfo = getFileInfoByAbsolutePath(currentPath) 65 | 66 | fun currentPathFileName(): String? = PathUtils.getFileName(currentPath) 67 | 68 | fun isRoot(): Boolean = PathUtils.isRoot(currentPath) 69 | 70 | init { 71 | assert(folder.isNotEmpty()) 72 | PARENT_FILE_INFO = FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.PARENT_PATH) 73 | ROOT_FILE_INFO = FileInfo(folder = folder, type = FileInfo.FileType.DIRECTORY, path = PathUtils.ROOT_PATH) 74 | navigateToAbsolutePath(PathUtils.ROOT_PATH) 75 | } 76 | 77 | fun setOnFolderChangedListener(onPathChangedListener: (() -> Unit)?) { 78 | mOnPathChangedListener = onPathChangedListener 79 | } 80 | 81 | private fun preloadFileInfoForCurrentPath() { 82 | logger.debug("trigger preload for folder = '{}'", folder) 83 | synchronized(preloadJobsLock) { 84 | currentPath.let { currentPath -> 85 | if (preloadJobs.contains(currentPath)) { 86 | preloadJobs.remove(currentPath) 87 | preloadJobs.add(currentPath) ///add last 88 | } else { 89 | preloadJobs.add(currentPath) 90 | executorService.submitLogging(object : Runnable { 91 | 92 | override fun run() { 93 | 94 | val preloadPath = 95 | synchronized(preloadJobsLock) { 96 | assert(!preloadJobs.isEmpty()) 97 | preloadJobs.last() //pop last job 98 | } 99 | 100 | logger.info("folder preload BEGIN for folder = '{}' path = '{}'", folder, preloadPath) 101 | getFileInfoByAbsolutePath(preloadPath) 102 | if (!PathUtils.isRoot(preloadPath)) { 103 | val parent = PathUtils.getParentPath(preloadPath) 104 | getFileInfoByAbsolutePath(parent) 105 | listFiles(parent) 106 | } 107 | for (record in listFiles(preloadPath)) { 108 | if (record.path == PARENT_FILE_INFO.path && record.isDirectory()) { 109 | listFiles(record.path) 110 | } 111 | } 112 | logger.info("folder preload END for folder = '{}' path = '{}'", folder, preloadPath) 113 | synchronized(preloadJobsLock) { 114 | preloadJobs.remove(preloadPath) 115 | if (isCacheReady()) { 116 | logger.info("cache ready, notify listeners") 117 | mOnPathChangedListener?.invoke() 118 | } else { 119 | logger.info("still {} job[s] left in cache loader", preloadJobs.size) 120 | executorService.submitLogging(this) 121 | } 122 | } 123 | } 124 | }) 125 | } 126 | } 127 | } 128 | } 129 | 130 | fun listFiles(path: String = currentPath): List { 131 | logger.debug("doListFiles for path = '{}' BEGIN", path) 132 | val list = ArrayList(indexRepository.findNotDeletedFilesByFolderAndParent(folder, path)) 133 | logger.debug("doListFiles for path = '{}' : {} records loaded)", path, list.size) 134 | if (includeParentInList && (!PathUtils.isRoot(path) || allowParentInRoot)) { 135 | list.add(0, PARENT_FILE_INFO) 136 | } 137 | return list.sortedWith(ordering) 138 | } 139 | 140 | fun getFileInfoByAbsolutePath(path: String): FileInfo { 141 | return if (PathUtils.isRoot(path)) { 142 | ROOT_FILE_INFO 143 | } else { 144 | logger.debug("doGetFileInfoByAbsolutePath for path = '{}' BEGIN", path) 145 | val fileInfo = indexRepository.findNotDeletedFileInfo(folder, path) ?: error("file not found for path = $path") 146 | logger.debug("doGetFileInfoByAbsolutePath for path = '{}' END", path) 147 | fileInfo 148 | } 149 | } 150 | 151 | fun navigateTo(fileInfo: FileInfo) { 152 | assert(fileInfo.isDirectory()) 153 | assert(fileInfo.folder == folder) 154 | return if (fileInfo.path == PARENT_FILE_INFO.path) 155 | navigateToAbsolutePath(PathUtils.getParentPath(currentPath)) 156 | else 157 | navigateToAbsolutePath(fileInfo.path) 158 | } 159 | 160 | fun navigateToNearestPath(oldPath: String) { 161 | if (!StringUtils.isBlank(oldPath)) { 162 | navigateToAbsolutePath(oldPath) 163 | } 164 | } 165 | 166 | private fun navigateToAbsolutePath(newPath: String) { 167 | if (PathUtils.isRoot(newPath)) { 168 | currentPath = PathUtils.ROOT_PATH 169 | } else { 170 | val fileInfo = getFileInfoByAbsolutePath(newPath) 171 | assert(fileInfo.isDirectory(), {"cannot navigate to path ${fileInfo.path}: not a directory"}) 172 | currentPath = fileInfo.path 173 | } 174 | logger.info("navigate to path = '{}'", currentPath) 175 | preloadFileInfoForCurrentPath() 176 | } 177 | 178 | override fun close() { 179 | logger.info("closing") 180 | indexHandler.unregisterIndexBrowser(this) 181 | executorService.shutdown() 182 | executorService.awaitTerminationSafe() 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /client/src/main/kotlin/net/syncthing/java/client/SyncthingClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.client 15 | 16 | import net.syncthing.java.bep.BlockPuller 17 | import net.syncthing.java.bep.BlockPusher 18 | import net.syncthing.java.bep.ConnectionHandler 19 | import net.syncthing.java.bep.IndexHandler 20 | import net.syncthing.java.core.beans.DeviceAddress 21 | import net.syncthing.java.core.beans.DeviceId 22 | import net.syncthing.java.core.beans.DeviceInfo 23 | import net.syncthing.java.core.configuration.Configuration 24 | import net.syncthing.java.core.interfaces.IndexRepository 25 | import net.syncthing.java.core.interfaces.TempRepository 26 | import net.syncthing.java.core.security.KeystoreHandler 27 | import net.syncthing.java.core.utils.awaitTerminationSafe 28 | import net.syncthing.java.discovery.DiscoveryHandler 29 | import org.slf4j.LoggerFactory 30 | import java.io.Closeable 31 | import java.io.IOException 32 | import java.util.Collections 33 | import java.util.TreeSet 34 | import java.util.concurrent.Executors 35 | import java.util.concurrent.TimeUnit 36 | import java.util.concurrent.atomic.AtomicBoolean 37 | import kotlin.collections.ArrayList 38 | 39 | class SyncthingClient( 40 | private val configuration: Configuration, 41 | private val repository: IndexRepository, 42 | private val tempRepository: TempRepository 43 | ) : Closeable { 44 | 45 | private val logger = LoggerFactory.getLogger(javaClass) 46 | val discoveryHandler: DiscoveryHandler 47 | val indexHandler: IndexHandler 48 | private val connections = Collections.synchronizedSet(createConnectionsSet()) 49 | private val connectByDeviceIdLocks = Collections.synchronizedMap(HashMap()) 50 | private val onConnectionChangedListeners = Collections.synchronizedList(mutableListOf<(DeviceId) -> Unit>()) 51 | private var connectDevicesScheduler = Executors.newSingleThreadScheduledExecutor() 52 | 53 | private fun createConnectionsSet() = TreeSet(compareBy { it.address.score }) 54 | 55 | init { 56 | indexHandler = IndexHandler(configuration, repository, tempRepository) 57 | discoveryHandler = DiscoveryHandler(configuration) 58 | connectDevicesScheduler.scheduleAtFixedRate(this::updateIndexFromPeers, 0, 15, TimeUnit.SECONDS) 59 | } 60 | 61 | fun clearCacheAndIndex() { 62 | indexHandler.clearIndex() 63 | configuration.folders = emptySet() 64 | configuration.persistLater() 65 | updateIndexFromPeers() 66 | } 67 | 68 | fun addOnConnectionChangedListener(listener: (DeviceId) -> Unit) { 69 | onConnectionChangedListeners.add(listener) 70 | } 71 | 72 | fun removeOnConnectionChangedListener(listener: (DeviceId) -> Unit) { 73 | assert(onConnectionChangedListeners.contains(listener)) 74 | onConnectionChangedListeners.remove(listener) 75 | } 76 | 77 | @Throws(IOException::class, KeystoreHandler.CryptoException::class) 78 | private fun openConnection(deviceAddress: DeviceAddress): ConnectionHandler { 79 | logger.debug("Connecting to ${deviceAddress.deviceId}, active connections: ${connections.map { it.deviceId().deviceId }}") 80 | val connectionHandler = ConnectionHandler( 81 | configuration, deviceAddress, indexHandler, { connectionHandler, _ -> 82 | connectionHandler.close() 83 | openConnection(deviceAddress) 84 | }, 85 | {connection -> 86 | if (!connection.isConnected) { 87 | connections.remove(connection) 88 | } 89 | onConnectionChangedListeners.forEach { it(connection.deviceId()) } 90 | }) 91 | 92 | try { 93 | connectionHandler.connect() 94 | } catch (ex: Exception) { 95 | connectionHandler.closeBg() 96 | 97 | throw ex 98 | } 99 | 100 | connections.add(connectionHandler) 101 | 102 | return connectionHandler 103 | } 104 | 105 | /** 106 | * Takes discovered addresses from [[DiscoveryHandler]] and connects to devices. 107 | * 108 | * We need to make sure that we are only connecting once to each device. 109 | */ 110 | private fun getPeerConnections(listener: (connection: ConnectionHandler) -> Unit, completeListener: () -> Unit) { 111 | // create an copy to prevent dispatching an action two times 112 | val connectionsWhichWereDispatched = createConnectionsSet() 113 | 114 | synchronized (connections) { 115 | connectionsWhichWereDispatched.addAll(connections) 116 | } 117 | 118 | connectionsWhichWereDispatched.forEach { listener(it) } 119 | 120 | discoveryHandler.newDeviceAddressSupplier() 121 | .takeWhile { it != null } 122 | .filterNotNull() 123 | .groupBy { it.deviceId() } 124 | .filterNot { it.value.isEmpty() } 125 | .forEach { (deviceId, addresses) -> 126 | // create an lock per device id to prevent multiple connections to one device 127 | 128 | synchronized (connectByDeviceIdLocks) { 129 | if (connectByDeviceIdLocks[deviceId] == null) { 130 | connectByDeviceIdLocks[deviceId] = Object() 131 | } 132 | } 133 | 134 | synchronized (connectByDeviceIdLocks[deviceId]!!) { 135 | val existingConnection = connections.find { it.deviceId() == deviceId && it.isConnected } 136 | 137 | if (existingConnection != null) { 138 | connectionsWhichWereDispatched.add(existingConnection) 139 | listener(existingConnection) 140 | 141 | return@synchronized 142 | } 143 | 144 | // try to use all addresses 145 | for (address in addresses.distinctBy { it.address }) { 146 | try { 147 | val newConnection = openConnection(address) 148 | 149 | connectionsWhichWereDispatched.add(newConnection) 150 | listener(newConnection) 151 | 152 | break // it worked, no need to try more 153 | } catch (e: IOException) { 154 | logger.warn("error connecting to device = $address", e) 155 | } catch (e: KeystoreHandler.CryptoException) { 156 | logger.warn("error connecting to device = $address", e) 157 | } 158 | } 159 | } 160 | } 161 | 162 | // use all connections which were added in the time between and were not added by this function call 163 | val newConnectionsBackup = createConnectionsSet() 164 | 165 | synchronized (connections) { 166 | newConnectionsBackup.addAll(connections) 167 | } 168 | 169 | connectionsWhichWereDispatched.forEach { newConnectionsBackup.remove(it) } 170 | 171 | newConnectionsBackup.forEach { listener(it) } 172 | 173 | completeListener() 174 | } 175 | 176 | private fun updateIndexFromPeers() { 177 | getPeerConnections({ connection -> 178 | try { 179 | indexHandler.waitForRemoteIndexAcquired(connection) 180 | } catch (ex: InterruptedException) { 181 | logger.warn("exception while waiting for index", ex) 182 | } 183 | }, {}) 184 | } 185 | 186 | private fun getConnectionForFolder(folder: String, listener: (connection: ConnectionHandler) -> Unit, 187 | errorListener: () -> Unit) { 188 | val isConnected = AtomicBoolean(false) 189 | getPeerConnections({ connection -> 190 | if (connection.hasFolder(folder) && !isConnected.get()) { 191 | listener(connection) 192 | isConnected.set(true) 193 | } 194 | }, { 195 | if (!isConnected.get()) { 196 | errorListener() 197 | } 198 | }) 199 | } 200 | 201 | fun getBlockPuller(folderId: String, listener: (BlockPuller) -> Unit, errorListener: () -> Unit) { 202 | getConnectionForFolder(folderId, { connection -> 203 | listener(connection.getBlockPuller()) 204 | }, errorListener) 205 | } 206 | 207 | fun getBlockPusher(folderId: String, listener: (BlockPusher) -> Unit, errorListener: () -> Unit) { 208 | getConnectionForFolder(folderId, { connection -> 209 | listener(connection.getBlockPusher()) 210 | }, errorListener) 211 | } 212 | 213 | fun getPeerStatus(): List { 214 | return configuration.peers.map { device -> 215 | val isConnected = connections.find { it.deviceId() == device.deviceId }?.isConnected ?: false 216 | device.copy(isConnected = isConnected) 217 | } 218 | } 219 | 220 | override fun close() { 221 | connectDevicesScheduler.awaitTerminationSafe() 222 | discoveryHandler.close() 223 | // Create copy of list, because it will be modified by handleConnectionClosedEvent(), causing ConcurrentModificationException. 224 | ArrayList(connections).forEach{it.close()} 225 | indexHandler.close() 226 | repository.close() 227 | tempRepository.close() 228 | assert(onConnectionChangedListeners.isEmpty()) 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /relay/src/main/kotlin/net/syncthing/java/client/protocol/rp/RelayClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.client.protocol.rp 15 | 16 | import net.syncthing.java.client.protocol.rp.beans.SessionInvitation 17 | import net.syncthing.java.core.beans.DeviceAddress 18 | import net.syncthing.java.core.beans.DeviceAddress.AddressType 19 | import net.syncthing.java.core.beans.DeviceId 20 | import net.syncthing.java.core.configuration.Configuration 21 | import net.syncthing.java.core.interfaces.RelayConnection 22 | import net.syncthing.java.core.security.KeystoreHandler 23 | import net.syncthing.java.core.utils.NetworkUtils 24 | import org.apache.commons.io.IOUtils 25 | import org.bouncycastle.util.encoders.Hex 26 | import org.slf4j.LoggerFactory 27 | import java.io.* 28 | import java.net.InetAddress 29 | import java.net.InetSocketAddress 30 | import java.net.Socket 31 | import java.nio.ByteBuffer 32 | 33 | class RelayClient(configuration: Configuration) { 34 | private val logger = LoggerFactory.getLogger(javaClass) 35 | private val keystoreHandler: KeystoreHandler = KeystoreHandler.Loader().loadKeystore(configuration) 36 | 37 | @Throws(IOException::class, KeystoreHandler.CryptoException::class) 38 | fun openRelayConnection(address: DeviceAddress): RelayConnection { 39 | assert(address.getType() == AddressType.RELAY) 40 | val sessionInvitation = getSessionInvitation(address.getSocketAddress(), address.deviceId()) 41 | return openConnectionSessionMode(sessionInvitation) 42 | } 43 | 44 | @Throws(IOException::class) 45 | private fun openConnectionSessionMode(sessionInvitation: SessionInvitation): RelayConnection { 46 | logger.debug("connecting to relay = {}:{} (session mode)", sessionInvitation.address, sessionInvitation.port) 47 | val socket = Socket(sessionInvitation.address, sessionInvitation.port) 48 | val inputStream = RelayDataInputStream(socket.getInputStream()) 49 | val outputStream = RelayDataOutputStream(socket.getOutputStream()) 50 | run { 51 | logger.debug("sending join session request, session key = {}", sessionInvitation.key) 52 | val key = Hex.decode(sessionInvitation.key) 53 | val lengthOfKey = key.size 54 | outputStream.writeHeader(JOIN_SESSION_REQUEST, 4 + lengthOfKey) 55 | outputStream.writeInt(lengthOfKey) 56 | outputStream.write(key) 57 | outputStream.flush() 58 | } 59 | run { 60 | logger.debug("reading relay response") 61 | val messageReader = inputStream.readMessage() 62 | NetworkUtils.assertProtocol(messageReader.type == RESPONSE) 63 | val response = messageReader.readResponse() 64 | logger.debug("response = {}", response) 65 | NetworkUtils.assertProtocol(response.code == ResponseSuccess, {"response code = ${response.code} (${response.message}) expected $ResponseSuccess"}) 66 | logger.debug("relay connection ready") 67 | } 68 | return object : RelayConnection { 69 | override fun getSocket(): Socket { 70 | return socket 71 | } 72 | 73 | override fun isServerSocket(): Boolean { 74 | return sessionInvitation.isServerSocket 75 | } 76 | 77 | } 78 | } 79 | 80 | @Throws(IOException::class, KeystoreHandler.CryptoException::class) 81 | fun getSessionInvitation(relaySocketAddress: InetSocketAddress, deviceId: DeviceId): SessionInvitation { 82 | logger.debug("connecting to relay = {} (temporary protocol mode)", relaySocketAddress) 83 | keystoreHandler.createSocket(relaySocketAddress, KeystoreHandler.RELAY).use { socket -> 84 | RelayDataInputStream(socket.getInputStream()).use { `in` -> 85 | RelayDataOutputStream(socket.getOutputStream()).use { out -> 86 | run { 87 | logger.debug("sending connect request for device = {}", deviceId) 88 | val deviceIdData = deviceId.toHashData() 89 | val lengthOfId = deviceIdData.size 90 | out.writeHeader(CONNECT_REQUEST, 4 + lengthOfId) 91 | out.writeInt(lengthOfId) 92 | out.write(deviceIdData) 93 | out.flush() 94 | } 95 | 96 | run { 97 | logger.debug("receiving session invitation") 98 | val messageReader = `in`.readMessage() 99 | logger.debug("received message = {}", messageReader.dumpMessageForDebug()) 100 | if (messageReader.type == RESPONSE) { 101 | val response = messageReader.readResponse() 102 | throw IOException(response.message) 103 | } 104 | NetworkUtils.assertProtocol(messageReader.type == SESSION_INVITATION, {"message type mismatch, expected $SESSION_INVITATION, got ${messageReader.type}"}) 105 | val builder = SessionInvitation.Builder() 106 | .setFrom(DeviceId.fromHashData(messageReader.readLengthAndData()).deviceId) 107 | .setKey(Hex.toHexString(messageReader.readLengthAndData())) 108 | val address = messageReader.readLengthAndData() 109 | if (address.isEmpty()) { 110 | builder.setAddress(socket.inetAddress) 111 | } else { 112 | val inetAddress = InetAddress.getByAddress(address) 113 | if (inetAddress == InetAddress.getByName("0.0.0.0")) { 114 | builder.setAddress(socket.inetAddress) 115 | } else { 116 | builder.setAddress(inetAddress) 117 | } 118 | } 119 | val zero = messageReader.buffer.short.toInt() 120 | NetworkUtils.assertProtocol(zero == 0, {"expected 0, found $zero"}) 121 | val port = messageReader.buffer.short.toInt() 122 | NetworkUtils.assertProtocol(port > 0, {"got invalid port value = $port"}) 123 | builder.setPort(port) 124 | val serverSocket = messageReader.buffer.int and 1 125 | builder.setServerSocket(serverSocket == 1) 126 | logger.debug("closing connection (temporary protocol mode)") 127 | return builder.build() 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | private class RelayDataOutputStream(out: OutputStream) : DataOutputStream(out) { 135 | 136 | @Throws(IOException::class) 137 | fun writeHeader(type: Int, length: Int) { 138 | writeInt(MAGIC) 139 | writeInt(type) 140 | writeInt(length) 141 | } 142 | 143 | } 144 | 145 | private class RelayDataInputStream(`in`: InputStream) : DataInputStream(`in`) { 146 | 147 | @Throws(IOException::class) 148 | fun readMessage(): MessageReader { 149 | val magic = readInt() 150 | NetworkUtils.assertProtocol(magic == MAGIC, {"magic mismatch, got = $magic, expected = $MAGIC"}) 151 | val type = readInt() 152 | val length = readInt() 153 | NetworkUtils.assertProtocol(length >= 0) 154 | val payload = ByteBuffer.allocate(length) 155 | IOUtils.readFully(this, payload.array()) 156 | return MessageReader(type, payload) 157 | } 158 | } 159 | 160 | private class Response(val code: Int, val message: String) { 161 | 162 | override fun toString(): String { 163 | return "Response{code=$code, message=$message}" 164 | } 165 | 166 | } 167 | 168 | private class MessageReader(val type: Int, val buffer: ByteBuffer) { 169 | 170 | fun readLengthAndData(): ByteArray { 171 | val length = buffer.int 172 | NetworkUtils.assertProtocol(length >= 0) 173 | val data = ByteArray(length) 174 | buffer.get(data) 175 | return data 176 | } 177 | 178 | fun readResponse(): Response { 179 | val code = buffer.int 180 | val messageLength = buffer.int 181 | val message = ByteArray(messageLength) 182 | buffer.get(message) 183 | return Response(code, String(message)) 184 | } 185 | 186 | fun cloneReader(): MessageReader { 187 | return MessageReader(type, ByteBuffer.wrap(buffer.array())) 188 | } 189 | 190 | fun dumpMessageForDebug(): String { 191 | return if (type == RESPONSE) { 192 | "Response(code=${cloneReader().readResponse().code}, message=${cloneReader().readResponse().message})" 193 | } else { 194 | "Message(type=$type, size=${buffer.capacity()})" 195 | } 196 | } 197 | } 198 | 199 | companion object { 200 | 201 | private const val MAGIC = -0x618643c0 202 | private const val JOIN_SESSION_REQUEST = 3 203 | private const val RESPONSE = 4 204 | private const val CONNECT_REQUEST = 5 205 | private const val SESSION_INVITATION = 6 206 | private const val ResponseSuccess = 0 207 | private const val ResponseNotFound = 1 208 | private const val ResponseAlreadyConnected = 2 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /client-cli/src/main/kotlin/net/syncthing/java/client/cli/Main.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.client.cli 15 | 16 | import net.syncthing.java.core.beans.DeviceId 17 | import net.syncthing.java.core.beans.DeviceInfo 18 | import net.syncthing.java.core.beans.FileInfo 19 | import net.syncthing.java.core.configuration.Configuration 20 | import net.syncthing.java.repository.repo.SqlRepository 21 | import net.syncthing.java.client.SyncthingClient 22 | import org.apache.commons.cli.* 23 | import org.apache.commons.io.FileUtils 24 | import org.slf4j.LoggerFactory 25 | import java.io.File 26 | import java.io.FileInputStream 27 | import java.io.IOException 28 | import java.util.concurrent.CountDownLatch 29 | 30 | class Main(private val commandLine: CommandLine) { 31 | 32 | companion object { 33 | @JvmStatic 34 | fun main(args: Array) { 35 | val options = generateOptions() 36 | val parser = DefaultParser() 37 | val cmd = parser.parse(options, args) 38 | if (cmd.hasOption("h")) { 39 | val formatter = HelpFormatter() 40 | formatter.printHelp("s-client", options) 41 | return 42 | } 43 | val configuration = if (cmd.hasOption("C")) Configuration(File(cmd.getOptionValue("C"))) 44 | else Configuration() 45 | 46 | val repository = SqlRepository(configuration.databaseFolder) 47 | 48 | SyncthingClient(configuration, repository, repository).use { syncthingClient -> 49 | val main = Main(cmd) 50 | cmd.options.forEach { main.handleOption(it, configuration, syncthingClient) } 51 | } 52 | } 53 | 54 | private fun generateOptions(): Options { 55 | val options = Options() 56 | options.addOption("C", "set-config", true, "set config file for s-client") 57 | options.addOption("c", "config", false, "dump config") 58 | options.addOption("S", "set-peers", true, "set peer, or comma-separated list of peers") 59 | options.addOption("p", "pull", true, "pull file from network") 60 | options.addOption("P", "push", true, "push file to network") 61 | options.addOption("o", "output", true, "set output file/directory") 62 | options.addOption("i", "input", true, "set input file/directory") 63 | options.addOption("a", "list-peers", false, "list peer addresses") 64 | options.addOption("a", "address", true, "use this peer addresses") 65 | options.addOption("L", "list-remote", false, "list folder (root) content from network") 66 | options.addOption("I", "list-info", false, "dump folder info from network") 67 | options.addOption("l", "list-info", false, "list folder info from local db") 68 | options.addOption("D", "delete", true, "push delete to network") 69 | options.addOption("M", "mkdir", true, "push directory create to network") 70 | options.addOption("h", "help", false, "print help") 71 | return options 72 | } 73 | } 74 | 75 | private val logger = LoggerFactory.getLogger(Main::class.java) 76 | 77 | private fun handleOption(option: Option, configuration: Configuration, syncthingClient: SyncthingClient) { 78 | when (option.opt) { 79 | "S" -> { 80 | val peers = option.value 81 | .split(",") 82 | .filterNot { it.isEmpty() } 83 | .map { DeviceId(it.trim()) } 84 | .toList() 85 | System.out.println("set peers = $peers") 86 | configuration.peers = peers.map { DeviceInfo(it, null) }.toSet() 87 | configuration.persistNow() 88 | } 89 | "p" -> { 90 | val folderAndPath = option.value 91 | System.out.println("file path = $folderAndPath") 92 | val folder = folderAndPath.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0] 93 | val path = folderAndPath.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1] 94 | val latch = CountDownLatch(1) 95 | val fileInfo = FileInfo(folder = folder, path = path, type = FileInfo.FileType.FILE) 96 | syncthingClient.getBlockPuller(folder, { blockPuller -> 97 | try { 98 | val observer = blockPuller.pullFile(fileInfo) 99 | val inputStream = observer.waitForComplete().inputStream() 100 | val fileName = syncthingClient.indexHandler.getFileInfoByPath(folder, path)!!.fileName 101 | val file = 102 | if (commandLine.hasOption("o")) { 103 | val param = File(commandLine.getOptionValue("o")) 104 | if (param.isDirectory) File(param, fileName) else param 105 | } else { 106 | File(fileName) 107 | } 108 | FileUtils.copyInputStreamToFile(inputStream, file) 109 | System.out.println("saved file to = $file.absolutePath") 110 | } catch (e: InterruptedException) { 111 | logger.warn("", e) 112 | } catch (e: IOException) { 113 | logger.warn("", e) 114 | } 115 | }, { logger.warn("Failed to pull file") }) 116 | latch.await() 117 | } 118 | "P" -> { 119 | var path = option.value 120 | val file = File(commandLine.getOptionValue("i")) 121 | assert(!path.startsWith("/")) //TODO check path syntax 122 | System.out.println("file path = $path") 123 | val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0] 124 | path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1] 125 | val latch = CountDownLatch(1) 126 | syncthingClient.getBlockPusher(folder, { blockPusher -> 127 | val observer = blockPusher.pushFile(FileInputStream(file), folder, path) 128 | while (!observer.isCompleted()) { 129 | try { 130 | observer.waitForProgressUpdate() 131 | } catch (e: InterruptedException) { 132 | logger.warn("", e) 133 | } 134 | 135 | System.out.println("upload progress ${observer.progressPercentage()}%") 136 | } 137 | latch.countDown() 138 | }, { logger.warn("Failed to upload file") }) 139 | latch.await() 140 | System.out.println("uploaded file to network") 141 | } 142 | "D" -> { 143 | var path = option.value 144 | val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0] 145 | path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1] 146 | System.out.println("delete path = $path") 147 | val latch = CountDownLatch(1) 148 | syncthingClient.getBlockPusher(folder, { blockPusher -> 149 | try { 150 | blockPusher.pushDelete(folder, path).waitForComplete() 151 | } catch (e: InterruptedException) { 152 | logger.warn("", e) 153 | } 154 | 155 | latch.countDown() 156 | }, { System.out.println("Failed to delete path") }) 157 | latch.await() 158 | System.out.println("deleted path") 159 | } 160 | "M" -> { 161 | var path = option.value 162 | val folder = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[0] 163 | path = path.split(":".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()[1] 164 | System.out.println("dir path = $path") 165 | val latch = CountDownLatch(1) 166 | syncthingClient.getBlockPusher(folder, { blockPusher -> 167 | try { 168 | blockPusher.pushDir(folder, path).waitForComplete() 169 | } catch (e: InterruptedException) { 170 | logger.warn("", e) 171 | } 172 | 173 | latch.countDown() 174 | }, { System.out.println("Failed to push directory") }) 175 | latch.await() 176 | System.out.println("uploaded dir to network") 177 | } 178 | "L" -> { 179 | waitForIndexUpdate(syncthingClient, configuration) 180 | for (folder in syncthingClient.indexHandler.folderList()) { 181 | syncthingClient.indexHandler.newIndexBrowser(folder).use { indexBrowser -> 182 | System.out.println("list folder = ${indexBrowser.folder}") 183 | for (fileInfo in indexBrowser.listFiles()) { 184 | System.out.println("${fileInfo.type.name.substring(0, 1)}\t${fileInfo.describeSize()}\t${fileInfo.path}") 185 | } 186 | } 187 | } 188 | } 189 | "I" -> { 190 | waitForIndexUpdate(syncthingClient, configuration) 191 | val folderInfo = StringBuilder() 192 | for (folder in syncthingClient.indexHandler.folderList()) { 193 | folderInfo.append("\nfolder info: ") 194 | .append(syncthingClient.indexHandler.getFolderInfo(folder)) 195 | folderInfo.append("\nfolder stats: ") 196 | .append(syncthingClient.indexHandler.newFolderBrowser().getFolderStats(folder).dumpInfo()) 197 | .append("\n") 198 | } 199 | System.out.println("folders:\n$folderInfo\n") 200 | } 201 | "l" -> { 202 | var folderInfo = "" 203 | for (folder in syncthingClient.indexHandler.folderList()) { 204 | folderInfo += "\nfolder info: " + syncthingClient.indexHandler.getFolderInfo(folder) 205 | folderInfo += "\nfolder stats: " + syncthingClient.indexHandler.newFolderBrowser().getFolderStats(folder).dumpInfo() + "\n" 206 | } 207 | System.out.println("folders:\n$folderInfo\n") 208 | } 209 | "a" -> { 210 | val deviceAddressSupplier = syncthingClient.discoveryHandler.newDeviceAddressSupplier() 211 | var deviceAddressesStr = "" 212 | for (deviceAddress in deviceAddressSupplier.toList()) { 213 | deviceAddressesStr += "\n" + deviceAddress?.deviceId + " : " + deviceAddress?.address 214 | } 215 | System.out.println("device addresses:\n$deviceAddressesStr\n") 216 | } 217 | } 218 | } 219 | 220 | @Throws(InterruptedException::class) 221 | private fun waitForIndexUpdate(client: SyncthingClient, configuration: Configuration) { 222 | val latch = CountDownLatch(configuration.peers.size) 223 | client.indexHandler.registerOnFullIndexAcquiredListenersListener { 224 | latch.countDown() 225 | } 226 | latch.await() 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /core/src/main/kotlin/net/syncthing/java/core/security/KeystoreHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.core.security 15 | 16 | import net.syncthing.java.core.beans.DeviceId 17 | import net.syncthing.java.core.configuration.Configuration 18 | import net.syncthing.java.core.interfaces.RelayConnection 19 | import net.syncthing.java.core.utils.NetworkUtils 20 | import org.apache.commons.codec.binary.Base32 21 | import org.apache.commons.lang3.tuple.Pair 22 | import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter 23 | import org.bouncycastle.cert.jcajce.JcaX509v1CertificateBuilder 24 | import org.bouncycastle.jce.provider.BouncyCastleProvider 25 | import org.bouncycastle.operator.OperatorCreationException 26 | import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder 27 | import org.bouncycastle.util.encoders.Base64 28 | import org.slf4j.LoggerFactory 29 | import java.io.ByteArrayInputStream 30 | import java.io.ByteArrayOutputStream 31 | import java.io.IOException 32 | import java.math.BigInteger 33 | import java.net.InetSocketAddress 34 | import java.net.Socket 35 | import java.security.* 36 | import java.security.cert.Certificate 37 | import java.security.cert.CertificateException 38 | import java.security.cert.CertificateFactory 39 | import java.security.cert.X509Certificate 40 | import java.util.* 41 | import java.util.concurrent.TimeUnit 42 | import javax.net.ssl.* 43 | import javax.security.auth.x500.X500Principal 44 | 45 | class KeystoreHandler private constructor(private val keyStore: KeyStore) { 46 | 47 | private val logger = LoggerFactory.getLogger(javaClass) 48 | 49 | class CryptoException internal constructor(t: Throwable) : GeneralSecurityException(t) 50 | 51 | private val socketFactory: SSLSocketFactory 52 | 53 | init { 54 | val sslContext = SSLContext.getInstance(TLS_VERSION) 55 | val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) 56 | keyManagerFactory.init(keyStore, KEY_PASSWORD.toCharArray()) 57 | 58 | sslContext.init(keyManagerFactory.keyManagers, arrayOf(object : X509TrustManager { 59 | @Throws(CertificateException::class) 60 | override fun checkClientTrusted(xcs: Array, string: String) {} 61 | @Throws(CertificateException::class) 62 | override fun checkServerTrusted(xcs: Array, string: String) {} 63 | override fun getAcceptedIssuers() = arrayOf() 64 | }), null) 65 | socketFactory = sslContext.socketFactory 66 | } 67 | 68 | @Throws(CryptoException::class, IOException::class) 69 | private fun exportKeystoreToData(): ByteArray { 70 | val out = ByteArrayOutputStream() 71 | try { 72 | keyStore.store(out, JKS_PASSWORD.toCharArray()) 73 | } catch (ex: NoSuchAlgorithmException) { 74 | throw CryptoException(ex) 75 | } catch (ex: CertificateException) { 76 | throw CryptoException(ex) 77 | } 78 | return out.toByteArray() 79 | } 80 | 81 | @Throws(CryptoException::class, IOException::class) 82 | private fun wrapSocket(socket: Socket, isServerSocket: Boolean, protocol: String): SSLSocket { 83 | try { 84 | logger.debug("wrapping plain socket, server mode = {}", isServerSocket) 85 | val sslSocket = socketFactory.createSocket(socket, null, socket.port, true) as SSLSocket 86 | if (isServerSocket) { 87 | sslSocket.useClientMode = false 88 | } 89 | return sslSocket 90 | } catch (e: KeyManagementException) { 91 | throw CryptoException(e) 92 | } catch (e: NoSuchAlgorithmException) { 93 | throw CryptoException(e) 94 | } catch (e: KeyStoreException) { 95 | throw CryptoException(e) 96 | } catch (e: UnrecoverableKeyException) { 97 | throw CryptoException(e) 98 | } 99 | 100 | } 101 | 102 | @Throws(CryptoException::class, IOException::class) 103 | fun createSocket(relaySocketAddress: InetSocketAddress, protocol: String): SSLSocket { 104 | try { 105 | val socket = socketFactory.createSocket() as SSLSocket 106 | socket.connect(relaySocketAddress, SOCKET_TIMEOUT) 107 | return socket 108 | } catch (e: KeyManagementException) { 109 | throw CryptoException(e) 110 | } catch (e: NoSuchAlgorithmException) { 111 | throw CryptoException(e) 112 | } catch (e: KeyStoreException) { 113 | throw CryptoException(e) 114 | } catch (e: UnrecoverableKeyException) { 115 | throw CryptoException(e) 116 | } 117 | } 118 | 119 | @Throws(SSLPeerUnverifiedException::class, CertificateException::class) 120 | fun checkSocketCertificate(socket: SSLSocket, deviceId: DeviceId) { 121 | val session = socket.session 122 | val certs = session.peerCertificates.toList() 123 | val certificateFactory = CertificateFactory.getInstance("X.509") 124 | val certPath = certificateFactory.generateCertPath(certs) 125 | val certificate = certPath.certificates[0] 126 | NetworkUtils.assertProtocol(certificate is X509Certificate) 127 | val derData = certificate.encoded 128 | val deviceIdFromCertificate = derDataToDeviceId(derData) 129 | logger.trace("remote pem certificate =\n{}", derToPem(derData)) 130 | NetworkUtils.assertProtocol(deviceIdFromCertificate == deviceId, {"device id mismatch! expected = $deviceId, got = $deviceIdFromCertificate"}) 131 | logger.debug("remote ssl certificate match deviceId = {}", deviceId) 132 | } 133 | 134 | @Throws(CryptoException::class, IOException::class) 135 | fun wrapSocket(relayConnection: RelayConnection, protocol: String): SSLSocket { 136 | return wrapSocket(relayConnection.getSocket(), relayConnection.isServerSocket(), protocol) 137 | } 138 | 139 | class Loader { 140 | 141 | private val logger = LoggerFactory.getLogger(javaClass) 142 | 143 | private fun getKeystoreAlgorithm(keystoreAlgorithm: String?): String { 144 | return keystoreAlgorithm?.let { algo -> 145 | if (!algo.isBlank()) algo else null 146 | } ?: { 147 | val defaultAlgo = KeyStore.getDefaultType()!! 148 | logger.debug("keystore algo set to {}", defaultAlgo) 149 | defaultAlgo 150 | }() 151 | } 152 | 153 | @Throws(CryptoException::class, IOException::class) 154 | fun generateKeystore(): Triple { 155 | val keystoreAlgorithm = getKeystoreAlgorithm(null) 156 | val keystore = generateKeystore(keystoreAlgorithm) 157 | val keystoreHandler = KeystoreHandler(keystore.left) 158 | val keystoreData = keystoreHandler.exportKeystoreToData() 159 | val hash = MessageDigest.getInstance("SHA-256").digest(keystoreData) 160 | keystoreHandlersCacheByHash[Base32().encodeAsString(hash)] = keystoreHandler 161 | logger.info("keystore ready, device id = {}", keystore.right) 162 | return Triple(keystore.right, keystoreData, keystoreAlgorithm) 163 | } 164 | 165 | fun loadKeystore(configuration: Configuration): KeystoreHandler { 166 | val hash = MessageDigest.getInstance("SHA-256").digest(configuration.keystoreData) 167 | val keystoreHandlerFromCache = keystoreHandlersCacheByHash[Base32().encodeAsString(hash)] 168 | if (keystoreHandlerFromCache != null) { 169 | return keystoreHandlerFromCache 170 | } 171 | val keystoreAlgo = getKeystoreAlgorithm(configuration.keystoreAlgorithm) 172 | val keystore = importKeystore(configuration.keystoreData, keystoreAlgo) 173 | val keystoreHandler = KeystoreHandler(keystore.left) 174 | keystoreHandlersCacheByHash[Base32().encodeAsString(hash)] = keystoreHandler 175 | logger.info("keystore ready, device id = {}", keystore.right) 176 | return keystoreHandler 177 | } 178 | 179 | @Throws(CryptoException::class, IOException::class) 180 | private fun generateKeystore(keystoreAlgorithm: String): Pair { 181 | try { 182 | logger.debug("generating key") 183 | val keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGO) 184 | keyPairGenerator.initialize(KEY_SIZE) 185 | val keyPair = keyPairGenerator.genKeyPair() 186 | 187 | val contentSigner = JcaContentSignerBuilder(SIGNATURE_ALGO).build(keyPair.private) 188 | 189 | val startDate = Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) 190 | val endDate = Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(10 * 365)) 191 | 192 | val certificateBuilder = JcaX509v1CertificateBuilder(X500Principal(CERTIFICATE_CN), BigInteger.ZERO, 193 | startDate, endDate, X500Principal(CERTIFICATE_CN), keyPair.public) 194 | 195 | val certificateHolder = certificateBuilder.build(contentSigner) 196 | 197 | val certificateDerData = certificateHolder.encoded 198 | logger.info("generated cert =\n{}", derToPem(certificateDerData)) 199 | val deviceId = derDataToDeviceId(certificateDerData) 200 | logger.info("device id from cert = {}", deviceId) 201 | 202 | val keyStore = KeyStore.getInstance(keystoreAlgorithm) 203 | keyStore.load(null, null) 204 | val certChain = arrayOfNulls(1) 205 | certChain[0] = JcaX509CertificateConverter().getCertificate(certificateHolder) 206 | keyStore.setKeyEntry("key", keyPair.private, KEY_PASSWORD.toCharArray(), certChain) 207 | return Pair.of(keyStore, deviceId) 208 | } catch (e: OperatorCreationException) { 209 | throw CryptoException(e) 210 | } catch (e: CertificateException) { 211 | throw CryptoException(e) 212 | } catch (e: NoSuchAlgorithmException) { 213 | throw CryptoException(e) 214 | } catch (e: KeyStoreException) { 215 | throw CryptoException(e) 216 | } 217 | 218 | } 219 | 220 | @Throws(CryptoException::class, IOException::class) 221 | private fun importKeystore(keystoreData: ByteArray, keystoreAlgorithm: String): Pair { 222 | try { 223 | val keyStore = KeyStore.getInstance(keystoreAlgorithm) 224 | keyStore.load(ByteArrayInputStream(keystoreData), JKS_PASSWORD.toCharArray()) 225 | val alias = keyStore.aliases().nextElement() 226 | val certificate = keyStore.getCertificate(alias) 227 | NetworkUtils.assertProtocol(certificate is X509Certificate) 228 | val derData = certificate.encoded 229 | val deviceId = derDataToDeviceId(derData) 230 | logger.debug("loaded device id from cert = {}", deviceId) 231 | return Pair.of(keyStore, deviceId) 232 | } catch (e: NoSuchAlgorithmException) { 233 | throw CryptoException(e) 234 | } catch (e: KeyStoreException) { 235 | throw CryptoException(e) 236 | } catch (e: CertificateException) { 237 | throw CryptoException(e) 238 | } 239 | 240 | } 241 | 242 | companion object { 243 | private val keystoreHandlersCacheByHash = mutableMapOf() 244 | } 245 | } 246 | 247 | companion object { 248 | 249 | private const val JKS_PASSWORD = "password" 250 | private const val KEY_PASSWORD = "password" 251 | private const val KEY_ALGO = "RSA" 252 | private const val SIGNATURE_ALGO = "SHA1withRSA" 253 | private const val CERTIFICATE_CN = "CN=syncthing" 254 | private const val KEY_SIZE = 3072 255 | private const val SOCKET_TIMEOUT = 2000 256 | private const val TLS_VERSION = "TLSv1.2" 257 | 258 | init { 259 | Security.addProvider(BouncyCastleProvider()) 260 | } 261 | 262 | private fun derToPem(der: ByteArray): String { 263 | return "-----BEGIN CERTIFICATE-----\n" + Base64.toBase64String(der).chunked(76).joinToString("\n") + "\n-----END CERTIFICATE-----" 264 | } 265 | 266 | fun derDataToDeviceId(certificateDerData: ByteArray): DeviceId { 267 | return DeviceId.fromHashData(MessageDigest.getInstance("SHA-256").digest(certificateDerData)) 268 | } 269 | 270 | const val BEP = "bep/1.0" 271 | const val RELAY = "bep-relay" 272 | } 273 | 274 | } 275 | -------------------------------------------------------------------------------- /http-relay/src/main/kotlin/net/syncthing/java/httprelay/HttpRelayConnection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.httprelay 15 | 16 | import com.google.protobuf.ByteString 17 | import net.syncthing.java.core.interfaces.RelayConnection 18 | import net.syncthing.java.core.utils.NetworkUtils 19 | import net.syncthing.java.core.utils.submitLogging 20 | import org.apache.http.HttpStatus 21 | import org.apache.http.client.methods.HttpPost 22 | import org.apache.http.entity.ByteArrayEntity 23 | import org.apache.http.impl.client.HttpClients 24 | import org.apache.http.util.EntityUtils 25 | import org.slf4j.LoggerFactory 26 | import java.io.* 27 | import java.net.* 28 | import java.util.concurrent.ExecutionException 29 | import java.util.concurrent.Executors 30 | import java.util.concurrent.LinkedBlockingQueue 31 | import java.util.concurrent.TimeUnit 32 | 33 | class HttpRelayConnection internal constructor(private val httpRelayServerUrl: String, deviceId: String) : RelayConnection, Closeable { 34 | 35 | private val logger = LoggerFactory.getLogger(javaClass) 36 | private val outgoingExecutorService = Executors.newSingleThreadExecutor() 37 | private val incomingExecutorService = Executors.newSingleThreadExecutor() 38 | private val flusherStreamService = Executors.newSingleThreadScheduledExecutor() 39 | private var peerToRelaySequence: Long = 0 40 | private var relayToPeerSequence: Long = 0 41 | private val sessionId: String 42 | private val incomingDataQueue = LinkedBlockingQueue() 43 | private val socket: Socket 44 | private val isServerSocket: Boolean 45 | private val inputStream: InputStream 46 | private val outputStream: OutputStream 47 | 48 | var isClosed = false 49 | private set 50 | 51 | override fun getSocket() = socket 52 | 53 | override fun isServerSocket() = isServerSocket 54 | 55 | init { 56 | val serverMessage = sendMessage(HttpRelayProtos.HttpRelayPeerMessage.newBuilder() 57 | .setMessageType(HttpRelayProtos.HttpRelayPeerMessageType.CONNECT) 58 | .setDeviceId(deviceId)) 59 | assert(serverMessage.messageType == HttpRelayProtos.HttpRelayServerMessageType.PEER_CONNECTED) 60 | assert(!serverMessage.sessionId.isNullOrEmpty()) 61 | sessionId = serverMessage.sessionId 62 | isServerSocket = serverMessage.isServerSocket 63 | outputStream = object : OutputStream() { 64 | 65 | private var buffer = ByteArrayOutputStream() 66 | private var lastFlush = System.currentTimeMillis() 67 | 68 | init { 69 | flusherStreamService.scheduleWithFixedDelay({ 70 | if (System.currentTimeMillis() - lastFlush > 1000) { 71 | try { 72 | flush() 73 | } catch (ex: IOException) { 74 | logger.warn("", ex) 75 | } 76 | 77 | } 78 | }, 1, 1, TimeUnit.SECONDS) 79 | } 80 | 81 | @Synchronized 82 | @Throws(IOException::class) 83 | override fun write(i: Int) { 84 | NetworkUtils.assertProtocol(!this@HttpRelayConnection.isClosed) 85 | buffer.write(i) 86 | } 87 | 88 | @Synchronized 89 | @Throws(IOException::class) 90 | override fun write(bytes: ByteArray, offset: Int, size: Int) { 91 | NetworkUtils.assertProtocol(!this@HttpRelayConnection.isClosed) 92 | buffer.write(bytes, offset, size) 93 | } 94 | 95 | @Synchronized 96 | @Throws(IOException::class) 97 | override fun flush() { 98 | val data = buffer.toByteArray().copyOf().toList() 99 | buffer = ByteArrayOutputStream() 100 | try { 101 | if (!data.isEmpty()) { 102 | outgoingExecutorService.submit { 103 | sendMessage(HttpRelayProtos.HttpRelayPeerMessage.newBuilder() 104 | .setMessageType(HttpRelayProtos.HttpRelayPeerMessageType.PEER_TO_RELAY) 105 | .setSequence(++peerToRelaySequence) 106 | .setData(data as ByteString)) 107 | }.get() 108 | } 109 | lastFlush = System.currentTimeMillis() 110 | } catch (ex: InterruptedException) { 111 | logger.error("error", ex) 112 | closeBg() 113 | throw IOException(ex) 114 | } catch (ex: ExecutionException) { 115 | logger.error("error", ex) 116 | closeBg() 117 | throw IOException(ex) 118 | } 119 | 120 | } 121 | 122 | @Synchronized 123 | @Throws(IOException::class) 124 | override fun write(bytes: ByteArray) { 125 | NetworkUtils.assertProtocol(!this@HttpRelayConnection.isClosed) 126 | buffer.write(bytes) 127 | } 128 | 129 | } 130 | incomingExecutorService.submitLogging { 131 | while (!isClosed) { 132 | val serverMessage1 = 133 | try { 134 | sendMessage(HttpRelayProtos.HttpRelayPeerMessage.newBuilder().setMessageType(HttpRelayProtos.HttpRelayPeerMessageType.WAIT_FOR_DATA)) 135 | } catch (e: IOException) { 136 | logger.warn("Failed to send relay message", e) 137 | return@submitLogging 138 | } 139 | if (isClosed) { 140 | return@submitLogging 141 | } 142 | NetworkUtils.assertProtocol(serverMessage1.messageType == HttpRelayProtos.HttpRelayServerMessageType.RELAY_TO_PEER) 143 | NetworkUtils.assertProtocol(serverMessage1.sequence == relayToPeerSequence + 1) 144 | if (!serverMessage1.data.isEmpty) { 145 | incomingDataQueue.add(serverMessage1.data.toByteArray()) 146 | } 147 | relayToPeerSequence = serverMessage1.sequence 148 | } 149 | } 150 | inputStream = object : InputStream() { 151 | 152 | private var noMoreData = false 153 | private var byteArrayInputStream = ByteArrayInputStream(ByteArray(0)) 154 | 155 | @Throws(IOException::class) 156 | override fun read(): Int { 157 | NetworkUtils.assertProtocol(!this@HttpRelayConnection.isClosed) 158 | if (noMoreData) { 159 | return -1 160 | } 161 | var bite = -1 162 | while (bite == -1) { 163 | bite = byteArrayInputStream.read() 164 | try { 165 | val data = incomingDataQueue.poll(1, TimeUnit.SECONDS) 166 | if (data == null) { 167 | //continue 168 | } else if (data.contentEquals(STREAM_CLOSED)) { 169 | noMoreData = true 170 | return -1 171 | } else { 172 | byteArrayInputStream = ByteArrayInputStream(data) 173 | } 174 | } catch (ex: InterruptedException) { 175 | logger.warn("", ex) 176 | } 177 | 178 | } 179 | return bite 180 | } 181 | 182 | } 183 | socket = object : Socket() { 184 | override fun isClosed(): Boolean { 185 | return this@HttpRelayConnection.isClosed 186 | } 187 | 188 | override fun isConnected(): Boolean { 189 | return !isClosed 190 | } 191 | 192 | @Throws(IOException::class) 193 | override fun shutdownOutput() { 194 | logger.debug("shutdownOutput") 195 | outputStream.flush() 196 | } 197 | 198 | @Throws(IOException::class) 199 | override fun shutdownInput() { 200 | logger.debug("shutdownInput") 201 | //do nothing 202 | } 203 | 204 | @Synchronized 205 | @Throws(IOException::class) 206 | override fun close() { 207 | logger.debug("received close on socket adapter") 208 | this@HttpRelayConnection.close() 209 | } 210 | 211 | @Throws(IOException::class) 212 | override fun getOutputStream(): OutputStream { 213 | return this@HttpRelayConnection.outputStream 214 | } 215 | 216 | @Throws(IOException::class) 217 | override fun getInputStream(): InputStream { 218 | return this@HttpRelayConnection.inputStream 219 | } 220 | 221 | @Throws(UnknownHostException::class) 222 | override fun getRemoteSocketAddress(): SocketAddress { 223 | return InetSocketAddress(inetAddress, port) 224 | } 225 | 226 | override fun getPort(): Int { 227 | return 22067 228 | } 229 | 230 | @Throws(UnknownHostException::class) 231 | override fun getInetAddress(): InetAddress { 232 | return InetAddress.getByName(URI.create(this@HttpRelayConnection.httpRelayServerUrl).host) 233 | } 234 | 235 | } 236 | } 237 | 238 | private fun closeBg() { 239 | 240 | Thread { close() }.start() 241 | } 242 | 243 | @Throws(IOException::class) 244 | private fun sendMessage(peerMessageBuilder: HttpRelayProtos.HttpRelayPeerMessage.Builder): HttpRelayProtos.HttpRelayServerMessage { 245 | if (!sessionId.isEmpty()) { 246 | peerMessageBuilder.sessionId = sessionId 247 | } 248 | logger.debug("send http relay peer message = {} session id = {} sequence = {}", peerMessageBuilder.messageType, peerMessageBuilder.sessionId, peerMessageBuilder.sequence) 249 | val httpClient = HttpClients.custom() 250 | // .setSSLSocketFactory(new SSLConnectionSocketFactory(new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build(), SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)) 251 | .build() 252 | val httpPost = HttpPost(httpRelayServerUrl) 253 | httpPost.entity = ByteArrayEntity(peerMessageBuilder.build().toByteArray()) 254 | val serverMessage = httpClient.execute(httpPost) { response -> 255 | NetworkUtils.assertProtocol(response.statusLine.statusCode == HttpStatus.SC_OK, {"http error ${response.statusLine}"}) 256 | HttpRelayProtos.HttpRelayServerMessage.parseFrom(EntityUtils.toByteArray(response.entity)) 257 | } 258 | logger.debug("received http relay server message = {}", serverMessage.messageType) 259 | NetworkUtils.assertProtocol(serverMessage.messageType != HttpRelayProtos.HttpRelayServerMessageType.ERROR, {"server error : ${serverMessage.data.toStringUtf8()}"}) 260 | return serverMessage 261 | } 262 | 263 | override fun close() { 264 | if (!isClosed) { 265 | isClosed = true 266 | logger.info("closing http relay connection {} : {}", httpRelayServerUrl, sessionId) 267 | flusherStreamService.shutdown() 268 | if (!sessionId.isEmpty()) { 269 | try { 270 | outputStream.flush() 271 | sendMessage(HttpRelayProtos.HttpRelayPeerMessage.newBuilder().setMessageType(HttpRelayProtos.HttpRelayPeerMessageType.PEER_CLOSING)) 272 | } catch (ex: IOException) { 273 | logger.warn("error closing http relay connection", ex) 274 | } 275 | 276 | } 277 | incomingExecutorService.shutdown() 278 | outgoingExecutorService.shutdown() 279 | try { 280 | incomingExecutorService.awaitTermination(1, TimeUnit.SECONDS) 281 | } catch (ex: InterruptedException) { 282 | logger.warn("", ex) 283 | } 284 | 285 | try { 286 | outgoingExecutorService.awaitTermination(1, TimeUnit.SECONDS) 287 | } catch (ex: InterruptedException) { 288 | logger.warn("", ex) 289 | } 290 | 291 | try { 292 | flusherStreamService.awaitTermination(1, TimeUnit.SECONDS) 293 | } catch (ex: InterruptedException) { 294 | logger.warn("", ex) 295 | } 296 | 297 | incomingDataQueue.add(STREAM_CLOSED) 298 | } 299 | } 300 | 301 | companion object { 302 | private val STREAM_CLOSED = "STREAM_CLOSED".toByteArray() 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /bep/src/main/kotlin/net/syncthing/java/bep/BlockPusher.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Davide Imbriaco 3 | * 4 | * This Java file is subject to the terms of the Mozilla Public 5 | * License, v. 2.0. If a copy of the MPL was not distributed with this 6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package net.syncthing.java.bep 15 | 16 | import com.google.protobuf.ByteString 17 | import net.syncthing.java.bep.BlockExchangeProtos.Vector 18 | import net.syncthing.java.core.beans.* 19 | import net.syncthing.java.core.beans.FileInfo.Version 20 | import net.syncthing.java.core.configuration.Configuration 21 | import net.syncthing.java.core.utils.BlockUtils 22 | import net.syncthing.java.core.utils.NetworkUtils 23 | import net.syncthing.java.core.utils.submitLogging 24 | import org.apache.commons.io.IOUtils 25 | import org.apache.commons.lang3.tuple.Pair 26 | import org.bouncycastle.util.encoders.Hex 27 | import org.slf4j.LoggerFactory 28 | import java.io.Closeable 29 | import java.io.IOException 30 | import java.io.InputStream 31 | import java.nio.ByteBuffer 32 | import java.security.MessageDigest 33 | import java.util.* 34 | import java.util.concurrent.ConcurrentHashMap 35 | import java.util.concurrent.ExecutionException 36 | import java.util.concurrent.Executors 37 | import java.util.concurrent.Future 38 | import java.util.concurrent.atomic.AtomicBoolean 39 | import java.util.concurrent.atomic.AtomicReference 40 | 41 | class BlockPusher internal constructor(private val localDeviceId: DeviceId, 42 | private val connectionHandler: ConnectionHandler, 43 | private val indexHandler: IndexHandler) { 44 | 45 | private val logger = LoggerFactory.getLogger(javaClass) 46 | 47 | 48 | fun pushDelete(folderId: String, targetPath: String): IndexEditObserver { 49 | val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath)!! 50 | NetworkUtils.assertProtocol(connectionHandler.hasFolder(fileInfo.folder), {"supplied connection handler $connectionHandler will not share folder ${fileInfo.folder}"}) 51 | return IndexEditObserver(sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder() 52 | .setName(targetPath) 53 | .setType(BlockExchangeProtos.FileInfoType.valueOf(fileInfo.type.name)) 54 | .setDeleted(true), fileInfo.versionList)) 55 | } 56 | 57 | fun pushDir(folder: String, path: String): IndexEditObserver { 58 | NetworkUtils.assertProtocol(connectionHandler.hasFolder(folder), {"supplied connection handler $connectionHandler will not share folder $folder"}) 59 | return IndexEditObserver(sendIndexUpdate(folder, BlockExchangeProtos.FileInfo.newBuilder() 60 | .setName(path) 61 | .setType(BlockExchangeProtos.FileInfoType.DIRECTORY), null)) 62 | } 63 | 64 | fun pushFile(inputStream: InputStream, folderId: String, targetPath: String): FileUploadObserver { 65 | val fileInfo = indexHandler.waitForRemoteIndexAcquired(connectionHandler).getFileInfoByPath(folderId, targetPath) 66 | NetworkUtils.assertProtocol(connectionHandler.hasFolder(folderId), {"supplied connection handler $connectionHandler will not share folder $folderId"}) 67 | assert(fileInfo == null || fileInfo.folder == folderId) 68 | assert(fileInfo == null || fileInfo.path == targetPath) 69 | val monitoringProcessExecutorService = Executors.newCachedThreadPool() 70 | val dataSource = DataSource(inputStream) 71 | val fileSize = dataSource.size 72 | val sentBlocks = Collections.newSetFromMap(ConcurrentHashMap()) 73 | val uploadError = AtomicReference() 74 | val isCompleted = AtomicBoolean(false) 75 | val updateLock = Object() 76 | val listener = {request: BlockExchangeProtos.Request -> 77 | if (request.folder == folderId && request.name == targetPath) { 78 | val hash = Hex.toHexString(request.hash.toByteArray()) 79 | logger.debug("handling block request = {}:{}-{} ({})", request.name, request.offset, request.size, hash) 80 | val data = dataSource.getBlock(request.offset, request.size, hash) 81 | val future = connectionHandler.sendMessage(BlockExchangeProtos.Response.newBuilder() 82 | .setCode(BlockExchangeProtos.ErrorCode.NO_ERROR) 83 | .setData(ByteString.copyFrom(data)) 84 | .setId(request.id) 85 | .build()) 86 | monitoringProcessExecutorService.submitLogging { 87 | try { 88 | future.get() 89 | sentBlocks.add(hash) 90 | synchronized(updateLock) { 91 | updateLock.notifyAll() 92 | } 93 | //TODO retry on error, register error and throw on watcher 94 | } catch (ex: InterruptedException) { 95 | //return and do nothing 96 | } catch (ex: ExecutionException) { 97 | uploadError.set(ex) 98 | synchronized(updateLock) { 99 | updateLock.notifyAll() 100 | } 101 | } 102 | } 103 | } 104 | } 105 | connectionHandler.registerOnRequestMessageReceivedListeners(listener) 106 | logger.debug("send index update for file = {}", targetPath) 107 | val indexListener = { folderInfo: FolderInfo, newRecords: List, indexInfo: IndexInfo -> 108 | if (folderInfo.folderId == folderId) { 109 | for (fileInfo2 in newRecords) { 110 | if (fileInfo2.path == targetPath && fileInfo2.hash == dataSource.getHash()) { //TODO check not invalid 111 | // sentBlocks.addAll(dataSource.getHashes()); 112 | isCompleted.set(true) 113 | synchronized(updateLock) { 114 | updateLock.notifyAll() 115 | } 116 | } 117 | } 118 | } 119 | } 120 | indexHandler.registerOnIndexRecordAcquiredListener(indexListener) 121 | val indexUpdate = sendIndexUpdate(folderId, BlockExchangeProtos.FileInfo.newBuilder() 122 | .setName(targetPath) 123 | .setSize(fileSize) 124 | .setType(BlockExchangeProtos.FileInfoType.FILE) 125 | .addAllBlocks(dataSource.blocks), fileInfo?.versionList).right 126 | return object : FileUploadObserver() { 127 | 128 | override fun progressPercentage() = if (isCompleted.get()) 100 else (sentBlocks.size.toFloat() / dataSource.getHashes().size).toInt() 129 | 130 | // return sentBlocks.size() == dataSource.getHashes().size(); 131 | override fun isCompleted() = isCompleted.get() 132 | 133 | override fun close() { 134 | logger.debug("closing upload process") 135 | monitoringProcessExecutorService.shutdown() 136 | indexHandler.unregisterOnIndexRecordAcquiredListener(indexListener) 137 | connectionHandler.unregisterOnRequestMessageReceivedListeners(listener) 138 | val fileInfo1 = indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single()) 139 | logger.info("sent file info record = {}", fileInfo1) 140 | } 141 | 142 | @Throws(InterruptedException::class, IOException::class) 143 | override fun waitForProgressUpdate(): Int { 144 | synchronized(updateLock) { 145 | updateLock.wait() 146 | } 147 | if (uploadError.get() != null) { 148 | throw IOException(uploadError.get()) 149 | } 150 | return progressPercentage() 151 | } 152 | 153 | } 154 | } 155 | 156 | private fun sendIndexUpdate(folderId: String, fileInfoBuilder: BlockExchangeProtos.FileInfo.Builder, 157 | oldVersions: Iterable?): Pair, BlockExchangeProtos.IndexUpdate> { 158 | run { 159 | val nextSequence = indexHandler.sequencer().nextSequence() 160 | val list = oldVersions ?: emptyList() 161 | logger.debug("version list = {}", list) 162 | val id = ByteBuffer.wrap(localDeviceId.toHashData()).long 163 | val version = BlockExchangeProtos.Counter.newBuilder() 164 | .setId(id) 165 | .setValue(nextSequence) 166 | .build() 167 | logger.debug("append new version = {}", version) 168 | fileInfoBuilder 169 | .setSequence(nextSequence) 170 | .setVersion(Vector.newBuilder().addAllCounters(list.map { record -> 171 | BlockExchangeProtos.Counter.newBuilder().setId(record.id).setValue(record.value).build() 172 | }) 173 | .addCounters(version)) 174 | } 175 | val lastModified = Date() 176 | val fileInfo = fileInfoBuilder 177 | .setModifiedS(lastModified.time / 1000) 178 | .setModifiedNs((lastModified.time % 1000 * 1000000).toInt()) 179 | .setNoPermissions(true) 180 | .build() 181 | val indexUpdate = BlockExchangeProtos.IndexUpdate.newBuilder() 182 | .setFolder(folderId) 183 | .addFiles(fileInfo) 184 | .build() 185 | logger.debug("index update = {}", fileInfo) 186 | return Pair.of(connectionHandler.sendMessage(indexUpdate), indexUpdate) 187 | } 188 | 189 | abstract inner class FileUploadObserver : Closeable { 190 | 191 | abstract fun progressPercentage(): Int 192 | 193 | abstract fun isCompleted(): Boolean 194 | 195 | @Throws(InterruptedException::class) 196 | abstract fun waitForProgressUpdate(): Int 197 | 198 | @Throws(InterruptedException::class) 199 | fun waitForComplete(): FileUploadObserver { 200 | while (!isCompleted()) { 201 | waitForProgressUpdate() 202 | } 203 | return this 204 | } 205 | } 206 | 207 | inner class IndexEditObserver(private val future: Future<*>, private val indexUpdate: BlockExchangeProtos.IndexUpdate) : Closeable { 208 | 209 | //throw exception if job has errors 210 | @Throws(InterruptedException::class, ExecutionException::class) 211 | fun isCompleted(): Boolean { 212 | return if (future.isDone) { 213 | future.get() 214 | true 215 | } else { 216 | false 217 | } 218 | } 219 | 220 | constructor(pair: Pair, BlockExchangeProtos.IndexUpdate>) : this(pair.left, pair.right) 221 | 222 | @Throws(InterruptedException::class, ExecutionException::class) 223 | fun waitForComplete() { 224 | future.get() 225 | } 226 | 227 | @Throws(IOException::class) 228 | override fun close() { 229 | indexHandler.pushRecord(indexUpdate.folder, indexUpdate.filesList.single()) 230 | } 231 | 232 | } 233 | 234 | private class DataSource @Throws(IOException::class) constructor(private val inputStream: InputStream) { 235 | 236 | var size: Long = 0 237 | private set 238 | lateinit var blocks: List 239 | private set 240 | private var hashes: Set? = null 241 | 242 | private var hash: String? = null 243 | 244 | init { 245 | inputStream.use { it -> 246 | val list = mutableListOf() 247 | var offset: Long = 0 248 | while (true) { 249 | var block = ByteArray(BLOCK_SIZE) 250 | val blockSize = it.read(block) 251 | if (blockSize <= 0) { 252 | break 253 | } 254 | if (blockSize < block.size) { 255 | block = Arrays.copyOf(block, blockSize) 256 | } 257 | 258 | val hash = MessageDigest.getInstance("SHA-256").digest(block) 259 | list.add(BlockExchangeProtos.BlockInfo.newBuilder() 260 | .setHash(ByteString.copyFrom(hash)) 261 | .setOffset(offset) 262 | .setSize(blockSize) 263 | .build()) 264 | offset += blockSize.toLong() 265 | } 266 | size = offset 267 | blocks = list 268 | } 269 | } 270 | 271 | @Throws(IOException::class) 272 | fun getBlock(offset: Long, size: Int, hash: String): ByteArray { 273 | val buffer = ByteArray(size) 274 | inputStream.use { it -> 275 | IOUtils.skipFully(it, offset) 276 | IOUtils.readFully(it, buffer) 277 | NetworkUtils.assertProtocol(Hex.toHexString(MessageDigest.getInstance("SHA-256").digest(buffer)) == hash, {"block hash mismatch!"}) 278 | return buffer 279 | } 280 | } 281 | 282 | 283 | fun getHashes(): Set { 284 | return hashes ?: let { 285 | val hashes2 = blocks.map { input -> Hex.toHexString(input.hash.toByteArray()) }.toSet() 286 | hashes = hashes2 287 | return hashes2 288 | } 289 | } 290 | 291 | fun getHash(): String { 292 | return hash ?: let { 293 | val blockInfo = blocks.map { input -> 294 | BlockInfo(input.offset, input.size, Hex.toHexString(input.hash.toByteArray())) 295 | } 296 | val hash2 = BlockUtils.hashBlocks(blockInfo) 297 | hash = hash2 298 | hash2 299 | } 300 | } 301 | } 302 | 303 | companion object { 304 | 305 | const val BLOCK_SIZE = 128 * 1024 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /docs/http_relay_protocol.graphml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | syncthing client 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | relay client module 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | http relay client module 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | core 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | http proxy 97 | and/or corporate firewall 98 | with transparent proxy 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | http 117 | forwar 118 | proxy 119 | (optional) 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | http relay server 138 | (standalone app) 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | relay server 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | http protocol 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | relay protocol 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | --------------------------------------------------------------------------------