commit 89a7c8eaefd89f17dfff3fde0d2dbbdb0f688ed8 Author: Hellings Date: Wed Sep 23 23:39:11 2026 -0400 Initial commit. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..566ef1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +# Ignore Kotlin plugin data +.kotlin + +# Node credentials and deployment-specific endpoints +node.yaml +deploy/*.yaml diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..08628cf --- /dev/null +++ b/Justfile @@ -0,0 +1,16 @@ +set shell := ["bash", "-eu", "-o", "pipefail", "-c"] + +build: + ./gradlew build + +fmt: + ./gradlew spotlessApply + +check: + ./gradlew spotlessCheck build + +native: + ./gradlew nativeImage + +deploy host user: + scripts/deploy-ssh.sh {{host}} {{user}} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a2e6ec5 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# CommonIP + +CommonIP, not to be confused with [CommonApp](https://commonapp.org), is a decentralized geolocation network that specifically avoids proprietary geolocation services whenever possible. It's written in the ever-so-portable Kotlin. Use it from here, use it from there, use it from anywhere! + +# donate + +Please donate to me to keep this project and its (centralized) [geofeeds](https://ai.crit.rip/automatedintelligence/geofeeds) alive. + +- `eth` 0xF30C281Aa2CD94618D902AE5bb9101E9F5385fE3 +- `sol` 99zJPASUz9PKLFaJWrxHCEtLZxeEePYpgZ74Ly9YNUHy +- `paypal` +1 (267) 806-2744 - Stephen Hellings +- `zelle` +1 (267) 806-2744 - STEPHEN HELLINGS diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..131beb3 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,32 @@ +import buildlogic.GraalNativeImageTask + +plugins { + id("buildlogic.kotlin-application-conventions") +} + +dependencies { + implementation("org.apache.commons:commons-text") + implementation(project(":packages:serialize")) + implementation(project(":packages:rawr")) + implementation(project(":proprietary")) + implementation(project(":packages:ml")) + implementation(project(":packages:decentraland")) +} + +application { + mainClass = "rip.crit.commonip.app.MainKtKt" +} + +tasks.named("run") { + standardInput = System.`in` +} + +val nativeImage = + tasks.register("nativeImage") { + group = "distribution" + description = "Builds a GraalVM native executable with the active Gradle toolchain." + dependsOn(tasks.named("classes")) + classpath.set(sourceSets.main.get().runtimeClasspath.files.map { it.absolutePath }) + mainClass.set("rip.crit.commonip.app.MainKtKt") + output.set(layout.buildDirectory.file("native/commonip")) + } diff --git a/app/src/main/kotlin/rip/crit/commonip/app/ClusterGeolocation.kt b/app/src/main/kotlin/rip/crit/commonip/app/ClusterGeolocation.kt new file mode 100644 index 0000000..8925c27 --- /dev/null +++ b/app/src/main/kotlin/rip/crit/commonip/app/ClusterGeolocation.kt @@ -0,0 +1,93 @@ +package rip.crit.commonip.app + +import java.net.InetSocketAddress +import java.util.concurrent.ConcurrentHashMap +import rip.crit.commonip.decentraland.git.RepositoryTrust +import rip.crit.commonip.decentraland.kademlia.KademliaNode +import rip.crit.commonip.decentraland.kademlia.NodeRegistry +import rip.crit.commonip.ml.GeoLocation as MlGeoLocation +import rip.crit.commonip.ml.LocationEstimator +import rip.crit.commonip.proprietary.OpenGeoFeed +import rip.crit.commonip.proprietary.StarlinkGeoFeed +import rip.crit.commonip.rawr.providers.RirGeoFeedLoader +import rip.crit.commonip.rawr.providers.RirMetadataField +import rip.crit.commonip.rawr.types.Cidr +import rip.crit.commonip.serialize.GeoLocation + +class ClusterGeolocation(private val registry: NodeRegistry, bind: InetSocketAddress) : + AutoCloseable { + private val feeds = ConcurrentHashMap() + private val local = ConcurrentHashMap() + private val dht = KademliaNode(bind, registry) + + fun start(seeds: List = emptyList()) { + dht.start() + seeds.forEach(dht::bootstrap) + } + + fun addFeed(url: String, trust: RepositoryTrust = RepositoryTrust.COMMUNITY) { + feeds[url] = trust + } + + fun addStarlinkFeed() { + feeds[StarlinkGeoFeed.URL] = RepositoryTrust.TAINTED + } + + fun pullRirRegistryObject( + source: String, + registryObject: String, + fields: Set, + trust: RepositoryTrust = RepositoryTrust.COMMUNITY, + ) { + RirGeoFeedLoader(source, fields).pull(registryObject).forEach { feed -> + feed.records.forEach { record -> + local[record.prefix] = record + dht.store( + "${if (trust == RepositoryTrust.TAINTED) "commonip/tainted-full" else "commonip/community"}:${record.prefix}", + record.prefix, + ) + } + registry.publish( + "local", + "rir geofeed pulled", + "$source yielded ${feed.records.size} records", + ) + } + } + + fun pullAll() { + feeds.forEach { (url, trust) -> + runCatching { OpenGeoFeed().pull(url).records } + .onSuccess { records -> + records.forEach { record -> + local[record.prefix] = record + dht.store( + "${if (trust == RepositoryTrust.TAINTED) "commonip/tainted-full" else "commonip/community"}:${record.prefix}", + record.prefix, + ) + } + registry.publish( + "local", + "${trust.name.lowercase()} feed pulled", + "$url yielded ${records.size} records", + ) + } + .onFailure { registry.publish("local", "feed pull failed", "$url ${it.message}") } + } + } + + fun lookup(address: String): GeoLocation? = + local.values + .filter { Cidr.contains(it.prefix, address) } + .maxByOrNull { Cidr.prefixLength(it.prefix) } + + fun estimate(address: String) = + LocationEstimator() + .estimate( + local.values + .filter { Cidr.contains(it.prefix, address) } + .map { MlGeoLocation(it.latitude, it.longitude, it.confidence) } + ) + + override fun close() = dht.close() +} diff --git a/app/src/main/kotlin/rip/crit/commonip/app/MainKt.kt b/app/src/main/kotlin/rip/crit/commonip/app/MainKt.kt new file mode 100644 index 0000000..4e6bf05 --- /dev/null +++ b/app/src/main/kotlin/rip/crit/commonip/app/MainKt.kt @@ -0,0 +1,116 @@ +package rip.crit.commonip.app + +import java.net.InetSocketAddress +import java.nio.file.Path +import java.nio.file.Files +import rip.crit.commonip.decentraland.git.NodeYamlReader +import rip.crit.commonip.decentraland.SshDashboardService +import rip.crit.commonip.decentraland.kademlia.NetworkNode +import rip.crit.commonip.decentraland.kademlia.NodeRegistry +import rip.crit.commonip.decentraland.kademlia.NodeState + +fun main() = CommonIpNode().run() + +private class CommonIpNode { + private val registry = NodeRegistry() + private val cluster = + ClusterGeolocation( + registry, + InetSocketAddress(System.getenv("COMMONIP_DHT_PORT")?.toIntOrNull() ?: 7400), + ) + + fun run() { + val configPath = Path.of(System.getenv("COMMONIP_NODE_CONFIG") ?: "/etc/commonip/node.yaml") + val configuredBootstrap = if (Files.isRegularFile(configPath)) NodeYamlReader.parse(Files.readString(configPath)).bootstrap else emptyList() + cluster.start((configuredBootstrap + System.getenv("COMMONIP_BOOTSTRAP")?.split(',').orEmpty()).mapNotNull(::parseEndpoint).distinct()) + System.getenv("COMMONIP_GEOFEEDS") + ?.split(',') + ?.filter(String::isNotBlank) + ?.forEach(cluster::addFeed) + if (System.getenv("COMMONIP_ENABLE_TAINTED_STARLINK") == "true") cluster.addStarlinkFeed() + cluster.pullAll() + val sshService = + System.getenv("COMMONIP_SSH_PASSWD")?.let { password -> + SshDashboardService( + System.getenv("COMMONIP_SSH_PORT")?.toIntOrNull() ?: 2222, + Path.of(System.getenv("COMMONIP_SSH_HOST_KEY") ?: "/opt/commonip/hostkey.ser"), + System.getenv("COMMONIP_SSH_USER") ?: "commonip", + password.toCharArray(), + ) { + registry.snapshot().joinToString("\n") { "${it.id} ${it.state}" } + } + .also { it.start() } + } + if (System.console() == null) { + while (true) { + Thread.sleep(60_000) + cluster.pullAll() + } + } + sshService?.close() + while (true) { + render() + print("commonip/network> ") + val command = readlnOrNull()?.trim()?.lowercase() ?: return + when (command) { + "q", + "quit", + "exit" -> return + "r", + "refresh" -> cluster.pullAll() + "l", + "lookup" -> lookup() + "e", + "estimate" -> estimate() + else -> println("Commands are r refresh, l lookup, e estimate, q quit.") + } + } + } + + private fun parseEndpoint(value: String): InetSocketAddress? = + value.trim().split(':', limit = 2).let { parts -> + parts.getOrNull(1)?.toIntOrNull()?.let { InetSocketAddress(parts[0], it) } + } + + private fun render() { + print("\u001b[H\u001b[2J") + val nodes = registry.snapshot() + println() + nodes.chunked(8).forEach { row -> + println(row.joinToString(" ") { node -> "${status(node)} ${node.id}" }) + } + println() + registry.feed().forEach { event -> + println( + "${event.at.toString().substring(11, 19)} ${event.nodeId} ${event.action} ${event.detail}" + ) + } + } + + private fun status(node: NetworkNode): String = + when (node.state) { + NodeState.ONLINE -> "\u001b[32m●\u001b[0m" + NodeState.DEGRADED -> "\u001b[33m●\u001b[0m" + NodeState.OFFLINE -> "\u001b[90m●\u001b[0m" + } + + private fun lookup() { + print("IPv4 address> ") + val address = readlnOrNull()?.trim().orEmpty() + val match = cluster.lookup(address) + registry.publish("local", "lookup", match?.prefix ?: "no matching CIDR for $address") + } + + private fun estimate() { + print("CIDR or IPv4 address> ") + val address = readlnOrNull()?.trim().orEmpty() + val result = cluster.estimate(address) + registry.publish( + "local", + "estimate", + result?.let { + "%.4f, %.4f confidence %.2f".format(it.latitude, it.longitude, it.confidence) + } ?: "no coordinate observations", + ) + } +} diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 0000000..8472e96 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() +} + +dependencies { + implementation(libs.kotlin.gradle.plugin) + implementation("com.diffplug.spotless:com.diffplug.spotless.gradle.plugin:8.10.2") +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 0000000..bd0f7d2 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,14 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This settings file is used to specify which projects to include in your build-logic build. + * This project uses @Incubating APIs which are subject to change. + */ + +dependencyResolutionManagement { + versionCatalogs { + create("libs", { from(files("../gradle/libs.versions.toml")) }) + } +} + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/buildlogic.kotlin-application-conventions.gradle.kts b/build-logic/src/main/kotlin/buildlogic.kotlin-application-conventions.gradle.kts new file mode 100644 index 0000000..88b0ae7 --- /dev/null +++ b/build-logic/src/main/kotlin/buildlogic.kotlin-application-conventions.gradle.kts @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id("buildlogic.kotlin-common-conventions") + + application +} diff --git a/build-logic/src/main/kotlin/buildlogic.kotlin-common-conventions.gradle.kts b/build-logic/src/main/kotlin/buildlogic.kotlin-common-conventions.gradle.kts new file mode 100644 index 0000000..74e7d05 --- /dev/null +++ b/build-logic/src/main/kotlin/buildlogic.kotlin-common-conventions.gradle.kts @@ -0,0 +1,54 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id("org.jetbrains.kotlin.jvm") + id("com.diffplug.spotless") +} + +repositories { + mavenCentral() +} + +dependencies { + constraints { + implementation("org.apache.commons:commons-text:1.14.0") + } +} + +testing { + suites { + val test = named("test") { + useJUnitJupiter("6.0.1") + } + } +} + +tasks.named("check") { + dependsOn("spotlessCheck") +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + vendor = JvmVendorSpec.GRAAL_VM + } +} + +spotless { + kotlin { + target("src/**/*.kt") + ktfmt() + trimTrailingWhitespace() + endWithNewline() + } + kotlinGradle { + target("*.gradle.kts") + ktfmt() + trimTrailingWhitespace() + endWithNewline() + } +} diff --git a/build-logic/src/main/kotlin/buildlogic.kotlin-fmt.gradle.kts b/build-logic/src/main/kotlin/buildlogic.kotlin-fmt.gradle.kts new file mode 100644 index 0000000..a719a07 --- /dev/null +++ b/build-logic/src/main/kotlin/buildlogic.kotlin-fmt.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("com.diffplug.spotless") +} + +spotless { + java { + target("src/**/*.java") + importOrder() + removeUnusedImports() + palantirJavaFormat() + } + + kotlin { + target("src/**/*.kt") + ktlint() + } +} diff --git a/build-logic/src/main/kotlin/buildlogic.kotlin-library-conventions.gradle.kts b/build-logic/src/main/kotlin/buildlogic.kotlin-library-conventions.gradle.kts new file mode 100644 index 0000000..a0091ef --- /dev/null +++ b/build-logic/src/main/kotlin/buildlogic.kotlin-library-conventions.gradle.kts @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id("buildlogic.kotlin-common-conventions") + + `java-library` +} diff --git a/build-logic/src/main/kotlin/buildlogic/GraalNativeImageTask.kt b/build-logic/src/main/kotlin/buildlogic/GraalNativeImageTask.kt new file mode 100644 index 0000000..a2db726 --- /dev/null +++ b/build-logic/src/main/kotlin/buildlogic/GraalNativeImageTask.kt @@ -0,0 +1,30 @@ +package buildlogic + +import javax.inject.Inject +import org.gradle.api.DefaultTask +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.jvm.toolchain.JvmVendorSpec +import org.gradle.process.ExecOperations + +abstract class GraalNativeImageTask @Inject constructor(private val toolchains: JavaToolchainService, private val exec: ExecOperations) : DefaultTask() { + @get:Classpath abstract val classpath: ListProperty + @get:OutputFile abstract val output: RegularFileProperty + abstract val mainClass: org.gradle.api.provider.Property + + @TaskAction fun build() { + val launcher = toolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + vendor.set(JvmVendorSpec.GRAAL_VM) + }.get() + val nativeImage = launcher.metadata.installationPath.file("bin/native-image").asFile + check(nativeImage.canExecute()) { "The configured Java 25 toolchain must be GraalVM with native-image installed" } + output.get().asFile.parentFile.mkdirs() + exec.exec { commandLine(nativeImage.absolutePath, "-cp", classpath.get().joinToString(java.io.File.pathSeparator), mainClass.get(), output.get().asFile.absolutePath) } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..005828e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.configuration-cache=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.java.installations.auto-download=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..d2f0631 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,8 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/version_catalogs.html#sec::toml-dependencies-format + +[versions] +kotlin-gradle-plugin = "2.4.0" + +[libraries] +kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..eddabd2 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ad7845b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..249efbb --- /dev/null +++ b/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# gradlew start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh gradlew +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..a51ec4f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem gradlew startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/node.yaml.example b/node.yaml.example new file mode 100644 index 0000000..0006903 --- /dev/null +++ b/node.yaml.example @@ -0,0 +1,7 @@ +remote: https://ai.crit.rip/automatedintelligence/geofeeds.git +branch: main +lfs: true +tainted: false +passwd: "set-through-secret-manager" +bootstrap: + - "peer.example.net:7400" diff --git a/packages/build.gradle.kts b/packages/build.gradle.kts new file mode 100644 index 0000000..2b76be9 --- /dev/null +++ b/packages/build.gradle.kts @@ -0,0 +1 @@ +plugins { base } diff --git a/packages/decentraland/build.gradle.kts b/packages/decentraland/build.gradle.kts new file mode 100644 index 0000000..06a2626 --- /dev/null +++ b/packages/decentraland/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("buildlogic.kotlin-library-conventions") +} + +dependencies { + api(project(":packages:serialize")) + api(project(":packages:decentraland:kademlia")) + api(project(":packages:decentraland:git")) + implementation("org.apache.sshd:sshd-core:2.15.0") +} diff --git a/packages/decentraland/chksum/build.gradle.kts b/packages/decentraland/chksum/build.gradle.kts new file mode 100644 index 0000000..8c88637 --- /dev/null +++ b/packages/decentraland/chksum/build.gradle.kts @@ -0,0 +1 @@ +plugins { id("buildlogic.kotlin-library-conventions") } diff --git a/packages/decentraland/chksum/src/main/kotlin/rip/crit/commonip/decentraland/chksum/ContentHash.kt b/packages/decentraland/chksum/src/main/kotlin/rip/crit/commonip/decentraland/chksum/ContentHash.kt new file mode 100644 index 0000000..f66033b --- /dev/null +++ b/packages/decentraland/chksum/src/main/kotlin/rip/crit/commonip/decentraland/chksum/ContentHash.kt @@ -0,0 +1,10 @@ +package rip.crit.commonip.decentraland.chksum + +import java.security.MessageDigest + +object ContentHash { + fun sha256(value: String): String = + MessageDigest.getInstance("SHA-256").digest(value.toByteArray()).joinToString("") { + "%02x".format(it) + } +} diff --git a/packages/decentraland/git/build.gradle.kts b/packages/decentraland/git/build.gradle.kts new file mode 100644 index 0000000..fb8e57a --- /dev/null +++ b/packages/decentraland/git/build.gradle.kts @@ -0,0 +1,3 @@ +plugins { id("buildlogic.kotlin-library-conventions") } + +dependencies { api(project(":packages:serialize")) } diff --git a/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/Provenance.kt b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/Provenance.kt new file mode 100644 index 0000000..c8a29ac --- /dev/null +++ b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/Provenance.kt @@ -0,0 +1,24 @@ +package rip.crit.commonip.decentraland.git + +import rip.crit.commonip.serialize.GeoLocation + +enum class Taint { + CLEAN, + TAINTED, +} + +data class ProvenancedObservation( + val location: GeoLocation, + val provider: String, + val taint: Taint, + val evidence: Set, +) + +object PromotionPolicy { + fun canPublishToCommunity(value: ProvenancedObservation): Boolean = + value.taint == Taint.CLEAN && value.evidence.size >= 2 + + fun destination(value: ProvenancedObservation): CommonIpRepository = + if (canPublishToCommunity(value)) CommonIpRepositories.community + else CommonIpRepositories.taintedFull +} diff --git a/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/Repository.kt b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/Repository.kt new file mode 100644 index 0000000..035b88d --- /dev/null +++ b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/Repository.kt @@ -0,0 +1,67 @@ +package rip.crit.commonip.decentraland.git + +import java.nio.file.Path + +enum class RepositoryTrust { + COMMUNITY, + TAINTED, +} + +data class CommonIpRepository( + val name: String, + val trust: RepositoryTrust, + val graph: RevisionGraph = RevisionGraph(), +) + +object CommonIpRepositories { + val community = CommonIpRepository("commonip/community", RepositoryTrust.COMMUNITY) + val taintedFull = CommonIpRepository("commonip/tainted-full", RepositoryTrust.TAINTED) +} + +data class NodeYaml( + val remote: String, + val branch: String = "main", + val lfs: Boolean = true, + val tainted: Boolean = false, + val passwd: String? = null, + val bootstrap: List = emptyList(), +) + +object NodeYamlReader { + fun parse(text: String): NodeYaml { + val values = + text + .lineSequence() + .map(String::trim) + .filter { ':' in it && !it.startsWith('#') } + .associate { + it.substringBefore(':').trim() to it.substringAfter(':').trim().trim('"', '\'') + } + val bootstrap = text.lineSequence().map(String::trim).filter { it.startsWith("-") }.map { it.removePrefix("-").trim().trim('"', '\'') }.toList() + return NodeYaml( + values["remote"] ?: error("node.yaml requires remote"), + values["branch"] ?: "main", + values["lfs"]?.toBooleanStrictOrNull() ?: true, + values["tainted"]?.toBooleanStrictOrNull() ?: false, + values["passwd"], + bootstrap, + ) + } +} + +class GitLfsSync(private val repository: Path) { + fun sync(config: NodeYaml): Boolean = + runCatching { + fun command(vararg args: String) = + ProcessBuilder(*args) + .directory(repository.toFile()) + .inheritIO() + .start() + .waitFor() == 0 + command("git", "remote", "get-url", "origin") || + command("git", "remote", "add", "origin", config.remote) + command("git", "fetch", "origin", config.branch) && + (!config.lfs || command("git", "lfs", "pull", "origin", config.branch)) + } + .getOrDefault(false) +} diff --git a/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/RevisionGraph.kt b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/RevisionGraph.kt new file mode 100644 index 0000000..54af434 --- /dev/null +++ b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/RevisionGraph.kt @@ -0,0 +1,99 @@ +package rip.crit.commonip.decentraland.git + +import java.time.Instant +import java.util.UUID +import rip.crit.commonip.serialize.GeoLocation + +data class GeoCommit( + val id: String = UUID.randomUUID().toString(), + val parents: Set, + val author: String, + val observation: GeoLocation, + val createdAt: Instant = Instant.now(), +) + +enum class MergeState { + FAST_FORWARD, + MERGED, + CONFLICT, +} + +data class MergeResult( + val state: MergeState, + val commit: GeoCommit?, + val competing: List, + val message: String, +) + +class RevisionGraph { + private val commits = linkedMapOf() + + fun import(commit: GeoCommit): Boolean { + if (commit.parents.any { it !in commits }) return false + commits.putIfAbsent(commit.id, commit) + return true + } + + fun commit(author: String, observation: GeoLocation, parents: Set = heads()): GeoCommit = + GeoCommit(parents = parents, author = author, observation = observation).also { + commits[it.id] = it + } + + fun heads(): Set = commits.keys - commits.values.flatMap { it.parents }.toSet() + + fun merge(left: String, right: String, author: String): MergeResult { + val a = + commits[left] + ?: return MergeResult( + MergeState.CONFLICT, + null, + emptyList(), + "left revision is unknown", + ) + val b = + commits[right] + ?: return MergeResult( + MergeState.CONFLICT, + null, + emptyList(), + "right revision is unknown", + ) + if (isAncestor(a.id, b.id)) + return MergeResult(MergeState.FAST_FORWARD, b, emptyList(), "left fast-forwards to right") + if (isAncestor(b.id, a.id)) + return MergeResult(MergeState.FAST_FORWARD, a, emptyList(), "right fast-forwards to left") + if (!compatible(a.observation, b.observation)) + return MergeResult( + MergeState.CONFLICT, + null, + listOf(a, b), + "country or coordinates disagree, proof-of-location required", + ) + val winner = listOf(a, b).maxBy { it.observation.confidence } + val merged = + GeoCommit( + parents = setOf(a.id, b.id), + author = author, + observation = + winner.observation.copy( + source = "merge:$author", + confidence = (a.observation.confidence + b.observation.confidence) / 2, + ), + ) + commits[merged.id] = merged + return MergeResult(MergeState.MERGED, merged, emptyList(), "two verified revisions merged") + } + + private fun isAncestor(ancestor: String, descendant: String): Boolean = + commits[descendant]?.parents?.let { parents -> + ancestor in parents || parents.any { isAncestor(ancestor, it) } + } ?: false + + private fun compatible(a: GeoLocation, b: GeoLocation): Boolean { + val aLat = a.latitude + val bLat = b.latitude + return a.prefix == b.prefix && + a.countryCode == b.countryCode && + (aLat == null || bLat == null || kotlin.math.abs(aLat - bLat) < 1.0) + } +} diff --git a/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/SignedCommitLedger.kt b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/SignedCommitLedger.kt new file mode 100644 index 0000000..d64f091 --- /dev/null +++ b/packages/decentraland/git/src/main/kotlin/rip/crit/commonip/decentraland/git/SignedCommitLedger.kt @@ -0,0 +1,72 @@ +package rip.crit.commonip.decentraland.git + +import java.nio.charset.StandardCharsets +import java.security.PrivateKey +import java.security.PublicKey +import java.security.Signature +import java.time.Instant +import java.util.Base64 + +data class SignedGeoCommit(val revision: GeoCommit, val signature: String) { + fun payload(): ByteArray = + listOf( + revision.id, + revision.parents.sorted().joinToString(","), + revision.author, + revision.observation.prefix, + revision.observation.countryCode, + revision.observation.latitude, + revision.observation.longitude, + revision.createdAt, + ) + .joinToString("|") + .toByteArray(StandardCharsets.UTF_8) +} + +object CommitSigner { + fun sign(revision: GeoCommit, key: PrivateKey): SignedGeoCommit { + val unsigned = SignedGeoCommit(revision, "") + val signature = + Signature.getInstance("Ed25519") + .apply { + initSign(key) + update(unsigned.payload()) + } + .sign() + return unsigned.copy(signature = Base64.getEncoder().encodeToString(signature)) + } + + fun verify(commit: SignedGeoCommit, key: PublicKey): Boolean = + runCatching { + Signature.getInstance("Ed25519") + .apply { + initVerify(key) + update(commit.payload()) + } + .verify(Base64.getDecoder().decode(commit.signature)) + } + .getOrDefault(false) +} + +data class ProofOfLocation( + val revisionId: String, + val observer: String, + val observedAt: Instant, + val reachable: Boolean, + val latencyMs: Long?, + val signature: String, +) + +class SignedCommitLedger( + private val graph: RevisionGraph, + private val keys: Map, +) { + fun accept(commit: SignedGeoCommit): Boolean = + (keys[commit.revision.author]?.let { CommitSigner.verify(commit, it) } ?: false) && + graph.import(commit.revision) + + fun resolve(left: SignedGeoCommit, right: SignedGeoCommit, author: String): MergeResult { + require(accept(left) && accept(right)) { "all merge inputs must have valid signatures" } + return graph.merge(left.revision.id, right.revision.id, author) + } +} diff --git a/packages/decentraland/git/src/test/kotlin/rip/crit/commonip/decentraland/git/RepositoryTest.kt b/packages/decentraland/git/src/test/kotlin/rip/crit/commonip/decentraland/git/RepositoryTest.kt new file mode 100644 index 0000000..420d332 --- /dev/null +++ b/packages/decentraland/git/src/test/kotlin/rip/crit/commonip/decentraland/git/RepositoryTest.kt @@ -0,0 +1,32 @@ +package rip.crit.commonip.decentraland.git + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import rip.crit.commonip.serialize.GeoLocation + +class RepositoryTest { + @Test + fun `node yaml retains trust and password fields`() { + val config = NodeYamlReader.parse("remote: ssh://example/repo\ntainted: true\npasswd: secret\nbootstrap:\n - peer.example:7400") + assertTrue(config.tainted) + assertEquals("secret", config.passwd) + assertEquals(listOf("peer.example:7400"), config.bootstrap) + } + + @Test + fun `community promotion needs clean independent evidence`() { + val location = GeoLocation("192.0.2.0/24", "US") + assertFalse( + PromotionPolicy.canPublishToCommunity( + ProvenancedObservation(location, "test", Taint.CLEAN, setOf("one")) + ) + ) + assertTrue( + PromotionPolicy.canPublishToCommunity( + ProvenancedObservation(location, "test", Taint.CLEAN, setOf("one", "two")) + ) + ) + } +} diff --git a/packages/decentraland/kademlia/build.gradle.kts b/packages/decentraland/kademlia/build.gradle.kts new file mode 100644 index 0000000..fc8115b --- /dev/null +++ b/packages/decentraland/kademlia/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { id("buildlogic.kotlin-library-conventions") } + +dependencies { + api(project(":packages:serialize")) + implementation("io.ep2p:kademlia-api:5.1.2-RELEASE") + implementation("io.ep2p:kademlia-netty:0.3.2-RELEASE") +} diff --git a/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/KademliaNode.kt b/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/KademliaNode.kt new file mode 100644 index 0000000..7d5a90e --- /dev/null +++ b/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/KademliaNode.kt @@ -0,0 +1,111 @@ +package rip.crit.commonip.decentraland.kademlia + +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetSocketAddress +import java.security.MessageDigest +import java.util.concurrent.ConcurrentHashMap +import kotlin.concurrent.thread + +data class KademliaPeer(val id: String, val address: InetSocketAddress) + +class KademliaNode(private val bind: InetSocketAddress, private val registry: NodeRegistry) : + AutoCloseable { + val id = sha256("${bind.hostString}:${bind.port}") + private val namer = NodeNamer() + private val peers = ConcurrentHashMap() + private val values = ConcurrentHashMap() + private val socket = DatagramSocket(bind) + private val lastOutbound = ConcurrentHashMap() + @Volatile private var running = true + + fun start() = + thread(name = "commonip-kademlia-$id", isDaemon = true) { + registry.register( + NetworkNode( + namer.name(id), + socket.localSocketAddress.toString(), + NodeState.ONLINE, + 0, + values.size, + ) + ) + val bytes = ByteArray(8192) + while (running) runCatching { + val packet = DatagramPacket(bytes, bytes.size) + socket.receive(packet) + handle(packet) + } + } + + fun bootstrap(seed: InetSocketAddress) { + if (peers.values.none { it.address == seed }) send(seed, "PING|$id|${bind.hostString}|${socket.localPort}") + } + + fun store(key: String, value: String) { + values[key] = value + closest(key).forEach { send(it.address, "STORE|$id|$key|${value.encodeBase64()}") } + } + + fun findValue(key: String): String? = values[key] + + fun closest(key: String, count: Int = 20): List = + peers.values.sortedBy { xorDistance(it.id, key) }.take(count) + + private fun handle(packet: DatagramPacket) { + val data = + packet.data + .copyOfRange(packet.offset, packet.offset + packet.length) + .decodeToString() + .split('|', limit = 5) + val remote = InetSocketAddress(packet.address, packet.port) + when (data.firstOrNull()) { + "PING" -> { + peers[data[1]] = KademliaPeer(data[1], InetSocketAddress(data[2], data[3].toInt())) + val name = namer.name(data[1]) + registry.register(NetworkNode(name, remote.toString(), NodeState.ONLINE, null, 0)) + registry.publish(name, "kademlia ping", remote.toString()) + send(remote, "PONG|$id") + } + "PONG" -> { + peers[data[1]] = KademliaPeer(data[1], remote) + val name = namer.name(data[1]) + registry.register(NetworkNode(name, remote.toString(), NodeState.ONLINE, null, 0)) + registry.publish(name, "kademlia bootstrap confirmed", remote.toString()) + } + "STORE" -> values[data[2]] = data[3].decodeBase64() + "FIND_VALUE" -> + values[data[2]]?.let { send(remote, "VALUE|$id|${data[2]}|${it.encodeBase64()}") } + "FIND_NODE" -> + send( + remote, + "NODES|$id|${closest(data[2]).joinToString(";") { "${it.id}@${it.address.hostString}:${it.address.port}" }}", + ) + } + } + + private fun send(target: InetSocketAddress, message: String) { + val key = "${target.hostString}:${target.port}:${message.substringBefore('|')}" + val previous = lastOutbound.put(key, System.currentTimeMillis()) ?: 0 + if (System.currentTimeMillis() - previous < 500) return + socket.send(DatagramPacket(message.encodeToByteArray(), message.length, target)) + } + + override fun close() { + running = false + socket.close() + } + + private fun sha256(value: String) = + MessageDigest.getInstance("SHA-256").digest(value.encodeToByteArray()).joinToString("") { + "%02x".format(it) + } + + private fun xorDistance(left: String, right: String) = + left.zip(right.padEnd(left.length, '0')).count { it.first != it.second } + + private fun String.encodeBase64() = + java.util.Base64.getUrlEncoder().encodeToString(encodeToByteArray()) + + private fun String.decodeBase64() = java.util.Base64.getUrlDecoder().decode(this).decodeToString() +} diff --git a/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/NodeNamer.kt b/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/NodeNamer.kt new file mode 100644 index 0000000..7d44adf --- /dev/null +++ b/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/NodeNamer.kt @@ -0,0 +1,25 @@ +package rip.crit.commonip.decentraland.kademlia + +import java.util.concurrent.ConcurrentHashMap + +class NodeNamer { + private val votes = ConcurrentHashMap>>() + + fun observe(nodeId: String, observerId: String, alias: String) { + votes + .getOrPut(nodeId) { ConcurrentHashMap() } + .getOrPut(alias) { ConcurrentHashMap.newKeySet() } + .add(observerId) + } + + fun name(nodeId: String): String = + votes[nodeId]?.maxByOrNull { it.value.size }?.takeIf { it.value.size >= 2 }?.key + ?: deterministic(nodeId) + + private fun deterministic(id: String): String { + val adjective = arrayOf("amber", "cedar", "delta", "ember", "flint", "grove", "harbor", "iris") + val noun = arrayOf("atlas", "beacon", "cinder", "drift", "echo", "forge", "glade", "horizon") + val number = id.take(8).toLong(16) + return "${adjective[(number % adjective.size).toInt()]}-${noun[((number / adjective.size) % noun.size).toInt()]}-${id.takeLast(4)}" + } +} diff --git a/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/NodeRegistry.kt b/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/NodeRegistry.kt new file mode 100644 index 0000000..821bf65 --- /dev/null +++ b/packages/decentraland/kademlia/src/main/kotlin/rip/crit/commonip/decentraland/kademlia/NodeRegistry.kt @@ -0,0 +1,58 @@ +package rip.crit.commonip.decentraland.kademlia + +import java.time.Instant +import java.util.concurrent.CopyOnWriteArrayList + +enum class NodeState { + ONLINE, + DEGRADED, + OFFLINE, +} + +data class NetworkNode( + val id: String, + val endpoint: String, + val state: NodeState, + val latencyMs: Long?, + val records: Int, + val lastSeen: Instant = Instant.now(), +) + +data class NetworkActivity( + val at: Instant, + val nodeId: String, + val action: String, + val detail: String, +) + +class NodeRegistry { + private val nodes = linkedMapOf() + private val activity = CopyOnWriteArrayList() + + @Synchronized + fun register(node: NetworkNode) { + nodes[node.id] = node + publish(node.id, "peer announced", "${node.endpoint} is ${node.state.name.lowercase()}") + } + + @Synchronized + fun update(id: String, state: NodeState, latencyMs: Long? = null, records: Int? = null) { + val prior = nodes[id] ?: return + nodes[id] = + prior.copy( + state = state, + latencyMs = latencyMs ?: prior.latencyMs, + records = records ?: prior.records, + lastSeen = Instant.now(), + ) + publish(id, "status changed", state.name.lowercase()) + } + + fun snapshot(): List = synchronized(this) { nodes.values.sortedBy { it.id } } + + fun feed(limit: Int = 12): List = activity.takeLast(limit).reversed() + + fun publish(nodeId: String, action: String, detail: String) { + activity += NetworkActivity(Instant.now(), nodeId, action, detail) + } +} diff --git a/packages/decentraland/kademlia/src/test/kotlin/rip/crit/commonip/decentraland/kademlia/NodeNamerTest.kt b/packages/decentraland/kademlia/src/test/kotlin/rip/crit/commonip/decentraland/kademlia/NodeNamerTest.kt new file mode 100644 index 0000000..2cb6543 --- /dev/null +++ b/packages/decentraland/kademlia/src/test/kotlin/rip/crit/commonip/decentraland/kademlia/NodeNamerTest.kt @@ -0,0 +1,14 @@ +package rip.crit.commonip.decentraland.kademlia + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class NodeNamerTest { + @Test + fun `two peer votes override deterministic node name`() { + val namer = NodeNamer() + namer.observe("abcdef012345", "peer-a", "north") + namer.observe("abcdef012345", "peer-b", "north") + assertEquals("north", namer.name("abcdef012345")) + } +} diff --git a/packages/decentraland/settings.gradle.kts b/packages/decentraland/settings.gradle.kts new file mode 100644 index 0000000..8e65ae0 --- /dev/null +++ b/packages/decentraland/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "commonip-decentraland" + +include("kademlia", "git", "chksum") diff --git a/packages/decentraland/src/main/kotlin/rip/crit/commonip/decentraland/SshDashboardService.kt b/packages/decentraland/src/main/kotlin/rip/crit/commonip/decentraland/SshDashboardService.kt new file mode 100644 index 0000000..0e6171e --- /dev/null +++ b/packages/decentraland/src/main/kotlin/rip/crit/commonip/decentraland/SshDashboardService.kt @@ -0,0 +1,33 @@ +package rip.crit.commonip.decentraland + +import java.nio.file.Path +import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory +import org.apache.sshd.server.SshServer +import org.apache.sshd.server.auth.password.PasswordAuthenticator +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider + +class SshDashboardService( + port: Int, + hostKey: Path, + username: String, + password: CharArray, + private val dashboard: () -> String, +) : AutoCloseable { + private val server = + SshServer.setUpDefaultServer().apply { + this.port = port + keyPairProvider = SimpleGeneratorHostKeyProvider(hostKey) + passwordAuthenticator = PasswordAuthenticator { user, candidate, _ -> + user == username && candidate.toCharArray().contentEquals(password) + } + fileSystemFactory = VirtualFileSystemFactory() + } + + fun start() { + server.start() + } + + fun render(): String = dashboard() + + override fun close() = server.close() +} diff --git a/packages/ml/build.gradle.kts b/packages/ml/build.gradle.kts new file mode 100644 index 0000000..15866f9 --- /dev/null +++ b/packages/ml/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id("buildlogic.kotlin-library-conventions") +} + +dependencies { + api(project(":packages:serialize")) +} diff --git a/packages/ml/src/main/kotlin/rip/crit/commonip/ml/LocationEstimator.kt b/packages/ml/src/main/kotlin/rip/crit/commonip/ml/LocationEstimator.kt new file mode 100644 index 0000000..4ef43ea --- /dev/null +++ b/packages/ml/src/main/kotlin/rip/crit/commonip/ml/LocationEstimator.kt @@ -0,0 +1,235 @@ +package rip.crit.commonip.ml + +import kotlin.math.* + +data class GeoLocation( + val latitude: Double?, + val longitude: Double?, + val confidence: Double = 1.0, +) + +data class LocationEstimate( + val latitude: Double, + val longitude: Double, + val confidence: Double, + val sampleCount: Int, + val discardedCount: Int, + val spatialSpreadKm: Double, +) + +data class Vector3D(val x: Double, val y: Double, val z: Double) { + fun normalize(): Vector3D { + val mag = sqrt(x * x + y * y + z * z) + return if (mag == 0.0) Vector3D(0.0, 0.0, 0.0) else Vector3D(x / mag, y / mag, z / mag) + } + + fun toGeoLocation(confidence: Double = 1.0): GeoLocation { + val norm = this.normalize() + val lat = Math.toDegrees(asin(norm.z.coerceIn(-1.0, 1.0))) + val lon = Math.toDegrees(atan2(norm.y, norm.x)) + return GeoLocation(lat, lon, confidence) + } +} + +fun GeoLocation.toVector3D(): Vector3D { + val latRad = Math.toRadians(this.latitude!!) + val lonRad = Math.toRadians(this.longitude!!) + return Vector3D( + x = cos(latRad) * cos(lonRad), + y = cos(latRad) * sin(lonRad), + z = sin(latRad), + ) +} + +class LocationEstimator( + private val dbscanEpsKm: Double = 250.0, + private val dbscanMinPts: Int = 2, + private val medianIterations: Int = 20, +) { + + fun estimate(observations: List): LocationEstimate? { + val validPoints = observations.filter { it.latitude != null && it.longitude != null } + if (validPoints.isEmpty()) return null + + val clusteredPoints = dbscanCluster(validPoints, dbscanEpsKm, dbscanMinPts) + val selected = clusteredPoints.ifEmpty { validPoints } + + val (estLat, estLon) = computeGeometricMedian(selected, medianIterations) + + val distances = selected.map { distanceKm(estLat, estLon, it.latitude!!, it.longitude!!) } + val standardDistanceKm = sqrt(distances.sumOf { it * it } / selected.size) + + val meanSampleConfidence = selected.map { max(it.confidence, 0.01) }.average() + val densityFactor = 1.0 / (1.0 + exp(-(selected.size - 3.0) / 2.0)) + val spreadPenalty = 1.0 / (1.0 + (standardDistanceKm / 300.0)) + + val overallConfidence = + (meanSampleConfidence * densityFactor * spreadPenalty).coerceIn(0.0, 1.0) + + return LocationEstimate( + latitude = estLat, + longitude = estLon, + confidence = overallConfidence, + sampleCount = selected.size, + discardedCount = validPoints.size - selected.size, + spatialSpreadKm = standardDistanceKm, + ) + } + + /** Finds the primary spatial cluster using DBSCAN logic. */ + private fun dbscanCluster( + points: List, + epsKm: Double, + minPts: Int, + ): List { + val visited = BooleanArray(points.size) + val clusters = mutableListOf>() + + for (i in points.indices) { + if (visited[i]) continue + visited[i] = true + + val neighbors = getNeighbors(points, i, epsKm) + if (neighbors.size >= minPts) { + val currentCluster = mutableListOf(points[i]) + val queue = neighbors.toMutableList() + var qIndex = 0 + + while (qIndex < queue.size) { + val pIndex = queue[qIndex++] + if (!visited[pIndex]) { + visited[pIndex] = true + val subNeighbors = getNeighbors(points, pIndex, epsKm) + if (subNeighbors.size >= minPts) { + queue.addAll(subNeighbors.filter { !queue.contains(it) }) + } + } + if (!currentCluster.contains(points[pIndex])) { + currentCluster.add(points[pIndex]) + } + } + clusters.add(currentCluster) + } + } + + return clusters.maxByOrNull { cluster -> cluster.sumOf { max(it.confidence, 0.05) } } + ?: emptyList() + } + + private fun getNeighbors(points: List, centerIdx: Int, epsKm: Double): List { + val center = points[centerIdx] + val result = mutableListOf() + for (i in points.indices) { + if ( + distanceKm( + center.latitude!!, + center.longitude!!, + points[i].latitude!!, + points[i].longitude!!, + ) <= epsKm + ) { + result.add(i) + } + } + return result + } + + /** Weiszfeld's algorithm for finding the spatial geometric median (L1 median). */ + private fun computeGeometricMedian( + points: List, + iterations: Int, + ): Pair { + if (points.size == 1) return Pair(points[0].latitude!!, points[0].longitude!!) + + val weights = points.map { max(it.confidence, 0.05) } + val sumWeight = weights.sum() + + var currentVector = + points + .zip(weights) + .map { (pt, w) -> + val vec = pt.toVector3D() + Vector3D(vec.x * w, vec.y * w, vec.z * w) + } + .reduce { acc, v -> + Vector3D(acc.x + v.x, acc.y + v.y, acc.z + v.z) + } + .let { Vector3D(it.x / sumWeight, it.y / sumWeight, it.z / sumWeight).normalize() } + + var currentGeo = currentVector.toGeoLocation() + + repeat(iterations) { + var sumX = 0.0 + var sumY = 0.0 + var sumZ = 0.0 + var totalW = 0.0 + + for (i in points.indices) { + val ptVec = points[i].toVector3D() + val dist = + max( + distanceKm( + currentGeo.latitude!!, + currentGeo.longitude!!, + points[i].latitude!!, + points[i].longitude!!, + ), + 0.1, + ) + val invWeight = weights[i] / dist + + sumX += ptVec.x * invWeight + sumY += ptVec.y * invWeight + sumZ += ptVec.z * invWeight + totalW += invWeight + } + + if (totalW > 0.0) { + currentVector = Vector3D(sumX / totalW, sumY / totalW, sumZ / totalW).normalize() + currentGeo = currentVector.toGeoLocation() + } + } + + return Pair(currentGeo.latitude!!, currentGeo.longitude!!) + } + + companion object { + fun distanceKm(aLat: Double, aLon: Double, bLat: Double, bLon: Double): Double { + val dLat = Math.toRadians(bLat - aLat) + val dLon = Math.toRadians(bLon - aLon) + val h = + sin(dLat / 2).pow(2) + + cos(Math.toRadians(aLat)) * cos(Math.toRadians(bLat)) * sin(dLon / 2).pow(2) + return 6371.0 * 2 * atan2(sqrt(h), sqrt(1 - h)) + } + } +} + +data class NodeTarget( + val nodeId: String, + val capacity: Int, + val latitude: Double?, + val longitude: Double?, +) + +object LocationDispatcher { + fun select( + nodes: List, + estimate: LocationEstimate, + count: Int = 3, + ): List = + nodes + .filter { it.capacity > 0 && it.latitude != null && it.longitude != null } + .sortedBy { node -> + val distance = + LocationEstimator.distanceKm( + estimate.latitude, + estimate.longitude, + node.latitude!!, + node.longitude!!, + ) + + distance / sqrt(node.capacity.toDouble()) + } + .take(count) +} diff --git a/packages/ml/src/test/kotlin/rip/crit/commonip/ml/LocationEstimatorTest.kt b/packages/ml/src/test/kotlin/rip/crit/commonip/ml/LocationEstimatorTest.kt new file mode 100644 index 0000000..929a990 --- /dev/null +++ b/packages/ml/src/test/kotlin/rip/crit/commonip/ml/LocationEstimatorTest.kt @@ -0,0 +1,20 @@ +package rip.crit.commonip.ml + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class LocationEstimatorTest { + @Test + fun `estimator rejects distant outlier`() { + val result = + LocationEstimator() + .estimate( + listOf( + GeoLocation(40.0, -75.0, 0.9), + GeoLocation(40.1, -75.1, 0.9), + GeoLocation(-33.0, 151.0, 0.9), + ) + )!! + assertEquals(1, result.discardedCount) + } +} diff --git a/packages/rawr/build.gradle.kts b/packages/rawr/build.gradle.kts new file mode 100644 index 0000000..15866f9 --- /dev/null +++ b/packages/rawr/build.gradle.kts @@ -0,0 +1,13 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id("buildlogic.kotlin-library-conventions") +} + +dependencies { + api(project(":packages:serialize")) +} diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RFC8805.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RFC8805.kt new file mode 100644 index 0000000..5d113a1 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RFC8805.kt @@ -0,0 +1,9 @@ +package rip.crit.commonip.rawr.feeds + +import rip.crit.commonip.serialize.GeoFeed +import rip.crit.commonip.serialize.GeoFeedCodec +import rip.crit.commonip.serialize.GeoFeedFormat + +object RFC8805 { + fun parse(csv: String): GeoFeed = GeoFeedCodec.decode(csv, GeoFeedFormat.CSV) +} diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RFC9632.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RFC9632.kt new file mode 100644 index 0000000..f3e6acc --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RFC9632.kt @@ -0,0 +1,20 @@ +package rip.crit.commonip.rawr.feeds + +data class RFC9632( + val feedUrl: String, + val signatureUrl: String?, + val valid: Boolean, + val signer: String? = null, +) { + companion object { + fun fromDnsRecord(record: String): RFC9632? { + val values = + record + .split(';') + .map { it.trim() } + .associate { it.substringBefore('=').lowercase() to it.substringAfter('=', "") } + val url = values["geofeed"] ?: return null + return RFC9632(url, values["sig"], values["valid"] != "false", values["signer"]) + } + } +} diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RirFeed.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RirFeed.kt new file mode 100644 index 0000000..80404c8 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/feeds/RirFeed.kt @@ -0,0 +1,37 @@ +package rip.crit.commonip.rawr.feeds + +import rip.crit.commonip.rawr.types.Ingest +import rip.crit.commonip.serialize.GeoFeed +import rip.crit.commonip.serialize.GeoFeedCodec +import rip.crit.commonip.serialize.GeoFeedFormat +import rip.crit.commonip.serialize.GeoLocation + +class RirFeed(private val name: String, private val loader: () -> String) : Ingest { + override fun ingest(): GeoFeed = + GeoFeedCodec.decode(loader(), GeoFeedFormat.CSV).let { feed -> + feed.copy(records = feed.records.map { it.copy(source = name) }) + } +} + +object DelegatedStats { + fun parse(payload: String, source: String): GeoFeed = + GeoFeed( + payload + .lineSequence() + .filter { it.isNotBlank() && !it.startsWith('#') && !it.startsWith("2|") } + .mapNotNull { line -> + val f = line.split('|') + if (f.size < 5 || f[2].lowercase() !in setOf("ipv4", "ipv6") || f[1] == "*") null + else { + val prefix = + if (f[2].lowercase() == "ipv4") "${f[3]}/${prefixFromCount(f[4])}" + else "${f[3]}/${f.getOrElse(4) { "48" }}" + GeoLocation(prefix, f[1].uppercase(), source = source, confidence = 0.25) + } + } + .toList() + ) + + private fun prefixFromCount(count: String): Int = + 32 - Integer.numberOfTrailingZeros(count.toIntOrNull() ?: 1) +} diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/AFRINIC.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/AFRINIC.kt new file mode 100644 index 0000000..0f995d7 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/AFRINIC.kt @@ -0,0 +1,8 @@ +package rip.crit.commonip.rawr.providers + +class AFRINIC(loader: (() -> String)? = null) : + RirProvider( + "AFRINIC", + "https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest", + loader, + ) diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/APNIC.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/APNIC.kt new file mode 100644 index 0000000..315243f --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/APNIC.kt @@ -0,0 +1,8 @@ +package rip.crit.commonip.rawr.providers + +class APNIC(loader: (() -> String)? = null) : + RirProvider( + "APNIC", + "https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest", + loader, + ) diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/ARIN.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/ARIN.kt new file mode 100644 index 0000000..ccf8521 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/ARIN.kt @@ -0,0 +1,8 @@ +package rip.crit.commonip.rawr.providers + +class ARINIngest(loader: (() -> String)? = null) : + RirProvider( + "ARIN", + "https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest", + loader, + ) diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/LACNIC.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/LACNIC.kt new file mode 100644 index 0000000..0dec57e --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/LACNIC.kt @@ -0,0 +1,8 @@ +package rip.crit.commonip.rawr.providers + +class LACNIC(loader: (() -> String)? = null) : + RirProvider( + "LACNIC", + "https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest", + loader, + ) diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RIPENCC.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RIPENCC.kt new file mode 100644 index 0000000..1456ec7 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RIPENCC.kt @@ -0,0 +1,8 @@ +package rip.crit.commonip.rawr.providers + +class RIPENCC(loader: (() -> String)? = null) : + RirProvider( + "RIPE NCC", + "https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-extended-latest", + loader, + ) diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RirGeoFeedLoader.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RirGeoFeedLoader.kt new file mode 100644 index 0000000..1eecca9 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RirGeoFeedLoader.kt @@ -0,0 +1,49 @@ +package rip.crit.commonip.rawr.providers + +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import rip.crit.commonip.serialize.GeoFeed +import rip.crit.commonip.serialize.GeoFeedCodec +import rip.crit.commonip.serialize.GeoFeedFormat + +enum class RirMetadataField(val keys: Set) { + REMARKS(setOf("remarks", "remark")), + COMMENT(setOf("comment", "comments")), +} + +class RirGeoFeedLoader( + private val source: String, + private val acceptedFields: Set, + private val fetch: (String) -> String = RirGeoFeedLoader::httpGet, +) { + fun urls(registryObject: String): Set = + registryObject + .lineSequence() + .map(String::trim) + .filter { ':' in it } + .filter { line -> + acceptedFields.any { field -> + line.substringBefore(':').trim().lowercase() in field.keys + } + } + .flatMap { urlPattern.findAll(it).map { match -> match.value } } + .toSet() + + fun pull(registryObject: String): List = + urls(registryObject).map { url -> GeoFeedCodec.decode(fetch(url), GeoFeedFormat.CSV) } + + private companion object { + val urlPattern = Regex("https?://[^\\s,;\"]+") + val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build() + + fun httpGet(url: String): String = + client + .send( + HttpRequest.newBuilder(URI(url)).GET().build(), + HttpResponse.BodyHandlers.ofString(), + ) + .body() + } +} diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RirProvider.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RirProvider.kt new file mode 100644 index 0000000..3f11e08 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/providers/RirProvider.kt @@ -0,0 +1,31 @@ +package rip.crit.commonip.rawr.providers + +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import rip.crit.commonip.rawr.feeds.DelegatedStats +import rip.crit.commonip.rawr.types.Ingest +import rip.crit.commonip.serialize.GeoFeed + +abstract class RirProvider( + private val name: String, + private val endpoint: String, + private val loader: (() -> String)? = null, +) : Ingest { + override fun ingest(): GeoFeed = + DelegatedStats.parse( + loader?.invoke() + ?: client + .send( + HttpRequest.newBuilder(URI(endpoint)).GET().build(), + HttpResponse.BodyHandlers.ofString(), + ) + .body(), + name, + ) + + private companion object { + val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build() + } +} diff --git a/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/types/Ingest.kt b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/types/Ingest.kt new file mode 100644 index 0000000..5446215 --- /dev/null +++ b/packages/rawr/src/main/kotlin/rip/crit/commonip/rawr/types/Ingest.kt @@ -0,0 +1,36 @@ +package rip.crit.commonip.rawr.types + +import rip.crit.commonip.serialize.GeoFeed +import rip.crit.commonip.serialize.GeoLocation + +interface Ingest { + fun ingest(): GeoFeed + + fun lookupOne(address: String): GeoLocation? = + ingest() + .records + .filter { Cidr.contains(it.prefix, address) } + .maxByOrNull { Cidr.prefixLength(it.prefix) } +} + +object Cidr { + fun prefixLength(cidr: String): Int = cidr.substringAfter('/').toIntOrNull() ?: -1 + + fun contains(cidr: String, address: String): Boolean { + val parts = cidr.split('/', limit = 2) + val bits = parts.getOrNull(1)?.toIntOrNull() ?: return false + if (bits !in 0..32) return false + val network = ipv4(parts[0]) ?: return false + val target = ipv4(address) ?: return false + val mask = if (bits == 0) 0 else (-1 shl (32 - bits)) + return network and mask == target and mask + } + + private fun ipv4(value: String): Int? { + val fields = value.split('.') + if (fields.size != 4) return null + return fields.fold(0) { result, field -> + field.toIntOrNull()?.takeIf { it in 0..255 }?.let { (result shl 8) or it } ?: return null + } + } +} diff --git a/packages/rawr/src/test/kotlin/rip/crit/commonip/rawr/providers/RirProviderTest.kt b/packages/rawr/src/test/kotlin/rip/crit/commonip/rawr/providers/RirProviderTest.kt new file mode 100644 index 0000000..8dbb56d --- /dev/null +++ b/packages/rawr/src/test/kotlin/rip/crit/commonip/rawr/providers/RirProviderTest.kt @@ -0,0 +1,13 @@ +package rip.crit.commonip.rawr.providers + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class RirProviderTest { + @Test + fun `arin parses delegated ipv4 fixture`() { + val feed = ARINIngest { "arin|US|ipv4|192.0.2.0|256|20260101|allocated" }.ingest() + assertEquals("192.0.2.0/24", feed.records.single().prefix) + assertEquals("US", feed.records.single().countryCode) + } +} diff --git a/packages/serialize/build.gradle.kts b/packages/serialize/build.gradle.kts new file mode 100644 index 0000000..60237da --- /dev/null +++ b/packages/serialize/build.gradle.kts @@ -0,0 +1,9 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + id("buildlogic.kotlin-library-conventions") +} diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/Deserialize.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/Deserialize.kt new file mode 100644 index 0000000..ba66dd4 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/Deserialize.kt @@ -0,0 +1,3 @@ +package rip.crit.commonip.serialize + +fun String.deserializeGeoFeed(format: GeoFeedFormat): GeoFeed = GeoFeedCodec.decode(this, format) diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/GeoFeed.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/GeoFeed.kt new file mode 100644 index 0000000..116fd70 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/GeoFeed.kt @@ -0,0 +1,34 @@ +package rip.crit.commonip.serialize + +import java.time.Instant + +data class GeoLocation( + val prefix: String, + val countryCode: String, + val region: String? = null, + val city: String? = null, + val postalCode: String? = null, + val latitude: Double? = null, + val longitude: Double? = null, + val source: String = "unknown", + val observedAt: Instant = Instant.now(), + val confidence: Double = 0.5, +) { + init { + require('/' in prefix) { "prefix must be CIDR notation" } + require(countryCode.length == 2) { "countryCode must be ISO 3166-1 alpha-2" } + require(confidence in 0.0..1.0) { "confidence must be between 0 and 1" } + } +} + +data class GeoFeed( + val records: List, + val generatedAt: Instant = Instant.now(), + val schemaVersion: String = "commonip/geofeed/v1", +) + +enum class GeoFeedFormat { + CSV, + JSON, + XML, +} diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/GeoFeedCodec.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/GeoFeedCodec.kt new file mode 100644 index 0000000..5d16793 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/GeoFeedCodec.kt @@ -0,0 +1,158 @@ +package rip.crit.commonip.serialize + +import java.time.Instant + +object GeoFeedCodec { + fun encode(feed: GeoFeed, format: GeoFeedFormat): String = + when (format) { + GeoFeedFormat.CSV -> + feed.records.joinToString("\n") { record -> + listOf( + record.prefix, + record.countryCode, + record.region.orEmpty(), + record.city.orEmpty(), + record.postalCode.orEmpty(), + ) + .joinToString(",") { csv(it) } + } + GeoFeedFormat.JSON -> + "{\"schema\":\"${feed.schemaVersion}\",\"generatedAt\":\"${feed.generatedAt}\",\"records\":[" + + feed.records.joinToString(",") { jsonRecord(it) } + + "]}" + GeoFeedFormat.XML -> + "" + + feed.records.joinToString("") { record -> + "" + } + + "" + } + + fun decode(payload: String, format: GeoFeedFormat): GeoFeed = + when (format) { + GeoFeedFormat.CSV -> + GeoFeed( + payload + .lineSequence() + .filter { it.isNotBlank() && !it.trimStart().startsWith("#") } + .map { csvRecord(it) } + .toList() + ) + GeoFeedFormat.JSON -> + GeoFeed( + jsonObjects( + payload.substringAfter("\"records\":[", "").substringBeforeLast("]", "") + ) + .map { jsonToRecord(it) } + ) + GeoFeedFormat.XML -> + GeoFeed( + Regex("]+)/>") + .findAll(payload) + .map { xmlToRecord(it.groupValues[1]) } + .toList() + ) + } + + private fun csvRecord(line: String): GeoLocation { + val fields = splitCsv(line) + require(fields.size >= 2) { "RFC 8805 record needs a prefix and country" } + return GeoLocation( + fields[0], + fields[1].uppercase(), + fields.getOrNull(2).blankToNull(), + fields.getOrNull(3).blankToNull(), + fields.getOrNull(4).blankToNull(), + source = "rfc8805", + ) + } + + private fun jsonRecord(r: GeoLocation) = + "{" + + listOf( + "prefix" to r.prefix, + "countryCode" to r.countryCode, + "region" to r.region, + "city" to r.city, + "postalCode" to r.postalCode, + "latitude" to r.latitude, + "longitude" to r.longitude, + "source" to r.source, + "observedAt" to r.observedAt.toString(), + "confidence" to r.confidence, + ) + .joinToString(",") { (k, v) -> + "\"$k\":" + + when (v) { + null -> "null" + is Number -> v.toString() + else -> "\"${json(v.toString())}\"" + } + } + + "}" + + private fun jsonToRecord(obj: String): GeoLocation { + fun value(name: String) = + Regex("\"$name\"\\s*:\\s*(null|\"((?:\\\\.|[^\"])*)\"|[-.0-9]+)").find(obj)?.let { + if (it.groupValues[1] == "null") null + else + it.groupValues[2] + .ifEmpty { it.groupValues[1] } + .replace("\\\"", "\"") + .replace("\\\\", "\\") + } + return GeoLocation( + value("prefix") ?: error("JSON record missing prefix"), + value("countryCode") ?: error("JSON record missing countryCode"), + value("region"), + value("city"), + value("postalCode"), + value("latitude")?.toDouble(), + value("longitude")?.toDouble(), + value("source") ?: "commonip", + value("observedAt")?.let(Instant::parse) ?: Instant.now(), + value("confidence")?.toDouble() ?: 0.5, + ) + } + + private fun xmlToRecord(attrs: String): GeoLocation { + fun value(name: String) = + Regex("$name=\"([^\"]*)\"").find(attrs)?.groupValues?.get(1)?.let { + it.replace(""", "\"").replace("&", "&") + } + return GeoLocation( + value("prefix") ?: error("XML record missing prefix"), + value("country") ?: error("XML record missing country"), + value("region"), + value("city"), + value("postalCode"), + value("latitude")?.toDouble(), + value("longitude")?.toDouble(), + value("source") ?: "commonip", + value("observedAt")?.let(Instant::parse) ?: Instant.now(), + value("confidence")?.toDouble() ?: 0.5, + ) + } + + private fun jsonObjects(raw: String): List = + Regex("\\{(?:[^{}]|\\{[^{}]*})*}").findAll(raw).map { it.value }.toList() + + private fun splitCsv(line: String): List = + Regex("(?:^|,)(\"(?:[^\"]|\"\")*\"|[^,]*)") + .findAll(line) + .map { it.groupValues[1].removeSurrounding("\"").replace("\"\"", "\"") } + .toList() + + private fun csv(value: String) = + if (value.any { it == ',' || it == '"' || it == '\n' }) "\"${value.replace("\"", "\"\"")}\"" + else value + + private fun json(value: String) = value.replace("\\", "\\\\").replace("\"", "\\\"") + + private fun xml(value: String) = value.replace("&", "&").replace("\"", """) + + private fun attr(name: String, value: Any?) = + value?.let { " $name=\"${xml(it.toString())}\"" }.orEmpty() + + private fun String?.blankToNull() = this?.takeIf { it.isNotBlank() } +} diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/Serialize.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/Serialize.kt new file mode 100644 index 0000000..c149fa3 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/Serialize.kt @@ -0,0 +1,3 @@ +package rip.crit.commonip.serialize + +fun GeoFeed.serialize(format: GeoFeedFormat): String = GeoFeedCodec.encode(this, format) diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/CSV.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/CSV.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/CSV.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/JSON.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/JSON.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/JSON.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/XML.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/XML.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/cityStateCountry/XML.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/CSVSpec.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/CSVSpec.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/CSVSpec.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/JSONSpec.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/JSONSpec.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/JSONSpec.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/Schema.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/Schema.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/Schema.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/XMLSpec.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/XMLSpec.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/common/XMLSpec.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/CSV.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/CSV.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/CSV.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/JSON.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/JSON.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/JSON.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/XML.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/XML.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/country/XML.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/CSV.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/CSV.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/CSV.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/JSON.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/JSON.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/JSON.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/XML.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/XML.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/stateCountry/XML.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/CSV.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/CSV.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/CSV.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/JSON.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/JSON.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/JSON.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/XML.kt b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/XML.kt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/packages/serialize/src/main/kotlin/rip/crit/commonip/serialize/zipCityStateCountry/XML.kt @@ -0,0 +1 @@ + diff --git a/packages/serialize/src/test/kotlin/rip/crit/commonip/serialize/GeoFeedCodecTest.kt b/packages/serialize/src/test/kotlin/rip/crit/commonip/serialize/GeoFeedCodecTest.kt new file mode 100644 index 0000000..b7e1e19 --- /dev/null +++ b/packages/serialize/src/test/kotlin/rip/crit/commonip/serialize/GeoFeedCodecTest.kt @@ -0,0 +1,15 @@ +package rip.crit.commonip.serialize + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class GeoFeedCodecTest { + @Test + fun `RFC 8805 records round trip`() { + val feed = GeoFeed(listOf(GeoLocation("192.0.2.0/24", "US", "PA", "Philadelphia", "19106"))) + val parsed = + GeoFeedCodec.decode(GeoFeedCodec.encode(feed, GeoFeedFormat.CSV), GeoFeedFormat.CSV) + assertEquals(feed.records.first().prefix, parsed.records.first().prefix) + assertEquals("Philadelphia", parsed.records.first().city) + } +} diff --git a/packages/settings.gradle.kts b/packages/settings.gradle.kts new file mode 100644 index 0000000..2d88bcd --- /dev/null +++ b/packages/settings.gradle.kts @@ -0,0 +1,3 @@ + +rootProject.name = "commonip-packages" +include("decentraland", "serialize", "ml", "rawr") \ No newline at end of file diff --git a/proprietary/build.gradle.kts b/proprietary/build.gradle.kts new file mode 100644 index 0000000..d0702b9 --- /dev/null +++ b/proprietary/build.gradle.kts @@ -0,0 +1,74 @@ +import java.net.URI +import javax.xml.parsers.DocumentBuilderFactory +import org.w3c.dom.Element +import org.w3c.dom.Node + +plugins { + id("buildlogic.kotlin-library-conventions") +} + +dependencies { + api(project(":packages:serialize")) + implementation(project(":packages:rawr")) + implementation("commons-net:commons-net:3.11.1") +} + +tasks.register("whois") { + group = "generation" + description = "Fetches latest WHOIS server mapping and generates WhoisServers.kt in source." + + val outputDirectory = file("src/main/kotlin/rip/crit/commonip/proprietary") + outputs.file(outputDirectory.resolve("WhoisServers.kt")) + + doLast { + val xmlUrl = + "https://raw.githubusercontent.com/whois-server-list/whois-server-list/refs/heads/master/whois-server-list.xml" + + val inputStream = URI(xmlUrl).toURL().openStream() + val dbFactory = DocumentBuilderFactory.newInstance() + val dBuilder = dbFactory.newDocumentBuilder() + val xmlDoc = dBuilder.parse(inputStream) + xmlDoc.documentElement.normalize() + + val serverMap = mutableMapOf() + val domainNodes = xmlDoc.getElementsByTagName("domain") + + for (i in 0 until domainNodes.length) { + val domainNode = domainNodes.item(i) + if (domainNode.nodeType == Node.ELEMENT_NODE) { + val domainElement = domainNode as Element + val domainName = domainElement.getAttribute("name") + + val whoisServerNodes = domainElement.getElementsByTagName("whoisServer") + if (whoisServerNodes.length > 0) { + val whoisServerElement = whoisServerNodes.item(0) as Element + val host = whoisServerElement.getAttribute("host") + if (host.isNotBlank() && domainName.isNotBlank()) { + serverMap[domainName] = host + } + } + } + } + + val mapEntries = StringBuilder() + serverMap.forEach { (tld, server) -> + mapEntries.append(" \"$tld\" to \"$server\",\n") + } + + val outputFile = outputDirectory.resolve("WhoisServers.kt") + outputFile.parentFile.mkdirs() + outputFile.writeText( + """ + package rip.crit.commonip.proprietary + + // Auto-generated file. Do not edit directly. + object WhoisServers { + val map: Map = mapOf( + $mapEntries ) + } + """ + .trimIndent() + ) + println("Successfully generated ${outputFile.path} with ${serverMap.size} active TLD servers.") + } +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/GeoProvider.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/GeoProvider.kt new file mode 100644 index 0000000..07ab39b --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/GeoProvider.kt @@ -0,0 +1,19 @@ +package rip.crit.commonip.proprietary + +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +fun interface HttpTransport { + fun get(url: String, headers: Map): String +} + +object DefaultHttpTransport : HttpTransport { + private val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build() + + override fun get(url: String, headers: Map): String { + val request = HttpRequest.newBuilder(URI(url)).apply { headers.forEach(::header) }.GET().build() + return client.send(request, HttpResponse.BodyHandlers.ofString()).body() + } +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/OpenGeoFeed.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/OpenGeoFeed.kt new file mode 100644 index 0000000..e220788 --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/OpenGeoFeed.kt @@ -0,0 +1,18 @@ +package rip.crit.commonip.proprietary + +import rip.crit.commonip.rawr.types.Cidr +import rip.crit.commonip.serialize.GeoFeed +import rip.crit.commonip.serialize.GeoFeedCodec +import rip.crit.commonip.serialize.GeoFeedFormat +import rip.crit.commonip.serialize.GeoLocation + +class OpenGeoFeed(private val transport: HttpTransport = DefaultHttpTransport) { + fun pull(url: String): GeoFeed = + GeoFeedCodec.decode(transport.get(url, emptyMap()), GeoFeedFormat.CSV) + + fun lookup(url: String, address: String): GeoLocation? = + pull(url) + .records + .filter { Cidr.contains(it.prefix, address) } + .maxByOrNull { Cidr.prefixLength(it.prefix) } +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/Rdap.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/Rdap.kt new file mode 100644 index 0000000..3025869 --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/Rdap.kt @@ -0,0 +1,6 @@ +package rip.crit.commonip.proprietary + +class Rdap(private val transport: HttpTransport = DefaultHttpTransport) { + fun lookup(address: String): String = + transport.get("https://rdap.org/ip/$address", mapOf("Accept" to "application/rdap+json")) +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/RequestGovernor.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/RequestGovernor.kt new file mode 100644 index 0000000..bd75434 --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/RequestGovernor.kt @@ -0,0 +1,31 @@ +package rip.crit.commonip.proprietary + +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Semaphore + +class RequestGovernor( + private val minimumInterval: Duration = Duration.ofMillis(500), + parallelism: Int = 2, +) { + private val permits = Semaphore(parallelism) + private val lastRequest = ConcurrentHashMap() + + fun execute(provider: String, action: () -> T): T { + permits.acquire() + try { + synchronized(lastRequest) { + val wait = + minimumInterval.toMillis() - (System.currentTimeMillis() - (lastRequest[provider] ?: 0)) + if (wait > 0) Thread.sleep(wait) + lastRequest[provider] = System.currentTimeMillis() + } + return action() + } finally { + permits.release() + } + } + + fun chunks(values: Collection, size: Int = 100): Sequence> = + values.asSequence().chunked(size) +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/StarlinkGeoFeed.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/StarlinkGeoFeed.kt new file mode 100644 index 0000000..2e25a0f --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/StarlinkGeoFeed.kt @@ -0,0 +1,13 @@ +package rip.crit.commonip.proprietary + +import rip.crit.commonip.serialize.GeoFeed +import rip.crit.commonip.serialize.GeoFeedCodec +import rip.crit.commonip.serialize.GeoFeedFormat + +class StarlinkGeoFeed(private val transport: HttpTransport = DefaultHttpTransport) { + companion object { + const val URL = "https://geoip.starlinkisp.net/feed.csv" + } + + fun pull(): GeoFeed = GeoFeedCodec.decode(transport.get(URL, emptyMap()), GeoFeedFormat.CSV) +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/Whois.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/Whois.kt new file mode 100644 index 0000000..232ab42 --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/Whois.kt @@ -0,0 +1,17 @@ +package rip.crit.commonip.proprietary + +import org.apache.commons.net.whois.WhoisClient + +class Whois(private val servers: Map = WhoisServers.map) { + fun lookup(query: String): String { + val server = + servers[query.substringAfterLast('.', "").lowercase()] ?: servers.getValue("default") + val client = WhoisClient() + return try { + client.connect(server) + client.query("$query\r\n") + } finally { + if (client.isConnected) client.disconnect() + } + } +} diff --git a/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/WhoisServers.kt b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/WhoisServers.kt new file mode 100644 index 0000000..20874ab --- /dev/null +++ b/proprietary/src/main/kotlin/rip/crit/commonip/proprietary/WhoisServers.kt @@ -0,0 +1,1535 @@ +package rip.crit.commonip.proprietary + +// Auto-generated file. Do not edit directly. +object WhoisServers { + val map: Map = + mapOf( + "aarp" to "whois.nic.aarp", + "abarth" to "whois.afilias-srs.net", + "abbott" to "whois.afilias-srs.net", + "abbvie" to "whois.afilias-srs.net", + "abc" to "whois.nic.abc", + "abogado" to "whois-dub.mm-registry.com", + "abudhabi" to "whois.nic.abudhabi", + "ac" to "whois.nic.ac", + "ac.ac" to "whois.nic.ac", + "co.ac" to "whois.nic.ac", + "gv.ac" to "whois.nic.ac", + "or.ac" to "whois.nic.ac", + "academy" to "whois.donuts.co", + "accountant" to "whois.nic.accountant", + "accountants" to "whois.donuts.co", + "aco" to "whois.afilias-srs.net", + "active" to "whois.afilias-srs.net", + "actor" to "whois.unitedtld.com", + "ad" to "whois.ripe.net", + "adac" to "whois.nic.adac", + "ads" to "domain-registry-whois.l.google.com", + "adult" to "whois.afilias-srs.net", + "ae" to "whois-check.aeda.net.ae", + "aeg" to "whois.nic.aeg", + "aero" to "whois.aero", + "af" to "whois.nic.af", + "afamilycompany" to "whois.nic.afamilycompany", + "afl" to "whois.nic.afl", + "ag" to "whois.nic.ag", + "agakhan" to "whois.afilias-srs.net", + "agency" to "whois.donuts.co", + "ai" to "whois.ai", + "aigo" to "whois.afilias-srs.net", + "airbus" to "whois.nic.airbus", + "airforce" to "whois.unitedtld.com", + "airtel" to "whois.nic.airtel", + "akdn" to "whois.afilias-srs.net", + "alfaromeo" to "whois.afilias-srs.net", + "alibaba" to "whois.afilias-srs.net", + "alipay" to "whois.afilias-srs.net", + "allfinanz" to "whois.ksregistry.net", + "allstate" to "whois.afilias-srs.net", + "ally" to "whois.afilias-srs.net", + "alsace" to "whois-alsace.nic.fr", + "alstom" to "whois.nic.alstom", + "am" to "whois.amnic.net", + "americanfamily" to "whois.nic.americanfamily", + "amfam" to "whois.nic.amfam", + "amsterdam" to "whois.nic.amsterdam", + "android" to "domain-registry-whois.l.google.com", + "anquan" to "whois.teleinfo.cn", + "anz" to "whois.nic.anz", + "aol" to "whois.nic.aol", + "apartments" to "whois.donuts.co", + "app" to "whois.nic.google", + "apple" to "whois.afilias-srs.net", + "aquarelle" to "whois-aquarelle.nic.fr", + "archi" to "whois.ksregistry.net", + "army" to "whois.rightside.co", + "arpa" to "whois.iana.org", + "e164.arpa" to "whois.ripe.net", + "art" to "whois.centralnic.com", + "arte" to "whois.nic.arte", + "as" to "whois.nic.as", + "asda" to "whois.nic.asda", + "asia" to "whois.nic.asia", + "associates" to "whois.donuts.co", + "at" to "whois.nic.at", + "ac.at" to "whois.nic.at", + "co.at" to "whois.nic.at", + "gv.at" to "whois.nic.at", + "or.at" to "whois.nic.at", + "priv.at" to "whois.nic.priv.at", + "attorney" to "whois.rightside.co", + "au" to "whois.audns.net.au", + "auction" to "whois.donuts.co", + "audi" to "whois.afilias-srs.net", + "audio" to "whois.uniregistry.net", + "auspost" to "whois.nic.auspost", + "auto" to "whois.uniregistry.net", + "autos" to "whois.afilias-srs.net", + "avianca" to "whois.afilias-srs.net", + "aw" to "whois.nic.aw", + "baidu" to "whois.ngtld.cn", + "band" to "whois.rightside.co", + "bank" to "whois.nic.bank", + "bar" to "whois.nic.bar", + "barcelona" to "whois.nic.barcelona", + "barclaycard" to "whois.nic.barclaycard", + "barclays" to "whois.nic.barclays", + "barefoot" to "whois.nic.barefoot", + "bargains" to "whois.donuts.co", + "basketball" to "whois.nic.basketball", + "bauhaus" to "whois.nic.bauhaus", + "bayern" to "whois-dub.mm-registry.com", + "bbc" to "whois.nic.bbc", + "bbt" to "whois.nic.bbt", + "bbva" to "whois.nic.bbva", + "bcg" to "whois.afilias-srs.net", + "bcn" to "whois.nic.bcn", + "be" to "whois.dns.be", + "ac.be" to "whois.dns.be", + "beats" to "whois.afilias-srs.net", + "beauty" to "whois.nic.beauty", + "beer" to "whois-dub.mm-registry.com", + "bentley" to "whois.nic.bentley", + "berlin" to "whois.nic.berlin", + "best" to "whois.nic.best", + "bestbuy" to "whois.afilias-srs.net", + "bet" to "whois.afilias.net", + "bg" to "whois.register.bg", + "bi" to "whois1.nic.bi", + "bid" to "whois.nic.bid", + "bike" to "whois.donuts.co", + "bingo" to "whois.donuts.co", + "bio" to "whois.ksregistry.net", + "biz" to "whois.biz", + "bj" to "whois.nic.bj", + "black" to "whois.afilias.net", + "blackfriday" to "whois.uniregistry.net", + "blanco" to "whois.nic.blanco", + "blockbuster" to "whois.afilias-srs.net", + "blog" to "whois.nic.blog", + "blue" to "whois.afilias.net", + "bms" to "whois.nic.bms", + "bmw" to "whois.ksregistry.net", + "bn" to "whois.bnnic.bn", + "bnl" to "whois.afilias-srs.net", + "bnpparibas" to "whois.afilias-srs.net", + "bo" to "whois.nic.bo", + "boats" to "whois.afilias-srs.net", + "boehringer" to "whois.afilias-srs.net", + "bofa" to "whois.nic.bofa", + "bom" to "whois.gtlds.nic.br", + "bond" to "whois.nic.bond", + "boo" to "domain-registry-whois.l.google.com", + "boots" to "whois.nic.boots", + "bosch" to "whois.nic.bosch", + "bostik" to "whois-bostik.nic.fr", + "boutique" to "whois.donuts.co", + "br" to "whois.nic.br", + "adm.br" to "whois.nic.br", + "adv.br" to "whois.nic.br", + "am.br" to "whois.nic.br", + "arq.br" to "whois.nic.br", + "art.br" to "whois.nic.br", + "bio.br" to "whois.nic.br", + "cng.br" to "whois.nic.br", + "cnt.br" to "whois.nic.br", + "com.br" to "whois.nic.br", + "ecn.br" to "whois.nic.br", + "eng.br" to "whois.nic.br", + "esp.br" to "whois.nic.br", + "etc.br" to "whois.nic.br", + "eti.br" to "whois.nic.br", + "fm.br" to "whois.nic.br", + "fot.br" to "whois.nic.br", + "fst.br" to "whois.nic.br", + "g12.br" to "whois.nic.br", + "gov.br" to "whois.nic.br", + "ind.br" to "whois.nic.br", + "inf.br" to "whois.nic.br", + "jor.br" to "whois.nic.br", + "lel.br" to "whois.nic.br", + "med.br" to "whois.nic.br", + "mil.br" to "whois.nic.br", + "net.br" to "whois.nic.br", + "nom.br" to "whois.nic.br", + "ntr.br" to "whois.nic.br", + "odo.br" to "whois.nic.br", + "org.br" to "whois.nic.br", + "ppg.br" to "whois.nic.br", + "pro.br" to "whois.nic.br", + "psc.br" to "whois.nic.br", + "psi.br" to "whois.nic.br", + "rec.br" to "whois.nic.br", + "slg.br" to "whois.nic.br", + "tmp.br" to "whois.nic.br", + "tur.br" to "whois.nic.br", + "tv.br" to "whois.nic.br", + "vet.br" to "whois.nic.br", + "zlg.br" to "whois.nic.br", + "bradesco" to "whois-cl01.mm-registry.com", + "bridgestone" to "whois.nic.bridgestone", + "broadway" to "whois-cl01.mm-registry.com", + "broker" to "whois.nic.broker", + "brother" to "whois.nic.brother", + "brussels" to "whois.nic.brussels", + "budapest" to "whois-dub.mm-registry.com", + "bugatti" to "whois.afilias-srs.net", + "build" to "whois.nic.build", + "builders" to "whois.donuts.co", + "business" to "whois.donuts.co", + "buy" to "whois.afilias-srs.net", + "buzz" to "whois.nic.buzz", + "bw" to "whois.nic.net.bw", + "by" to "whois.cctld.by", + "bz" to "whois.afilias-grs.info", + "za.bz" to "whois.centralnic.com", + "bzh" to "whois-bzh.nic.fr", + "ca" to "whois.cira.ca", + "co.ca" to "whois.co.ca", + "cab" to "whois.donuts.co", + "cafe" to "whois.donuts.co", + "cal" to "domain-registry-whois.l.google.com", + "cam" to "whois.ksregistry.net", + "camera" to "whois.donuts.co", + "camp" to "whois.donuts.co", + "cancerresearch" to "whois.nic.cancerresearch", + "canon" to "whois.nic.canon", + "capetown" to "capetown-whois.registry.net.za", + "capital" to "whois.donuts.co", + "capitalone" to "whois.nic.capitalone", + "car" to "whois.uniregistry.net", + "cards" to "whois.donuts.co", + "care" to "whois.donuts.co", + "career" to "whois.nic.career", + "careers" to "whois.donuts.co", + "cars" to "whois.uniregistry.net", + "casa" to "whois-dub.mm-registry.com", + "case" to "whois.afilias-srs.net", + "caseih" to "whois.afilias-srs.net", + "cash" to "whois.donuts.co", + "casino" to "whois.donuts.co", + "cat" to "whois.cat", + "catering" to "whois.donuts.co", + "cba" to "whois.nic.cba", + "cbs" to "whois.afilias-srs.net", + "cc" to "ccwhois.verisign-grs.com", + "cd" to "whois.nic.cd", + "ceb" to "whois.afilias-srs.net", + "center" to "whois.donuts.co", + "ceo" to "whois.nic.ceo", + "cern" to "whois.afilias-srs.net", + "cf" to "whois.dot.cf", + "cfa" to "whois.nic.cfa", + "cfd" to "whois.nic.cfd", + "ch" to "whois.nic.ch", + "chanel" to "whois.nic.chanel", + "channel" to "domain-registry-whois.l.google.com", + "chat" to "whois.donuts.co", + "cheap" to "whois.donuts.co", + "chintai" to "whois.nic.chintai", + "christmas" to "whois.uniregistry.net", + "chrome" to "domain-registry-whois.l.google.com", + "chrysler" to "whois.afilias-srs.net", + "church" to "whois.donuts.co", + "ci" to "whois.nic.ci", + "cipriani" to "whois.afilias-srs.net", + "city" to "whois.donuts.co", + "cityeats" to "whois.nic.cityeats", + "cl" to "whois.nic.cl", + "claims" to "whois.donuts.co", + "cleaning" to "whois.donuts.co", + "click" to "whois.uniregistry.net", + "clinic" to "whois.donuts.co", + "clinique" to "whois.afilias-srs.net", + "clothing" to "whois.donuts.co", + "cloud" to "whois.nic.cloud", + "club" to "whois.nic.club", + "clubmed" to "whois.nic.clubmed", + "cm" to "whois.netcom.cm", + "cn" to "whois.cnnic.cn", + "ac.cn" to "whois.cnnic.net.cn", + "ah.cn" to "whois.cnnic.net.cn", + "bj.cn" to "whois.cnnic.net.cn", + "com.cn" to "whois.cnnic.net.cn", + "cq.cn" to "whois.cnnic.net.cn", + "edu.cn" to "whois.cnnic.net.cn", + "gd.cn" to "whois.cnnic.net.cn", + "gov.cn" to "whois.cnnic.net.cn", + "gs.cn" to "whois.cnnic.net.cn", + "gx.cn" to "whois.cnnic.net.cn", + "gz.cn" to "whois.cnnic.net.cn", + "hb.cn" to "whois.cnnic.net.cn", + "he.cn" to "whois.cnnic.net.cn", + "hi.cn" to "whois.cnnic.net.cn", + "hk.cn" to "whois.cnnic.net.cn", + "hl.cn" to "whois.cnnic.net.cn", + "hn.cn" to "whois.cnnic.net.cn", + "jl.cn" to "whois.cnnic.net.cn", + "js.cn" to "whois.cnnic.net.cn", + "ln.cn" to "whois.cnnic.net.cn", + "mo.cn" to "whois.cnnic.net.cn", + "net.cn" to "whois.cnnic.net.cn", + "nm.cn" to "whois.cnnic.net.cn", + "nx.cn" to "whois.cnnic.net.cn", + "org.cn" to "whois.cnnic.net.cn", + "qh.cn" to "whois.cnnic.net.cn", + "sc.cn" to "whois.cnnic.net.cn", + "sh.cn" to "whois.cnnic.net.cn", + "sn.cn" to "whois.cnnic.net.cn", + "tj.cn" to "whois.cnnic.net.cn", + "tw.cn" to "whois.cnnic.net.cn", + "xj.cn" to "whois.cnnic.net.cn", + "xz.cn" to "whois.cnnic.net.cn", + "yn.cn" to "whois.cnnic.net.cn", + "zj.cn" to "whois.cnnic.net.cn", + "co" to "whois.nic.co", + "com.co" to "whois.nic.co", + "net.co" to "whois.nic.co", + "nom.co" to "whois.nic.co", + "uk.co" to "whois.uk.co", + "coach" to "whois.donuts.co", + "codes" to "whois.donuts.co", + "coffee" to "whois.donuts.co", + "college" to "whois.centralnic.com", + "cologne" to "whois-fe1.pdt.cologne.tango.knipp.de", + "com" to "whois.crsnic.net", + "ae.com" to "whois.centralnic.net", + "africa.com" to "srs-whois.dns.net.za", + "ar.com" to "whois.centralnic.com", + "br.com" to "whois.centralnic.com", + "cn.com" to "whois.centralnic.com", + "co.com" to "whois.centralnic.net", + "de.com" to "whois.centralnic.com", + "eu.com" to "whois.centralnic.com", + "gb.com" to "whois.centralnic.com", + "gr.com" to "whois.centralnic.com", + "hu.com" to "whois.centralnic.com", + "jpn.com" to "whois.centralnic.com", + "kr.com" to "whois.centralnic.com", + "lovesnaija.com" to "srs-whois.dns.net.za", + "no.com" to "whois.centralnic.com", + "qc.com" to "whois.centralnic.com", + "ru.com" to "whois.centralnic.com", + "sa.com" to "whois.centralnic.com", + "se.com" to "whois.centralnic.com", + "uk.com" to "whois.centralnic.com", + "us.com" to "whois.centralnic.com", + "uy.com" to "whois.centralnic.com", + "web.com" to "whois.centralnic.net", + "za.com" to "whois.centralnic.com", + "comcast" to "whois.nic.comcast", + "commbank" to "whois.nic.commbank", + "community" to "whois.donuts.co", + "company" to "whois.donuts.co", + "compare" to "whois.nic.compare", + "computer" to "whois.donuts.co", + "comsec" to "whois.nic.comsec", + "condos" to "whois.donuts.co", + "construction" to "whois.donuts.co", + "consulting" to "whois.unitedtld.com", + "contact" to "whois.nic.contact", + "contractors" to "whois.donuts.co", + "cooking" to "whois-dub.mm-registry.com", + "cookingchannel" to "whois.nic.cookingchannel", + "cool" to "whois.donuts.co", + "coop" to "whois.nic.coop", + "corsica" to "whois-corsica.nic.fr", + "country" to "whois-dub.mm-registry.com", + "coupons" to "whois.donuts.co", + "courses" to "whois.aridnrs.net.au", + "cr" to "whois.nic.cr", + "credit" to "whois.donuts.co", + "creditcard" to "whois.donuts.co", + "creditunion" to "whois.afilias-srs.net", + "cricket" to "whois.nic.cricket", + "cruises" to "whois.donuts.co", + "csc" to "whois.nic.csc", + "cuisinella" to "whois.nic.cuisinella", + "cx" to "whois.nic.cx", + "cymru" to "whois.nic.cymru", + "cyou" to "whois.afilias-srs.net", + "cz" to "whois.nic.cz", + "dabur" to "whois.afilias-srs.net", + "dad" to "domain-registry-whois.l.google.com", + "dance" to "whois.unitedtld.com", + "date" to "whois.nic.date", + "dating" to "whois.donuts.co", + "datsun" to "whois.nic.gmo", + "day" to "domain-registry-whois.l.google.com", + "dclk" to "domain-registry-whois.l.google.com", + "dds" to "whois-dub.mm-registry.com", + "de" to "whois.denic.de", + "com.de" to "whois.centralnic.com", + "deals" to "whois.donuts.co", + "degree" to "whois.rightside.co", + "delivery" to "whois.donuts.co", + "deloitte" to "whois.nic.deloitte", + "delta" to "whois.afilias-srs.net", + "democrat" to "whois.unitedtld.com", + "dental" to "whois.donuts.co", + "dentist" to "whois.rightside.co", + "desi" to "whois.ksregistry.net", + "design" to "whois.nic.design", + "dev" to "domain-registry-whois.l.google.com", + "diamonds" to "whois.donuts.co", + "diet" to "whois.uniregistry.net", + "digital" to "whois.donuts.co", + "direct" to "whois.donuts.co", + "directory" to "whois.donuts.co", + "discount" to "whois.donuts.co", + "dish" to "whois.afilias-srs.net", + "diy" to "whois.nic.diy", + "dk" to "whois.dk-hostmaster.dk", + "dm" to "whois.nic.dm", + "docs" to "domain-registry-whois.l.google.com", + "doctor" to "whois.donuts.co", + "dodge" to "whois.afilias-srs.net", + "dog" to "whois.donuts.co", + "doha" to "whois.nic.doha", + "domains" to "whois.donuts.co", + "doosan" to "whois.nic.xn--cg4bki", + "dot" to "whois.afilias-srs.net", + "download" to "whois.nic.download", + "drive" to "whois.nic.google", + "dtv" to "whois.afilias-srs.net", + "dubai" to "whois.nic.dubai", + "duck" to "whois.nic.duck", + "dunlop" to "whois.afilias-srs.net", + "durban" to "durban-whois.registry.net.za", + "dvag" to "whois.ksregistry.net", + "dvr" to "whois.afilias-srs.net", + "dz" to "whois.nic.dz", + "eat" to "domain-registry-whois.l.google.com", + "ec" to "whois.nic.ec", + "com.ec" to "whois.lac.net", + "fin.ec" to "whois.lac.net", + "gov.ec" to "whois.lac.net", + "med.ec" to "whois.lac.net", + "mil.ec" to "whois.lac.net", + "net.ec" to "whois.lac.net", + "org.ec" to "whois.lac.net", + "eco" to "whois.afilias-srs.net", + "edeka" to "whois.afilias-srs.net", + "edu" to "whois.educause.edu", + "education" to "whois.donuts.co", + "ee" to "whois.eenet.ee", + "email" to "whois.donuts.co", + "emerck" to "whois.afilias-srs.net", + "energy" to "whois.donuts.co", + "engineer" to "whois.rightside.co", + "engineering" to "whois.donuts.co", + "enterprises" to "whois.donuts.co", + "epson" to "whois.aridnrs.net.au", + "equipment" to "whois.donuts.co", + "ericsson" to "whois.nic.ericsson", + "erni" to "whois.nic.erni", + "es" to "whois.nic.es", + "esq" to "domain-registry-whois.l.google.com", + "estate" to "whois.donuts.co", + "esurance" to "whois.afilias-srs.net", + "eu" to "whois.eu", + "eurovision" to "whois.nic.eurovision", + "eus" to "whois.eus.coreregistry.net", + "events" to "whois.donuts.co", + "everbank" to "whois.nic.everbank", + "exchange" to "whois.donuts.co", + "expert" to "whois.donuts.co", + "exposed" to "whois.donuts.co", + "express" to "whois.donuts.co", + "extraspace" to "whois.afilias-srs.net", + "fage" to "whois.afilias-srs.net", + "fail" to "whois.donuts.co", + "fairwinds" to "whois.nic.fairwinds", + "faith" to "whois.nic.faith", + "family" to "whois.rightside.co", + "fan" to "whois.nic.fan", + "fans" to "whois.nic.fans", + "farm" to "whois.donuts.co", + "fashion" to "whois-dub.mm-registry.com", + "fedex" to "whois.afilias-srs.net", + "feedback" to "whois.centralnic.com", + "ferrari" to "whois.afilias-srs.net", + "fi" to "whois.fi", + "fiat" to "whois.afilias-srs.net", + "fidelity" to "whois.nic.fidelity", + "fido" to "whois.afilias-srs.net", + "film" to "whois.nic.film", + "final" to "whois.gtlds.nic.br", + "finance" to "whois.donuts.co", + "financial" to "whois.donuts.co", + "firestone" to "whois.nic.firestone", + "firmdale" to "whois.nic.firmdale", + "fish" to "whois.donuts.co", + "fishing" to "whois-dub.mm-registry.com", + "fit" to "whois-dub.mm-registry.com", + "fitness" to "whois.donuts.co", + "fj" to "whois.usp.ac.fj", + "flights" to "whois.donuts.co", + "florist" to "whois.donuts.co", + "flowers" to "whois.uniregistry.net", + "flsmidth" to "whois.ksregistry.net", + "fly" to "domain-registry-whois.l.google.com", + "fm" to "whois.nic.fm", + "fo" to "whois.nic.fo", + "foo" to "domain-registry-whois.l.google.com", + "foodnetwork" to "whois.nic.foodnetwork", + "football" to "whois.donuts.co", + "forex" to "whois.nic.forex", + "forsale" to "whois.unitedtld.com", + "forum" to "whois.nic.forum", + "foundation" to "whois.donuts.co", + "fr" to "whois.nic.fr", + "aeroport.fr" to "whois.smallregistry.net", + "asso.fr" to "whois.nic.fr", + "avocat.fr" to "whois.smallregistry.net", + "chambagri.fr" to "whois.smallregistry.net", + "chirurgiens-dentistes.fr" to "whois.smallregistry.net", + "com.fr" to "whois.nic.fr", + "experts-comptables.fr" to "whois.smallregistry.net", + "geometre-expert.fr" to "whois.smallregistry.net", + "medecin.fr" to "whois.smallregistry.net", + "notaires.fr" to "whois.smallregistry.net", + "pharmacien.fr" to "whois.smallregistry.net", + "port.fr" to "whois.smallregistry.net", + "presse.fr" to "whois.nic.fr", + "tm.fr" to "whois.nic.fr", + "veterinaire.fr" to "whois.smallregistry.net", + "fresenius" to "whois.ksregistry.net", + "frl" to "whois.nic.frl", + "frogans" to "whois-frogans.nic.fr", + "frontdoor" to "whois.nic.frontdoor", + "fujitsu" to "whois.nic.gmo", + "fujixerox" to "whois.nic.fujixerox", + "fund" to "whois.donuts.co", + "furniture" to "whois.donuts.co", + "futbol" to "whois.unitedtld.com", + "fyi" to "whois.donuts.co", + "gal" to "whois.gal.coreregistry.net", + "gallery" to "whois.donuts.co", + "gallo" to "whois.nic.gallo", + "gallup" to "whois.afilias-srs.net", + "game" to "whois.uniregistry.net", + "games" to "whois.rightside.co", + "garden" to "whois-dub.mm-registry.com", + "gbiz" to "domain-registry-whois.l.google.com", + "gd" to "whois.nic.gd", + "gdn" to "whois.nic.gdn", + "gea" to "whois.afilias-srs.net", + "gent" to "whois.nic.gent", + "genting" to "whois.nic.genting", + "george" to "whois.nic.george", + "gg" to "whois.channelisles.net", + "ggee" to "whois.nic.ggee", + "gi" to "whois.afilias-grs.info", + "gift" to "whois.uniregistry.net", + "gifts" to "whois.donuts.co", + "gives" to "whois.rightside.co", + "giving" to "whois.nic.giving", + "gl" to "whois.nic.gl", + "glade" to "whois.nic.glade", + "glass" to "whois.donuts.co", + "gle" to "domain-registry-whois.l.google.com", + "global" to "whois.afilias-srs.net", + "globo" to "whois.gtlds.nic.br", + "gmail" to "domain-registry-whois.l.google.com", + "gmbh" to "whois.donuts.co", + "gmx" to "whois-fe1.gmx.tango.knipp.de", + "godaddy" to "whois.afilias-srs.net", + "gold" to "whois.donuts.co", + "goldpoint" to "whois.nic.goldpoint", + "golf" to "whois.donuts.co", + "goo" to "whois.nic.gmo", + "goodhands" to "whois.afilias-srs.net", + "goodyear" to "whois.afilias-srs.net", + "goog" to "domain-registry-whois.l.google.com", + "google" to "domain-registry-whois.l.google.com", + "gop" to "whois-cl01.mm-registry.com", + "gov" to "whois.dotgov.gov", + "gq" to "whois.dominio.gq", + "graphics" to "whois.donuts.co", + "gratis" to "whois.donuts.co", + "green" to "whois.afilias.net", + "gripe" to "whois.donuts.co", + "group" to "whois.donuts.co", + "gs" to "whois.adamsnames.tc", + "guge" to "domain-registry-whois.l.google.com", + "guide" to "whois.donuts.co", + "guitars" to "whois.uniregistry.net", + "guru" to "whois.donuts.co", + "gy" to "whois.registry.gy", + "hamburg" to "whois.nic.hamburg", + "hangout" to "domain-registry-whois.l.google.com", + "haus" to "whois.unitedtld.com", + "hdfc" to "whois.afilias-srs.net", + "hdfcbank" to "whois.afilias-srs.net", + "healthcare" to "whois.donuts.co", + "help" to "whois.uniregistry.net", + "helsinki" to "whois.afilias-srs.net", + "here" to "domain-registry-whois.l.google.com", + "hermes" to "whois.afilias-srs.net", + "hgtv" to "whois.nic.hgtv", + "hiphop" to "whois.uniregistry.net", + "hisamitsu" to "whois.nic.gmo", + "hitachi" to "whois.nic.gmo", + "hiv" to "whois.afilias-srs.net", + "hk" to "whois.hkirc.hk", + "com.hk" to "whois.hkdnr.net.hk", + "edu.hk" to "whois.hkdnr.net.hk", + "net.hk" to "whois.hkdnr.net.hk", + "org.hk" to "whois.hkdnr.net.hk", + "hkt" to "whois.afilias-srs.net", + "hm" to "whois.registry.hm", + "hn" to "whois.nic.hn", + "hockey" to "whois.donuts.co", + "holdings" to "whois.donuts.co", + "holiday" to "whois.donuts.co", + "homedepot" to "whois.afilias-srs.net", + "homes" to "whois.afilias-srs.net", + "honda" to "whois.nic.honda", + "horse" to "whois-dub.mm-registry.com", + "host" to "whois.centralnic.com", + "hosting" to "whois.uniregistry.net", + "house" to "whois.donuts.co", + "how" to "domain-registry-whois.l.google.com", + "hr" to "whois.dns.hr", + "ht" to "whois.nic.ht", + "hu" to "whois.nic.hu", + "hughes" to "whois.afilias-srs.net", + "hyundai" to "whois.nic.hyundai", + "ibm" to "whois.nic.ibm", + "icbc" to "whois.afilias-srs.net", + "ice" to "whois.nic.ice", + "icu" to "whois.nic.icu", + "id" to "whois.pandi.or.id", + "go.id" to "whois.idnic.net.id", + "mil.id" to "whois.idnic.net.id", + "ie" to "whois.domainregistry.ie", + "ifm" to "whois.nic.ifm", + "iinet" to "whois.aridnrs.net.au", + "ikano" to "whois.ikano.tld-box.at", + "il" to "whois.isoc.org.il", + "ac.il" to "whois.isoc.org.il", + "co.il" to "whois.isoc.org.il", + "gov.il" to "whois.isoc.org.il", + "k12.il" to "whois.isoc.org.il", + "muni.il" to "whois.isoc.org.il", + "net.il" to "whois.isoc.org.il", + "org.il" to "whois.isoc.org.il", + "im" to "whois.nic.im", + "imamat" to "whois.afilias-srs.net", + "immo" to "whois.donuts.co", + "immobilien" to "whois.unitedtld.com", + "in" to "whois.inregistry.in", + "ac.in" to "whois.inregistry.in", + "co.in" to "whois.inregistry.in", + "ernet.in" to "whois.inregistry.in", + "gov.in" to "whois.inregistry.in", + "net.in" to "whois.inregistry.in", + "org.in" to "whois.inregistry.in", + "res.in" to "whois.inregistry.in", + "industries" to "whois.donuts.co", + "infiniti" to "whois.nic.gmo", + "info" to "whois.afilias.info", + "ing" to "domain-registry-whois.l.google.com", + "ink" to "whois.centralnic.com", + "institute" to "whois.donuts.co", + "insurance" to "whois.nic.insurance", + "insure" to "whois.donuts.co", + "int" to "whois.iana.org", + "international" to "whois.donuts.co", + "investments" to "whois.donuts.co", + "io" to "whois.nic.io", + "ir" to "whois.nic.ir", + "irish" to "whois.afilias-srs.net", + "is" to "whois.isnic.is", + "iselect" to "whois.nic.iselect", + "ismaili" to "whois.afilias-srs.net", + "ist" to "whois.afilias-srs.net", + "istanbul" to "whois.afilias-srs.net", + "it" to "whois.nic.it", + "itv" to "whois.afilias-srs.net", + "iveco" to "whois.afilias-srs.net", + "jaguar" to "whois.nic.jaguar", + "java" to "whois.nic.java", + "jcb" to "whois.nic.gmo", + "jcp" to "whois.afilias-srs.net", + "je" to "whois.channelisles.net", + "jeep" to "whois.afilias-srs.net", + "jetzt" to "whois.nic.jetzt", + "jewelry" to "whois.donuts.co", + "jll" to "whois.afilias-srs.net", + "jobs" to "whois.nic.jobs", + "joburg" to "joburg-whois.registry.net.za", + "jp" to "whois.jprs.jp", + "ac.jp" to "whois.jprs.jp", + "juegos" to "whois.uniregistry.net", + "juniper" to "whois.nic.juniper", + "kaufen" to "whois.unitedtld.com", + "kddi" to "whois.nic.kddi", + "ke" to "whois.kenic.or.ke", + "ac.ke" to "whois.kenic.or.ke", + "co.ke" to "whois.kenic.or.ke", + "kerryhotels" to "whois.nic.kerryhotels", + "kerrylogistics" to "whois.nic.kerrylogistics", + "kerryproperties" to "whois.nic.kerryproperties", + "kfh" to "whois.nic.kfh", + "ki" to "whois.nic.ki", + "kia" to "whois.nic.kia", + "kim" to "whois.afilias.net", + "kitchen" to "whois.donuts.co", + "kiwi" to "whois.dot-kiwi.com", + "kn" to "whois.nic.kn", + "koeln" to "whois-fe1.pdt.koeln.tango.knipp.de", + "komatsu" to "whois.nic.komatsu", + "kosher" to "whois.afilias-srs.net", + "kr" to "whois.kr", + "ac.kr" to "whois.nic.or.kr", + "co.kr" to "whois.nic.or.kr", + "go.kr" to "whois.nic.or.kr", + "ne.kr" to "whois.nic.or.kr", + "nm.kr" to "whois.nic.or.kr", + "or.kr" to "whois.nic.or.kr", + "re.kr" to "whois.nic.or.kr", + "krd" to "whois.aridnrs.net.au", + "kred" to "whois.nic.kred", + "kuokgroup" to "whois.nic.kuokgroup", + "ky" to "whois.kyregistry.ky", + "kyoto" to "whois.nic.kyoto", + "kz" to "whois.nic.kz", + "la" to "whois.nic.la", + "lacaixa" to "whois.nic.lacaixa", + "ladbrokes" to "whois.nic.ladbrokes", + "lamborghini" to "whois.afilias-srs.net", + "lamer" to "whois.afilias-srs.net", + "lancaster" to "whois-lancaster.nic.fr", + "lancia" to "whois.afilias-srs.net", + "lancome" to "whois.nic.lancome", + "land" to "whois.donuts.co", + "landrover" to "whois.nic.landrover", + "lasalle" to "whois.afilias-srs.net", + "lat" to "whois.nic.lat", + "latino" to "whois.afilias-srs.net", + "latrobe" to "whois.nic.latrobe", + "law" to "whois-dub.mm-registry.com", + "lawyer" to "whois.rightside.co", + "lc" to "whois.afilias-grs.info", + "lds" to "whois.afilias-srs.net", + "lease" to "whois.donuts.co", + "leclerc" to "whois-leclerc.nic.fr", + "lefrak" to "whois.nic.lefrak", + "legal" to "whois.donuts.co", + "lego" to "whois.nic.lego", + "lexus" to "whois.nic.lexus", + "lgbt" to "whois.afilias.net", + "li" to "whois.nic.li", + "liaison" to "whois.nic.liaison", + "lidl" to "whois.nic.lidl", + "life" to "whois.donuts.co", + "lifestyle" to "whois.nic.lifestyle", + "lighting" to "whois.donuts.co", + "limited" to "whois.donuts.co", + "limo" to "whois.donuts.co", + "linde" to "whois.nic.linde", + "link" to "whois.uniregistry.net", + "lipsy" to "whois.nic.lipsy", + "live" to "whois.rightside.co", + "lixil" to "whois.nic.lixil", + "loan" to "whois.nic.loan", + "loans" to "whois.donuts.co", + "locker" to "whois.afilias-srs.net", + "locus" to "whois.nic.locus", + "lol" to "whois.uniregistry.net", + "london" to "whois-lon.mm-registry.com", + "lotte" to "whois.nic.lotte", + "lotto" to "whois.afilias.net", + "love" to "whois.centralnic.com", + "lpl" to "whois.nic.lpl", + "lplfinancial" to "whois.nic.lplfinancial", + "lt" to "whois.domreg.lt", + "ltd" to "whois.donuts.co", + "ltda" to "whois.afilias-srs.net", + "lu" to "whois.dns.lu", + "lundbeck" to "whois.nic.lundbeck", + "luxe" to "whois-dub.mm-registry.com", + "luxury" to "whois.donuts.co", + "lv" to "whois.nic.lv", + "eu.lv" to "whois.biz", + "ly" to "whois.nic.ly", + "ma" to "whois.iam.net.ma", + "macys" to "whois.nic.macys", + "madrid" to "whois.madrid.rs.corenic.net", + "maison" to "whois.donuts.co", + "makeup" to "whois.nic.makeup", + "man" to "whois.nic.man", + "management" to "whois.donuts.co", + "mango" to "whois.mango.coreregistry.net", + "market" to "whois.rightside.co", + "marketing" to "whois.donuts.co", + "markets" to "whois.nic.markets", + "marriott" to "whois.afilias-srs.net", + "maserati" to "whois.afilias-srs.net", + "mba" to "whois.donuts.co", + "mc" to "whois.ripe.net", + "asso.mc" to "whois.ripe.net", + "tm.mc" to "whois.ripe.net", + "mckinsey" to "whois.afilias-srs.net", + "md" to "whois.nic.md", + "me" to "whois.nic.me", + "med" to "whois.nic.med", + "media" to "whois.donuts.co", + "meet" to "whois.afilias.net", + "melbourne" to "whois.aridnrs.net.au", + "meme" to "domain-registry-whois.l.google.com", + "memorial" to "whois.donuts.co", + "men" to "whois.nic.men", + "menu" to "whois.nic.menu", + "metlife" to "whois.afilias-srs.net", + "mg" to "whois.nic.mg", + "miami" to "whois-dub.mm-registry.com", + "mil" to "whois.internic.net", + "mini" to "whois.ksregistry.net", + "mit" to "whois.afilias-srs.net", + "mitsubishi" to "whois.nic.gmo", + "mk" to "whois.marnet.mk", + "ml" to "whois.dot.ml", + "mls" to "whois.nic.mls", + "mm" to "whois.nic.mm", + "com.mm" to "whois.nic.mm", + "edu.mm" to "whois.nic.mm", + "gov.mm" to "whois.nic.mm", + "net.mm" to "whois.nic.mm", + "org.mm" to "whois.nic.mm", + "mma" to "whois-mma.nic.fr", + "mn" to "whois.afilias-grs.info", + "mo" to "whois.monic.mo", + "mobi" to "whois.afilias.net", + "moda" to "whois.unitedtld.com", + "moe" to "whois.nic.moe", + "mom" to "whois.uniregistry.net", + "monash" to "whois.nic.monash", + "money" to "whois.donuts.co", + "monster" to "whois.afilias-srs.net", + "mopar" to "whois.afilias-srs.net", + "mormon" to "whois.afilias-srs.net", + "mortgage" to "whois.rightside.co", + "moscow" to "whois.nic.moscow", + "motorcycles" to "whois.afilias-srs.net", + "mov" to "domain-registry-whois.l.google.com", + "movie" to "whois.donuts.co", + "movistar" to "whois-fe.movistar.tango.knipp.de", + "ms" to "whois.adamsnames.tc", + "mtn" to "whois.nic.mtn", + "mtpc" to "whois.nic.gmo", + "mtr" to "whois.nic.mtr", + "mu" to "whois.nic.mu", + "museum" to "whois.museum", + "mutuelle" to "whois-mutuelle.nic.fr", + "mx" to "whois.mx", + "com.mx" to "whois.nic.mx", + "edu.mx" to "whois.nic.mx", + "gob.mx" to "whois.nic.mx", + "gov.mx" to "whois.nic.mx", + "net.mx" to "whois.nic.mx", + "org.mx" to "whois.nic.mx", + "my" to "whois.mynic.net.my", + "com.my" to "whois.mynic.net.my", + "edu.my" to "whois.mynic.net.my", + "gov.my" to "whois.mynic.net.my", + "net.my" to "whois.mynic.net.my", + "org.my" to "whois.mynic.net.my", + "mz" to "whois.nic.mz", + "na" to "whois.na-nic.com.na", + "nab" to "whois.nic.nab", + "nadex" to "whois.nic.nadex", + "nagoya" to "whois.gmoregistry.net", + "name" to "whois.nic.name", + "nationwide" to "whois.nic.nationwide", + "natura" to "whois.afilias-srs.net", + "navy" to "whois.rightside.co", + "nc" to "whois.nc", + "nec" to "whois.nic.nec", + "net" to "whois.crsnic.net", + "gb.net" to "whois.centralnic.com", + "hu.net" to "whois.centralnic.com", + "in.net" to "whois.centralnic.com", + "jp.net" to "whois.centralnic.com", + "se.net" to "whois.centralnic.com", + "uk.net" to "whois.centralnic.com", + "za.net" to "whois.za.net", + "netbank" to "whois.nic.netbank", + "network" to "whois.donuts.co", + "new" to "domain-registry-whois.l.google.com", + "newholland" to "whois.afilias-srs.net", + "news" to "whois.rightside.co", + "next" to "whois.nic.next", + "nextdirect" to "whois.nic.nextdirect", + "nexus" to "domain-registry-whois.l.google.com", + "nf" to "whois.nic.nf", + "ng" to "whois.nic.net.ng", + "ngo" to "whois.publicinterestregistry.net", + "nico" to "whois.nic.nico", + "nikon" to "whois.nic.nikon", + "ninja" to "whois.donuts.co", + "nissan" to "whois.nic.gmo", + "nissay" to "whois.nic.nissay", + "nl" to "whois.domain-registry.nl", + "no" to "whois.norid.no", + "nokia" to "whois.afilias-srs.net", + "norton" to "whois.nic.norton", + "nowruz" to "whois.agitsys.net", + "nowtv" to "whois.afilias-srs.net", + "nra" to "whois.afilias-srs.net", + "nrw" to "whois.nic.nrw", + "nu" to "whois.iis.nu", + "nyc" to "whois.nic.nyc", + "nz" to "whois.srs.net.nz", + "ac.nz" to "whois.srs.net.nz", + "co.nz" to "whois.srs.net.nz", + "geek.nz" to "whois.srs.net.nz", + "gen.nz" to "whois.srs.net.nz", + "iwi.nz" to "whois.srs.net.nz", + "kiwi.nz" to "whois.srs.net.nz", + "maori.nz" to "whois.srs.net.nz", + "net.nz" to "whois.srs.net.nz", + "org.nz" to "whois.srs.net.nz", + "school.nz" to "whois.srs.net.nz", + "obi" to "whois.nic.obi", + "observer" to "whois.nic.observer", + "off" to "whois.nic.off", + "olayan" to "whois.nic.olayan", + "olayangroup" to "whois.nic.olayangroup", + "ollo" to "whois.afilias-srs.net", + "om" to "whois.registry.om", + "omega" to "whois.nic.omega", + "one" to "whois.nic.one", + "ong" to "whois.publicinterestregistry.net", + "onl" to "whois.afilias-srs.net", + "online" to "whois.centralnic.com", + "onyourside" to "whois.nic.onyourside", + "ooo" to "whois.nic.ooo", + "oracle" to "whois.nic.oracle", + "orange" to "whois.nic.orange", + "org" to "whois.pir.org", + "ae.org" to "whois.centralnic.com", + "eu.org" to "whois.eu.org", + "us.org" to "whois.centralnic.com", + "za.org" to "whois.za.org", + "organic" to "whois.afilias-srs.net", + "orientexpress" to "whois.afilias-srs.net", + "origin" to "whois.afilias-srs.net", + "origins" to "whois.afilias-srs.net", + "osaka" to "whois.nic.osaka", + "ott" to "whois.afilias-srs.net", + "ovh" to "whois-ovh.nic.fr", + "page" to "domain-registry-whois.l.google.com", + "panasonic" to "whois.nic.gmo", + "paris" to "whois-paris.nic.fr", + "pars" to "whois.agitsys.net", + "partners" to "whois.donuts.co", + "parts" to "whois.donuts.co", + "party" to "whois.nic.party", + "pccw" to "whois.afilias-srs.net", + "pe" to "kero.yachay.pe", + "pet" to "whois.afilias.net", + "pf" to "whois.registry.pf", + "philips" to "whois.nic.philips", + "photo" to "whois.uniregistry.net", + "photography" to "whois.donuts.co", + "photos" to "whois.donuts.co", + "physio" to "whois.nic.physio", + "pics" to "whois.uniregistry.net", + "pictures" to "whois.donuts.co", + "pid" to "whois.nic.pid", + "pink" to "whois.afilias.net", + "pioneer" to "whois.nic.gmo", + "pizza" to "whois.donuts.co", + "pl" to "whois.dns.pl", + "agro.pl" to "whois.dns.pl", + "aid.pl" to "whois.dns.pl", + "atm.pl" to "whois.dns.pl", + "auto.pl" to "whois.dns.pl", + "biz.pl" to "whois.dns.pl", + "co.pl" to "whois.co.pl", + "com.pl" to "whois.dns.pl", + "edu.pl" to "whois.dns.pl", + "gmina.pl" to "whois.dns.pl", + "gsm.pl" to "whois.dns.pl", + "info.pl" to "whois.dns.pl", + "mail.pl" to "whois.dns.pl", + "media.pl" to "whois.dns.pl", + "miasta.pl" to "whois.dns.pl", + "mil.pl" to "whois.dns.pl", + "net.pl" to "whois.dns.pl", + "nom.pl" to "whois.dns.pl", + "org.pl" to "whois.dns.pl", + "pc.pl" to "whois.dns.pl", + "priv.pl" to "whois.dns.pl", + "realestate.pl" to "whois.dns.pl", + "rel.pl" to "whois.dns.pl", + "shop.pl" to "whois.dns.pl", + "sklep.pl" to "whois.dns.pl", + "sos.pl" to "whois.dns.pl", + "targi.pl" to "whois.dns.pl", + "tm.pl" to "whois.dns.pl", + "tourism.pl" to "whois.dns.pl", + "travel.pl" to "whois.dns.pl", + "turystyka.pl" to "whois.dns.pl", + "place" to "whois.donuts.co", + "play" to "whois.nic.google", + "playstation" to "whois.nic.playstation", + "plumbing" to "whois.donuts.co", + "plus" to "whois.donuts.co", + "pm" to "whois.nic.fr", + "pnc" to "whois.afilias-srs.net", + "pohl" to "whois.ksregistry.net", + "poker" to "whois.afilias.net", + "politie" to "whois.nic.politie", + "porn" to "whois.afilias-srs.net", + "post" to "whois.dotpostregistry.net", + "pr" to "whois.nic.pr", + "press" to "whois.centralnic.com", + "pro" to "whois.afilias.net", + "prod" to "domain-registry-whois.l.google.com", + "productions" to "whois.donuts.co", + "prof" to "domain-registry-whois.l.google.com", + "progressive" to "whois.afilias-srs.net", + "promo" to "whois.afilias.net", + "properties" to "whois.donuts.co", + "property" to "whois.uniregistry.net", + "protection" to "whois.centralnic.com", + "ps" to "whois.pnina.ps", + "pt" to "whois.dns.pt", + "com.pt" to "whois.dns.pt", + "edu.pt" to "whois.dns.pt", + "pub" to "whois.unitedtld.com", + "pw" to "whois.nic.pw", + "pwc" to "whois.afilias-srs.net", + "qa" to "whois.registry.qa", + "qpon" to "whois.nic.qpon", + "quebec" to "whois.nic.quebec", + "quest" to "whois.nic.quest", + "racing" to "whois.nic.racing", + "radio" to "whois.nic.radio", + "raid" to "whois.nic.raid", + "re" to "whois.nic.fr", + "realestate" to "whois.nic.realestate", + "realty" to "whois.nic.realty", + "recipes" to "whois.donuts.co", + "red" to "whois.afilias.net", + "redstone" to "whois.afilias-srs.net", + "redumbrella" to "whois.afilias-srs.net", + "rehab" to "whois.rightside.co", + "reise" to "whois.nic.reise", + "reisen" to "whois.donuts.co", + "reit" to "whois.nic.reit", + "rent" to "whois.nic.rent", + "rentals" to "whois.donuts.co", + "repair" to "whois.donuts.co", + "report" to "whois.donuts.co", + "republican" to "whois.rightside.co", + "rest" to "whois.centralnic.com", + "restaurant" to "whois.donuts.co", + "review" to "whois.nic.review", + "reviews" to "whois.unitedtld.com", + "rexroth" to "whois.nic.rexroth", + "rich" to "whois.afilias-srs.net", + "richardli" to "whois.afilias-srs.net", + "ricoh" to "whois.nic.ricoh", + "rightathome" to "whois.nic.rightathome", + "rio" to "whois.gtlds.nic.br", + "rip" to "whois.rightside.co", + "ro" to "whois.rotld.ro", + "arts.ro" to "whois.rotld.ro", + "com.ro" to "whois.rotld.ro", + "firm.ro" to "whois.rotld.ro", + "info.ro" to "whois.rotld.ro", + "nom.ro" to "whois.rotld.ro", + "nt.ro" to "whois.rotld.ro", + "org.ro" to "whois.rotld.ro", + "rec.ro" to "whois.rotld.ro", + "store.ro" to "whois.rotld.ro", + "tm.ro" to "whois.rotld.ro", + "www.ro" to "whois.rotld.ro", + "rocks" to "whois.unitedtld.com", + "rodeo" to "whois-dub.mm-registry.com", + "rogers" to "whois.afilias-srs.net", + "rs" to "whois.rnids.rs", + "co.rs" to "whois.rnids.rs", + "edu.rs" to "whois.rnids.rs", + "in.rs" to "whois.rnids.rs", + "org.rs" to "whois.rnids.rs", + "rsvp" to "domain-registry-whois.l.google.com", + "ru" to "whois.ripn.net", + "com.ru" to "whois.ripn.net", + "edu.ru" to "whois.informika.ru", + "msk.ru" to "whois.nic.ru", + "net.ru" to "whois.ripn.net", + "org.ru" to "whois.nic.ru", + "pp.ru" to "whois.nic.ru", + "spb.ru" to "whois.nic.ru", + "ruhr" to "whois.nic.ruhr", + "run" to "whois.donuts.co", + "rw" to "whois.ricta.org.rw", + "rwe" to "whois.nic.rwe", + "sa" to "whois.nic.net.sa", + "saarland" to "whois.ksregistry.net", + "sale" to "whois.rightside.co", + "salon" to "whois.donuts.co", + "samsclub" to "whois.nic.samsclub", + "samsung" to "whois.nic.samsung", + "sandvik" to "whois.nic.sandvik", + "sandvikcoromant" to "whois.nic.sandvikcoromant", + "sanofi" to "whois.nic.sanofi", + "sap" to "whois.nic.sap", + "sarl" to "whois.donuts.co", + "saxo" to "whois.aridnrs.net.au", + "sb" to "whois.nic.net.sb", + "sbi" to "whois.afilias-srs.net", + "sbs" to "whois.nic.sbs", + "sc" to "whois.afilias-grs.info", + "sca" to "whois.nic.sca", + "scb" to "whois.nic.scb", + "schaeffler" to "whois.afilias-srs.net", + "schmidt" to "whois.nic.schmidt", + "scholarships" to "whois.afilias-srs.net", + "school" to "whois.donuts.co", + "schule" to "whois.donuts.co", + "schwarz" to "whois.nic.schwarz", + "science" to "whois.nic.science", + "scjohnson" to "whois.nic.scjohnson", + "scor" to "whois.nic.scor", + "scot" to "whois.scot.coreregistry.net", + "se" to "whois.iis.se", + "com.se" to "whois.centralnic.com", + "seat" to "whois.nic.seat", + "security" to "whois.nic.security", + "seek" to "whois.nic.seek", + "select" to "whois.nic.select", + "services" to "whois.donuts.co", + "ses" to "whois.nic.ses", + "seven" to "whois.nic.seven", + "sew" to "whois.afilias-srs.net", + "sex" to "whois.afilias-srs.net", + "sexy" to "whois.uniregistry.net", + "sfr" to "whois.nic.sfr", + "sg" to "whois.nic.net.sg", + "com.sg" to "whois.nic.net.sg", + "edu.sg" to "whois.nic.net.sg", + "gov.sg" to "whois.nic.net.sg", + "net.sg" to "whois.nic.net.sg", + "org.sg" to "whois.nic.net.sg", + "sh" to "whois.nic.sh", + "shangrila" to "whois.nic.shangrila", + "sharp" to "whois.nic.gmo", + "shaw" to "whois.afilias-srs.net", + "shell" to "whois.nic.shell", + "shia" to "whois.agitsys.net", + "shiksha" to "whois.afilias.net", + "shoes" to "whois.donuts.co", + "shopping" to "whois.donuts.co", + "shouji" to "whois.teleinfo.cn", + "show" to "whois.donuts.co", + "showtime" to "whois.afilias-srs.net", + "shriram" to "whois.afilias-srs.net", + "si" to "whois.arnes.si", + "sina" to "whois.afilias-srs.net", + "singles" to "whois.donuts.co", + "site" to "whois.centralnic.com", + "sk" to "whois.sk-nic.sk", + "ski" to "whois.ksregistry.net", + "skin" to "whois.nic.skin", + "sky" to "whois.nic.sky", + "sl" to "whois.nic.sl", + "sling" to "whois.afilias-srs.net", + "sm" to "whois.nic.sm", + "smart" to "whois-gtld.smart.com.ph", + "sn" to "whois.nic.sn", + "sncf" to "whois-sncf.nic.fr", + "so" to "whois.nic.so", + "soccer" to "whois.donuts.co", + "social" to "whois.unitedtld.com", + "softbank" to "whois.nic.softbank", + "software" to "whois.rightside.co", + "sohu" to "whois.gtld.knet.cn", + "solar" to "whois.donuts.co", + "solutions" to "whois.donuts.co", + "sony" to "whois.nic.sony", + "soy" to "domain-registry-whois.l.google.com", + "space" to "whois.nic.space", + "spiegel" to "whois.ksregistry.net", + "spreadbetting" to "whois.nic.spreadbetting", + "srl" to "whois.afilias-srs.net", + "srt" to "whois.afilias-srs.net", + "st" to "whois.nic.st", + "stada" to "whois.afilias-srs.net", + "star" to "whois.afilias-srs.net", + "starhub" to "whois.nic.starhub", + "statebank" to "whois.afilias-srs.net", + "statoil" to "whois.nic.statoil", + "stc" to "whois.centralnic.com", + "stcgroup" to "whois.centralnic.com", + "stockholm" to "whois.afilias-srs.net", + "storage" to "whois.afilias-srs.net", + "store" to "whois.nic.store", + "studio" to "whois.rightside.co", + "study" to "whois.nic.study", + "style" to "whois.donuts.co", + "su" to "whois.ripn.net", + "sochi.su" to "whois.nic.ru", + "sucks" to "whois.nic.sucks", + "supplies" to "whois.donuts.co", + "supply" to "whois.donuts.co", + "support" to "whois.donuts.co", + "surf" to "whois-dub.mm-registry.com", + "surgery" to "whois.donuts.co", + "swatch" to "whois.nic.swatch", + "swiss" to "whois.nic.swiss", + "sx" to "whois.sx", + "sy" to "whois.tld.sy", + "sydney" to "whois.nic.sydney", + "symantec" to "whois.nic.symantec", + "systems" to "whois.donuts.co", + "tab" to "whois.nic.tab", + "taipei" to "whois.nic.taipei", + "tatamotors" to "whois.nic.tatamotors", + "tatar" to "whois.nic.tatar", + "tattoo" to "whois.uniregistry.net", + "tax" to "whois.donuts.co", + "taxi" to "whois.donuts.co", + "tc" to "whois.adamsnames.tc", + "tci" to "whois.agitsys.net", + "tdk" to "whois.nic.tdk", + "team" to "whois.donuts.co", + "tech" to "whois.nic.tech", + "technology" to "whois.donuts.co", + "tel" to "whois.nic.tel", + "telecity" to "whois.nic.telecity", + "telefonica" to "whois-fe.telefonica.tango.knipp.de", + "temasek" to "whois.afilias-srs.net", + "tennis" to "whois.donuts.co", + "teva" to "whois.nic.teva", + "tf" to "whois.adamsnames.tc", + "tg" to "whois.nic.tg", + "th" to "whois.thnic.co.th", + "ac.th" to "whois.thnic.net", + "co.th" to "whois.thnic.net", + "go.th" to "whois.thnic.net", + "in.th" to "whois.thnic.net", + "mi.th" to "whois.thnic.net", + "net.th" to "whois.thnic.net", + "or.th" to "whois.thnic.net", + "thd" to "whois.afilias-srs.net", + "theater" to "whois.donuts.co", + "theatre" to "whois.nic.theatre", + "tiaa" to "whois.nic.tiaa", + "tickets" to "whois.nic.tickets", + "tienda" to "whois.donuts.co", + "tiffany" to "whois.nic.tiffany", + "tiffay" to "whois.nic.tiffany", + "tips" to "whois.donuts.co", + "tires" to "whois.donuts.co", + "tirol" to "whois.nic.tirol", + "tk" to "whois.dot.tk", + "tl" to "whois.nic.tl", + "tm" to "whois.nic.tm", + "tn" to "whois.ati.tn", + "to" to "monarch.tonic.to", + "today" to "whois.donuts.co", + "tokyo" to "whois.nic.tokyo", + "tools" to "whois.donuts.co", + "top" to "whois.nic.top", + "toray" to "whois.nic.toray", + "toshiba" to "whois.nic.toshiba", + "total" to "whois-total.nic.fr", + "tours" to "whois.donuts.co", + "town" to "whois.donuts.co", + "toyota" to "whois.nic.toyota", + "toys" to "whois.donuts.co", + "tr" to "whois.nic.tr", + "bbs.tr" to "whois.metu.edu.tr", + "com.tr" to "whois.metu.edu.tr", + "edu.tr" to "whois.metu.edu.tr", + "gov.tr" to "whois.metu.edu.tr", + "k12.tr" to "whois.metu.edu.tr", + "mil.tr" to "whois.metu.edu.tr", + "net.tr" to "whois.metu.edu.tr", + "org.tr" to "whois.metu.edu.tr", + "trade" to "whois.nic.trade", + "trading" to "whois.nic.trading", + "training" to "whois.donuts.co", + "travel" to "whois.nic.travel", + "travelchannel" to "whois.nic.travelchannel", + "travelers" to "whois.afilias-srs.net", + "travelersinsurance" to "whois.afilias-srs.net", + "trust" to "whois.nic.trust", + "trv" to "whois.afilias-srs.net", + "tui" to "whois.ksregistry.net", + "tv" to "tvwhois.verisign-grs.com", + "tvs" to "whois.afilias-srs.net", + "tw" to "whois.twnic.net.tw", + "com.tw" to "whois.twnic.net", + "net.tw" to "whois.twnic.net", + "org.tw" to "whois.twnic.net", + "tz" to "whois.tznic.or.tz", + "ua" to "whois.ua", + "biz.ua" to "whois.biz.ua", + "co.ua" to "whois.co.ua", + "com.ua" to "whois.net.ua", + "dn.ua" to "whois.net.ua", + "in.ua" to "whois.in.ua", + "kh.ua" to "whois.net.ua", + "kiev.ua" to "whois.net.ua", + "lg.ua" to "whois.net.ua", + "lviv.ua" to "whois.net.ua", + "net.ua" to "whois.net.ua", + "org.ua" to "whois.net.ua", + "pp.ua" to "whois.pp.ua", + "ubank" to "whois.nic.ubank", + "ubs" to "whois.nic.ubs", + "uconnect" to "whois.afilias-srs.net", + "ug" to "whois.co.ug", + "uk" to "whois.nic.uk", + "ac.uk" to "whois.ja.net", + "co.uk" to "whois.nic.uk", + "gov.uk" to "whois.ja.net", + "ltd.uk" to "whois.nic.uk", + "me.uk" to "whois.nic.uk", + "net.uk" to "whois.nic.uk", + "org.uk" to "whois.nic.uk", + "plc.uk" to "whois.nic.uk", + "university" to "whois.donuts.co", + "uno" to "whois.nic.uno", + "uol" to "whois.gtlds.nic.br", + "ups" to "whois.afilias-srs.net", + "us" to "whois.nic.us", + "fed.us" to "whois.nic.gov", + "uy" to "whois.nic.org.uy", + "uz" to "whois.cctld.uz", + "vacations" to "whois.donuts.co", + "vana" to "whois.nic.vana", + "vanguard" to "whois.nic.vanguard", + "vc" to "whois.adamsnames.tc", + "ve" to "whois.nic.ve", + "co.ve" to "whois.nic.ve", + "com.ve" to "whois.nic.ve", + "info.ve" to "whois.nic.ve", + "net.ve" to "whois.nic.ve", + "org.ve" to "whois.nic.ve", + "web.ve" to "whois.nic.ve", + "vegas" to "whois.afilias-srs.net", + "ventures" to "whois.donuts.co", + "verisign" to "whois.nic.verisign", + "vermögensberater" to "whois.ksregistry.net", + "vermögensberatung" to "whois.ksregistry.net", + "versicherung" to "whois.nic.versicherung", + "vet" to "whois.rightside.co", + "vg" to "ccwhois.ksregistry.net", + "viajes" to "whois.donuts.co", + "video" to "whois.rightside.co", + "vig" to "whois.afilias-srs.net", + "viking" to "whois.afilias-srs.net", + "villas" to "whois.donuts.co", + "vin" to "whois.donuts.co", + "vip" to "whois-dub.mm-registry.com", + "virgin" to "whois.nic.virgin", + "visa" to "whois.nic.visa", + "vision" to "whois.donuts.co", + "vista" to "whois.nic.vista", + "vistaprint" to "whois.nic.vistaprint", + "viva" to "whois.centralnic.com", + "vlaanderen" to "whois.nic.vlaanderen", + "vodka" to "whois-dub.mm-registry.com", + "volkswagen" to "whois.afilias-srs.net", + "volvo" to "whois.nic.volvo", + "vote" to "whois.afilias.net", + "voting" to "whois.voting.tld-box.at", + "voto" to "whois.afilias.net", + "voyage" to "whois.donuts.co", + "vu" to "vunic.vu", + "wales" to "whois.nic.wales", + "walmart" to "whois.nic.walmart", + "walter" to "whois.nic.walter", + "wang" to "whois.gtld.knet.cn", + "warman" to "whois.nic.warman", + "watch" to "whois.donuts.co", + "webcam" to "whois.nic.webcam", + "weber" to "whois.nic.weber", + "website" to "whois.nic.website", + "wed" to "whois.nic.wed", + "wedding" to "whois-dub.mm-registry.com", + "weibo" to "whois.afilias-srs.net", + "wf" to "whois.nic.fr", + "whoswho" to "whois.nic.whoswho", + "wien" to "whois.nic.wien", + "wiki" to "whois.nic.wiki", + "win" to "whois.nic.win", + "wine" to "whois.donuts.co", + "wme" to "whois.centralnic.com", + "wolterskluwer" to "whois.afilias-srs.net", + "woodside" to "whois.nic.woodside", + "work" to "whois-dub.mm-registry.com", + "works" to "whois.donuts.co", + "world" to "whois.donuts.co", + "ws" to "whois.website.ws", + "wtc" to "whois.nic.wtc", + "wtf" to "whois.donuts.co", + "xerox" to "whois.nic.xerox", + "xfinity" to "whois.nic.xfinity", + "xihuan" to "whois.teleinfo.cn", + "xin" to "whois.afilias-srs.net", + "xperia" to "whois.nic.xperia", + "xxx" to "whois.nic.xxx", + "xyz" to "whois.nic.xyz", + "yachts" to "whois.afilias-srs.net", + "yodobashi" to "whois.nic.gmo", + "yoga" to "whois-dub.mm-registry.com", + "youtube" to "domain-registry-whois.l.google.com", + "yt" to "whois.nic.fr", + "yun" to "whois.teleinfo.cn", + "za" to "whois.ac.za", + "ac.za" to "whois.ac.za", + "alt.za" to "whois.alt.za", + "co.za" to "coza-whois.registry.net.za", + "edu.za" to "whois.co.za", + "gov.za" to "whois.gov.za", + "mil.za" to "whois.co.za", + "net.za" to "net-whois.registry.net.za", + "ngo.za" to "whois.co.za", + "nom.za" to "whois.co.za", + "org.za" to "org-whois.registry.net.za", + "school.za" to "whois.co.za", + "tm.za" to "whois.co.za", + "web.za" to "web-whois.registry.net.za", + "zara" to "whois.afilias-srs.net", + "zip" to "domain-registry-whois.l.google.com", + "zm" to "whois.nic.zm", + "zone" to "whois.donuts.co", + "zuerich" to "whois.ksregistry.net", + "бг" to "whois.imena.bg", + "бел" to "whois.cctld.by", + "дети" to "whois.nic.xn--d1acj3b", + "ею" to "whois.eu", + "ком" to "whois.nic.xn--j1aef", + "мкд" to "whois.marnet.mk", + "москва" to "whois.nic.xn--80adxhks", + "онлайн" to "whois.online.rs.corenic.net", + "орг" to "whois.publicinterestregistry.net", + "рус" to "whois.nic.xn--p1acf", + "рф" to "whois.ripn.net", + "сайт" to "whois.online.rs.corenic.net", + "срб" to "whois.rnids.rs", + "укр" to "whois.dotukr.com", + "қаз" to "whois.nic.kz", + "հայ" to "whois.amnic.net", + "קום" to "whois.nic.xn--9dbq2a", + "ابوظبي" to "whois.afilias-srs.net", + "الجزائر" to "whois.nic.dz", + "السعودية" to "whois.nic.net.sa", + "العليان" to "whois.nic.xn--mgba7c0bbn0a", + "امارات" to "whois.aeda.net.ae", + "ایران" to "whois.nic.ir", + "بيتك" to "whois.nic.xn--ngbe9e0a", + "بھارت" to "whois.inregistry.net", + "سورية" to "whois.tld.sy", + "شبكة" to "whois.nic.xn--ngbc5azd", + "عمان" to "whois.registry.om", + "فلسطين" to "whois.pnina.ps", + "قطر" to "whois.registry.qa", + "كوم" to "whois.nic.xn--fhbei", + "مصر" to "whois.dotmasr.eg", + "موقع" to "whois.afilias-srs.net", + "همراه" to "whois.agitsys.net", + "कॉम" to "whois.nic.xn--11b4c3d", + "नेट" to "whois.nic.xn--c2br7g", + "भारत" to "whois.inregistry.net", + "संगठन" to "whois.publicinterestregistry.net", + "ভারত" to "whois.inregistry.net", + "ਭਾਰਤ" to "whois.inregistry.net", + "ભારત" to "whois.inregistry.net", + "இந்தியா" to "whois.inregistry.net", + "சிங்கப்பூர்" to "whois.sgnic.sg", + "భారత్" to "whois.inregistry.net", + "คอม" to "whois.nic.xn--42c2d9a", + "ไทย" to "whois.thnic.co.th", + "გე" to "whois.itdc.ge", + "みんな" to "domain-registry-whois.l.google.com", + "グーグル" to "domain-registry-whois.l.google.com", + "コム" to "whois.nic.xn--tckwe", + "世界" to "whois.nic.xn--rhqv96g", + "中信" to "whois.gtld.knet.cn", + "中国" to "cwhois.cnnic.cn", + "中國" to "cwhois.cnnic.cn", + "中文网" to "whois.afilias-srs.net", + "企业" to "whois.donuts.co", + "佛山" to "whois.ngtld.cn", + "信息" to "whois.ngtld.cn", + "八卦" to "whois.nic.xn--45q11c", + "公司" to "whois.ngtld.cn", + "公益" to "whois.conac.cn", + "台湾" to "whois.twnic.net.tw", + "台灣" to "whois.twnic.net.tw", + "商城" to "whois.gtld.knet.cn", + "商店" to "whois.donuts.co", + "嘉里" to "whois.nic.xn--w4rs40l", + "嘉里大酒店" to "whois.nic.xn--w4r85el8fhu5dnra", + "在线" to "whois.afilias-srs.net", + "大众汽车" to "whois.afilias-srs.net", + "大拿" to "whois.nic.xn--pssy2u", + "娱乐" to "whois.donuts.co", + "工行" to "whois.afilias-srs.net", + "广东" to "whois.ngtld.cn", + "微博" to "whois.afilias-srs.net", + "慈善" to "whois.gtld.knet.cn", + "我爱你" to "whois.gtld.knet.cn", + "手机" to "whois.afilias-srs.net", + "政务" to "whois.conac.cn", + "政府" to "whois.nic.xn--mxtq1m", + "新加坡" to "whois.sgnic.sg", + "新闻" to "whois.nic.xn--efvy88h", + "时尚" to "whois.gtld.knet.cn", + "机构" to "whois.publicinterestregistry.net", + "淡马锡" to "whois.afilias-srs.net", + "游戏" to "whois.donuts.co", + "澳門" to "whois.monic.mo", + "点看" to "whois.nic.xn--3pxu8k", + "移动" to "whois.afilias.net", + "组织机构" to "whois.publicinterestregistry.net", + "网址" to "whois.registry.knet.cn", + "网店" to "whois.nic.xn--hxt814e", + "网站" to "whois.afilias-srs.net", + "网络" to "whois.ngtld.cn", + "联通" to "whois.imena.bg", + "诺基亚" to "whois.afilias-srs.net", + "谷歌" to "domain-registry-whois.l.google.com", + "购物" to "whois.afilias-srs.net", + "集团" to "whois.gtld.knet.cn", + "電訊盈科" to "whois.afilias-srs.net", + "飞利浦" to "whois.nic.xn--kcrx77d1x4a", + "香格里拉" to "whois.nic.xn--5su34j936bgsg", + "香港" to "whois.hkirc.hk", + "닷넷" to "whois.nic.xn--t60b56a", + "닷컴" to "whois.nic.xn--mk1bu44c", + "삼성" to "whois.kr", + "한국" to "whois.kr", + ) +} diff --git a/proprietary/src/test/kotlin/rip/crit/commonip/proprietary/LiveProviderTest.kt b/proprietary/src/test/kotlin/rip/crit/commonip/proprietary/LiveProviderTest.kt new file mode 100644 index 0000000..69863ab --- /dev/null +++ b/proprietary/src/test/kotlin/rip/crit/commonip/proprietary/LiveProviderTest.kt @@ -0,0 +1,13 @@ +package rip.crit.commonip.proprietary + +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable + +class LiveProviderTest { + @Test + @EnabledIfEnvironmentVariable(named = "COMMONIP_LIVE_TESTS", matches = "true") + fun `starlink live feed has records in isolated opt in test`() { + assertTrue(StarlinkGeoFeed().pull().records.isNotEmpty()) + } +} diff --git a/proprietary/src/test/kotlin/rip/crit/commonip/proprietary/ProviderTest.kt b/proprietary/src/test/kotlin/rip/crit/commonip/proprietary/ProviderTest.kt new file mode 100644 index 0000000..45a20b2 --- /dev/null +++ b/proprietary/src/test/kotlin/rip/crit/commonip/proprietary/ProviderTest.kt @@ -0,0 +1,19 @@ +package rip.crit.commonip.proprietary + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ProviderTest { + @Test + fun `open geofeed uses injected fixture transport`() { + val feed = + OpenGeoFeed(HttpTransport { _, _ -> "203.0.113.0/24,US,CA,Los Angeles,90001" }) + .pull("https://fixture.invalid/feed.csv") + assertEquals("Los Angeles", feed.records.single().city) + } + + @Test + fun `starlink endpoint is opt in provider`() { + assertEquals("https://geoip.starlinkisp.net/feed.csv", StarlinkGeoFeed.URL) + } +} diff --git a/scripts/commonip.service b/scripts/commonip.service new file mode 100644 index 0000000..61d4bb8 --- /dev/null +++ b/scripts/commonip.service @@ -0,0 +1,12 @@ +[Unit] +Description=CommonIP node +After=network-online.target + +[Service] +User=commonip +WorkingDirectory=/opt/commonip +ExecStart=/opt/commonip/commonip +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/scripts/deploy-ssh.sh b/scripts/deploy-ssh.sh new file mode 100755 index 0000000..f696fcb --- /dev/null +++ b/scripts/deploy-ssh.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +deploy_host="${1:?host is required}" +deploy_user="${2:?user is required}" +deploy_path="${COMMONIP_REMOTE_PATH:-/opt/commonip}" + +./gradlew :app:nativeImage + +ssh "${deploy_user}@${deploy_host}" "sudo useradd --system --create-home --shell /usr/sbin/nologin commonip 2>/dev/null || true; sudo install -d -o commonip -g commonip ${deploy_path}" +scp app/build/native/commonip "${deploy_user}@${deploy_host}:/tmp/commonip" +ssh "${deploy_user}@${deploy_host}" "sudo install -o commonip -g commonip -m 0755 /tmp/commonip ${deploy_path}/commonip" +scp scripts/commonip.service "${deploy_user}@${deploy_host}:/tmp/commonip.service" +ssh "${deploy_user}@${deploy_host}" "sudo install -m 0644 /tmp/commonip.service /etc/systemd/system/commonip.service; sudo systemctl daemon-reload; sudo systemctl enable --now commonip" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..fe175a1 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + includeBuild("build-logic") +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +rootProject.name = "commonip" + +include("app", "proprietary", "packages") +include(":packages:serialize", ":packages:rawr", ":packages:ml", ":packages:decentraland") +include(":packages:decentraland:kademlia", ":packages:decentraland:git", ":packages:decentraland:chksum")