├── .gitattributes ├── settings.gradle ├── Sentinel ├── src │ └── main │ │ ├── resources │ │ ├── pack.mcmeta │ │ ├── assets │ │ │ └── remote_sync_sentinel │ │ │ │ └── lang │ │ │ │ ├── zh_cn.json │ │ │ │ └── en_us.json │ │ └── META-INF │ │ │ └── mods.toml │ │ └── java │ │ └── org │ │ └── teacon │ │ └── sync │ │ └── sentinel │ │ └── RemoteSyncSentinel.java └── build.gradle ├── src └── main │ ├── resources │ └── META-INF │ │ └── services │ │ └── net.minecraftforge.forgespi.locating.IModLocator │ └── java │ └── org │ └── teacon │ └── sync │ ├── ModEntry.java │ ├── Config.java │ ├── SyncedModLocator.java │ ├── Utils.java │ └── PGPKeyStore.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── COPYING ├── gradlew.bat ├── .github └── workflows │ └── release.yml ├── gradlew ├── README.md └── COPYING.LGPL /.gitattributes: -------------------------------------------------------------------------------- 1 | gradlew.bat text eol=crlf -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'RemoteSync' 2 | 3 | include ':Sentinel' -------------------------------------------------------------------------------- /Sentinel/src/main/resources/pack.mcmeta: -------------------------------------------------------------------------------- 1 | {"pack":{"pack_format":6,"description":""}} -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.minecraftforge.forgespi.locating.IModLocator: -------------------------------------------------------------------------------- 1 | org.teacon.sync.SyncedModLocator -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/RemoteSync/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /Sentinel/src/main/resources/assets/remote_sync_sentinel/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote_sync.warn.incomplete": "RemoteSync 在加载最新 Mod 时遇到错误。当前 Mod 列表因此有可能不完整。你仍然可以尝试加入服务器,但你可能会遇到错误。请小心行事。" 3 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | 16 | # gradle 17 | build 18 | .gradle 19 | 20 | # other 21 | eclipse 22 | run 23 | -------------------------------------------------------------------------------- /Sentinel/src/main/resources/assets/remote_sync_sentinel/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote_sync.warn.incomplete": "RemoteSync has encountered error while fetching latest mods. The mod list is thus incomplete. You may still try joining server, but it may lead to error. Proceed with caution." 3 | } -------------------------------------------------------------------------------- /Sentinel/src/main/resources/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | modLoader="javafml" 2 | loaderVersion="[36,)" 3 | issueTrackerURL="https://github.com/teaconmc/RemoteSync" 4 | license="LGPL-2.1-or-later" 5 | 6 | [[mods]] 7 | modId="remote_sync_sentinel" 8 | displayName="RemoteSync Sentinel" 9 | version="${file.jarVersion}" 10 | authors="TeaConMC members" 11 | credits="cpw, for original idea" 12 | description="Companion mod used in conjunction with RemoteSync, for friendlier UX." 13 | 14 | [[dependencies.remote_sync_sentinel]] 15 | modId="forge" 16 | mandatory=true 17 | versionRange="[36.0.0,)" 18 | ordering="AFTER" 19 | side="BOTH" -------------------------------------------------------------------------------- /src/main/java/org/teacon/sync/ModEntry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 3TUSK 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | // SPDX-Identifier: LGPL-2.1-or-later 20 | 21 | package org.teacon.sync; 22 | 23 | import java.net.URL; 24 | 25 | public final class ModEntry { 26 | 27 | /** 28 | * File name to be used. This name also assumes that, the signature file 29 | * will have {@code .sig} appended to this name as the file name. 30 | */ 31 | public String name; 32 | /** 33 | * URL pointing to the mod file itself. 34 | */ 35 | public URL file; 36 | /** 37 | * URL pointing to the detached signature of the mod file. 38 | */ 39 | public URL sig; 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Sentinel/src/main/java/org/teacon/sync/sentinel/RemoteSyncSentinel.java: -------------------------------------------------------------------------------- 1 | package org.teacon.sync.sentinel; 2 | 3 | import net.minecraftforge.eventbus.api.EventPriority; 4 | import net.minecraftforge.fml.ModContainer; 5 | import net.minecraftforge.fml.ModList; 6 | import net.minecraftforge.fml.ModLoader; 7 | import net.minecraftforge.fml.ModLoadingStage; 8 | import net.minecraftforge.fml.ModLoadingWarning; 9 | import net.minecraftforge.fml.common.Mod; 10 | import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; 11 | import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; 12 | import net.minecraftforge.forgespi.language.IModInfo; 13 | 14 | @Mod("remote_sync_sentinel") 15 | public class RemoteSyncSentinel { 16 | 17 | public RemoteSyncSentinel() { 18 | FMLJavaModLoadingContext.get() 19 | .getModEventBus() 20 | .addListener(EventPriority.NORMAL, false, FMLCommonSetupEvent.class, RemoteSyncSentinel::setup); 21 | } 22 | 23 | public static void setup(FMLCommonSetupEvent event) { 24 | if (Boolean.getBoolean("org.teacon.sync.failed")) { 25 | IModInfo modInfo = ModList.get().getModContainerById("remote_sync_sentinel") 26 | .map(ModContainer::getModInfo) 27 | .orElseThrow(() -> new IllegalStateException("ModInfo absent while mod itself is present")); 28 | ModLoader.get().addWarning(new ModLoadingWarning(modInfo, ModLoadingStage.COMMON_SETUP, "remote_sync.warn.incomplete")); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 3TUSK 2 | 3 | This library is free software; you can redistribute it and/or 4 | modify it under the terms of the GNU Lesser General Public 5 | License as published by the Free Software Foundation; either 6 | version 2.1 of the License, or (at your option) any later version. 7 | 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | Lesser General Public License for more details. 12 | 13 | You should have received a copy of the GNU Lesser General Public 14 | License along with this library; if not, write to the Free Software 15 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 | 17 | ------------ 18 | 19 | This library uses and repackages parts of Bouncy Castle, a Java cryptography library 20 | licensed under the MIT/X11 license. Its license is reproduced below verbatim: 21 | 22 | Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 25 | and associated documentation files (the "Software"), to deal in the Software without restriction, 26 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 27 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 28 | subject to the following conditions: 29 | 30 | The above copyright notice and this permission notice shall be included in all copies or substantial 31 | portions of the Software. 32 | 33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 34 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 35 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 36 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 37 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 38 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/org/teacon/sync/Config.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 3TUSK 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | // SPDX-Identifier: LGPL-2.1-or-later 20 | 21 | package org.teacon.sync; 22 | 23 | import java.net.URL; 24 | import java.util.Collections; 25 | import java.util.List; 26 | 27 | public final class Config { 28 | 29 | /** 30 | * URL that points to the mod list to download. Supported protocols include 31 | * http, https, files (i.e. local file) and others. 32 | * 33 | * @see URL 34 | * @see URL#getProtocol() 35 | */ 36 | public URL modList; 37 | /** 38 | * Path to the directory where all downloaded mods are held, relative to the 39 | * Minecraft home directory. 40 | */ 41 | public String modDir = "synced_mods"; 42 | /** 43 | * Local mod list location for downloading 44 | */ 45 | public String localModList = "mod_list.json"; 46 | /** 47 | * Path to the public keys ring file, relative to the Minecraft home directory. 48 | */ 49 | // TODO Re-evaluate this one, we might want to hard code this 50 | // Convention over configuration. 51 | public String keyRingPath = "pub_key.asc"; 52 | /** 53 | * List of URLs of key servers. These key servers SHOULD support the HTTP 54 | * Keyserver Protocol. 55 | */ 56 | public List keyServers = Collections.emptyList(); 57 | /** 58 | * List of public keys identified by key IDs. 32-bit key ID, 64-bit key ID, 59 | * version 3 fingerprint and version 4 fingerprint are acceptable; these key 60 | * IDs SHOULD be prefixed with {@code 0x} as a mark of hexadecimal number. 61 | */ 62 | public List keyIds = Collections.emptyList(); 63 | /** 64 | * Amount of time to wait before giving up a connection, measured in milliseconds. 65 | */ 66 | public int timeout = 15000; 67 | /** 68 | * If true, RemoteSync will always try using local cache, even if the file with the 69 | * same name on the remote server has been updated. 70 | *

71 | * Note that this config option will not take effect when fetching the mod list. The 72 | * mod list is expected to be always the latest. 73 | *

74 | */ 75 | public boolean preferLocalCache = true; 76 | } 77 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "**" 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | # Fetch all history 14 | fetch-depth: 0 15 | - name: Setup Java 17 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: 'temurin' 19 | java-version: '17' 20 | - name: Maven Publish 21 | id: maven_publish 22 | env: 23 | ARCHIVE_ENDPOINT: ${{ secrets.TEACON_ARCHIVE_ENDPOINT }} 24 | ARCHIVE_ACCESS_KEY: ${{ secrets.TEACON_ARCHIVE_ACCESS_KEY }} 25 | ARCHIVE_SECRET_KEY: ${{ secrets.TEACON_ARCHIVE_SECRET_KEY }} 26 | run: ./gradlew -Dorg.gradle.s3.endpoint=$ARCHIVE_ENDPOINT publishReleasePublicationToTeaconRepository githubActionOutput 27 | - name: Generate Changelog 28 | id: changelog 29 | shell: bash 30 | env: 31 | CURRENT: ${{ github.ref }} 32 | # Special thanks to this post on Stack Overflow regarding change set between two tags: 33 | # https://stackoverflow.com/questions/12082981 34 | # Do note that actions/checkout will enter detach mode by default, so you won't have 35 | # access to HEAD ref. Use GitHub-Action-supplied `github.ref` instead. 36 | # Special thanks to this issue ticket regarding escaping newline: 37 | # https://github.com/actions/create-release/issues/25 38 | # We use Bash parameter expansion to do find-and-replace. 39 | # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html 40 | # Also we cannot use git rev-list because it always prepend "commit " 41 | # See https://stackoverflow.com/questions/36927089/ 42 | run: | 43 | current_tag=${CURRENT/refs\/tags\//} 44 | last_tag=`git describe --tags --abbrev=0 "$current_tag"^ 2>/dev/null || echo` 45 | if [ last_tag ]; then 46 | changelog=`git log --pretty="format:%H: %s" ${last_tag}..$current_tag` 47 | else 48 | changelog=`git log --pretty="format:%H: %s"` 49 | fi 50 | changelog="${changelog//'%'/'%25'}" 51 | changelog="${changelog//$'\n'/' %0A'}" 52 | echo "::set-output name=value::Change set since ${last_tag:-the beginning}: %0A%0A$changelog" 53 | - name: GitHub Release 54 | id: create_release 55 | uses: actions/create-release@v1.1.1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | tag_name: ${{ github.ref }} 60 | release_name: ${{ github.ref }} 61 | draft: false 62 | prerelease: false 63 | body: | 64 | ${{ steps.changelog.outputs.value }} 65 | - name: GitHub Release Artifact 66 | uses: actions/upload-release-asset@v1.0.2 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | upload_url: ${{ steps.create_release.outputs.upload_url }} 71 | asset_path: ${{ steps.maven_publish.outputs.artifact_path }} 72 | asset_name: ${{ steps.maven_publish.outputs.artifact_name }} 73 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 74 | asset_content_type: "application/java-archive" -------------------------------------------------------------------------------- /Sentinel/build.gradle: -------------------------------------------------------------------------------- 1 | import java.time.Instant 2 | import java.time.format.DateTimeFormatter 3 | import java.time.temporal.ChronoUnit 4 | 5 | buildscript { 6 | repositories { 7 | maven { url = 'https://maven.minecraftforge.net' } 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '5.1.+', changing: true 12 | } 13 | } 14 | apply plugin: 'net.minecraftforge.gradle' 15 | // Only edit below this line, the above code adds and enables the necessary things for Forge to be setup. 16 | apply plugin: 'eclipse' 17 | apply plugin: 'maven-publish' 18 | 19 | version = '1.0' 20 | group = 'org.teacon.sync.sentinel' // http://maven.apache.org/guides/mini/guide-naming-conventions.html 21 | archivesBaseName = 'RemoteSyncSentinel' 22 | 23 | // We need to target the lowest version possible to make sure this still runs on pre-1.17+ (Java 16+) 24 | // TODO We might want to do a multi-release or something in the future. 25 | java.toolchain.languageVersion = JavaLanguageVersion.of(17) 26 | 27 | // Print out current JVM info for debug purpose 28 | println('Java: ' + System.getProperty('java.version') + ' JVM: ' + System.getProperty('java.vm.version') + '(' + System.getProperty('java.vendor') + ') Arch: ' + System.getProperty('os.arch')) 29 | 30 | minecraft { 31 | // The mappings can be changed at any time and must be in the following format. 32 | // Channel: Version: 33 | // snapshot YYYYMMDD Snapshot are built nightly. 34 | // stable # Stables are built at the discretion of the MCP team. 35 | // official MCVersion Official field/method names from Mojang mapping files 36 | // 37 | // You must be aware of the Mojang license when using the 'official' mappings. 38 | // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md 39 | // 40 | // Use non-default mappings at your own risk. They may not always work. 41 | // Simply re-run your setup task after changing the mappings to update your workspace. 42 | mappings channel: 'official', version: '1.18.2' 43 | 44 | runs { 45 | client { 46 | workingDirectory project.file('run_client') 47 | 48 | // Recommended logging data for a userdev environment 49 | // The markers can be added/remove as needed separated by commas. 50 | // "SCAN": For mods scan. 51 | // "REGISTRIES": For firing of registry events. 52 | // "REGISTRYDUMP": For getting the contents of all registries. 53 | //property 'forge.logging.markers', 'REGISTRIES' 54 | 55 | property 'forge.logging.console.level', 'info' 56 | property 'fml.earlyprogresswindow', 'false' 57 | property 'mixin.env.remapRefMap', 'true' 58 | property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" 59 | 60 | mods { 61 | remote_sync_sentinel { 62 | source sourceSets.main 63 | } 64 | } 65 | } 66 | 67 | server { 68 | workingDirectory project.file('run_server') 69 | 70 | // property 'forge.logging.markers', 'REGISTRIES' 71 | 72 | property 'forge.logging.console.level', 'info' 73 | 74 | mods { 75 | remote_sync_sentinel { 76 | source sourceSets.main 77 | } 78 | } 79 | } 80 | 81 | // There is no data to generate. 82 | } 83 | } 84 | 85 | repositories { 86 | // Put repositories for dependencies here 87 | // ForgeGradle automatically adds the Forge maven and Maven Central for you 88 | 89 | // If you have mod jar dependencies in ./libs, you can declare them as a repository like so: 90 | // flatDir { 91 | // dir 'libs' 92 | // } 93 | } 94 | 95 | dependencies { 96 | minecraft 'net.minecraftforge:forge:1.18.2-40.1.59' 97 | } 98 | 99 | // Example for how to get properties into the manifest for reading at runtime. 100 | jar { 101 | manifest { 102 | attributes([ 103 | "Specification-Title": "ModSyncSentinel", 104 | "Specification-Vendor": "3TUSK", 105 | "Specification-Version": "1.0", 106 | "Implementation-Title": "ModSyncSentinel", 107 | "Implementation-Version": archiveVersion.get(), 108 | "Implementation-Vendor": "3TUSK", 109 | "Implementation-Timestamp": DateTimeFormatter.ISO_INSTANT.format(Instant.now().truncatedTo(ChronoUnit.SECONDS)) 110 | ], 'org.teacon.sync.sentinel') 111 | } 112 | } 113 | 114 | // Example configuration to allow publishing using the maven-publish plugin 115 | // This is the preferred method to reobfuscate your jar file 116 | jar.finalizedBy('reobfJar') 117 | // However if you are in a multi-project build, dev time needs unobfed jar files, so you can delay the obfuscation until publishing by doing 118 | // publish.dependsOn('reobfJar') 119 | 120 | publishing { 121 | publications { 122 | release(MavenPublication) { 123 | groupId = "org.teacon" 124 | artifactId = "RemoteSync-Sentinel-FML" 125 | 126 | artifact jar 127 | 128 | pom { 129 | name = 'RemoteSync Sentinel for FML' 130 | description = 'Companion mod for RemonteSync' 131 | url = 'https://github.com/teaconmc/RemoteSync' 132 | licenses { 133 | license { 134 | name = 'LGPL-2.1-or-later' 135 | url = 'https://github.com/teaconmc/RemoteSync/tree/bleeding/COPYING.LGPL' 136 | } 137 | } 138 | developers { 139 | developer { 140 | id = '3TUSK' 141 | name = '3TUSK' 142 | } 143 | } 144 | issueManagement { 145 | system = 'GitHub Issues' 146 | url = 'https://github.com/teaconmc/RemoteSync/issues' 147 | } 148 | scm { 149 | url = 'https://github.com/teaconmc/RemoteSync' 150 | connection = 'scm:git:git://github.com/teaconmc/RemoteSync.git' 151 | developerConnection = 'scm:git:git@github.com:teaconmc/RemoteSync.git' 152 | } 153 | } 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RemoteSync 2 | 3 | ... is forbidden magic that keeps your mods updated. 4 | 5 | ## Preface 6 | 7 | The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 8 | "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be 9 | interpreted as described in [RFC 2119](https://tools.ietf.org/html/rfc2119). 10 | 11 | ## Tl;dr: how to? 12 | 13 | 1. This mod requires Forge (FML, to be exact). 14 | - For any Forge versions 28.1.65 or onward, drop this into your `mods` 15 | folder. Still, you SHOULD use sufficiently new Minecraft and Forge 16 | versions (i.e. those in active development or LTS). 17 | You MUST NOT try using RemoteSync to load RemoteSync itself. It will 18 | never work. 19 | - For any Forge versions 25.0.0 - 28.1.64: this *might* work, but you 20 | *MUST* somehow put this into your classpath. 21 | If you choose these Forge versions, you will also not receive any 22 | technical support here. Updates. 23 | - For any Forge versions 14.23.5.2855 or downward: this does NOT work 24 | on older versions, at all. See "Alternatives" section below for 25 | possible solutions. 26 | 2. Prepare [a collection of PGP public keys](#trust-model). You SHOULD make 27 | sure that your users trust these public keys. 28 | 3. Host all of your mods and [your mod list](#mod-list-format) publicly. 29 | How to keep your list updated is up to you. 30 | 4. Prepare your [`remote_sync.json`](#config-file-remote_syncjson). 31 | 5. When packaging, only include the key rings and config file. 32 | 33 | 37 | 38 | ## Workflow 39 | 40 | RemoteSync works in the following way: 41 | 42 | 1. The "mod" itself *MUST* be in the `mods` directory for technical 43 | reason. 44 | 2. When game starts, RemoteSync loads the config file `remote_sync.json` 45 | (under your `gameDir`) and load public key rings from the path 46 | specified in the config file. 47 | 3. RemoteSync will then fetch the mod list from the URL given in the 48 | config file `remote_sync.json` (under your `gameDir`). RemoteSync 49 | caches the mod list file and uses the cached version if remote does 50 | not have updates. 51 | 3. RemoteSync then downloads and caches all mods and their signatures 52 | according to the mod list, verify their identity and handle them 53 | to FML if and only if verified. 54 | 55 | ## Trust Model 56 | 57 | RemoteSync uses [PGP][ref-1] to verify if a mod file is trustworthy. 58 | Naturally, it means that the trust model of RemoteSync is [the 59 | Web of Trust][ref-2]. 60 | 61 | Put it simple, users establish trusts by trusting third parties' public 62 | key(s) *and thus* public keys that those third parties trust. Example: 63 | 64 | 1. Alice and Bob meets in real life, exchanged and signed their public 65 | keys. They thus start to trust each other. 66 | 2. Later, Bob and Clara meets in real life, exchanged and signed their 67 | public keys. Bob now trusts Clara. 68 | 3. Now, Alice can trust Clara because Alice trusts Bob and Bob trusts 69 | Clara. 70 | 4. The network expands organically as more people (e.g. David, Elise, 71 | Frank, ...) join in the same manner. 72 | 73 | RemoteSync uses detached signatures, so that mod files themselves are 74 | verbatim. 75 | 76 | [ref-1]: https://en.wikipedia.org/wiki/Pretty_Good_Privacy 77 | [ref-2]: https://en.wikipedia.org/wiki/Web_of_trust 78 | 79 | ## Config file `remote_sync.json` 80 | 81 | RemoteSync intentionally does not create this file. It is upon modpack 82 | creators to create and properly configure this file. 83 | 84 | The following is explanation of this file: 85 | 86 | ```json 87 | { 88 | "modDir": "remote_synced_mods", 89 | "keyRingPath": "public_keys.asc", 90 | "keyServers": [], 91 | "keyIds": [], 92 | "modList": "http://example.com/", 93 | "timeout": 15000 94 | } 95 | ``` 96 | 97 | - `modDir` is the path to the directory where RemoteSync stores downloaded 98 | mods. It is relative to your game directory (a.k.a. the `.minecraft` 99 | directory). 100 | - `keyRingPath` is the path to the PGP key ring files whose contents are 101 | public keys that users trust. It also functions as the storage for keys 102 | received from designated key servers. 103 | It is also relative to your game directory. 104 | - `keyServers` is a list of key server URLs to receive keys from. 105 | - `keyIds` is a list of keys to search on key servers specified above. 106 | Usually, keys are described as a 16-digit-long hexadecimal number 107 | (i.e. the long format in GPG), without the `0x` prefix. 108 | - `modList` is the URL to the mod list. The format of mod list is in next 109 | section. 110 | - `timeout` is the number of milliseconds for all remote connections to wait 111 | before giving up. 112 | 113 | ## Mod List Format 114 | 115 | The mod list is a JSON file containing exactly one JSON Array that looks like: 116 | 117 | ```json 118 | [ 119 | { 120 | "name": "mod-a-1.0.jar", 121 | "file": "http://example.invalid/mod-a-1.0.jar", 122 | "sig": "http://example.invalid/mod-b-1.0.jar.sig" 123 | }, 124 | { 125 | "name": "mod-b-1.0.jar", 126 | "file": "http://example.invalid/mod-b-1.0.jar", 127 | "sig": "http://example.invalid/mod-b-1.0.jar.sig" 128 | } 129 | ] 130 | ``` 131 | 132 | - `name` is the file name to use for the local cache. 133 | - `file` is the URL to the mod file itself. 134 | URL must be direct and publicly accessible. 135 | - `sig` is the URL to the signature for the mod file. 136 | Again, URL must be direct publicly accessible. 137 | - The same structure can be repeated as many times as needed. 138 | 139 | ## Alternatives 140 | 141 | [cpw's serverpackloactor][ref-3] is a good candidate to consider if you feel 142 | uncomfortable about setting up PGP keys. 143 | 144 | If you accept using launch arguments, you can try [the `--fml.mavenRoots` 145 | option][ref-4]. Do note that there is also `--fml.modLists` option which lets 146 | you use several list files instead of stuffing everything in the arguments. 147 | These list files MUST have extension of `list` (i.e. `foo.list`, `bar.list`), 148 | and their contents are simply maven coordinates, one per line. 149 | 150 | Older versions of Forge (for Minecraft 1.12.2 or older) has [two options with 151 | similar functionalities: `--mods` and `--modListFile`][ref-5]. 152 | 153 | There is currently no alternatives if you are from Fabric ecosystem. I have 154 | started looking into it, but it is very tricky... 155 | 156 | [ref-3]: https://github.com/cpw/serverpacklocator 157 | [ref-4]: https://github.com/MinecraftForge/MinecraftForge/issues/5495#issuecomment-464916093 158 | [ref-5]: https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format 159 | 160 | ## To developers who want to go down the rabbit hole 161 | 162 | Directly import this project as a *Gradle Project* to your IDE, 163 | no special configuration needed. 164 | 165 | It is worth noting that this project does not depend on Minecraft 166 | itself, only ForgeSPI and ModLauncher. One implication is that you 167 | cannot test it directly in your IDE unless you manually set it up. 168 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/sync/SyncedModLocator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 3TUSK 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | // SPDX-Identifier: LGPL-2.1-or-later 20 | 21 | package org.teacon.sync; 22 | 23 | import com.google.gson.Gson; 24 | import com.google.gson.JsonParseException; 25 | import cpw.mods.modlauncher.Launcher; 26 | import cpw.mods.modlauncher.api.IEnvironment; 27 | import net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator; 28 | import net.minecraftforge.forgespi.Environment; 29 | import org.apache.logging.log4j.LogManager; 30 | import org.apache.logging.log4j.Logger; 31 | import org.bouncycastle.openpgp.PGPCompressedData; 32 | import org.bouncycastle.openpgp.PGPSignatureList; 33 | import org.bouncycastle.openpgp.PGPUtil; 34 | import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; 35 | 36 | import java.io.IOException; 37 | import java.io.InputStream; 38 | import java.io.Reader; 39 | import java.nio.channels.Channels; 40 | import java.nio.channels.FileChannel; 41 | import java.nio.charset.StandardCharsets; 42 | import java.nio.file.Files; 43 | import java.nio.file.Path; 44 | import java.nio.file.Paths; 45 | import java.nio.file.StandardOpenOption; 46 | import java.util.Arrays; 47 | import java.util.Collection; 48 | import java.util.List; 49 | import java.util.Map; 50 | import java.util.concurrent.CompletableFuture; 51 | import java.util.concurrent.CompletionException; 52 | import java.util.function.Consumer; 53 | import java.util.stream.Stream; 54 | 55 | public final class SyncedModLocator extends AbstractJarFileModLocator { 56 | 57 | private static final Logger LOGGER = LogManager.getLogger("RemoteSync"); 58 | 59 | private static final Gson GSON = new Gson(); 60 | 61 | private final Path modDirBase; 62 | private final Consumer progressFeed; 63 | 64 | private final PGPKeyStore keyStore; 65 | 66 | private final CompletableFuture> fetchPathsTask; 67 | 68 | public SyncedModLocator() throws Exception { 69 | this.progressFeed = Launcher.INSTANCE.environment().getProperty(Environment.Keys.PROGRESSMESSAGE.get()).orElse(msg -> {}); 70 | final Path gameDir = Launcher.INSTANCE.environment().getProperty(IEnvironment.Keys.GAMEDIR.get()).orElse(Paths.get(".")); 71 | final Path cfgPath = gameDir.resolve("remote_sync.json"); 72 | final Config cfg; 73 | if (Files.exists(cfgPath)) { 74 | cfg = GSON.fromJson(Files.newBufferedReader(cfgPath, StandardCharsets.UTF_8), Config.class); 75 | } else { 76 | LOGGER.warn("RemoteSync config remote_sync.json does not exist. All configurable values will use their default values instead."); 77 | cfg = new Config(); 78 | } 79 | final Path keyStorePath = gameDir.resolve(cfg.keyRingPath); 80 | this.keyStore = new PGPKeyStore(keyStorePath, cfg.keyServers, cfg.keyIds); 81 | this.keyStore.debugDump(); 82 | this.modDirBase = Files.createDirectories(gameDir.resolve(cfg.modDir)); 83 | this.fetchPathsTask = CompletableFuture.supplyAsync(() -> { 84 | Path localCache = gameDir.resolve(cfg.localModList); 85 | try { 86 | this.progressFeed.accept("RemoteSync: fetching mod list"); 87 | // Intentionally do not use config value to ensure that the mod list is always up-to-date 88 | return Utils.fetch(cfg.modList, localCache, cfg.timeout, false); 89 | } catch (IOException e) { 90 | LOGGER.warn("Failed to download mod list, will try using locally cached mod list instead. Mods may be outdated.", e); 91 | System.setProperty("org.teacon.sync.failed", "true"); 92 | try { 93 | return FileChannel.open(localCache); 94 | } catch (Exception e2) { 95 | throw new RuntimeException("Failed to open locally cached mod list", e2); 96 | } 97 | } 98 | }).thenApplyAsync((fcModList) -> { 99 | try (Reader reader = Channels.newReader(fcModList, StandardCharsets.UTF_8)) { 100 | return GSON.fromJson(reader, ModEntry[].class); 101 | } catch (JsonParseException e) { 102 | LOGGER.warn("Error parsing mod list", e); 103 | throw e; 104 | } catch (IOException e) { 105 | LOGGER.warn("Failed to open mod list file", e); 106 | throw new RuntimeException(e); 107 | } 108 | }).thenComposeAsync(entries -> { 109 | List> futures = Arrays.stream(entries).flatMap(e -> Stream.of( 110 | Utils.downloadIfMissingAsync(this.modDirBase.resolve(e.name), e.file, cfg.timeout, cfg.preferLocalCache, this.progressFeed), 111 | Utils.downloadIfMissingAsync(this.modDirBase.resolve(e.name + ".sig"), e.sig, cfg.timeout, cfg.preferLocalCache, this.progressFeed) 112 | )).toList(); 113 | return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) 114 | .thenApply(v -> Arrays.stream(entries) 115 | .map(it -> this.modDirBase.resolve(it.name)) 116 | .filter(this::isValid).toList()); 117 | }); 118 | } 119 | 120 | private static PGPSignatureList getSigList(FileChannel fc) throws Exception { 121 | PGPSignatureList sigList; 122 | try (InputStream input = PGPUtil.getDecoderStream(Channels.newInputStream(fc))) { 123 | BcPGPObjectFactory factory = new BcPGPObjectFactory(input); 124 | Object o = factory.nextObject(); 125 | if (o instanceof PGPCompressedData compressedData) { 126 | factory = new BcPGPObjectFactory(compressedData.getDataStream()); 127 | sigList = (PGPSignatureList) factory.nextObject(); 128 | } else { 129 | sigList = (PGPSignatureList) o; 130 | } 131 | } 132 | return sigList; 133 | } 134 | 135 | @Override 136 | public Stream scanCandidates() { 137 | try { 138 | return this.fetchPathsTask.join().stream(); 139 | } catch (Exception e) { 140 | LOGGER.error("Mod downloading worker encountered error. " + 141 | "You may observe missing mods or outdated mods. ", e instanceof CompletionException ? e.getCause() : e); 142 | System.setProperty("org.teacon.sync.failed", "true"); 143 | return Stream.empty(); 144 | } 145 | } 146 | 147 | @Override 148 | public String name() { 149 | return "Remote Synced"; 150 | } 151 | 152 | @Override 153 | public void initArguments(Map arguments) { 154 | } 155 | 156 | private boolean isValid(Path modFile) { 157 | LOGGER.debug("Verifying {}", modFile.getFileName()); 158 | this.progressFeed.accept("RemoteSync: verifying " + modFile.getFileName()); 159 | final Path sigPath = modFile.resolveSibling(modFile.getFileName() + ".sig"); 160 | try (FileChannel mod = FileChannel.open(modFile, StandardOpenOption.READ)) { 161 | try (FileChannel sig = FileChannel.open(sigPath, StandardOpenOption.READ)) { 162 | final PGPSignatureList sigList; 163 | try { 164 | sigList = getSigList(sig); 165 | } catch (Exception e) { 166 | LOGGER.warn("Failed to read signature for {}, verification automatically fails", modFile.getFileName()); 167 | return false; 168 | } 169 | if (sigList == null) { 170 | LOGGER.warn("Failed to load any signature for {}, check if you downloaded the wrong file", modFile.getFileName()); 171 | return false; 172 | } 173 | final boolean pass = this.keyStore.verify(mod, sigList); 174 | if (pass) { 175 | LOGGER.debug("Verification pass for {}", modFile.getFileName()); 176 | } else { 177 | LOGGER.warn("Verification fail for {}, will be excluded from loading", modFile.getFileName()); 178 | Files.deleteIfExists(modFile.toAbsolutePath().normalize()); 179 | Files.deleteIfExists(sigPath.toAbsolutePath().normalize()); 180 | } 181 | return pass; 182 | } 183 | } catch (IOException e) { 184 | LOGGER.warn("Failed to read {}, verification automatically fails", modFile.getFileName()); 185 | return false; 186 | } 187 | } 188 | 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/sync/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 3TUSK 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | // SPDX-Identifier: LGPL-2.1-or-later 20 | 21 | package org.teacon.sync; 22 | 23 | import org.apache.logging.log4j.LogManager; 24 | import org.apache.logging.log4j.Logger; 25 | import org.apache.logging.log4j.Marker; 26 | import org.apache.logging.log4j.MarkerManager; 27 | 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.net.HttpURLConnection; 31 | import java.net.URL; 32 | import java.net.URLConnection; 33 | import java.nio.channels.Channels; 34 | import java.nio.channels.FileChannel; 35 | import java.nio.channels.ReadableByteChannel; 36 | import java.nio.file.Files; 37 | import java.nio.file.OpenOption; 38 | import java.nio.file.Path; 39 | import java.nio.file.StandardOpenOption; 40 | import java.util.Set; 41 | import java.util.concurrent.CompletableFuture; 42 | import java.util.function.Consumer; 43 | 44 | import static java.nio.file.StandardOpenOption.*; 45 | import static org.bouncycastle.bcpg.HashAlgorithmTags.*; 46 | import static org.bouncycastle.bcpg.PublicKeyAlgorithmTags.*; 47 | 48 | public final class Utils { 49 | 50 | private static final Logger LOGGER = LogManager.getLogger("RemoteSync"); 51 | private static final Marker MARKER = MarkerManager.getMarker("Downloader"); 52 | 53 | private static final Set OPTIONS = Set.of(CREATE, TRUNCATE_EXISTING, READ, WRITE); 54 | 55 | private Utils() { 56 | // No instance for you 57 | } 58 | 59 | public static String getKeyAlgorithm(int algo) { 60 | return switch (algo) { 61 | case RSA_GENERAL, RSA_ENCRYPT, RSA_SIGN -> "RSA"; 62 | case DSA -> "DSA"; 63 | case ECDH -> "ECDH"; 64 | case ECDSA -> "ECDSA"; 65 | case EDDSA -> "EDDSA"; 66 | case ELGAMAL_GENERAL, ELGAMAL_ENCRYPT -> "ELGAMAL"; 67 | case DIFFIE_HELLMAN -> "DIFFIE_HELLMAN"; 68 | default -> "UNKNOWN"; 69 | }; 70 | } 71 | 72 | public static String getHashAlgorithm(int algo) { 73 | return switch (algo) { 74 | // MD 75 | case MD2 -> "MD2"; 76 | case MD5 -> "MD5"; 77 | 78 | // SHA 79 | case SHA1 -> "SHA1"; 80 | case SHA224 -> "SHA224"; 81 | case SHA256 -> "SHA256"; 82 | case SHA384 -> "SHA384"; 83 | case SHA512 -> "SHA512"; 84 | case DOUBLE_SHA -> "DOUBLE-SHA"; 85 | 86 | // RIPEMD 87 | // https://en.wikipedia.org/wiki/RIPEMD 88 | case RIPEMD160 -> "RIPEMD160"; 89 | 90 | // Tiger 91 | // https://en.wikipedia.org/wiki/Tiger_(hash_function) 92 | case TIGER_192 -> "TIGER192"; 93 | 94 | // HAVAL 95 | // https://en.wikipedia.org/wiki/HAVAL 96 | case HAVAL_5_160 -> "HAVAL-5-160"; 97 | default -> "UNKNOWN"; 98 | }; 99 | } 100 | 101 | /** 102 | * Blindly fetch bytes from the specified URL with no timeout, write to the given 103 | * destination, and then return the open {@link FileChannel} back. 104 | * @param src The source URL of the mod to download 105 | * @param dst the destination path of the mod to save 106 | * @param biasedTowardLocalCache If true, local files are always used regardless of remote updates. 107 | * @return An open {@link FileChannel} with {@link StandardOpenOption#READ} set, 108 | * position at zero. 109 | * @throws IOException thrown if download fail 110 | */ 111 | public static FileChannel fetch(URL src, Path dst, boolean biasedTowardLocalCache) throws IOException { 112 | return fetch(src, dst, 0, biasedTowardLocalCache); 113 | } 114 | 115 | /** 116 | * Blindly fetch bytes from the specified URL with the specified timeout, write to 117 | * the given destination, and then return the open {@link FileChannel} back. 118 | * @param src The source URL of the mod to download 119 | * @param dst the destination path of the mod to save 120 | * @param timeout Time to wait before giving up the connection 121 | * @param biasedTowardLocalCache If true, local files are always used regardless of remote updates. 122 | * @return An open {@link FileChannel} with {@link StandardOpenOption#READ} set, 123 | * position at zero. 124 | * @throws IOException thrown if download fail 125 | */ 126 | public static FileChannel fetch(URL src, Path dst, int timeout, boolean biasedTowardLocalCache) throws IOException { 127 | LOGGER.debug(MARKER, "Trying to decide how to get {}", src); 128 | final URLConnection conn = src.openConnection(); 129 | conn.setConnectTimeout(timeout); 130 | if (Files.exists(dst)) { 131 | if (biasedTowardLocalCache) { 132 | LOGGER.debug(MARKER, "Prefer to local copy at {} according to configurations", dst); 133 | return FileChannel.open(dst, READ); 134 | } 135 | conn.setIfModifiedSince(Files.getLastModifiedTime(dst).toMillis()); 136 | try { 137 | conn.connect(); 138 | } catch (IOException e) { 139 | // Connection failed, prefer local copy instead 140 | LOGGER.debug(MARKER, "Failed to connect to {}, fallback to local copy at {}", src, dst); 141 | return FileChannel.open(dst, READ); 142 | } 143 | if (conn instanceof HttpURLConnection) { 144 | int resp = ((HttpURLConnection) conn).getResponseCode(); 145 | if (resp == HttpURLConnection.HTTP_NOT_MODIFIED) { 146 | // If remote does not update, we use local copy. 147 | LOGGER.debug(MARKER, "Remote {} does not have updates, prefer use local copy at {}", src, dst); 148 | return FileChannel.open(dst, READ); 149 | } else if (resp >= 400) { 150 | LOGGER.warn(MARKER, "Remote {} fails with status code {}, prefer use local copy at {}", src, resp, dst); 151 | return FileChannel.open(dst, READ); 152 | } 153 | } 154 | } 155 | // Otherwise, we have to fetch the newer version. 156 | // It could very well be a local file, but there are also things like FTP 157 | // which are pretty rare these days (it's 2020s after all!) 158 | LOGGER.debug(MARKER, "Fetching remote resource {}", src); 159 | return fetch(conn.getInputStream(), dst); 160 | } 161 | 162 | /** 163 | * Blindly fetch bytes from the input stream, write to the given destination, 164 | * and then return the open {@link FileChannel} back. 165 | * @param src The source stream of the mod to download 166 | * @param dst the destination path of the mod to save 167 | * @return An open {@link FileChannel} with {@link StandardOpenOption#READ} set, 168 | * position at zero. 169 | * @throws IOException thrown if download fail 170 | * @see Referred code on Stack Overflow 171 | */ 172 | public static FileChannel fetch(InputStream src, Path dst) throws IOException { 173 | final FileChannel dstChannel = FileChannel.open(dst, OPTIONS); 174 | try (ReadableByteChannel srcChannel = Channels.newChannel(src)) { 175 | // TODO do we need a larger value for the max. bytes to transfer? 176 | dstChannel.transferFrom(srcChannel, 0, Integer.MAX_VALUE); 177 | dstChannel.position(0L); // Reset position back to start 178 | return dstChannel; 179 | } 180 | } 181 | 182 | /** 183 | * Asynchronously download the resource to the specified destination if it does 184 | * not exist yet. 185 | * @param target The path to the file 186 | * @param src Fallback URL to download from if target does not exist 187 | * @param timeout Number of milliseconds to wait before giving up connection 188 | * @return A {@link CompletableFuture} that represents this task. 189 | */ 190 | public static CompletableFuture downloadIfMissingAsync(Path target, URL src, int timeout, boolean biasedTowardLocalCache, Consumer progressFeedback) { 191 | return CompletableFuture.supplyAsync(() -> { 192 | try { 193 | progressFeedback.accept("RemoteSync: considering " + target.getFileName()); 194 | return fetch(src, target, timeout, biasedTowardLocalCache); 195 | } catch (IOException e) { 196 | throw new RuntimeException(e); 197 | } 198 | }).handleAsync((fc, e) -> { 199 | if (e != null) { 200 | LOGGER.warn(MARKER,"Failed to download {}", src); 201 | LOGGER.debug(MARKER, "Details: src = {}, dst = {}", src, target, e); 202 | } else if (fc != null) { 203 | try { 204 | fc.close(); 205 | } catch (IOException ignored) { 206 | // No-op 207 | } 208 | } 209 | return null; 210 | }); 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/sync/PGPKeyStore.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 3TUSK 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation; either 7 | * version 2.1 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, write to the Free Software 16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | */ 18 | 19 | // SPDX-Identifier: LGPL-2.1-or-later 20 | 21 | package org.teacon.sync; 22 | 23 | import org.apache.logging.log4j.Level; 24 | import org.apache.logging.log4j.LogManager; 25 | import org.apache.logging.log4j.Logger; 26 | import org.apache.logging.log4j.Marker; 27 | import org.apache.logging.log4j.MarkerManager; 28 | import org.bouncycastle.openpgp.PGPException; 29 | import org.bouncycastle.openpgp.PGPObjectFactory; 30 | import org.bouncycastle.openpgp.PGPPublicKey; 31 | import org.bouncycastle.openpgp.PGPPublicKeyRing; 32 | import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; 33 | import org.bouncycastle.openpgp.PGPSignature; 34 | import org.bouncycastle.openpgp.PGPSignatureList; 35 | import org.bouncycastle.openpgp.PGPUtil; 36 | import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; 37 | import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; 38 | import org.bouncycastle.util.encoders.Hex; 39 | 40 | import javax.naming.Context; 41 | import javax.naming.directory.Attribute; 42 | import javax.naming.directory.Attributes; 43 | import javax.naming.directory.DirContext; 44 | import javax.naming.directory.InitialDirContext; 45 | import java.io.IOException; 46 | import java.io.InputStream; 47 | import java.io.OutputStream; 48 | import java.net.URL; 49 | import java.nio.ByteBuffer; 50 | import java.nio.channels.FileChannel; 51 | import java.nio.file.Files; 52 | import java.nio.file.Path; 53 | import java.time.Instant; 54 | import java.time.temporal.ChronoUnit; 55 | import java.util.HashMap; 56 | import java.util.Hashtable; 57 | import java.util.List; 58 | import java.util.Map; 59 | 60 | public final class PGPKeyStore { 61 | 62 | /* 63 | * Implementation overview 64 | * 65 | * This class implements a partial client that supports retrieving 66 | * public PGP keys via HTTP Keyserver Protocol (HKP). 67 | * 68 | * More info about HKP may be found at this IETF draft: 69 | * https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 70 | */ 71 | 72 | private static final Logger LOGGER = LogManager.getLogger("RemoteSync"); 73 | private static final Marker MARKER = MarkerManager.getMarker("KeyStore"); 74 | 75 | private static final boolean RESOLVE_SRV_RECORD; 76 | 77 | static { 78 | boolean resolveSrv = false; 79 | try { 80 | Class.forName("com.sun.jndi.dns.DnsContextFactory", false, null); 81 | resolveSrv = true; 82 | } catch (Exception ignored) { 83 | 84 | } 85 | RESOLVE_SRV_RECORD = resolveSrv; 86 | } 87 | 88 | private final PGPPublicKeyRingCollection keyRings; 89 | 90 | public PGPKeyStore(Path localKeyStorePath, List keyServers, List keyIds) throws Exception { 91 | final Map keyRings = new HashMap<>(); 92 | if (Files.exists(localKeyStorePath)) { 93 | LOGGER.debug(MARKER, "Try reading keys from local key ring at {}", localKeyStorePath); 94 | readKeys(Files.newInputStream(localKeyStorePath), keyRings); 95 | } 96 | for (String keyId : keyIds) { 97 | if (keyRings.containsKey(keyId.startsWith("0x") ? Long.parseUnsignedLong(keyId.substring(2), 16) : Long.parseUnsignedLong(keyId, 16))) { 98 | LOGGER.debug(MARKER,"Not trying to load key {} because it exists locally", keyId); 99 | continue; 100 | } 101 | final String queryParams = "/pks/lookup?op=get&search=".concat(keyId); 102 | for (URL keyServer : keyServers) { 103 | final URL resolved = resolveSrv(keyServer); 104 | final URL keyQuery = new URL(resolved.getProtocol(), resolved.getHost(), resolved.getPort(), queryParams); 105 | try (InputStream input = keyQuery.openStream()) { 106 | LOGGER.debug(MARKER, "Receiving key {} from {}", keyId, keyServer); 107 | readKeys(input, keyRings); 108 | break; // Stop on first available key server 109 | } catch (Exception ignored) { 110 | // No-op 111 | } 112 | } 113 | } 114 | this.keyRings = new PGPPublicKeyRingCollection(keyRings.values()); 115 | this.saveTo(localKeyStorePath); 116 | } 117 | 118 | public void debugDump() { 119 | for (PGPPublicKeyRing ring : this.keyRings) { 120 | for (PGPPublicKey pubKey : ring) { 121 | LOGGER.printf(Level.DEBUG, MARKER, "Public Key ID = %1$016X, Algo = %2$s, Fingerprint = %3$s", 122 | pubKey.getKeyID(), Utils.getKeyAlgorithm(pubKey.getAlgorithm()), Hex.toHexString(pubKey.getFingerprint())); 123 | } 124 | } 125 | } 126 | 127 | public boolean verify(FileChannel src, PGPSignatureList sigList) { 128 | for (PGPSignature sig : sigList) { 129 | try { 130 | PGPPublicKey pubKey = this.keyRings.getPublicKey(sig.getKeyID()); 131 | if (pubKey == null) { 132 | LOGGER.printf(Level.WARN, MARKER, "Cannot find key %1$016X in current key ring, signature cannot be verified", 133 | sig.getKeyID()); 134 | return false; 135 | } 136 | sig.init(new BcPGPContentVerifierBuilderProvider(), pubKey); 137 | // Has to be a heap buffer, BouncyCastle only supports passing in byte[] 138 | ByteBuffer buf = ByteBuffer.allocate(1 << 12); 139 | src.position(0); 140 | int limit; 141 | while ((limit = src.read(buf)) != -1) { 142 | buf.flip(); // limit = pos, pos = 0 143 | sig.update(buf.array(), 0, limit); 144 | buf.clear(); // limit = cap, pos = 0 145 | } 146 | if (!sig.verify()) { 147 | LOGGER.printf(Level.WARN, MARKER, "Signature verification failed (%1$s key %2$016X, made on %3$tc)", 148 | Utils.getKeyAlgorithm(sig.getKeyAlgorithm()), sig.getKeyID(), sig.getCreationTime()); 149 | return false; 150 | } else if (pubKey.hasRevocation()) { 151 | LOGGER.printf(Level.WARN, MARKER, "Signature verified (%1$s key %2$016X, made on %3$tc) but the key-pair has been revoked", 152 | Utils.getKeyAlgorithm(sig.getKeyAlgorithm()), sig.getKeyID(), sig.getCreationTime()); 153 | return false; 154 | } else if (hasExpired(pubKey)) { 155 | LOGGER.printf(Level.WARN, MARKER, "Signature verified (%1$s key %2$016X, made on %3$tc) but the key-pair has expired", 156 | Utils.getKeyAlgorithm(sig.getKeyAlgorithm()), sig.getKeyID(), sig.getCreationTime()); 157 | return false; 158 | } else { 159 | LOGGER.printf(Level.DEBUG, MARKER, "Signature verified: %1$s key %2$016X, made on %3$tc", 160 | Utils.getKeyAlgorithm(sig.getKeyAlgorithm()), sig.getKeyID(), sig.getCreationTime()); 161 | } 162 | } catch (PGPException e) { 163 | LOGGER.warn(MARKER, "Unknown error occurred, signature verification automatically fails"); 164 | LOGGER.debug(MARKER, "Error details: ", e); 165 | return false; 166 | } catch (IOException e) { 167 | LOGGER.warn(MARKER,"Failed to read file while checking signature", e); 168 | return false; 169 | } 170 | } 171 | return true; 172 | } 173 | 174 | public void saveTo(Path path) { 175 | try (OutputStream output = Files.newOutputStream(path)) { 176 | this.keyRings.encode(output); 177 | } catch (IOException e) { 178 | LOGGER.warn(MARKER, "Failed to save key store", e); 179 | } 180 | } 181 | 182 | private static void readKeys(InputStream input, Map keyRings) throws Exception { 183 | try (InputStream wrapped = PGPUtil.getDecoderStream(input)) { 184 | PGPObjectFactory factory = new BcPGPObjectFactory(wrapped); 185 | for (Object o : factory) { 186 | if (o instanceof PGPPublicKeyRing keyRing) { 187 | keyRings.put(keyRing.getPublicKey().getKeyID(), keyRing); 188 | } else { 189 | LOGGER.warn(MARKER, "Invalid PGP object {} (type {}) found and ignored", o, o.getClass()); 190 | } 191 | } 192 | } 193 | } 194 | 195 | private static URL resolveSrv(URL original) { 196 | if (!RESOLVE_SRV_RECORD) { 197 | return original; 198 | } 199 | /* 200 | * Perform DNS query via JNDI. 201 | * 202 | * Documentation: 203 | * https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html 204 | */ 205 | try { 206 | Hashtable params = new Hashtable<>(); 207 | params.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); 208 | params.put(Context.PROVIDER_URL, "dns:"); 209 | params.put("com.sun.jndi.dns.timeout.retries", "1"); 210 | DirContext context = new InitialDirContext(params); 211 | // https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 Chapter 7 212 | Attributes attrs = context.getAttributes("_hkp._tcp." + original.getHost(), new String[]{"SRV"}); 213 | Attribute attr = attrs.get("srv"); 214 | if (attr != null) { 215 | String[] results = attr.get().toString().split(" ", 4); 216 | int port = 80; 217 | try { 218 | port = Integer.parseInt(results[2]); 219 | } catch (Exception ignored) { 220 | 221 | } 222 | return new URL(original.getProtocol(), results[3], port, "/"); 223 | } 224 | } catch (Exception e) { 225 | LOGGER.debug(MARKER, "Failed to resolve SRV record for '{}'", original, e); 226 | } 227 | return original; 228 | } 229 | 230 | private static boolean hasExpired(PGPPublicKey pubKey) { 231 | long duration = pubKey.getValidSeconds(); 232 | if (duration > 0) { 233 | // Only check expiration if getValidSeconds() returns a positive value 234 | Instant creationTime = pubKey.getCreationTime().toInstant(); 235 | Instant expirationTime = creationTime.plus(duration, ChronoUnit.SECONDS); 236 | return Instant.now().isAfter(expirationTime); 237 | } else { 238 | // If getValidSeconds() returns 0 or less it means no expiration. 239 | return false; 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /COPYING.LGPL: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! --------------------------------------------------------------------------------