├── 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 | [](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 |
--------------------------------------------------------------------------------