diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..826f7b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,121 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/codeStyles/ +.idea/**/codeStyleSettings.xml + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/shelf/ + + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +###################### +# End JetBrains IDEs # +###################### + + +# From https://github.com/github/gitignore/blob/master/Gradle.gitignore +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + + + + +# From https://github.com/github/gitignore/blob/master/Java.gitignore +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + + +########################################################## +# Specific to this module + +# iml files are generated by intellij/gradle now +**/*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..75ee84e --- /dev/null +++ b/LICENSE @@ -0,0 +1,119 @@ + - NetworkUtils - Provides various utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands) + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/NetworkUtils + Copyright 2020 + Dorkbox LLC + + Extra license information + - Netty - + [The Apache Software License, Version 2.0] + https://netty.io/ + Copyright 2014 + The Netty Project + This product contains a modified portion of Netty Network Utils + + - Apache Harmony - + [The Apache Software License, Version 2.0] + http://archive.apache.org/dist/harmony/ + Copyright 2010 + The Apache Software Foundation + This product contains a modified portion of 'Apache Harmony', an open source Java SE + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - kotlin-logging - Lightweight logging framework for Kotlin + [The Apache Software License, Version 2.0] + https://github.com/MicroUtils/kotlin-logging + Copyright 2020 + Ohad Shai + + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + http://www.slf4j.org + Copyright 2019 + QOS.ch + + - Executor - Shell, JVM, and SSH command execution on Linux, MacOS, or Windows for Java 11+ + [The Apache Software License, Version 2.0] + https://git.dorkbox.com/dorkbox/Executor + Copyright 2020 + Dorkbox LLC + + Extra license information + - ZT Process Executor - + [The Apache Software License, Version 2.0] + https://github.com/zeroturnaround/zt-exec + Copyright 2014 + ZeroTurnaround LLC + + - Apache Commons Exec - + [The Apache Software License, Version 2.0] + https://commons.apache.org/proper/commons-exec/ + Copyright 2014 + The Apache Software Foundation + + - Kotlin - + [The Apache Software License, Version 2.0] + https://github.com/JetBrains/kotlin + Copyright 2020 + JetBrains s.r.o. and Kotlin Programming Language contributors + Kotlin Compiler, Test Data+Libraries, and Tools repository contain third-party code, to which different licenses may apply + See: https://github.com/JetBrains/kotlin/blob/master/license/README.md + + - kotlinx.coroutines - Library support for Kotlin coroutines with multiplatform support + [The Apache Software License, Version 2.0] + https://github.com/Kotlin/kotlinx.coroutines + Copyright 2020 + JetBrains s.r.o. + + - kotlin-logging - Lightweight logging framework for Kotlin + [The Apache Software License, Version 2.0] + https://github.com/MicroUtils/kotlin-logging + Copyright 2020 + Ohad Shai + + - SLF4J - Simple facade or abstraction for various logging frameworks + [MIT License] + http://www.slf4j.org + Copyright 2019 + QOS.ch + + - SSHJ - SSHv2 library for Java + [The Apache Software License, Version 2.0] + https://github.com/hierynomus/sshj + Copyright 2020 + Jeroen van Erp + SSHJ Contributors + + Extra license information + - Apache MINA - + [The Apache Software License, Version 2.0] + https://mina.apache.org/sshd-project/ + The Apache Software Foundation + + - Apache Commons-Net - + [The Apache Software License, Version 2.0] + https://commons.apache.org/proper/commons-net/ + The Apache Software Foundation + + - JZlib - + [The Apache Software License, Version 2.0] + http://www.jcraft.com/jzlib + Atsuhiko Yamanaka + JCraft, Inc. + + - Bouncy Castle Crypto - + [The Apache Software License, Version 2.0] + http://www.bouncycastle.org + The Legion of the Bouncy Castle Inc + + - ed25519-java - + [Public Domain, per Creative Commons CC0] + https://github.com/str4d/ed25519-java + https://github.com/str4d diff --git a/LICENSE.Apachev2 b/LICENSE.Apachev2 new file mode 100644 index 0000000..79d1b97 --- /dev/null +++ b/LICENSE.Apachev2 @@ -0,0 +1,218 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE.CC0 b/LICENSE.CC0 new file mode 100644 index 0000000..1625c17 --- /dev/null +++ b/LICENSE.CC0 @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. \ No newline at end of file diff --git a/LICENSE.MIT b/LICENSE.MIT new file mode 100644 index 0000000..2e9f5aa --- /dev/null +++ b/LICENSE.MIT @@ -0,0 +1,21 @@ + MIT License + + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e69de29..9a06884 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,41 @@ +NetworkUtils +============ + +###### [![Dorkbox](https://badge.dorkbox.com/dorkbox.svg "Dorkbox")](https://git.dorkbox.com/dorkbox/ObjectPool) [![Github](https://badge.dorkbox.com/github.svg "Github")](https://github.com/dorkbox/ObjectPool) [![Gitlab](https://badge.dorkbox.com/gitlab.svg "Gitlab")](https://gitlab.com/dorkbox/ObjectPool) [![Bitbucket](https://badge.dorkbox.com/bitbucket.svg "Bitbucket")](https://bitbucket.org/dorkbox/ObjectPool) + +Provides various Network Utilities, for IP/MAC address conversion, PING (without having to be root or with proper permissions) and managing various linux network properties + + + +``` + +  +  + + +Maven Info +--------- +``` + + ... + + com.dorkbox + NetworkUtils + 0.1 + + +``` + +Gradle Info +--------- +```` +dependencies { + ... + compile 'com.dorkbox:NetworkUtils:0.1' +} +```` + +License +--------- +This project is © 2014 dorkbox llc, and is distributed under the terms of the Apache v2.0 License. See file "LICENSE" for further references. + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..6088b46 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,153 @@ +/* + * Copyright 2020 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import dorkbox.gradle.kotlin +import java.time.Instant + +/////////////////////////////// +////// PUBLISH TO SONATYPE / MAVEN CENTRAL +////// TESTING : (to local maven repo) <'publish and release' - 'publishToMavenLocal'> +////// RELEASE : (to sonatype/maven central), <'publish and release' - 'publishToSonatypeAndRelease'> +/////////////////////////////// + +plugins { + java + + id("com.dorkbox.GradleUtils") version "1.9" + id("com.dorkbox.Licensing") version "2.2" + id("com.dorkbox.VersionUpdate") version "2.0" + id("com.dorkbox.GradlePublish") version "1.4" + id("com.dorkbox.GradleModuleInfo") version "1.0" + + kotlin("jvm") version "1.3.72" +} + +object Extras { + // set for the project + const val description = "Utilities for managing network configurations, IP/MAC address conversion, and ping (via OS native commands)" + const val group = "com.dorkbox" + const val version = "0.1" + + // set as project.ext + const val name = "NetworkUtils" + const val id = "NetworkUtils" + const val vendor = "Dorkbox LLC" + const val vendorUrl = "https://dorkbox.com/" + const val url = "https://git.dorkbox.com/dorkbox/NetworkUtils" + + val buildDate = Instant.now().toString() +} + +/////////////////////////////// +///// assign 'Extras' +/////////////////////////////// +GradleUtils.load("$projectDir/../../gradle.properties", Extras) +GradleUtils.fixIntellijPaths() +GradleUtils.defaultResolutionStrategy() +GradleUtils.compileConfiguration(JavaVersion.VERSION_11) + + +licensing { + license(License.APACHE_2) { + description(Extras.description) + url(Extras.url) + author(Extras.vendor) + extra("Netty", License.APACHE_2) { + it.copyright(2014) + it.author("The Netty Project") + it.url("https://netty.io/") + it.note("This product contains a modified portion of Netty Network Utils") + } + extra("Apache Harmony", License.APACHE_2) { + it.copyright(2010) + it.author("The Apache Software Foundation") + it.url("http://archive.apache.org/dist/harmony/") + it.note("This product contains a modified portion of 'Apache Harmony', an open source Java SE") + } + } +} + + +sourceSets { + main { + java { + setSrcDirs(listOf("src")) + + // want to include java files for the source. 'setSrcDirs' resets includes... + include("**/*.java") + } + + kotlin { + setSrcDirs(listOf("src")) + + // want to include java files for the source. 'setSrcDirs' resets includes... + include("**/*.java", "**/*.kt") + } + } + + test { + java { + setSrcDirs(listOf("test")) + + // want to include java files for the source. 'setSrcDirs' resets includes... + include("**/*.java") + } + + kotlin { + setSrcDirs(listOf("test")) + + // want to include java files for the source. 'setSrcDirs' resets includes... + include("**/*.java", "**/*.kt") + } + } +} + +repositories { + mavenLocal() // this must be first! + jcenter() +} + + +tasks.jar.get().apply { + manifest { + // https://docs.oracle.com/javase/tutorial/deployment/jar/packageman.html + attributes["Name"] = Extras.name + + attributes["Specification-Title"] = Extras.name + attributes["Specification-Version"] = Extras.version + attributes["Specification-Vendor"] = Extras.vendor + + attributes["Implementation-Title"] = "${Extras.group}.${Extras.id}" + attributes["Implementation-Version"] = Extras.buildDate + attributes["Implementation-Vendor"] = Extras.vendor + + attributes["Automatic-Module-Name"] = Extras.id + } +} + +dependencies { + implementation(kotlin("stdlib-jdk8")) + + // https://github.com/MicroUtils/kotlin-logging + implementation("io.github.microutils:kotlin-logging:1.7.9") // slick kotlin wrapper for slf4j + implementation("org.slf4j:slf4j-api:1.7.25") + + implementation("com.dorkbox:Executor:1.0") + + testImplementation("junit:junit:4.13") + testImplementation("ch.qos.logback:logback-classic:1.2.3") + testImplementation("com.dorkbox:Utilities:1.6") +} diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..b0d6d0a --- /dev/null +++ b/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# 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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9991c50 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@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 http://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 + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..cce9d6c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,15 @@ +/* + * Copyright 2018 dorkbox, llc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/src/dorkbox/netUtil/Arp.kt b/src/dorkbox/netUtil/Arp.kt new file mode 100644 index 0000000..8a21897 --- /dev/null +++ b/src/dorkbox/netUtil/Arp.kt @@ -0,0 +1,38 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor +import java.io.File + +object Arp { + // Now setup ARP Proxy for this interface (so ARP requests are answered correctly) + fun proxyAdd(interfaceName: String) { + if (Common.OS_LINUX) { + val file = File("/proc/sys/net/ipv4/conf/$interfaceName/proxy_arp") + if (!file.readText().contains("1")) { + file.appendText("1") + } + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun proxyDel(interfaceName: String) { + if (Common.OS_LINUX) { + val file = File("/proc/sys/net/ipv4/conf/$interfaceName/proxy_arp") + file.delete() + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun add(interfaceName: String, targetIpAddress: String, targetMacAddress: String) { + if (Common.OS_LINUX) { + // have to make sure that the host interface will answer ARP for the "target" ip (normally, it will not for veth interfaces) + Executor() + .command("/usr/sbin/arp", "-i", interfaceName, "-s", targetIpAddress, targetMacAddress) + .startBlocking() + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/Common.kt b/src/dorkbox/netUtil/Common.kt new file mode 100644 index 0000000..10a6312 --- /dev/null +++ b/src/dorkbox/netUtil/Common.kt @@ -0,0 +1,109 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package dorkbox.netUtil + +import mu.KotlinLogging +import java.security.AccessController +import java.security.PrivilegedAction + +/** + * Network Utilities. MAC, IP, NameSpace, etc + */ +internal object Common { + val OS_LINUX: Boolean + val OS_WINDOWS: Boolean + val OS_MAC: Boolean + + init { + val osName: String = try { + if (System.getSecurityManager() == null) { + System.getProperty("os.name", "linux") + } + else { + AccessController.doPrivileged(PrivilegedAction { System.getProperty("os.name", "linux") }) + } + } catch (ignored: java.lang.Exception) { + "linux" + } + + if (osName.startsWith("mac") || osName.startsWith("darwin")) { + OS_LINUX = false + OS_WINDOWS = false + OS_MAC = true + } else if (osName.startsWith("windows")) { + OS_LINUX = false + OS_WINDOWS = true + OS_MAC = false + } else { + OS_LINUX = true + OS_WINDOWS = false + OS_MAC = false + } + } + + + internal val logger = KotlinLogging.logger(Common::class.java.simpleName) + + fun getBoolean(property: String, defaultValue: Boolean): Boolean { + var value: String = System.getProperty(property, defaultValue.toString()) ?: return defaultValue + value = value.trim().toLowerCase() + + if (value.isEmpty()) { + return defaultValue + } + + if ("false" == value || "no" == value || "0" == value) { + return false + } + return if ("true" == value || "yes" == value || "1" == value) { + true + } + else defaultValue + } + + private val HEX2B: IntArray = IntArray('f'.toInt()+1).apply { + set('0'.toInt(), 0) + set('1'.toInt(), 1) + set('2'.toInt(), 2) + set('3'.toInt(), 3) + set('4'.toInt(), 4) + set('5'.toInt(), 5) + set('6'.toInt(), 6) + set('7'.toInt(), 7) + set('8'.toInt(), 8) + set('9'.toInt(), 9) + set('A'.toInt(), 10) + set('B'.toInt(), 11) + set('C'.toInt(), 12) + set('D'.toInt(), 13) + set('E'.toInt(), 14) + set('F'.toInt(), 15) + set('a'.toInt(), 10) + set('b'.toInt(), 11) + set('c'.toInt(), 12) + set('d'.toInt(), 13) + set('e'.toInt(), 14) + set('f'.toInt(), 15) + } + + /** + * Helper to decode half of a hexadecimal number from a string. + * + * @param c The ASCII character of the hexadecimal number to decode. + * Must be in the range `[0-9a-fA-F]`. + * + * @return The hexadecimal value represented in the ASCII character + * given, or `-1` if the character is invalid. + */ + fun decodeHexNibble(c: Char): Int { + // Character.digit() is not used here, as it addresses a larger + // set of characters (both ASCII and full-width latin letters). + val index = c.toInt() + + return try { + HEX2B[index] + } catch (e: Exception) { + -1 + } + } +} diff --git a/src/dorkbox/netUtil/Dhcp.kt b/src/dorkbox/netUtil/Dhcp.kt new file mode 100644 index 0000000..1f12d59 --- /dev/null +++ b/src/dorkbox/netUtil/Dhcp.kt @@ -0,0 +1,39 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor +import java.io.File + +/** + * + */ +object Dhcp { + + fun start(nameSpace: String, id: String, interfaceName: String) { + if (Common.OS_LINUX) { + stop(nameSpace, id, interfaceName) + val dhcpPidFile = "/var/run/dhclient-$id.pid" + Executor().command("/sbin/dhclient", "-pf", dhcpPidFile, interfaceName).startBlocking(); + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun stop(nameSpace: String, id: String, interfaceName: String) { + if (Common.OS_LINUX) { + val dhcpPidFile = "/var/run/dhclient-$id.pid" + + // close the dhclient if it was already running (based on pid file), and delete the pid file + Executor().command("/sbin/dhclient", "-r -pf", dhcpPidFile, interfaceName).startBlocking() + + // short break + try { + Thread.sleep(500) + } catch (e: InterruptedException) { + e.printStackTrace() + } + File(dhcpPidFile).delete() + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/Dns.kt b/src/dorkbox/netUtil/Dns.kt new file mode 100644 index 0000000..2d4d57a --- /dev/null +++ b/src/dorkbox/netUtil/Dns.kt @@ -0,0 +1,41 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException + +/** + * + */ +object Dns { + /** + * @throws IOException if the DNS resolve.conf file cannot be read + */ + fun setDNSServers(dnsServersString: String) { + if (Common.OS_LINUX) { + val dnsServers = dnsServersString.split(","); + val dnsFile = File("/etc/resolvconf/resolv.conf.d/head"); + + if (!dnsFile.canRead()) { + throw IOException("Unable to initialize dns server file. Something is SERIOUSLY wrong"); + } + + BufferedWriter(FileWriter(dnsFile)).use { + it.write("# File location: /etc/resolvconf/resolv.conf.d/head\n"); + it.write("# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)\n"); + + dnsServers.forEach { dns -> + it.write("nameserver $dns\n"); + } + + it.flush(); + } + + Executor().command("resolvconf", "-u").startBlocking() + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/IP.kt b/src/dorkbox/netUtil/IP.kt new file mode 100644 index 0000000..03a9484 --- /dev/null +++ b/src/dorkbox/netUtil/IP.kt @@ -0,0 +1,275 @@ +package dorkbox.netUtil + +import dorkbox.netUtil.Common.logger +import java.net.* + +/** + * A class that holds a number of network-related constants, also from: + * (Netty, apache 2.0 license) + * https://github.com/netty/netty/blob/4.1/common/src/main/java/io/netty/util/NetUtil.java + * + * This class borrowed some of its methods from a modified fork of the + * [Inet6Util class] + * (http://svn.apache.org/repos/asf/harmony/enhanced/java/branches/java6/classlib/modules/luni/src/main/java/org/apache/harmony/luni/util/Inet6Util.java) which was part of Apache Harmony. + */ +object IP { + + /** + * The [InetAddress] that represents the loopback address. If IPv6 stack is available, it will refer to + * [.LOCALHOST6]. Otherwise, [.LOCALHOST4]. + */ + val LOCALHOST: InetAddress + + /** + * The loopback [NetworkInterface] of the current machine + */ + val LOOPBACK_IF: NetworkInterface + + /** + * The loopback address + */ + const val LOOPBACK = "127.0.0.1" + + init { + logger.debug { + "-Djava.net.preferIPv4Stack: ${IPv4.isPreferred}\n" + + "-Djava.net.preferIPv6Addresses: ${IPv6.isPreferred}" + } + + // Retrieve the list of available network interfaces. + val netInterfaces = mutableListOf() + try { + val interfaces = NetworkInterface.getNetworkInterfaces() + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + val iface: NetworkInterface = interfaces.nextElement() + // Use the interface with proper INET addresses only. + if (SocketUtils.addressesFromNetworkInterface(iface).hasMoreElements()) { + netInterfaces.add(iface) + } + } + } + } catch (e: SocketException) { + logger.warn("Failed to retrieve the list of available network interfaces", e) + } + + + // Find the first loopback interface available from its INET address (127.0.0.1 or ::1) + // Note that we do not use NetworkInterface.isLoopback() in the first place because it takes long time + // on a certain environment. (e.g. Windows with -Djava.net.preferIPv4Stack=true) + + var loopbackIface: NetworkInterface? = null + var loopbackAddr: InetAddress? = null + + loop@ for (iface in netInterfaces) { + val i = SocketUtils.addressesFromNetworkInterface(iface) + while (i.hasMoreElements()) { + val addr: InetAddress = i.nextElement() + if (addr.isLoopbackAddress) { + // Found + loopbackIface = iface + loopbackAddr = addr + break@loop + } + } + } + + // If failed to find the loopback interface from its INET address, fall back to isLoopback(). + if (loopbackIface == null) { + try { + for (iface in netInterfaces) { + if (iface.isLoopback) { + val i = SocketUtils.addressesFromNetworkInterface(iface) + if (i.hasMoreElements()) { + // Found the one with INET address. + loopbackIface = iface + loopbackAddr = i.nextElement() + break + } + } + } + if (loopbackIface == null) { + logger.warn("Failed to find the loopback interface") + } + } catch (e: SocketException) { + logger.warn("Failed to find the loopback interface", e) + } + } + + if (loopbackIface != null) { + // Found the loopback interface with an INET address. + logger.debug { + "Loopback interface: ${loopbackIface.name} (${loopbackIface.displayName}, ${loopbackAddr!!.hostAddress})" + } + } else { + // Could not find the loopback interface, but we can't leave LOCALHOST as null. + // Use LOCALHOST6 or LOCALHOST4, preferably the IPv6 one. + if (loopbackAddr == null) { + try { + if (NetworkInterface.getByInetAddress(IPv6.LOCALHOST) != null) { + logger.debug { + "Using hard-coded IPv6 localhost address: ${IPv6.LOCALHOST}" + } + + loopbackAddr = IPv6.LOCALHOST + } + } catch (ignore: Exception) { + + } + if (loopbackAddr == null) { + logger.debug { + "Using hard-coded IPv4 localhost address: ${IPv4.LOCALHOST}" + } + loopbackAddr = IPv4.LOCALHOST + } + } + } + + LOOPBACK_IF = loopbackIface!! + LOCALHOST = loopbackAddr!! + } + + /** + * Creates an byte[] based on an ipAddressString. No error handling is performed here. + */ + fun toBytes(ipAddressString: String): ByteArray { + if (IPv4.isValid(ipAddressString)) { + return IPv4.toBytes(ipAddressString) + } + + return IPv6.toBytes(ipAddressString) + } + + /** + * Converts 4-byte or 16-byte data into an IPv4 or IPv6 string respectively. + * + * @throws IllegalArgumentException + * if `length` is not `4` nor `16` + */ + fun toString(bytes: ByteArray, offset: Int = 0, length: Int = bytes.size): String { + return when (length) { + 4 -> { + StringBuilder(15) + .append(bytes[offset].toInt()) + .append('.') + .append(bytes[offset + 1].toInt()) + .append('.') + .append(bytes[offset + 2].toInt()) + .append('.') + .append(bytes[offset + 3].toInt()).toString() + } + 16 -> IPv6.toString(bytes, offset) + else -> throw IllegalArgumentException("length: $length (expected: 4 or 16)") + } + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * + * The output does not include Scope ID. + * + * @param ip [InetAddress] to be converted to an address string + * @param ipv4Mapped + * + * * `true` to stray from strict rfc 5952 and support the "IPv4 mapped" format + * defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) while still + * following the updated guidelines in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * * `false` to strictly follow rfc 5952 + * + * @return `String` containing the text-formatted IP address + */ + fun toString(ip: InetAddress, ipv4Mapped: Boolean = false): String { + if (ip is Inet4Address) { + return ip.getHostAddress() + } + + require(ip is Inet6Address) { "Unhandled type: $ip" } + + return IPv6.toString(ip, ipv4Mapped) + } + + /** + * Returns the [String] representation of an [InetSocketAddress]. + * + * + * The output does not include Scope ID. + * @param addr [InetSocketAddress] to be converted to an address string + * @return `String` containing the text-formatted IP address + */ + fun toString(addr: InetSocketAddress): String { + val port = addr.port.toString() + val sb: StringBuilder + + sb = if (addr.isUnresolved) { + val hostname = getHostname(addr) + newSocketAddressStringBuilder(hostname, port, !IPv6.isValid(hostname)) + } else { + val address = addr.address + val hostString = toString(address) + newSocketAddressStringBuilder(hostString, port, address is Inet4Address) + } + return sb.append(':').append(port).toString() + } + + /** + * Returns the [String] representation of a host port combo. + */ + fun toString(host: String, port: Int): String { + val portStr = port.toString() + return newSocketAddressStringBuilder(host, portStr, !IPv6.isValid(host)).append(':').append(portStr).toString() + } + + private fun newSocketAddressStringBuilder(host: String, port: String, ipv4: Boolean): StringBuilder { + val hostLen = host.length + if (ipv4) { + // Need to include enough space for hostString:port. + return StringBuilder(hostLen + 1 + port.length).append(host) + } + + // Need to include enough space for [hostString]:port. + val stringBuilder = StringBuilder(hostLen + 3 + port.length) + return if (hostLen > 1 && host[0] == '[' && host[hostLen - 1] == ']') { + stringBuilder.append(host) + } else { + stringBuilder.append('[').append(host).append(']') + } + } + + /** + * Returns the [InetAddress] representation of a [CharSequence] IP address. + * + * This method will treat all IPv4 type addresses as "IPv4 mapped" (see [.getByName]) + * + * @param ip [CharSequence] IP address to be converted to a [InetAddress] + * @return [InetAddress] representation of the `ip` or `null` if not a valid IP address. + */ + fun getByName(ip: String): InetAddress? { + return if (IPv4.isValid(ip)) { + IPv4.getByName(ip) + } else { + IPv6.getByName(ip) + } + } + + /** + * Returns [InetSocketAddress.getHostString] if Java >= 7, + * or [InetSocketAddress.getHostName] otherwise. + * @param addr The address + * @return the host string + */ + fun getHostname(addr: InetSocketAddress): String { + return addr.hostString + } +} diff --git a/src/dorkbox/netUtil/IPRoute.kt b/src/dorkbox/netUtil/IPRoute.kt new file mode 100644 index 0000000..3080327 --- /dev/null +++ b/src/dorkbox/netUtil/IPRoute.kt @@ -0,0 +1,59 @@ +package dorkbox.netUtil + +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.util.* + +/** + * + */ +object IPRoute { + private val reservedTable = StringBuilder(2048) + private val tableNames: MutableMap = HashMap(256) + + /** + * @throws IOException if the policy routing tables are unable to initialize + */ + fun addRtTables(tableNames: Map) { + if (Common.OS_LINUX) { + for ((tableNumber, tableName) in tableNames) { + if (tableNumber == 0 || tableNumber == 253 || tableNumber == 254 || tableNumber == 255) { + Common.logger.error("Trying to add table with same number as reserved value. Skipping.") + continue + } + + if (!IPRoute.tableNames.containsKey(tableNumber)) { + IPRoute.tableNames[tableNumber] = tableName + } else { + if (IPRoute.tableNames[tableNumber] != tableName) { + Common.logger.error("Trying to add table with the same number as another table. Skipping") + } + } + } + + val table = StringBuilder(2048) + for ((tableNumber, tableName) in IPRoute.tableNames) { + table.append(tableNumber).append(" ").append(tableName).append("\n"); + } + + val policyRouteFile = File("/etc/iproute2/rt_tables").absoluteFile + if (!policyRouteFile.canRead()) { + throw IOException("Unable to initialize policy routing tables. Something is SERIOUSLY wrong, aborting startup!") + } + + try { + BufferedWriter(FileWriter(policyRouteFile)).use { writer -> + writer.write(reservedTable.toString()) + writer.write(table.toString()) + writer.flush() + } + } catch (e: IOException) { + Common.logger.error("Error saving routing table file: {}", policyRouteFile, e) + } + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/IPv4.kt b/src/dorkbox/netUtil/IPv4.kt new file mode 100644 index 0000000..fde353b --- /dev/null +++ b/src/dorkbox/netUtil/IPv4.kt @@ -0,0 +1,678 @@ +/* + * Copyright 2020 Dorkbox, llc + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.netUtil + +import java.io.IOException +import java.io.Writer +import java.net.Inet4Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Socket +import java.util.* +import kotlin.math.floor +import kotlin.math.ln +import kotlin.math.pow + +/** + * A class that holds a number of network-related constants, also from: + * (Netty, apache 2.0 license) + * https://github.com/netty/netty/blob/4.1/common/src/main/java/io/netty/util/NetUtil.java + * + * This class borrowed some of its methods from a modified fork of the + * [Inet6Util class] + * (http://svn.apache.org/repos/asf/harmony/enhanced/java/branches/java6/classlib/modules/luni/src/main/java/org/apache/harmony/luni/util/Inet6Util.java) which was part of Apache Harmony. + */ +object IPv4 { + /** + * Returns `true` if IPv4 should be used even if the system supports both IPv4 and IPv6. Setting this + * property to `true` will disable IPv6 support. The default value of this property is `false`. + * + * @see [Java SE networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) + */ + val isPreferred = Common.getBoolean("java.net.preferIPv4Stack", false) + + /** + * The [Inet4Address] that represents the IPv4 loopback address '127.0.0.1' + */ + val LOCALHOST: Inet4Address by lazy { + // Create IPv4 loopback address. + // this will ALWAYS work + InetAddress.getByAddress("localhost", byteArrayOf(127, 0, 0, 1)) as Inet4Address + } + + /** + * Windows is unable to work with 0.0.0.0 directly, and if you use LOOPBACK, you might not be able to access the server from another + * machine. + * + * What this does is open a connection to 1.1.1.1 and see get the interface this traffic was on, and use that interface IP address + */ + val WILDCARD: String by lazy { + if (Common.OS_WINDOWS) { + // silly windows can't work with 0.0.0.0, BUT we can't use loopback because we might need to reach this machine from a different host + // what we do is open a connection to 1.1.1.1 and see what interface this happened on, and this is used as the accessible + // interface + var ip = "127.0.0.1" + + runCatching { + Socket().use { + it.connect(InetSocketAddress("1.1.1.1", 80)) + ip = it.localAddress.hostAddress + } + }.onFailure { + Common.logger.error("Unable to determine outbound traffic local address. Using loopback instead.", it) + } + + ip + } else { + // everyone else works correctly + "0.0.0.0" + } + } + + private val SLASH_REGEX = "\\.".toRegex() + + + /** + * Determine whether a given string is a valid CIDR IP address. Accepts only 1.2.3.4/24 + * + * @param ipAsString The string that will be checked. + * + * @return return true if the string is a valid IP address, false if it is not. + */ + fun isValidCidr(ipAsString: String): Boolean { + if (ipAsString.isEmpty()) { + return false + } + + val slashIndex = ipAsString.indexOf('/') + if (slashIndex < 6) { + // something is malformed. + return false + } + + val ipOnly = ipAsString.substring(0, slashIndex) + if (!isValid(ipOnly)) { + return false + } + + try { + val cidr = ipAsString.substring(slashIndex + 1).toInt() + if (cidr in 0..32) { + return true + } + } catch (ignored: Exception) { + } + + return false + } + + /** + * Takes a [String] and parses it to see if it is a valid IPV4 address. + * + * @return true, if the string represents an IPV4 address in dotted notation, false otherwise + */ + fun isValid(ip: String): Boolean { + return isValidIpV4Address(ip, 0, ip.length) + } + + internal fun isValidIpV4Address(ip: String, from: Int, toExcluded: Int): Boolean { + val len = toExcluded - from + if (len !in 7..15) { + return false + } + + var from = from + var i = ip.indexOf('.', from) + + if (i <= 0 || !isValidIpV4Word(ip, from, i)) { + return false + } + + from = i + 1 + i = ip.indexOf('.', from) + if (i <= 0 || !isValidIpV4Word(ip, from, i)) { + return false + } + + from = i + 1 + i = ip.indexOf('.', from) + if (i <= 0 || !isValidIpV4Word(ip, from, i)) { + return false + } + + if (i <= 0 || !isValidIpV4Word(ip, i + 1, toExcluded)) { + return false + } + + return true + } + + private fun isValidIpV4Word(word: CharSequence, from: Int, toExclusive: Int): Boolean { + val len = toExclusive - from + var c0 = ' ' + var c1 = ' ' + var c2 = ' ' + + if (len < 1 || len > 3 || word[from].also { c0 = it } < '0') { + return false + } + return if (len == 3) { + word[from + 1].also { c1 = it } >= '0' + && word[from + 2].also { c2 = it } >= '0' + && (c0 <= '1' && c1 <= '9' && c2 <= '9' || c0 == '2' && c1 <= '5' && (c2 <= '5' || c1 < '5' && c2 <= '9')) + } + else c0 <= '9' && (len == 1 || isValidNumericChar(word[from + 1])) + } + + internal fun isValidNumericChar(c: Char): Boolean { + return c in '0'..'9' + } + + internal fun isValidIPv4MappedChar(c: Char): Boolean { + return c == 'f' || c == 'F' + } + + + fun toBytesorNull(ip: String): ByteArray? { + if (!isValid(ip)) { + return null + } + + var i = 0 + return byteArrayOf( + ipv4WordToByte(ip, 0, ip.indexOf('.', 1).also { i = it }), + ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }), + ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }), + ipv4WordToByte(ip, i + 1, ip.length) + ) + } + + fun toBytes(ip: String): ByteArray { + var i = 0 + return byteArrayOf( + ipv4WordToByte(ip, 0, ip.indexOf('.', 1).also { i = it }), + ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }), + ipv4WordToByte(ip, i + 1, ip.indexOf('.', i + 2).also { i = it }), + ipv4WordToByte(ip, i + 1, ip.length) + ) + } + + private fun decimalDigit(str: CharSequence, pos: Int): Int { + return str[pos] - '0' + } + + private fun ipv4WordToByte(ip: CharSequence, from: Int, toExclusive: Int): Byte { + var newFrom = from + var ret = decimalDigit(ip, newFrom) + newFrom++ + + if (newFrom == toExclusive) { + return ret.toByte() + } + + ret = ret * 10 + decimalDigit(ip, newFrom) + newFrom++ + + return if (newFrom == toExclusive) { + ret.toByte() + } else (ret * 10 + decimalDigit(ip, newFrom)).toByte() + } + + fun findFreeSubnet24(): ByteArray? { + Common.logger.info("Scanning for available cidr...") + + // have to find a free cidr + // start with 10.x.x.x /24 and just march through starting at 0 -> 200 for each, ping to see if there is a gateway (should be) + // and go from there. + + + // on linux, PING has the setuid bit set - so it runs "as root". isReachable() requires either java to have the setuid bit set + // (ie: sudo setcap cap_net_raw=ep /usr/lib/jvm/jdk/bin/java) or it requires to be run as root. We run as root in production, so it + // works. + val ip = byteArrayOf(10, 0, 0, 0) + var subnet24Counter = 0 + + while (true) { + ip[3]++ + + if (ip[3] > 255) { + ip[3] = 1 + ip[2]++ + subnet24Counter = 0 + } + if (ip[2] > 255) { + ip[2] = 0 + ip[1]++ + } + if (ip[1] > 255) { + Common.logger.error("Exhausted all ip searches. FATAL ERROR.") + return null + } + + try { + val address = InetAddress.getByAddress(ip) + val reachable = address.isReachable(100) + + if (!reachable) { + subnet24Counter++ + } + if (subnet24Counter == 250) { + // this means that we tried all /24 IPs, and ALL of them came back an "non-responsive". 100ms timeout is OK, because + // we are on a LAN, that should have MORE than one IP in the cidr, and it should be fairly responsive (ie: <10ms ping) + + // we found an empty cidr + ip[3] = 1 + return ip + } + } catch (e: IOException) { + e.printStackTrace() + return null + } + } + } + + /** + * Scans for existing IP addresses on the network. + * + * @param startingIp the IP address to start scanning at + * @param numberOfHosts the number of hosts to scan for. A /28 is 14 hosts : 2^(32-28) - 2 = 14 + * + * @return true if no hosts were reachable (pingable) + */ + fun scanHosts(startingIp: String, numberOfHosts: Int): Boolean { + Common.logger.info("Scanning {} hosts, starting at IP {}.", numberOfHosts, startingIp) + + + val split = startingIp.split(SLASH_REGEX).toTypedArray() + val a = split[0].toByte() + val b = split[1].toByte() + val c = split[2].toByte() + val d = split[3].toByte() + + val ip = byteArrayOf(a, b, c, d) + var counter = numberOfHosts + + while (counter >= 0) { + counter-- + + ip[3]++ + if (ip[3] > 255) { + ip[3] = 1 + ip[2]++ + } + if (ip[2] > 255) { + ip[2] = 0 + ip[1]++ + } + if (ip[1] > 255) { + Common.logger.error("Exhausted all ip searches. FATAL ERROR.") + return false + } + + try { + val address = InetAddress.getByAddress(ip) + val reachable = address.isReachable(100) + if (reachable) { + Common.logger.error("IP address {} is already reachable on the network. Unable to continue.", address.hostAddress) + return false + } + } catch (e: IOException) { + Common.logger.error("Error pinging the IP address", e) + return false + } + } + return true + } + + /** + * @param cidr the CIDR notation, ie: 24, 16, etc. That we want to convert into a netmask, as a string + * + * @return the netmask or if there were errors, the default /0 netmask + */ + fun getCidrAsNetmask(cidr: Int): String { + return when (cidr) { + 32 -> "255.255.255.255" + 31 -> "255.255.255.254" + 30 -> "255.255.255.252" + 29 -> "255.255.255.248" + 28 -> "255.255.255.240" + 27 -> "255.255.255.224" + 26 -> "255.255.255.192" + 25 -> "255.255.255.128" + 24 -> "255.255.255.0" + 23 -> "255.255.254.0" + 22 -> "255.255.252.0" + 21 -> "255.255.248.0" + 20 -> "255.255.240.0" + 19 -> "255.255.224.0" + 18 -> "255.255.192.0" + 17 -> "255.255.128.0" + 16 -> "255.255.0.0" + 15 -> "255.254.0.0" + 14 -> "255.252.0.0" + 13 -> "255.248.0.0" + 12 -> "255.240.0.0" + 11 -> "255.224.0.0" + 10 -> "255.192.0.0" + 9 -> "255.128.0.0" + 8 -> "255.0.0.0" + 7 -> "254.0.0.0" + 6 -> "252.0.0.0" + 5 -> "248.0.0.0" + 4 -> "240.0.0.0" + 3 -> "224.0.0.0" + 2 -> "192.0.0.0" + 1 -> "128.0.0.0" + else -> "0.0.0.0" + } + } + + /** + * @param cidr the CIDR notation, ie: 24, 16, etc. That we want to convert into a netmask, as a SIGNED INTEGER (the bits are still + * correct, but to see this "as unix would", you must convert to an unsigned integer. + * + * @return the netmask (as a signed int), or if there were errors, the default /0 netmask + */ + fun getCidrAsIntNetmask(cidr: Int): Int { + return when (cidr) { + 32 -> -1 + 31 -> -2 + 30 -> -4 + 29 -> -8 + 28 -> -16 + 27 -> -32 + 26 -> -64 + 25 -> -128 + 24 -> -256 + 23 -> -512 + 22 -> -1024 + 21 -> -2048 + 20 -> -4096 + 19 -> -8192 + 18 -> -16384 + 17 -> -32768 + 16 -> -65536 + 15 -> -131072 + 14 -> -262144 + 13 -> -524288 + 12 -> -1048576 + 11 -> -2097152 + 10 -> -4194304 + 9 -> -8388608 + 8 -> -16777216 + 7 -> -33554432 + 6 -> -67108864 + 5 -> -134217728 + 4 -> -268435456 + 3 -> -536870912 + 2 -> -1073741824 + 1 -> -2147483648 + else -> 0 + } + } + + fun getCidrFromMask(mask: String): Int { + return when (mask) { + "255.255.255.255" -> 32 + "255.255.255.254" -> 31 + "255.255.255.252" -> 30 + "255.255.255.248" -> 29 + "255.255.255.240" -> 28 + "255.255.255.224" -> 27 + "255.255.255.192" -> 26 + "255.255.255.128" -> 25 + "255.255.255.0" -> 24 + "255.255.254.0" -> 23 + "255.255.252.0" -> 22 + "255.255.248.0" -> 21 + "255.255.240.0" -> 20 + "255.255.224.0" -> 19 + "255.255.192.0" -> 18 + "255.255.128.0" -> 17 + "255.255.0.0" -> 16 + "255.254.0.0" -> 15 + "255.252.0.0" -> 14 + "255.248.0.0" -> 13 + "255.240.0.0" -> 12 + "255.224.0.0" -> 11 + "255.192.0.0" -> 10 + "255.128.0.0" -> 9 + "255.0.0.0" -> 8 + "254.0.0.0" -> 7 + "252.0.0.0" -> 6 + "248.0.0.0" -> 5 + "240.0.0.0" -> 4 + "224.0.0.0" -> 3 + "192.0.0.0" -> 2 + "128.0.0.0" -> 1 + else -> 0 + } + } + + private val CIDR2MASK = intArrayOf( + 0x00000000, + -0x80000000, + -0x40000000, + -0x20000000, + -0x10000000, + -0x8000000, + -0x4000000, + -0x2000000, + -0x1000000, + -0x800000, + -0x400000, + -0x200000, + -0x100000, + -0x80000, + -0x40000, + -0x20000, + -0x10000, + -0x8000, + -0x4000, + -0x2000, + -0x1000, + -0x800, + -0x400, + -0x200, + -0x100, + -0x80, + -0x40, + -0x20, + -0x10, + -0x8, + -0x4, + -0x2, + -0x1) + + fun range2Cidr(startIp: String, endIp: String): List { + var start = toInt(startIp).toLong() + val end = toInt(endIp).toLong() + + val pairs: MutableList = ArrayList() + + while (end >= start) { + var maxsize = 32.toByte() + while (maxsize > 0) { + val mask = CIDR2MASK[maxsize - 1].toLong() + val maskedBase = start and mask + if (maskedBase != start) { + break + } + maxsize-- + } + + val x = ln(end - start + 1.toDouble()) / ln(2.0) + val maxDiff = (32 - floor(x)).toByte() + if (maxsize < maxDiff) { + maxsize = maxDiff + } + + val ip = toString(start) + pairs.add("$ip/$maxsize") + start += 2.0.pow(32 - maxsize.toDouble()).toLong() + } + return pairs + } + + + /* Mask to convert unsigned int to a long (i.e. keep 32 bits) */ + private const val UNSIGNED_INT_MASK = 0x0FFFFFFFFL + + /** + * Check if the IP address is in the range of a specific IP/CIDR + * + * a prefix of 0 will ALWAYS return true + * + * @param address the address to check + * @param networkAddress the network address that will have the other address checked against + * @param networkPrefix 0-32 the network prefix (subnet) to use for the network address + * + * @return true if it is in range + */ + fun isInRange(address: Int, networkAddress: Int, networkPrefix: Int): Boolean { + // System.err.println(" ip: " + IP.toString(address)); + // System.err.println(" networkAddress: " + IP.toString(networkAddress)); + // System.err.println(" networkSubnetPrefix: " + networkPrefix); + if (networkPrefix == 0) { + // a prefix of 0 means it is always true (even though the broadcast address is '-1'). So we short-cut it here + return true + } + val netmask = ((1 shl 32 - networkPrefix) - 1).inv() + + // Calculate base network address + val network = (networkAddress and netmask and UNSIGNED_INT_MASK.toInt()).toLong() + // System.err.println(" network " + IP.toString(network)); + + // Calculate broadcast address + val broadcast = network or netmask.inv().toLong() and UNSIGNED_INT_MASK + // System.err.println(" broadcast " + IP.toString(broadcast)); + + val addressLong = (address and UNSIGNED_INT_MASK.toInt()).toLong() + return addressLong in network..broadcast + } + + + fun toInt(ipBytes: ByteArray): Int { + return ipBytes[0].toInt() shl 24 or + (ipBytes[1].toInt() shl 16) or + (ipBytes[2].toInt() shl 8) or + (ipBytes[3].toInt()) + } + + /** + * Converts a 32-bit integer into an IPv4 address. + */ + fun toString(ipAddress: Int): String { + val buf = StringBuilder(15) + buf.append(ipAddress shr 24 and 0xFF) + buf.append('.') + buf.append(ipAddress shr 16 and 0xFF) + buf.append('.') + buf.append(ipAddress shr 8 and 0xF) + buf.append('.') + buf.append(ipAddress and 0xFF) + return buf.toString() + } + + fun toString(ipBytes: ByteArray): String { + val buf = StringBuilder(15) + buf.append(ipBytes[0].toUByte()) + buf.append('.') + buf.append(ipBytes[1].toUByte()) + buf.append('.') + buf.append(ipBytes[2].toUByte()) + buf.append('.') + buf.append(ipBytes[3].toUByte()) + return buf.toString() + } + + /** + * Returns the [String] representation of an [InetAddress]. Results are identical to [InetAddress.getHostAddress] + * + * @param ip [InetAddress] to be converted to an address string + * + * @return `String` containing the text-formatted IP address + */ + fun toString(ip: InetAddress): String { + return (ip as Inet4Address).hostAddress + } + + + @Throws(Exception::class) + fun writeString(ipAddress: Int, writer: Writer) { + writer.write((ipAddress shr 24 and 0x000000FF).toString()) + writer.write('.'.toInt()) + writer.write((ipAddress shr 16 and 0x000000FF).toString()) + writer.write('.'.toInt()) + writer.write((ipAddress shr 8 and 0x000000FF).toString()) + writer.write('.'.toInt()) + writer.write((ipAddress and 0x000000FF).toString()) + } + + fun toString(ipAddress: Long): String { + val ipString = StringBuilder(15) + ipString.append(ipAddress shr 24 and 0x000000FF) + ipString.append('.') + ipString.append(ipAddress shr 16 and 0x000000FF) + ipString.append('.') + ipString.append(ipAddress shr 8 and 0x000000FF) + ipString.append('.') + ipString.append(ipAddress and 0x000000FF) + return ipString.toString() + } + + fun toBytes(bytes: Int): ByteArray { + return byteArrayOf((bytes ushr 24 and 0xFF).toByte(), + (bytes ushr 16 and 0xFF).toByte(), + (bytes ushr 8 and 0xFF).toByte(), + (bytes and 0xFF).toByte()) + } + + fun toInt(ipAsString: String): Int { + return if (isValid(ipAsString)) { + val bytes = toBytes(ipAsString) + + var address = 0 + address = address or (bytes[0].toInt() shl 24) + address = address or (bytes[1].toInt() shl 16) + address = address or (bytes[2].toInt() shl 8) + address = address or bytes[3].toInt() + + address + } else { + 0 + } + } + + /** + * Returns the [Inet4Address] representation of a [String] IP address. + * + * This method will treat all IPv4 type addresses as "IPv4 mapped" (see [.getByName]) + * + * @param ip [String] IP address to be converted to a [Inet4Address] + * @return [Inet4Address] representation of the `ip` or `null` if not a valid IP address. + */ + fun getByName(ip: String): Inet4Address? { + return if (isValid(ip)) { + val asBytes = toBytes(ip) + return Inet4Address.getByAddress(ip, asBytes) as Inet4Address + } else { + null + } + } +} diff --git a/src/dorkbox/netUtil/IPv6.kt b/src/dorkbox/netUtil/IPv6.kt new file mode 100644 index 0000000..5563450 --- /dev/null +++ b/src/dorkbox/netUtil/IPv6.kt @@ -0,0 +1,691 @@ +/* + * Copyright 2020 Dorkbox, llc + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package dorkbox.netUtil + +import java.net.Inet6Address +import java.net.InetAddress +import java.net.UnknownHostException + +/** + * A class that holds a number of network-related constants, also from: + * (Netty, apache 2.0 license) + * https://github.com/netty/netty/blob/4.1/common/src/main/java/io/netty/util/NetUtil.java + * + * This class borrowed some of its methods from a modified fork of the + * [Inet6Util class] + * (http://svn.apache.org/repos/asf/harmony/enhanced/java/branches/java6/classlib/modules/luni/src/main/java/org/apache/harmony/luni/util/Inet6Util.java) which was part of Apache Harmony. + */ +object IPv6 { + /** + * Maximum amount of value adding characters in between IPV6 separators + */ + private const val IPV6_MAX_CHAR_BETWEEN_SEPARATOR = 4 + + /** + * Minimum number of separators that must be present in an IPv6 string + */ + private const val IPV6_MIN_SEPARATORS = 2 + + /** + * Maximum number of separators that must be present in an IPv6 string + */ + private const val IPV6_MAX_SEPARATORS = 8 + + /** + * Maximum amount of value adding characters in between IPV4 separators + */ + private const val IPV4_MAX_CHAR_BETWEEN_SEPARATOR = 3 + + /** + * Number of separators that must be present in an IPv4 string + */ + private const val IPV4_SEPARATORS = 3 + + /** + * Number of bytes needed to represent and IPV6 value + */ + private const val IPV6_BYTE_COUNT = 16 + + /** + * This defines how many words (represented as ints) are needed to represent an IPv6 address + */ + private const val IPV6_WORD_COUNT = 8 + + + /** + * The maximum number of characters for an IPV6 string with no scope + */ + private const val IPV6_MAX_CHAR_COUNT = 39 + + /** + * Returns `true` if an IPv6 address should be preferred when a host has both an IPv4 address and an IPv6 + * address. The default value of this property is `false`. + * + * @see [Java SE + * networking properties](https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html) + */ + val isPreferred = Common.getBoolean("java.net.preferIPv6Addresses", false) + + /** + * The [Inet6Address] that represents the IPv6 loopback address '::1' + */ + val LOCALHOST: Inet6Address by lazy { + // Create IPv6 loopback address. + // should never fail + InetAddress.getByAddress("localhost", byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)) as Inet6Address + } + + /** + * Takes a [String] and parses it to see if it is a valid IPV6 address. + * + * @return true, if the string represents an IPV6 address, false otherwise + */ + fun isValid(ip: String): Boolean { + var end = ip.length + if (end < 2) { + return false + } + + // strip "[]" + var start: Int + var c = ip[0] + if (c == '[') { + end-- + if (ip[end] != ']') { + // must have a close ] + return false + } + start = 1 + c = ip[1] + } + else { + start = 0 + } + var colons: Int + var compressBegin: Int + if (c == ':') { + // an IPv6 address can start with "::" or with a number + if (ip[start + 1] != ':') { + return false + } + colons = 2 + compressBegin = start + start += 2 + } + else { + colons = 0 + compressBegin = -1 + } + var wordLen = 0 + loop@ for (i in start until end) { + c = ip[i] + if (isValidHexChar(c)) { + if (wordLen < 4) { + wordLen++ + continue + } + return false + } + when (c) { + ':' -> { + if (colons > 7) { + return false + } + if (ip[i - 1] == ':') { + if (compressBegin >= 0) { + return false + } + compressBegin = i - 1 + } + else { + wordLen = 0 + } + colons++ + } + '.' -> { + // case for the last 32-bits represented as IPv4 x:x:x:x:x:x:d.d.d.d + + // check a normal case (6 single colons) + if (compressBegin < 0 && colons != 6 || // a special case ::1:2:3:4:5:d.d.d.d allows 7 colons with an + // IPv4 ending, otherwise 7 :'s is bad + colons == 7 && compressBegin >= start || colons > 7) { + return false + } + + // Verify this address is of the correct structure to contain an IPv4 address. + // It must be IPv4-Mapped or IPv4-Compatible + // (see https://tools.ietf.org/html/rfc4291#section-2.5.5). + val ipv4Start = i - wordLen + var j = ipv4Start - 2 // index of character before the previous ':'. + if (IPv4.isValidIPv4MappedChar(ip[j])) { + if (!IPv4.isValidIPv4MappedChar(ip[j - 1]) + || !IPv4.isValidIPv4MappedChar(ip[j - 2]) + || !IPv4.isValidIPv4MappedChar(ip[j - 3])) { + return false + } + j -= 5 + } + while (j >= start) { + val tmpChar = ip[j] + if (tmpChar != '0' && tmpChar != ':') { + return false + } + --j + } + + // 7 - is minimum IPv4 address length + var ipv4End = ip.indexOf('%', ipv4Start + 7) + if (ipv4End < 0) { + ipv4End = end + } + return IPv4.isValidIpV4Address(ip, ipv4Start, ipv4End) + } + '%' -> { + // strip the interface name/index after the percent sign + end = i + break@loop + } + else -> return false + } + } + + // normal case without compression + return if (compressBegin < 0) { + colons == 7 && wordLen > 0 + } + else compressBegin + 2 == end || // 8 colons is valid only if compression in start or end + wordLen > 0 && (colons < 8 || compressBegin <= start) + } + + private fun isValidHexChar(c: Char): Boolean { + return c in '0'..'9' || c in 'A'..'F' || c in 'a'..'f' + } + + /** + * Returns the [Inet6Address] representation of a [CharSequence] IP address. + * + * This method will treat all IPv4 type addresses as "IPv4 mapped" (see [.getByName]) + * + * @param ip [CharSequence] IP address to be converted to a [Inet6Address] + * @return [Inet6Address] representation of the `ip` or `null` if not a valid IP address. + */ + fun getByName(ip: String): Inet6Address? { + return getByName(ip, true) + } + + + /** + * Returns the [Inet6Address] representation of a [String] IP address. + *

+ * The [ipv4Mapped] parameter specifies how IPv4 addresses should be treated. The "IPv4 mapped" format is + * defined in rfc 4291 section 2 is supported. + * + * @param ip [String] IP address to be converted to a [Inet6Address] + * @param ipv4Mapped + *

    + *
  • {@code true} To allow IPv4 mapped inputs to be translated into {@link Inet6Address}
  • + *
  • {@code false} Consider IPv4 mapped addresses as invalid.
  • + *
+ * @return [Inet6Address] representation of the [ip] or [null] if not a valid IP address. + */ + fun getByName(ip: String, ipv4Mapped: Boolean): Inet6Address? { + val bytes = getIPv6ByName(ip, ipv4Mapped) ?: return null + return try { + Inet6Address.getByAddress(null, bytes, -1) + } catch (e: UnknownHostException) { + throw RuntimeException(e) // Should never happen + } + +// return NetUtil.getByName(ip, ipv4Mapped) + } + + /** + * Returns the byte array representation of a [CharSequence] IP address. + * + * + * The `ipv4Mapped` parameter specifies how IPv4 addresses should be treated. + * "IPv4 mapped" format as + * defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) is supported. + * @param ip [CharSequence] IP address to be converted to a [Inet6Address] + * @param ipv4Mapped + * + * * `true` To allow IPv4 mapped inputs to be translated into [Inet6Address] + * * `false` Consider IPv4 mapped addresses as invalid. + * + * @return byte array representation of the `ip` or `null` if not a valid IP address. + */ + private fun getIPv6ByName(ip: CharSequence, ipv4Mapped: Boolean): ByteArray? { + val bytes = ByteArray(IPV6_BYTE_COUNT) + val ipLength = ip.length + var compressBegin = 0 + var compressLength = 0 + var currentIndex = 0 + var value = 0 + var begin = -1 + var i = 0 + var ipv6Separators = 0 + var ipv4Separators = 0 + var tmp: Int + var needsShift = false + while (i < ipLength) { + val c = ip[i] + when (c) { + ':' -> { + ++ipv6Separators + if (i - begin > IPV6_MAX_CHAR_BETWEEN_SEPARATOR || ipv4Separators > 0 || ipv6Separators > IPV6_MAX_SEPARATORS || currentIndex + 1 >= bytes.size) { + return null + } + value = value shl (IPV6_MAX_CHAR_BETWEEN_SEPARATOR - (i - begin) shl 2) + if (compressLength > 0) { + compressLength -= 2 + } + + // The value integer holds at most 4 bytes from right (most significant) to left (least significant). + // The following bit shifting is used to extract and re-order the individual bytes to achieve a + // left (most significant) to right (least significant) ordering. + bytes[currentIndex++] = (value and 0xf shl 4 or (value shr 4 and 0xf)).toByte() + bytes[currentIndex++] = (value shr 8 and 0xf shl 4 or (value shr 12 and 0xf)).toByte() + tmp = i + 1 + if (tmp < ipLength && ip[tmp] == ':') { + ++tmp + if (compressBegin != 0 || tmp < ipLength && ip[tmp] == ':') { + return null + } + ++ipv6Separators + needsShift = ipv6Separators == 2 && value == 0 + compressBegin = currentIndex + compressLength = bytes.size - compressBegin - 2 + ++i + } + value = 0 + begin = -1 + } + '.' -> { + ++ipv4Separators + tmp = i - begin // tmp is the length of the current segment. + if (tmp > IPV4_MAX_CHAR_BETWEEN_SEPARATOR || begin < 0 || ipv4Separators > IPV4_SEPARATORS || ipv6Separators > 0 && currentIndex + compressLength < 12 || i + 1 >= ipLength || currentIndex >= bytes.size || ipv4Separators == 1 && + + // We also parse pure IPv4 addresses as IPv4-Mapped for ease of use. + (!ipv4Mapped || currentIndex != 0 && !isValidIPv4Mapped(bytes, currentIndex, compressBegin, compressLength) || tmp == 3 && (!IPv4.isValidNumericChar( + ip[i - 1]) || !IPv4.isValidNumericChar(ip[i - 2]) || !IPv4.isValidNumericChar(ip[i - 3])) || tmp == 2 && (!IPv4.isValidNumericChar( + ip[i - 1]) || !IPv4.isValidNumericChar(ip[i - 2])) || tmp == 1 && !IPv4.isValidNumericChar(ip[i - 1]))) { + return null + } + value = value shl (IPV4_MAX_CHAR_BETWEEN_SEPARATOR - tmp shl 2) + + // The value integer holds at most 3 bytes from right (most significant) to left (least significant). + // The following bit shifting is to restructure the bytes to be left (most significant) to + // right (least significant) while also accounting for each IPv4 digit is base 10. + begin = (value and 0xf) * 100 + (value shr 4 and 0xf) * 10 + (value shr 8 and 0xf) + if (begin < 0 || begin > 255) { + return null + } + bytes[currentIndex++] = begin.toByte() + value = 0 + begin = -1 + } + else -> { + if (!isValidHexChar(c) || ipv4Separators > 0 && !IPv4.isValidNumericChar(c)) { + return null + } + if (begin < 0) { + begin = i + } + else if (i - begin > IPV6_MAX_CHAR_BETWEEN_SEPARATOR) { + return null + } + // The value is treated as a sort of array of numbers because we are dealing with + // at most 4 consecutive bytes we can use bit shifting to accomplish this. + // The most significant byte will be encountered first, and reside in the right most + // position of the following integer + value += Common.decodeHexNibble(c) shl (i - begin shl 2) + } + } + ++i + } + val isCompressed = compressBegin > 0 + // Finish up last set of data that was accumulated in the loop (or before the loop) + if (ipv4Separators > 0) { + if (begin > 0 && i - begin > IPV4_MAX_CHAR_BETWEEN_SEPARATOR || ipv4Separators != IPV4_SEPARATORS || currentIndex >= bytes.size) { + return null + } + if (ipv6Separators == 0) { + compressLength = 12 + } + else if (ipv6Separators >= IPV6_MIN_SEPARATORS && (!isCompressed && ipv6Separators == 6 && ip[0] != ':' || isCompressed && ipv6Separators < IPV6_MAX_SEPARATORS && (ip[0] != ':' || compressBegin <= 2))) { + compressLength -= 2 + } + else { + return null + } + value = value shl (IPV4_MAX_CHAR_BETWEEN_SEPARATOR - (i - begin) shl 2) + + // The value integer holds at most 3 bytes from right (most significant) to left (least significant). + // The following bit shifting is to restructure the bytes to be left (most significant) to + // right (least significant) while also accounting for each IPv4 digit is base 10. + begin = (value and 0xf) * 100 + (value shr 4 and 0xf) * 10 + (value shr 8 and 0xf) + if (begin < 0 || begin > 255) { + return null + } + bytes[currentIndex++] = begin.toByte() + } + else { + tmp = ipLength - 1 + if (begin > 0 && i - begin > IPV6_MAX_CHAR_BETWEEN_SEPARATOR || ipv6Separators < IPV6_MIN_SEPARATORS || !isCompressed && (ipv6Separators + 1 != IPV6_MAX_SEPARATORS || ip[0] == ':' || ip[tmp] == ':') || isCompressed && (ipv6Separators > IPV6_MAX_SEPARATORS || ipv6Separators == IPV6_MAX_SEPARATORS && (compressBegin <= 2 && ip[0] != ':' || compressBegin >= 14 && ip[tmp] != ':')) || currentIndex + 1 >= bytes.size || begin < 0 && ip[tmp - 1] != ':' || compressBegin > 2 && ip[0] == ':') { + return null + } + if (begin >= 0 && i - begin <= IPV6_MAX_CHAR_BETWEEN_SEPARATOR) { + value = value shl (IPV6_MAX_CHAR_BETWEEN_SEPARATOR - (i - begin) shl 2) + } + // The value integer holds at most 4 bytes from right (most significant) to left (least significant). + // The following bit shifting is used to extract and re-order the individual bytes to achieve a + // left (most significant) to right (least significant) ordering. + bytes[currentIndex++] = (value and 0xf shl 4 or (value shr 4 and 0xf)).toByte() + bytes[currentIndex++] = (value shr 8 and 0xf shl 4 or (value shr 12 and 0xf)).toByte() + } + i = currentIndex + compressLength + if (needsShift || i >= bytes.size) { + // Right shift array + if (i >= bytes.size) { + ++compressBegin + } + i = currentIndex + while (i < bytes.size) { + begin = bytes.size - 1 + while (begin >= compressBegin) { + bytes[begin] = bytes[begin - 1] + --begin + } + bytes[begin] = 0 + ++compressBegin + ++i + } + } + else { + // Selectively move elements + i = 0 + while (i < compressLength) { + begin = i + compressBegin + currentIndex = begin + compressLength + if (currentIndex < bytes.size) { + bytes[currentIndex] = bytes[begin] + bytes[begin] = 0 + } + else { + break + } + ++i + } + } + if (ipv4Separators > 0) { + // We only support IPv4-Mapped addresses [1] because IPv4-Compatible addresses are deprecated [2]. + // [1] https://tools.ietf.org/html/rfc4291#section-2.5.5.2 + // [2] https://tools.ietf.org/html/rfc4291#section-2.5.5.1 + bytes[11] = 0xff.toByte() + bytes[10] = bytes[11] + } + return bytes + } + + private fun isValidIPv4Mapped(bytes: ByteArray, currentIndex: Int, compressBegin: Int, compressLength: Int): Boolean { + val mustBeZero = compressBegin + compressLength >= 14 + return currentIndex <= 12 && currentIndex >= 2 && (!mustBeZero || compressBegin < 12) + && isValidIPv4MappedSeparators(bytes[currentIndex - 1], bytes[currentIndex - 2], mustBeZero) && + isZero(bytes, 0, currentIndex - 3) + } + + private fun isZero(bytes: ByteArray, startPos: Int, length: Int): Boolean { + var start = startPos + val end = start + length + val zeroByteByte = 0.toByte() + + while (start < end) { + if (bytes[start++] != zeroByteByte) { + return false + } + } + return true + } + + private fun isValidIPv4MappedSeparators(b0: Byte, b1: Byte, mustBeZero: Boolean): Boolean { + // We allow IPv4 Mapped (https://tools.ietf.org/html/rfc4291#section-2.5.5.1) + // and IPv4 compatible (https://tools.ietf.org/html/rfc4291#section-2.5.5.1). + // The IPv4 compatible is deprecated, but it allows parsing of plain IPv4 addressed into IPv6-Mapped addresses. + return b0 == b1 && (b0.toInt() == 0 || !mustBeZero && b1.toInt() == -1) + } + + /** + * Creates an byte[] based on an ipAddressString. No error handling is performed here. + */ + fun toBytesOrNull(ipAddress: String): ByteArray? { + if (isValid(ipAddress)) { + return getByName(ipAddress)?.address + } + + return null + } + /** + * Creates an byte[] based on an ipAddressString. No error handling is performed here. + */ + fun toBytes(ipAddress: String): ByteArray { + // always return a byte array + var fixedIp = ipAddress + if (fixedIp[0] == '[') { + fixedIp = fixedIp.substring(1, fixedIp.length - 1) + } + val percentPos: Int = fixedIp.indexOf('%') + if (percentPos >= 0) { + fixedIp = fixedIp.substring(0, percentPos) + } + + return getByName(fixedIp)?.address ?: ByteArray(32) + } + + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * + * The output does not include Scope ID. + * + * @param bytes [InetAddress] to be converted to an address string + * + * @return `String` containing the text-formatted IP address + */ + fun toString(bytes: ByteArray): String { + return toString(bytes, 0) + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * + * The output does not include Scope ID. + * + * @param bytes [InetAddress] to be converted to an address string + * + * @return `String` containing the text-formatted IP address + */ + fun toString(bytes: ByteArray, offset: Int): String { + return toAddressString(bytes, offset, false) + } + + /** + * Returns the [String] representation of an [InetAddress]. + * + * * Inet4Address results are identical to [InetAddress.getHostAddress] + * * Inet6Address results adhere to + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) if + * `ipv4Mapped` is false. If `ipv4Mapped` is true then "IPv4 mapped" format + * from [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) will be supported. + * The compressed result will always obey the compression rules defined in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * + * + * The output does not include Scope ID. + * + * @param ip [InetAddress] to be converted to an address string + * @param ipv4Mapped + * + * * `true` to stray from strict rfc 5952 and support the "IPv4 mapped" format + * defined in [rfc 4291 section 2](http://tools.ietf.org/html/rfc4291#section-2.5.5) while still + * following the updated guidelines in + * [rfc 5952 section 4](http://tools.ietf.org/html/rfc5952#section-4) + * + * * `false` to strictly follow rfc 5952 + * + * @return `String` containing the text-formatted IP address + */ + fun toString(ip: InetAddress, ipv4Mapped: Boolean = false): String { + return toAddressString(ip.address, 0, ipv4Mapped) + } + + private fun toAddressString(bytes: ByteArray, offset: Int, ipv4Mapped: Boolean): String { + val words = IntArray(IPV6_WORD_COUNT) + var i: Int + val end = offset + words.size + i = offset + while (i < end) { + words[i] = bytes[i shl 1].toInt() and 0xff shl 8 or (bytes[(i shl 1) + 1].toInt() and 0xff) + ++i + } + + // Find longest run of 0s, tie goes to first found instance + var currentStart = -1 + var currentLength: Int + var shortestStart = -1 + var shortestLength = 0 + i = 0 + while (i < words.size) { + if (words[i] == 0) { + if (currentStart < 0) { + currentStart = i + } + } + else if (currentStart >= 0) { + currentLength = i - currentStart + if (currentLength > shortestLength) { + shortestStart = currentStart + shortestLength = currentLength + } + currentStart = -1 + } + ++i + } + // If the array ends on a streak of zeros, make sure we account for it + if (currentStart >= 0) { + currentLength = i - currentStart + if (currentLength > shortestLength) { + shortestStart = currentStart + shortestLength = currentLength + } + } + // Ignore the longest streak if it is only 1 long + if (shortestLength == 1) { + shortestLength = 0 + shortestStart = -1 + } + + // Translate to string taking into account longest consecutive 0s + val shortestEnd = shortestStart + shortestLength + val b = StringBuilder(IPV6_MAX_CHAR_COUNT) + if (shortestEnd < 0) { // Optimization when there is no compressing needed + b.append(Integer.toHexString(words[0])) + i = 1 + while (i < words.size) { + b.append(':') + b.append(Integer.toHexString(words[i])) + ++i + } + } + else { // General case that can handle compressing (and not compressing) + // Loop unroll the first index (so we don't constantly check i==0 cases in loop) + val isIpv4Mapped: Boolean + isIpv4Mapped = if (inRangeEndExclusive(0, shortestStart, shortestEnd)) { + b.append("::") + ipv4Mapped && shortestEnd == 5 && words[5] == 0xffff + } + else { + b.append(Integer.toHexString(words[0])) + false + } + i = 1 + while (i < words.size) { + if (!inRangeEndExclusive(i, shortestStart, shortestEnd)) { + if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { + // If the last index was not part of the shortened sequence + if (!isIpv4Mapped || i == 6) { + b.append(':') + } + else { + b.append('.') + } + } + if (isIpv4Mapped && i > 5) { + b.append(words[i] shr 8) + b.append('.') + b.append(words[i] and 0xff) + } + else { + b.append(Integer.toHexString(words[i])) + } + } + else if (!inRangeEndExclusive(i - 1, shortestStart, shortestEnd)) { + // If we are in the shortened sequence and the last index was not + b.append("::") + } + ++i + } + } + return b.toString() + } + + /** + * Does a range check on `value` if is within `start` (inclusive) and `end` (exclusive). + * @param value The value to checked if is within `start` (inclusive) and `end` (exclusive) + * @param start The start of the range (inclusive) + * @param end The end of the range (exclusive) + * @return + * + * * `true` if `value` if is within `start` (inclusive) and `end` (exclusive) + * * `false` otherwise + * + */ + private fun inRangeEndExclusive(value: Int, start: Int, end: Int): Boolean { + return value >= start && value < end + } +} diff --git a/src/dorkbox/netUtil/IfConfig.kt b/src/dorkbox/netUtil/IfConfig.kt new file mode 100644 index 0000000..7c499f1 --- /dev/null +++ b/src/dorkbox/netUtil/IfConfig.kt @@ -0,0 +1,33 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor + +/** + * + */ +object IfConfig { + fun assignMac(interfaceName: String, interfaceMac: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ifconfig", interfaceName, "hw", "ether", interfaceMac) + } else { + throw RuntimeException("NOT IMPL.") + } + + } + + fun up(interfaceName: String, interfaceCIDR: String) { + if (Common.OS_LINUX) { + up(interfaceName, "0.0.0.0", interfaceCIDR) + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun up(interfaceName: String, interfaceIP: String, interfaceCIDR: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ifconfig", interfaceName, "$interfaceIP/$interfaceCIDR", "up"); + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/Iface.kt b/src/dorkbox/netUtil/Iface.kt new file mode 100644 index 0000000..cfa33fb --- /dev/null +++ b/src/dorkbox/netUtil/Iface.kt @@ -0,0 +1,96 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor +import java.util.* + +/** + * + */ +object Iface { + private val ifToIp: MutableMap = HashMap() + + /** + * On disconnect, it will get the IP address for the interface from the cache, instead of from `ifconfig` (since the interface + * is down at this point) + */ + fun getIpFromIf(interfaceName: String, isOnClientConnect: Boolean): String { + if (Common.OS_LINUX) { + if (isOnClientConnect) { + val ifaceInfo = Executor.run("/sbin/ifconfig", interfaceName) + + val str = "inet addr:" + var index = ifaceInfo.indexOf(str) + if (index > -1) { + index += str.length + val possibleAddr = ifaceInfo.substring(index, ifaceInfo.indexOf(" ", index)); + + Common.logger.debug("Found on '{}' possible addr '{}' : ADD", interfaceName, possibleAddr); + synchronized(ifToIp) { + ifToIp.put(interfaceName, possibleAddr); + } + return possibleAddr; + } + } + else { + var possibleAddr: String? + synchronized(ifToIp) { possibleAddr = ifToIp.remove(interfaceName) } + + Common.logger.debug("Found on '{}' possible addr '{}' : REMOVE", interfaceName, possibleAddr) + if (possibleAddr != null) { + return possibleAddr!! + } + } + } + else { + throw RuntimeException("NOT IMPL.") + } + + return "" + } + + fun down(interfaceName: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ip", "link", "set", "dev", interfaceName, "down") + } + else { + throw RuntimeException("NOT IMPL.") + } + } + + fun up(interfaceName: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ip", "link", "set", "dev", interfaceName, "up") + } + else { + throw RuntimeException("NOT IMPL.") + } + } + + fun setMac(interfaceName: String, interfaceMac: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ip", "link", "set", "dev", interfaceName, "address", interfaceMac) + } + else { + throw RuntimeException("NOT IMPL.") + } + } + + fun assignCIDR(interfaceName: String?, cidr: Int) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ifconfig", "$interfaceName 0.0.0.0/$cidr up") + } + else { + throw RuntimeException("NOT IMPL.") + } + } + + fun addLoopback() { + if (Common.OS_LINUX) { + Executor.Companion.run("/sbin/ip", "link", "set", "dev", "lo", "up") + Executor.Companion.run("/sbin/ip", "addr", "add", "127.0.0.1", "dev", "lo") + } + else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/Mac.kt b/src/dorkbox/netUtil/Mac.kt new file mode 100644 index 0000000..11b6a0c --- /dev/null +++ b/src/dorkbox/netUtil/Mac.kt @@ -0,0 +1,414 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor +import mu.KLogger +import java.io.File +import java.io.Writer +import java.math.BigInteger +import java.net.InetAddress +import java.net.NetworkInterface +import java.util.* +import java.util.regex.Pattern + +/** + * + */ +object Mac { + enum class MacDelimiter(val delimiter: String) { + COLON(":"), PERIOD("."), SPACE(" "); + } + + private const val MAC_ADDRESS_LENGTH = 6 + + private val random = Random() + private val fakeMacsInUse: MutableSet = HashSet() + + private val MAC_ADDRESS_PATTERN by lazy { + Pattern.compile("^([a-fA-F0-9]{2}[:\\\\.-]?){5}[a-fA-F0-9]{2}$") + } + + private val COLON_REGEX = ":".toRegex() + + init { + synchronized(fakeMacsInUse) {} + } + + fun getMacAddress(ip: String, logger: KLogger? = null): String { + try { + val mac = getMacAddressByte(ip, logger) + if (mac == null) { + logger?.error("Unable to get MAC address for IP '{}'", ip) + return "" + } + + val s = StringBuilder(18) + for (b in mac) { + if (s.isNotEmpty()) { + s.append(':') + } + s.append(String.format("%02x", b)) + } + return s.toString() + } catch (e: Exception) { + if (logger != null) { + logger.error("Unable to get MAC address for IP '{}'", ip, e) + } else { + e.printStackTrace() + } + } + + logger?.error("Unable to get MAC address for IP '{}'", ip) + + return "" + } + + fun getMacAddressByte(ip: String, logger: KLogger? = null): ByteArray? { + try { + val addr = InetAddress.getByName(ip) + val networkInterface = NetworkInterface.getByInetAddress(addr) + if (networkInterface == null) { + logger?.error("Unable to get MAC address for IP '{}'", ip) + return null + } + + val mac = networkInterface.hardwareAddress + if (mac == null) { + logger?.error("Unable to get MAC address for IP '{}'", ip) + return null + } + + return mac + } catch (e: Exception) { + if (logger != null) { + logger.error("Unable to get MAC address for IP '{}'", ip, e) + } else { + e.printStackTrace() + } + } + + return null + } + + /** + * Removes a mac that was in use, to free it's use later on + */ + fun freeFakeMac(fakeMac: String) { + synchronized(fakeMacsInUse) { fakeMacsInUse.remove(fakeMac) } + } + + /** + * Removes a mac that was in use, to free it's use later on + */ + fun freeFakeMac(vararg fakeMacs: String) { + synchronized(fakeMacsInUse) { + fakeMacs.forEach { + fakeMacsInUse.remove(it) + } + } + } + + /** + * will also make sure that this mac doesn't already exist + * + * @return a unique MAC that can be used for VPN devices + setup. This is a LOWERCASE string! + */ + val fakeMac: String + get() { + synchronized(fakeMacsInUse) { + var mac = fakeVpnMacUnsafe + + // gotta make sure it doesn't already exist + while (fakeMacsInUse.contains(mac)) { + mac = fakeVpnMacUnsafe + } + + fakeMacsInUse.add(mac) + return mac + } + } + + /** + * will also make sure that this mac doesn't already exist + * + * @return a unique MAC that can be used for VPN devices + setup. This is a LOWERCASE string! + */ + val fakeDockerMac: String + get() { + synchronized(fakeMacsInUse) { + var mac = fakeDockerMacUnsafe + + // gotta make sure it doesn't already exist + while (fakeMacsInUse.contains(mac)) { + mac = fakeDockerMacUnsafe + } + fakeMacsInUse.add(mac) + + return mac + } + } + + /** + * http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines + * + * @return a mac that is safe to use for fake interfaces. THIS IS LOWERCASE! + */ + val fakeVpnMacUnsafe: String + get() { + var vpnID = randHex + while (vpnID == "d0") { + // this is what is used for docker. NEVER assign it to a VPN mac!! + vpnID = randHex + } + + return "02:$vpnID:$randHex:$randHex:$randHex:$randHex" + } + + /** + * http://serverfault.com/questions/40712/what-range-of-mac-addresses-can-i-safely-use-for-my-virtual-machines + * + * @return a mac that is safe to use for fake interfaces. THIS IS LOWERCASE! + */ + val fakeDockerMacUnsafe: String + get() { + return "02:d0:$randHex:$randHex:$randHex:$randHex" + } + + /** + * will also make sure that this mac doesn't already exist + * + * @return a unique MAC that can be used for VPN devices + setup + */ + fun getFakeVpnMac(vpnKeyDirForExistingVpnMacs: String): String { + synchronized(fakeMacsInUse) { + var mac = fakeVpnMacUnsafe + + // gotta make sure it doesn't already exist + while (fakeMacsInUse.contains(mac) || File(vpnKeyDirForExistingVpnMacs, "$mac.crt").exists()) { + mac = fakeVpnMacUnsafe + } + + fakeMacsInUse.add(mac) + return mac + } + } + + private val randHex: String + get() { + val i = random.nextInt(255) + + return if (i < 16) { + "0" + Integer.toHexString(i) + } else { + Integer.toHexString(i) + } + } + + /** + * Converts a long into a properly formatted lower-case string + */ + fun toStringLowerCase(mac: Long): String { + // we only use the right-most 6 bytes (of 8 bytes). + val macBytes = toBytes(mac) + + // mac should have 6 bytes. + val buf = StringBuilder() + (0..8).forEach { index -> + val byte = macBytes[index] + if (buf.isNotEmpty()) { + buf.append(':'); + } + + if (byte in 0..15) { + buf.append('0'); + } + + buf.append(Integer.toHexString(byte.toUByte().toInt())) + } + + return buf.toString() + } + + @Throws(Exception::class) + fun writeStringLowerCase(mac: Long, writer: Writer) { + // we only use the right-most 6 bytes. + val macBytes = toBytes(mac) + + // mac should have 6 bytes. + var bytesWritten = 0; + (0..8).forEach { index -> + val byte = macBytes[index] + + if (bytesWritten != 0) { + writer.write(":"); + bytesWritten++; + } + + if (byte in 0..15) { + writer.append('0'); + } + + writer.write(Integer.toHexString(byte.toUByte().toInt())) + bytesWritten += 2; + } + } + + private fun toBytes(x: Long): ByteArray { + return byteArrayOf((x shr 56).toByte(), + (x shr 48).toByte(), + (x shr 40).toByte(), + (x shr 32).toByte(), + (x shr 24).toByte(), + (x shr 16).toByte(), + (x shr 8).toByte(), + (x shr 0).toByte()) + } + + + fun toStringLowerCase(mac: ByteArray): String { + // mac should have 6 bytes. + val buf = StringBuilder() + for (b in mac) { + if (buf.isNotEmpty()) { + buf.append(':') + } + if (b in 0..15) { + buf.append('0') + } + + buf.append(Integer.toHexString(if (b < 0) b + 256 else b.toInt())) + } + return buf.toString() + } + + fun toStringUpperCase(mac: ByteArray): String { + val buf = StringBuilder() + for (b in mac) { + if (buf.isNotEmpty()) { + buf.append(':') + } + if (b in 0..15) { + buf.append('0') + } + + buf.append(Integer.toHexString(b.toUByte().toInt()).toUpperCase()) + } + return buf.toString() + } + + fun toBytes(mac: String): ByteArray { + val s = mac.replace(COLON_REGEX, "") + return BigInteger(s, 16).toByteArray() + } + + fun toLong(mac: ByteArray): Long { + return ((mac[5].toLong() and 0xff) + + (mac[4].toLong() and 0xff shl 8) + + (mac[3].toLong() and 0xff shl 16) + + (mac[2].toLong() and 0xff shl 24) + + (mac[1].toLong() and 0xff shl 32) + + (mac[0].toLong() and 0xff shl 40)) + } + + fun toLong(mac: String, delimiter: MacDelimiter? = MacDelimiter.COLON): Long { + val addressInBytes = ByteArray(MAC_ADDRESS_LENGTH) + try { + val elements: Array + + if (delimiter != null) { + elements = mac.split(delimiter.delimiter.toRegex()).toTypedArray() + } else { + elements = arrayOfNulls(MAC_ADDRESS_LENGTH) + var index = 0 + var substringPos = 0 + while (index < MAC_ADDRESS_LENGTH) { + elements[index] = mac.substring(substringPos, substringPos + 2) + index++ + substringPos += 2 + } + } + + for (i in 0 until MAC_ADDRESS_LENGTH) { + val element = elements[i] + addressInBytes[i] = element!!.toInt(16).toByte() + } + } catch (e: Exception) { + Common.logger.error("Error parsing MAC address '{}'", mac, e) + } + + return toLong(addressInBytes) + } + + fun isValid(macAsString: String): Boolean { + if (macAsString.isEmpty()) { + return false + } + + // Check whether mac is separated by a colon, period, space, or nothing at all. + // Must standardize to a colon. + var normalizedMac: String = macAsString + if (normalizedMac.split(COLON_REGEX).toTypedArray().size != 6) { + + // Does not already use colons, must modify the mac to use colons. + when { + normalizedMac.contains(".") -> { + // Period notation + normalizedMac = normalizedMac.replace(".", ":") + } + normalizedMac.contains(" ") -> { + // Space notation + normalizedMac = normalizedMac.replace(" ", ":") + } + else -> { + // No delimiter, manually add colons. + normalizedMac = "" + var index = 0 + var substringPos = 0 + + // We ensure that the substring function will not exceed the length of the given string if the string is not at least + //(MAC_ADDRESS_LENGTH * 2) characters long. + while (index < MAC_ADDRESS_LENGTH && macAsString.length >= substringPos + 2) { + // Reconstruct the string, adding colons. + normalizedMac = if (index != MAC_ADDRESS_LENGTH - 1) { + normalizedMac + macAsString.substring(substringPos, substringPos + 2) + ":" + } else { + normalizedMac + macAsString.substring(substringPos, substringPos + 2) + } + + index++ + substringPos += 2 + } + } + } + } + val matcher = MAC_ADDRESS_PATTERN.matcher(normalizedMac) + return matcher.matches() + } + + /** + * Returns the type of delmiter based on how a mac address is constructed. Returns null if no delimiter. + * @return a MacDelimiter if there is one, or null if the mac address is one long string. + */ + fun getMacDelimiter(macAsString: String): MacDelimiter? { + when { + macAsString.contains(":") -> { + return MacDelimiter.COLON + } + macAsString.contains(".") -> { + return MacDelimiter.PERIOD + } + macAsString.contains(" ") -> { + return MacDelimiter.SPACE + } + else -> return null + } + } + + fun assign(interfaceName: String, macAddress: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ifconfig", interfaceName, "hw", "ether", macAddress); + throw RuntimeException("NOT IMPL.") + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/NameSpace.kt b/src/dorkbox/netUtil/NameSpace.kt new file mode 100644 index 0000000..384f65c --- /dev/null +++ b/src/dorkbox/netUtil/NameSpace.kt @@ -0,0 +1,127 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor +import java.io.File +import java.util.* + +/** + * + */ +object NameSpace { + private val nameSpaceToIifToIp: MutableMap> = HashMap() + + fun add(nameSpace: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ip", "netns", "add", nameSpace) + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun delete(nameSpace: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ip", "netns", "del", nameSpace); + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun dhcpStart(nameSpace: String, id: String, interfaceName: String) { + if (Common.OS_LINUX) { + dhcpStop(nameSpace, id, interfaceName) + val dhcpPidFile = "/var/run/dhclient-$id.pid" + run(nameSpace, "/sbin/dhclient", "-pf", dhcpPidFile, interfaceName) + } else { + throw RuntimeException("NOT IMPL.") + } + + } + + fun dhcpStop(nameSpace: String, id: String, interfaceName: String) { + if (Common.OS_LINUX) { + val dhcpPidFile = "/var/run/dhclient-$id.pid" + + // close the dhclient if it was already running (based on pid file), and delete the pid file + run(nameSpace, "/sbin/dhclient", "-r -pf", dhcpPidFile, interfaceName) + + // short break + try { + Thread.sleep(500) + } catch (e: InterruptedException) { + e.printStackTrace() + } + File(dhcpPidFile).delete() + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun run(nameSpace: String, vararg args: String): String { + if (Common.OS_LINUX) { + val command = mutableListOf() + command.add("/sbin/ip") + command.add("netns") + command.add("exec") + command.add(nameSpace) + command.addAll(listOf(*args)) + + return Executor.run(command) + } else { + throw RuntimeException("NOT IMPL.") + } + } + + /** + * On client disconnect, it will get the IP address for the interface from the cache, instead of from `ifconfig` (since the + * interface + * is down at this point) + */ + fun getIpFromIf(nameSpace: String, interfaceName: String, isOnClientConnect: Boolean): String { + if (Common.OS_LINUX) { + if (isOnClientConnect) { + val ifaceInfo = run(nameSpace, "/sbin/ifconfig", interfaceName) + val str = "inet addr:" + var index = ifaceInfo.indexOf(str) + if (index > -1) { + index += str.length + val possibleAddr = ifaceInfo.substring(index, ifaceInfo.indexOf(" ", index)) + Common.logger.debug("Found on '{}' possible addr '{}' : ADD", interfaceName, possibleAddr) + synchronized(nameSpaceToIifToIp) { + var ifToIp = nameSpaceToIifToIp[nameSpace] + if (ifToIp == null) { + ifToIp = HashMap() + nameSpaceToIifToIp[nameSpace] = ifToIp + } + ifToIp.put(interfaceName, possibleAddr) + } + return possibleAddr + } + } else { + var possibleAddr: String? = "" + synchronized(nameSpaceToIifToIp) { + val ifToIp = nameSpaceToIifToIp[nameSpace] + if (ifToIp != null) { + possibleAddr = ifToIp.remove(interfaceName) + } + } + Common.logger.debug("Found on '{}' possible addr '{}' : REMOVE", interfaceName, possibleAddr) + if (possibleAddr != null) { + return possibleAddr!! + } + } + return "" + } else { + throw RuntimeException("NOT IMPL.") + } + + } + + fun addLoopback(nameSpace: String) { + if (Common.OS_LINUX) { + run(nameSpace, "/sbin/ip link set dev lo up") + run(nameSpace, "/sbin/ip addr add 127.0.0.1 dev lo") + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/Route.kt b/src/dorkbox/netUtil/Route.kt new file mode 100644 index 0000000..88c5ad1 --- /dev/null +++ b/src/dorkbox/netUtil/Route.kt @@ -0,0 +1,24 @@ +package dorkbox.netUtil + +import dorkbox.executor.Executor + +/** + * + */ +object Route { + fun flush(nameSpace: String) { + if (Common.OS_LINUX) { + NameSpace.run(nameSpace, "/sbin/ip route flush cache") + } else { + throw RuntimeException("NOT IMPL.") + } + } + + fun add(targetIpAndCidr: String, hostIP: String, hostInterface: String) { + if (Common.OS_LINUX) { + Executor.run("/sbin/ip", "route", "add", targetIpAndCidr, "via", hostIP, "dev", hostInterface); + } else { + throw RuntimeException("NOT IMPL.") + } + } +} diff --git a/src/dorkbox/netUtil/Sntp.kt b/src/dorkbox/netUtil/Sntp.kt new file mode 100644 index 0000000..dae0a1e --- /dev/null +++ b/src/dorkbox/netUtil/Sntp.kt @@ -0,0 +1,410 @@ +package dorkbox.netUtil + +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetAddress +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * + */ +object Sntp { + /** + * Simple SNTP client class for retrieving network time. + * + * https://tools.ietf.org/html/rfc2030 + * + * Sample usage: + *
SntpClient client = new SntpClient();
+     * if (client.requestTime("time.foo.com")) {
+     * long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
+     * }
+    
* + */ + class SntpClient { + companion object { + private class InvalidServerReplyException(message: String) : Exception(message) + + private const val TAG = "SntpClient" + private const val DBG = true + private const val REFERENCE_TIME_OFFSET = 16 + private const val ORIGINATE_TIME_OFFSET = 24 + private const val RECEIVE_TIME_OFFSET = 32 + private const val TRANSMIT_TIME_OFFSET = 40 + private const val NTP_PACKET_SIZE = 48 + private const val NTP_PORT = 123 + private const val NTP_MODE_CLIENT = 3 + private const val NTP_MODE_SERVER = 4 + private const val NTP_MODE_BROADCAST = 5 + private const val NTP_VERSION = 3 + private const val NTP_LEAP_NOSYNC = 3 + private const val NTP_STRATUM_DEATH = 0 + private const val NTP_STRATUM_MAX = 15 + + // Number of seconds between Jan 1, 1900 and Jan 1, 1970 + // 70 years plus 17 leap days + private const val OFFSET_1900_TO_1970 = (365L * 70L + 17L) * 24L * 60L * 60L + + @Throws(InvalidServerReplyException::class) + private fun checkValidServerReply(leap: Int, mode: Int, stratum: Int, transmitTime: Long) { + if (leap == NTP_LEAP_NOSYNC) { + throw InvalidServerReplyException("unsynchronized server") + } + if (mode != NTP_MODE_SERVER && mode != NTP_MODE_BROADCAST) { + throw InvalidServerReplyException("untrusted mode: $mode") + } + if (stratum == NTP_STRATUM_DEATH || stratum > NTP_STRATUM_MAX) { + throw InvalidServerReplyException("untrusted stratum: $stratum") + } + if (transmitTime == 0L) { + throw InvalidServerReplyException("zero transmitTime") + } + } + } + + /** + * This is a two-bit code warning of an impending leap second to be + * inserted/deleted in the last minute of the current day. It's values + * may be as follows: + * + * Value Meaning + * ----- ------- + * 0 no warning + * 1 last minute has 61 seconds + * 2 last minute has 59 seconds) + * 3 alarm condition (clock not synchronized) + */ + var leap: Int = 0 + private set + + + /** + * This value indicates the NTP/SNTP version number. The version number + * is 3 for Version 3 (IPv4 only) and 4 for Version 4 (IPv4, IPv6 and OSI). + * If necessary to distinguish between IPv4, IPv6 and OSI, the + * encapsulating context must be inspected. + */ + var version: Int = 0 + private set + + /** + * This value indicates the mode, with values defined as follows: + * + * Mode Meaning + * ---- ------- + * 0 reserved + * 1 symmetric active + * 2 symmetric passive + * 3 client + * 4 server + * 5 broadcast + * 6 reserved for NTP control message + * 7 reserved for private use + * + * In unicast and anycast modes, the client sets this field to 3 (client) + * in the request and the server sets it to 4 (server) in the reply. In + * multicast mode, the server sets this field to 5 (broadcast). + */ + var mode: Int = 0 + private set + + /** + * This value indicates the stratum level of the local clock, with values + * defined as follows: + * + * Stratum Meaning + * ---------------------------------------------- + * 0 unspecified or unavailable + * 1 primary reference (e.g., radio clock) + * 2-15 secondary reference (via NTP or SNTP) + * 16-255 reserved + */ + var stratum: Int = 0 + private set + + /** + * This value indicates the maximum interval between successive messages, + * in seconds to the nearest power of two. The values that can appear in + * this field presently range from 4 (16 s) to 14 (16284 s); however, most + * applications use only the sub-range 6 (64 s) to 10 (1024 s). + */ + var pollInterval: Int = 0 + private set + + /** + * This value indicates the precision of the local clock, in seconds to + * the nearest power of two. The values that normally appear in this field + * range from -6 for mains-frequency clocks to -20 for microsecond clocks + * found in some workstations. + */ + var precision: Int = 0 + private set + + /** + * This value indicates the total roundtrip delay to the primary reference + * source, in seconds. Note that this variable can take on both positive + * and negative values, depending on the relative time and frequency + * offsets. The values that normally appear in this field range from + * negative values of a few milliseconds to positive values of several + * hundred milliseconds. + */ + var rootDelay = 0.0 + + + /** + * This value indicates the nominal error relative to the primary reference + * source, in seconds. The values that normally appear in this field + * range from 0 to several hundred milliseconds. + */ + var rootDispersion = 0.0 + + /** + * This is the time at which the local clock was last set or corrected, in + * seconds since 00:00 1-Jan-1900. + */ + var referenceTimestamp: Long = 0L + private set + + /** + * This is the time at which the request departed the client for the + * server, in seconds since 00:00 1-Jan-1900. + */ + var originateTimestamp: Long = 0L + private set + + /** + * This is the time at which the request arrived at the server, in seconds + * since 00:00 1-Jan-1900. + */ + var receiveTimestamp: Long = 0L + private set + + /** + * This is the time at which the reply departed the server for the client, + * in seconds since 00:00 1-Jan-1900. + */ + var transmitTimestamp: Long = 0L + private set + + /** + * This is the time at which the client received the NTP server response + */ + var destinationTimestamp: Long = 0L + private set + + + /** + * @return time value computed from NTP server response. + */ + var ntpTime: Long = 0 + private set + + /** + * Returns the round trip time of the NTP transaction + * + * @return round trip time in milliseconds. + */ + var roundTripDelay: Long = 0 + private set + + var localClockOffset: Long = 0 + private set + + /** + * Sends an SNTP request to the given host and processes the response. + * + * @param host host name of the server. + * @param timeoutMS network timeout in milliseconds to wait for a response. + * + * @return true if the transaction was successful. + */ + internal fun requestTime(host: String, timeoutMS: Int): SntpClient { + var socket: DatagramSocket? = null + var address: InetAddress? = null + + try { + socket = DatagramSocket() + socket.soTimeout = timeoutMS + address = InetAddress.getByName(host) + + val buffer = ByteArray(NTP_PACKET_SIZE) + val request = DatagramPacket(buffer, buffer.size, address, NTP_PORT) + + // set mode = 3 (client) and version = 3 + // mode is in low 3 bits of first byte + // version is in bits 3-5 of first byte + buffer[0] = (NTP_MODE_CLIENT or (NTP_VERSION shl 3)).toByte() + + + // don't want random number generation polluting the timing + val randomNumber = kotlin.random.Random.nextInt(255).toByte() + + // get current time and write it to the request packet + val timeAtSend = System.currentTimeMillis() // this is UTC from epic + writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, timeAtSend, randomNumber) + + socket.send(request) + + // read the response + val response = DatagramPacket(buffer, buffer.size) + socket.receive(response) + + destinationTimestamp = System.currentTimeMillis() // this is UTC from epic + + // we don't want the socket close operation time to pollute the NTP response timing, so close it afterwards + socket.close() + + // extract the results + + // See the packet format diagram in RFC 2030 for more information + leap = buffer[0].toInt() shr 6 and 0x3 + version = buffer[0].toInt() shr 3 and 0x7 + mode = buffer[0].toInt() and 0x7 + stratum = buffer[1].toInt() + pollInterval = buffer[2].toInt() + precision = buffer[3].toInt() + + rootDelay = (buffer[4] * 256.0) + + buffer[5].toInt() + + (buffer[6].toInt() / 256.0) + + (buffer[7].toInt() / 65536.0) + + rootDispersion = (buffer[8].toInt() * 256.0) + + buffer[9].toInt() + + (buffer[10].toInt() / 256.0) + + (buffer[11].toInt() / 65536.0) + + + referenceTimestamp = readTimeStamp(buffer, REFERENCE_TIME_OFFSET) + originateTimestamp = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET) + receiveTimestamp = readTimeStamp(buffer, RECEIVE_TIME_OFFSET) + transmitTimestamp = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET) + + checkValidServerReply(leap, mode, stratum, transmitTimestamp) + + // Formula for delay according to the RFC2030 + // Timestamp Name ID When Generated + // ------------------------------------------------------------ + // Originate Timestamp T1 time request sent by client + // Receive Timestamp T2 time request received by server + // Transmit Timestamp T3 time reply sent by server + // Destination Timestamp T4 time reply received by client + // + // The roundtrip delay d and local clock offset t are defined as follows: + // + // delay = (T4 - T1) - (T3 - T2) + // offset = ((T2 - T1) + (T3 - T4)) / 2 + + roundTripDelay = (destinationTimestamp - originateTimestamp) - (transmitTimestamp - receiveTimestamp) + localClockOffset = ((receiveTimestamp - originateTimestamp) + (transmitTimestamp - destinationTimestamp)) / 2L + } catch (e: Exception) { + System.err.println("Error with NTP to $address. $e") + } finally { + socket?.close() + } + + return this + } + + /** + * Reads an unsigned 32 bit big endian number from the given offset in the buffer. + */ + private fun read32(buffer: ByteArray, offset: Int): Long { + val b0 = buffer[offset].toInt() + val b1 = buffer[offset + 1].toInt() + val b2 = buffer[offset + 2].toInt() + val b3 = buffer[offset + 3].toInt() + + // convert signed bytes to unsigned values + val i0 = (if (b0 and 0x80 == 0x80) (b0 and 0x7F) + 0x80 else b0).toLong() + val i1 = (if (b1 and 0x80 == 0x80) (b1 and 0x7F) + 0x80 else b1).toLong() + val i2 = (if (b2 and 0x80 == 0x80) (b2 and 0x7F) + 0x80 else b2).toLong() + val i3 = (if (b3 and 0x80 == 0x80) (b3 and 0x7F) + 0x80 else b3).toLong() + return (i0 shl 24) + (i1 shl 16) + (i2 shl 8) + i3 + } + + /** + * Reads the NTP time stamp at the given offset in the buffer and returns + * it as a system time (milliseconds since January 1, 1970). + */ + private fun readTimeStamp(buffer: ByteArray, offset: Int): Long { + val seconds = read32(buffer, offset) + val fraction = read32(buffer, offset + 4) + + // Special case: zero means zero. + return if (seconds == 0L && fraction == 0L) { + 0 + } else (seconds - OFFSET_1900_TO_1970) * 1000 + fraction * 1000L / 0x100000000L + } + + /** + * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp + * at the given offset in the buffer. + */ + private fun writeTimeStamp(buffer: ByteArray, @Suppress("SameParameterValue") offset_: Int, time: Long, randomNumber: Byte) { + var offset = offset_ + + if (time == 0L) { + // Special case: zero means zero. + Arrays.fill(buffer, offset, offset + 8, 0x00.toByte()) + return + } + + var seconds: Long = TimeUnit.MILLISECONDS.toSeconds(time) + val remainingNotSeconds = time - TimeUnit.SECONDS.toMillis(seconds) + + // now apply the required offset (NTP is based on 1900) + seconds += OFFSET_1900_TO_1970 + + val fraction = remainingNotSeconds * 0x100000000L / 1000L + + // write seconds in big endian format + buffer[offset++] = (seconds shr 24).toByte() + buffer[offset++] = (seconds shr 16).toByte() + buffer[offset++] = (seconds shr 8).toByte() + buffer[offset++] = (seconds shr 0).toByte() + + // write fraction in big endian format + buffer[offset++] = (fraction shr 24).toByte() + buffer[offset++] = (fraction shr 16).toByte() + buffer[offset++] = (fraction shr 8).toByte() + + // From RFC 2030: It is advisable to fill the non-significant + // low order bits of the timestamp with a random, unbiased + // bitstring, both to avoid systematic roundoff errors and as + // a means of loop detection and replay detection. + buffer[offset] = randomNumber + } + } + + /** + * Sends an SNTP request to the given host and processes the response. + * + * @param server host name of the SNTP server. + * @param timeoutInMS network timeout in milliseconds to wait for a response. + */ + fun update(server: String, timeoutInMS: Int = 10_000): SntpClient { + return SntpClient().requestTime(server, timeoutInMS) + } + +// @JvmStatic +// fun main(args: Array) { +// val ntp = update("time.mit.edu") +// +// // Display response +// val now = System.currentTimeMillis() +// val cor = now + ntp.localClockOffset +// +// System.out.printf ("Originate time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.originateTimestamp); +// System.out.printf ("Receive time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.receiveTimestamp) +// System.out.printf ("Transmit time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.transmitTimestamp) +// System.out.printf ("Destination time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", ntp.destinationTimestamp) +// +// System.out.println("RoundTripDelay : ${ntp.roundTripDelay}") +// System.out.println("ClockOffset : ${ntp.localClockOffset}") +// +// System.out.printf ("Local time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", now) +// System.out.printf ("Corrected time: %1\$ta, %1\$td %1\$tb %1\$tY, %1\$tI:%1\$tm:%1\$tS.%1\$tL %1\$tp %1\$tZ%n", cor) +// } +} diff --git a/src/dorkbox/netUtil/SocketUtils.kt b/src/dorkbox/netUtil/SocketUtils.kt new file mode 100644 index 0000000..87585f3 --- /dev/null +++ b/src/dorkbox/netUtil/SocketUtils.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2016 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.netUtil + +import java.io.IOException +import java.net.* +import java.nio.channels.DatagramChannel +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel +import java.security.AccessController +import java.security.PrivilegedAction +import java.security.PrivilegedActionException +import java.security.PrivilegedExceptionAction +import java.util.* + +/** + * Provides socket operations with privileges enabled. This is necessary for applications that use the + * [SecurityManager] to restrict [SocketPermission] to their application. + * + * By asserting that these operations are privileged, the operations can proceed even if some code in the calling chain lacks + * the appropriate [SocketPermission]. + */ +object SocketUtils { + private val EMPTY = Collections.enumeration(emptyList()) + private fun empty(): Enumeration { + return EMPTY as Enumeration + } + + @Throws(IOException::class) + fun connect(socket: Socket, remoteAddress: SocketAddress?, timeout: Int) { + try { + AccessController.doPrivileged(PrivilegedExceptionAction { + socket.connect(remoteAddress, timeout) + null + }) + } catch (e: PrivilegedActionException) { + throw (e.cause as IOException?)!! + } + } + + @Throws(IOException::class) + fun bind(socket: Socket, bindpoint: SocketAddress?) { + try { + AccessController.doPrivileged(PrivilegedExceptionAction { + socket.bind(bindpoint) + null + }) + } catch (e: PrivilegedActionException) { + throw (e.cause as IOException?)!! + } + } + + @Throws(IOException::class) + fun connect(socketChannel: SocketChannel, remoteAddress: SocketAddress?): Boolean { + return try { + AccessController.doPrivileged(PrivilegedExceptionAction { socketChannel.connect(remoteAddress) }) + } catch (e: PrivilegedActionException) { + throw (e.cause as IOException?)!! + } + } + + @Throws(IOException::class) + fun bind(socketChannel: SocketChannel, address: SocketAddress?) { + try { + AccessController.doPrivileged(PrivilegedExceptionAction { + socketChannel.bind(address) + null + }) + } catch (e: PrivilegedActionException) { + throw (e.cause as IOException?)!! + } + } + + @Throws(IOException::class) + fun accept(serverSocketChannel: ServerSocketChannel): SocketChannel { + return try { + AccessController.doPrivileged(PrivilegedExceptionAction { serverSocketChannel.accept() }) + } catch (e: PrivilegedActionException) { + throw (e.cause as IOException?)!! + } + } + + @Throws(IOException::class) + fun bind(networkChannel: DatagramChannel, address: SocketAddress?) { + try { + AccessController.doPrivileged(PrivilegedExceptionAction { + networkChannel.bind(address) + null + }) + } catch (e: PrivilegedActionException) { + throw (e.cause as IOException?)!! + } + } + + fun localSocketAddress(socket: ServerSocket): SocketAddress { + return AccessController.doPrivileged(PrivilegedAction { socket.localSocketAddress }) + } + + @Throws(UnknownHostException::class) + fun addressByName(hostname: String?): InetAddress { + return try { + AccessController.doPrivileged(PrivilegedExceptionAction { InetAddress.getByName(hostname) }) + } catch (e: PrivilegedActionException) { + throw (e.cause as UnknownHostException?)!! + } + } + + @Throws(UnknownHostException::class) + fun allAddressesByName(hostname: String?): Array { + return try { + AccessController.doPrivileged(PrivilegedExceptionAction { InetAddress.getAllByName(hostname) }) + } catch (e: PrivilegedActionException) { + throw (e.cause as UnknownHostException?)!! + } + } + + fun socketAddress(hostname: String?, port: Int): InetSocketAddress { + return AccessController.doPrivileged(PrivilegedAction { InetSocketAddress(hostname, port) }) + } + + fun addressesFromNetworkInterface(intf: NetworkInterface): Enumeration { + // Android seems to sometimes return null even if this is not a valid return value by the api docs. + // Just return an empty Enumeration in this case. + // See https://github.com/netty/netty/issues/10045 + return AccessController.doPrivileged(PrivilegedAction { intf.inetAddresses }) + ?: return empty() + } + + fun loopbackAddress(): InetAddress { + return AccessController.doPrivileged(PrivilegedAction { + return@PrivilegedAction InetAddress.getLoopbackAddress() + }) + } + + @Throws(SocketException::class) + fun hardwareAddressFromNetworkInterface(intf: NetworkInterface): ByteArray { + return try { + AccessController.doPrivileged(PrivilegedExceptionAction { intf.hardwareAddress }) + } catch (e: PrivilegedActionException) { + throw (e.cause as SocketException?)!! + } + } +} diff --git a/src/dorkbox/netUtil/VirtualEth.kt b/src/dorkbox/netUtil/VirtualEth.kt new file mode 100644 index 0000000..b2842d6 --- /dev/null +++ b/src/dorkbox/netUtil/VirtualEth.kt @@ -0,0 +1,21 @@ +package dorkbox.netUtil + +/** + * + */ +object VirtualEth { + fun add(host: String?, guest: String?) { + // ShellExecutor.Companion.run("/sbin/ip", "link add name " + host + " type veth peer name " + guest); + throw RuntimeException("NOT IMPL.") + } + + fun delete(host: String?) { + // ShellExecutor.Companion.run("/sbin/ip", "link del " + host); + throw RuntimeException("NOT IMPL.") + } + + fun assignNameSpace(nameSpace: String?, guest: String?) { + // ShellExecutor.Companion.run("/sbin/ip", "link set " + guest + " netns " + nameSpace); + throw RuntimeException("NOT IMPL.") + } +} diff --git a/src/dorkbox/netUtil/ping/Ping.kt b/src/dorkbox/netUtil/ping/Ping.kt new file mode 100644 index 0000000..c6c792d --- /dev/null +++ b/src/dorkbox/netUtil/ping/Ping.kt @@ -0,0 +1,84 @@ +package dorkbox.netUtil.ping + +import dorkbox.executor.Executor +import dorkbox.netUtil.Common +import org.slf4j.LoggerFactory +import java.io.IOException +import java.time.Duration + +/** + * + */ +class Ping { + companion object { + private val logger = LoggerFactory.getLogger(Ping::class.java.simpleName) + } + + private val count = 4 + private val host = "1.1.1.1" + private val waitTime = Duration.ofSeconds(4) + private val deadline: Duration? = null + private val ttl: Short? = null + + @Throws(IOException::class) + fun run(): PingResult { + val ping = Executor() + .command("ping") + + if (Common.OS_WINDOWS) { + ping.addCommand("-n") + ping.addCommand("$count") + } + else { + ping.addCommand("-q") + ping.addCommand("-c $count") + } + + if (waitTime != null) { + when { + Common.OS_MAC -> { + ping.addCommand("-W " + waitTime.toMillis()) + } + Common.OS_WINDOWS -> { + ping.addCommand("-w " + waitTime.toMillis()) + } + else -> { + ping.addCommand("-W " + waitTime.seconds) + } + } + } + + if (deadline != null) { + when { + Common.OS_MAC -> { + ping.addCommand("-t " + deadline.seconds) + } + Common.OS_WINDOWS -> { + logger.info("Deadline is not supported on Windows") + } + else -> { + ping.addCommand("-w " + deadline.seconds) + } + } + } + + if (ttl != null) { + when { + Common.OS_MAC -> { + ping.addCommand("-m $ttl") + } + Common.OS_WINDOWS -> { + ping.addCommand("-i $ttl") + } + else -> { + ping.addCommand("-t $ttl") + } + } + } + ping.command("ping $host") + + // wait for it to finish running + val output: String = ping.readOutput().startAsShellBlocking().output.utf8() + return PingResultBuilder.fromOutput(output) + } +} diff --git a/src/dorkbox/netUtil/ping/PingResult.kt b/src/dorkbox/netUtil/ping/PingResult.kt new file mode 100644 index 0000000..dbf46c6 --- /dev/null +++ b/src/dorkbox/netUtil/ping/PingResult.kt @@ -0,0 +1,28 @@ +package dorkbox.netUtil.ping + +import java.time.Duration +import java.util.* + +class PingResult { + var host: String? = null + var ip: String? = null + var responses: MutableList = LinkedList() + var transmittedPackets = 0 + var receivedPackets = 0 + var packetLoss = 0.0 + var time: Duration? = null + var minRoundTripTime: Duration? = null + var avgRoundTripTime: Duration? = null + var maxRoundTripTime: Duration? = null + var mdevRoundTripTime: Duration? = null + + class Response(val bytes: Int, + val host: String, + val icmpSeq: Int, + val ttl: Int, + val time: Duration) + + override fun toString(): String { + return "PingResult(host=$host, ip=$ip, responses=$responses, transmittedPackets=$transmittedPackets, receivedPackets=$receivedPackets, packetLoss=$packetLoss, time=$time, minRoundTripTime=$minRoundTripTime, avgRoundTripTime=$avgRoundTripTime, maxRoundTripTime=$maxRoundTripTime, mdevRoundTripTime=$mdevRoundTripTime)" + } +} diff --git a/src/dorkbox/netUtil/ping/PingResultBuilder.kt b/src/dorkbox/netUtil/ping/PingResultBuilder.kt new file mode 100644 index 0000000..eb51e42 --- /dev/null +++ b/src/dorkbox/netUtil/ping/PingResultBuilder.kt @@ -0,0 +1,129 @@ +package dorkbox.netUtil.ping + +import dorkbox.netUtil.Common +import dorkbox.netUtil.IPv4 +import java.time.Duration +import java.time.temporal.ChronoUnit +import java.util.regex.Matcher + +internal object PingResultBuilder { + + private val roundTripTimeParser: (PingResult, Matcher) -> PingResult = { result, matcher -> + val minRTT = matcher.group(1).toDouble() + val avgRTT = matcher.group(2).toDouble() + val maxRTT = matcher.group(3).toDouble() + val mdevRTT = matcher.group(4).toDouble() + + result.minRoundTripTime = Duration.ofNanos((1000 * 1000 * minRTT).toLong()) + result.avgRoundTripTime = Duration.ofNanos((1000 * 1000 * avgRTT).toLong()) + result.maxRoundTripTime = Duration.ofNanos((1000 * 1000 * maxRTT).toLong()) + result.mdevRoundTripTime = Duration.ofNanos((1000 * 1000 * mdevRTT).toLong()) + + result + } + + + private val resultParsers = + when { + Common.OS_MAC -> { + listOf( + /* BSD Ping (MacOS) */ + ResultParser.of("(.*) packets transmitted, (.*) packets received, (.*)% packet loss") { result, matcher -> + val transmittedPackets: Int = matcher.group(1).toInt() + result.transmittedPackets = transmittedPackets + + val receivedPackets: Int = matcher.group(2).toInt() + result.receivedPackets = receivedPackets + + val packetLoss: Double = 0.01 * matcher.group(3).toDouble() + result.packetLoss = packetLoss + + result + }, + ResultParser.of("round-trip min\\/avg\\/max\\/stddev = (.*)\\/(.*)\\/(.*)\\/(.*) ms", roundTripTimeParser), + ResultParser.of("(.*) bytes from (.*): icmp_seq=(.*) ttl=(.*) time=(.*) ms") { result, matcher -> + val bytes: Int = matcher.group(1).toInt() + val host: String = matcher.group(2) + val icmpSeq: Int = matcher.group(3).toInt() + val ttl: Int = matcher.group(4).toInt() + val time = Duration.ofNanos((1000 * 1000 * matcher.group(5).toDouble()).toLong()) + val response = PingResult.Response(bytes, host, icmpSeq, ttl, time) + + result.responses.add(response) + result + } + ) + } + Common.OS_WINDOWS -> { + listOf( + /* Windows */ + ResultParser.of("Pinging (.*) with") { result, matcher -> + result.host = IPv4.WILDCARD // note: this is REALLY the host used for default traffic + result.ip = matcher.group(1) + result + }, + ResultParser.of("(.*) packets transmitted, (.*) packets received, (.*)% packet loss") { result, matcher -> + val transmittedPackets: Int = matcher.group(1).toInt() + result.transmittedPackets = transmittedPackets + + val receivedPackets: Int = matcher.group(2).toInt() + result.receivedPackets = receivedPackets + + val packetLoss: Double = 0.01 * matcher.group(3).toDouble() + result.packetLoss = packetLoss + + result + }, + ResultParser.of("round-trip min\\/avg\\/max\\/stddev = (.*)\\/(.*)\\/(.*)\\/(.*) ms", roundTripTimeParser), + ResultParser.of("(.*) bytes from (.*): icmp_seq=(.*) ttl=(.*) time=(.*) ms") { result, matcher -> + val bytes: Int = matcher.group(1).toInt() + val host: String = matcher.group(2) + val icmpSeq: Int = matcher.group(3).toInt() + val ttl: Int = matcher.group(4).toInt() + val time = Duration.ofNanos((1000 * 1000 * matcher.group(5).toDouble()) as Long) + val response = PingResult.Response(bytes, host, icmpSeq, ttl, time) + + result.responses.add(response) + result + } + ) + } + else -> { + listOf( + /* GNU Ping (Debian, etc.) */ + ResultParser.of("PING (.*) \\((.*?)\\)") { result, matcher -> + result.host = matcher.group(1) + result.ip = matcher.group(2) + result + }, + ResultParser.of("(.*) packets transmitted, (.*) received, (.*)% packet loss, time (.*)ms") { result, matcher -> + val transmittedPackets: Int = matcher.group(1).toInt() + result.transmittedPackets = transmittedPackets + + val receivedPackets: Int = matcher.group(2).toInt() + result.receivedPackets = receivedPackets + + val packetLoss: Double = 0.01 * Integer.valueOf(matcher.group(3)) + result.packetLoss = packetLoss + + val time = Duration.of(matcher.group(4).toLong(), ChronoUnit.MILLIS) + result.time = time + + result + }, + ResultParser.of("rtt min\\/avg\\/max\\/mdev = (.*)\\/(.*)\\/(.*)\\/(.*) ms", roundTripTimeParser) + ) + } + } + + + fun fromOutput(output: String): PingResult { + val pingResult = PingResult() + + resultParsers.forEach { rp: ResultParser -> + rp.fill(pingResult, output) + } + + return pingResult + } +} diff --git a/src/dorkbox/netUtil/ping/ResultParser.kt b/src/dorkbox/netUtil/ping/ResultParser.kt new file mode 100644 index 0000000..76b7d74 --- /dev/null +++ b/src/dorkbox/netUtil/ping/ResultParser.kt @@ -0,0 +1,23 @@ +package dorkbox.netUtil.ping + +import java.util.regex.Matcher +import java.util.regex.Pattern + +class ResultParser(private val pattern: Pattern, private val reader: (PingResult, Matcher) -> PingResult) { + fun fill(result: PingResult, output: String): PingResult { + var result = result + val matcher = pattern.matcher(output) + while (matcher.find()) { + result = reader(result, matcher) + } + + return result + } + + companion object { + fun of(regex: String, reader: (PingResult, Matcher) -> PingResult): ResultParser { + val compile = Pattern.compile(regex) + return ResultParser(compile, reader) + } + } +} diff --git a/test/dorkbox/netUtil/NetUtilTest.kt b/test/dorkbox/netUtil/NetUtilTest.kt new file mode 100644 index 0000000..825b2c4 --- /dev/null +++ b/test/dorkbox/netUtil/NetUtilTest.kt @@ -0,0 +1,749 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package dorkbox.netUtil + +import dorkbox.util.Sys +import org.junit.Assert +import org.junit.Assert.* +import org.junit.Test +import java.net.Inet6Address +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.UnknownHostException + +class NetUtilTest { + companion object { + private val validIpV4Hosts = mapOf( + "192.168.1.0" to "c0a80100", + "10.255.255.254" to "0afffffe", + "172.18.5.4" to "ac120504", + "0.0.0.0" to "00000000", + "127.0.0.1" to "7f000001", + "255.255.255.255" to "ffffffff", + "1.2.3.4" to "01020304") + + private val invalidIpV4Hosts = mapOf( + "1.256.3.4" to null, + "256.0.0.1" to null, + "1.1.1.1.1" to null, + "x.255.255.255" to null, + "0.1:0.0" to null, + "0.1.0.0:" to null, + "127.0.0." to null, + "1.2..4" to null, + "192.0.1" to null, + "192.0.1.1.1" to null, + "192.0.1.a" to null, + "19a.0.1.1" to null, + "a.0.1.1" to null, + ".0.1.1" to null, + "127.0.0" to null, + "192.0.1.256" to null, + "0.0.200.259" to null, + "1.1.-1.1" to null, + "1.1. 1.1" to null, + "1.1.1.1 " to null, + "1.1.+1.1" to null, + "0.0x1.0.255" to null, + "0.01x.0.255" to null, + "0.x01.0.255" to null, + "0.-.0.0" to null, + "0..0.0" to null, + "0.A.0.0" to null, + "0.1111.0.0" to null, + "..." to null) + + private val validIpV6Hosts = mapOf( + "::ffff:5.6.7.8" to "00000000000000000000ffff05060708", + "fdf8:f53b:82e4::53" to "fdf8f53b82e400000000000000000053", + "fe80::200:5aee:feaa:20a2" to "fe8000000000000002005aeefeaa20a2", + "2001::1" to "20010000000000000000000000000001", + "2001:0000:4136:e378:8000:63bf:3fff:fdd2" to "200100004136e378800063bf3ffffdd2", + "2001:0002:6c::430" to "20010002006c00000000000000000430", + "2001:10:240:ab::a" to "20010010024000ab000000000000000a", + "2002:cb0a:3cdd:1::1" to "2002cb0a3cdd00010000000000000001", + "2001:db8:8:4::2" to "20010db8000800040000000000000002", + "ff01:0:0:0:0:0:0:2" to "ff010000000000000000000000000002", + "[fdf8:f53b:82e4::53]" to "fdf8f53b82e400000000000000000053", + "[fe80::200:5aee:feaa:20a2]" to "fe8000000000000002005aeefeaa20a2", + "[2001::1]" to "20010000000000000000000000000001", + "[2001:0000:4136:e378:8000:63bf:3fff:fdd2]" to "200100004136e378800063bf3ffffdd2", + "0:1:2:3:4:5:6:789a" to "0000000100020003000400050006789a", + "0:1:2:3::f" to "0000000100020003000000000000000f", + "0:0:0:0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001", + "0:0:0:0:0::10.0.0.1" to "00000000000000000000ffff0a000001", + "0:0:0:0::10.0.0.1" to "00000000000000000000ffff0a000001", + "::0:0:0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001", + "0::0:0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001", + "0:0::0:0:0:10.0.0.1" to "00000000000000000000ffff0a000001", + "0:0:0::0:0:10.0.0.1" to "00000000000000000000ffff0a000001", + "0:0:0:0::0:10.0.0.1" to "00000000000000000000ffff0a000001", + "0:0:0:0:0:ffff:10.0.0.1" to "00000000000000000000ffff0a000001", + "::ffff:192.168.0.1" to "00000000000000000000ffffc0a80001", + // Test if various interface names after the percent sign are recognized. + "[::1%1]" to "00000000000000000000000000000001", + "[::1%eth0]" to "00000000000000000000000000000001", + "[::1%%]" to "00000000000000000000000000000001", + "0:0:0:0:0:ffff:10.0.0.1%" to "00000000000000000000ffff0a000001", + "0:0:0:0:0:ffff:10.0.0.1%1" to "00000000000000000000ffff0a000001", + "[0:0:0:0:0:ffff:10.0.0.1%1]" to "00000000000000000000ffff0a000001", + "[0:0:0:0:0::10.0.0.1%1]" to "00000000000000000000ffff0a000001", + "[::0:0:0:0:ffff:10.0.0.1%1]" to "00000000000000000000ffff0a000001", + "::0:0:0:0:ffff:10.0.0.1%1" to "00000000000000000000ffff0a000001", + "::1%1" to "00000000000000000000000000000001", + "::1%eth0" to "00000000000000000000000000000001", + "::1%%" to "00000000000000000000000000000001", + // Tests with leading or trailing compression + "0:0:0:0:0:0:0::" to "00000000000000000000000000000000", + "0:0:0:0:0:0::" to "00000000000000000000000000000000", + "0:0:0:0:0::" to "00000000000000000000000000000000", + "0:0:0:0::" to "00000000000000000000000000000000", + "0:0:0::" to "00000000000000000000000000000000", + "0:0::" to "00000000000000000000000000000000", + "0::" to "00000000000000000000000000000000", + "::" to "00000000000000000000000000000000", + "::0" to "00000000000000000000000000000000", + "::0:0" to "00000000000000000000000000000000", + "::0:0:0" to "00000000000000000000000000000000", + "::0:0:0:0" to "00000000000000000000000000000000", + "::0:0:0:0:0" to "00000000000000000000000000000000", + "::0:0:0:0:0:0" to "00000000000000000000000000000000", + "::0:0:0:0:0:0:0" to "00000000000000000000000000000000") + + + private val invalidIpV6Hosts = mapOf( + // Test method with garbage. + "Obvious Garbage" to null, + // Test method with preferred style, too many : + "0:1:2:3:4:5:6:7:8" to null, + // Test method with preferred style, not enough : + "0:1:2:3:4:5:6" to null, + // Test method with preferred style, bad digits. + "0:1:2:3:4:5:6:x" to null, + // Test method with preferred style, adjacent : + "0:1:2:3:4:5:6::7" to null, + // Too many : separators trailing + "0:1:2:3:4:5:6:7::" to null, + // Too many : separators leading + "::0:1:2:3:4:5:6:7" to null, + // Too many : separators trailing + "1:2:3:4:5:6:7:" to null, + // Too many : separators leading + ":1:2:3:4:5:6:7" to null, + // Compression with : separators trailing + "0:1:2:3:4:5::7:" to null, + "0:1:2:3:4::7:" to null, + "0:1:2:3::7:" to null, + "0:1:2::7:" to null, + "0:1::7:" to null, + "0::7:" to null, + // Compression at start with : separators trailing + "::0:1:2:3:4:5:7:" to null, + "::0:1:2:3:4:7:" to null, + "::0:1:2:3:7:" to null, + "::0:1:2:7:" to null, + "::0:1:7:" to null, + "::7:" to null, + // The : separators leading and trailing + ":1:2:3:4:5:6:7:" to null, + ":1:2:3:4:5:6:" to null, + ":1:2:3:4:5:" to null, + ":1:2:3:4:" to null, + ":1:2:3:" to null, + ":1:2:" to null, + ":1:" to null, + // Compression with : separators leading + ":1::2:3:4:5:6:7" to null, + ":1::3:4:5:6:7" to null, + ":1::4:5:6:7" to null, + ":1::5:6:7" to null, + ":1::6:7" to null, + ":1::7" to null, + ":1:2:3:4:5:6::7" to null, + ":1:3:4:5:6::7" to null, + ":1:4:5:6::7" to null, + ":1:5:6::7" to null, + ":1:6::7" to null, + ":1::" to null, + // Compression trailing with : separators leading + ":1:2:3:4:5:6:7::" to null, + ":1:3:4:5:6:7::" to null, + ":1:4:5:6:7::" to null, + ":1:5:6:7::" to null, + ":1:6:7::" to null, + ":1:7::" to null, + // Double compression + "1::2:3:4:5:6::" to null, + "::1:2:3:4:5::6" to null, + "::1:2:3:4:5:6::" to null, + "::1:2:3:4:5::" to null, + "::1:2:3:4::" to null, + "::1:2:3::" to null, + "::1:2::" to null, + "::0::" to null, + "12::0::12" to null, + // Too many : separators leading 0 + "0::1:2:3:4:5:6:7" to null, + // Test method with preferred style, too many digits. + "0:1:2:3:4:5:6:789abcdef" to null, + // Test method with compressed style, bad digits. + "0:1:2:3::x" to null, + // Test method with compressed style to too many adjacent : + "0:1:2:::3" to null, + // Test method with compressed style, too many digits. + "0:1:2:3::abcde" to null, + // Test method with compressed style, not enough : + "0:1" to null, + // Test method with ipv4 style, bad ipv6 digits. + "0:0:0:0:0:x:10.0.0.1" to null, + // Test method with ipv4 style, bad ipv4 digits. + "0:0:0:0:0:0:10.0.0.x" to null, + // Test method with ipv4 style, too many ipv6 digits. + "0:0:0:0:0:00000:10.0.0.1" to null, + // Test method with ipv4 style, too many : + "0:0:0:0:0:0:0:10.0.0.1" to null, + // Test method with ipv4 style, not enough : + "0:0:0:0:0:10.0.0.1" to null, + // Test method with ipv4 style, too many . + "0:0:0:0:0:0:10.0.0.0.1" to null, + // Test method with ipv4 style, not enough . + "0:0:0:0:0:0:10.0.1" to null, + // Test method with ipv4 style, adjacent . + "0:0:0:0:0:0:10..0.0.1" to null, + // Test method with ipv4 style, leading . + "0:0:0:0:0:0:.0.0.1" to null, + // Test method with ipv4 style, leading . + "0:0:0:0:0:0:.10.0.0.1" to null, + // Test method with ipv4 style, trailing . + "0:0:0:0:0:0:10.0.0." to null, + // Test method with ipv4 style, trailing . + "0:0:0:0:0:0:10.0.0.1." to null, + // Test method with compressed ipv4 style, bad ipv6 digits. + "::fffx:192.168.0.1" to null, + // Test method with compressed ipv4 style, bad ipv4 digits. + "::ffff:192.168.0.x" to null, + // Test method with compressed ipv4 style, too many adjacent : + ":::ffff:192.168.0.1" to null, + // Test method with compressed ipv4 style, too many ipv6 digits. + "::fffff:192.168.0.1" to null, + // Test method with compressed ipv4 style, too many ipv4 digits. + "::ffff:1923.168.0.1" to null, + // Test method with compressed ipv4 style, not enough : + ":ffff:192.168.0.1" to null, + // Test method with compressed ipv4 style, too many . + "::ffff:192.168.0.1.2" to null, + // Test method with compressed ipv4 style, not enough . + "::ffff:192.168.0" to null, + // Test method with compressed ipv4 style, adjacent . + "::ffff:192.168..0.1" to null, + // Test method, bad ipv6 digits. + "x:0:0:0:0:0:10.0.0.1" to null, + // Test method, bad ipv4 digits. + "0:0:0:0:0:0:x.0.0.1" to null, + // Test method, too many ipv6 digits. + "00000:0:0:0:0:0:10.0.0.1" to null, + // Test method, too many ipv4 digits. + "0:0:0:0:0:0:10.0.0.1000" to null, + // Test method, too many : + "0:0:0:0:0:0:0:10.0.0.1" to null, + // Test method, not enough : + "0:0:0:0:0:10.0.0.1" to null, + // Test method, out of order trailing : + "0:0:0:0:0:10.0.0.1:" to null, + // Test method, out of order leading : + ":0:0:0:0:0:10.0.0.1" to null, + // Test method, out of order leading : + "0:0:0:0::10.0.0.1:" to null, + // Test method, out of order trailing : + ":0:0:0:0::10.0.0.1" to null, + // Test method, too many . + "0:0:0:0:0:0:10.0.0.0.1" to null, + // Test method, not enough . + "0:0:0:0:0:0:10.0.1" to null, + // Test method, adjacent . + "0:0:0:0:0:0:10.0.0..1" to null, + // Empty contents + "" to null, + // Invalid single compression + ":" to null, + ":::" to null, + // Trailing : (max number of : = 8) + "2001:0:4136:e378:8000:63bf:3fff:fdd2:" to null, + // Leading : (max number of : = 8) + ":aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222" to null, + // Invalid character + "1234:2345:3456:4567:5678:6789::X890" to null, + // Trailing . in IPv4 + "::ffff:255.255.255.255." to null, + // To many characters in IPv4 + "::ffff:0.0.1111.0" to null, + // Test method, adjacent . + "::ffff:0.0..0" to null, + // Not enough IPv4 entries trailing . + "::ffff:127.0.0." to null, + // Invalid trailing IPv4 character + "::ffff:127.0.0.a" to null, + // Invalid leading IPv4 character + "::ffff:a.0.0.1" to null, + // Invalid middle IPv4 character + "::ffff:127.a.0.1" to null, + // Invalid middle IPv4 character + "::ffff:127.0.a.1" to null, + // Not enough IPv4 entries no trailing . + "::ffff:1.2.4" to null, + // Extra IPv4 entry + "::ffff:192.168.0.1.255" to null, + // Not enough IPv6 content + ":ffff:192.168.0.1.255" to null, + // Intermixed IPv4 and IPv6 symbols + "::ffff:255.255:255.255." to null, + // Invalid IPv4 mapped address - invalid ipv4 separator + "0:0:0::0:0:00f.0.0.1" to null, + // Invalid IPv4 mapped address - not enough f's + "0:0:0:0:0:fff:1.0.0.1" to null, + // Invalid IPv4 mapped address - not IPv4 mapped, not IPv4 compatible + "0:0:0:0:0:ff00:1.0.0.1" to null, + // Invalid IPv4 mapped address - not IPv4 mapped, not IPv4 compatible + "0:0:0:0:0:ff:1.0.0.1" to null, + // Invalid IPv4 mapped address - too many f's + "0:0:0:0:0:fffff:1.0.0.1" to null, + // Invalid IPv4 mapped address - too many bytes (too many 0's) + "0:0:0:0:0:0:ffff:1.0.0.1" to null, + // Invalid IPv4 mapped address - too many bytes (too many 0's) + "::0:0:0:0:0:ffff:1.0.0.1" to null, + // Invalid IPv4 mapped address - too many bytes (too many 0's) + "0:0:0:0:0:0::1.0.0.1" to null, + // Invalid IPv4 mapped address - too many bytes (too many 0's) + "0:0:0:0:0:00000:1.0.0.1" to null, + // Invalid IPv4 mapped address - too few bytes (not enough 0's) + "0:0:0:0:ffff:1.0.0.1" to null, + // Invalid IPv4 mapped address - too few bytes (not enough 0's) + "ffff:192.168.0.1" to null, + // Invalid IPv4 mapped address - 0's after the mapped ffff indicator + "0:0:0:0:0:ffff::10.0.0.1" to null, + // Invalid IPv4 mapped address - 0's after the mapped ffff indicator + "0:0:0:0:ffff::10.0.0.1" to null, + // Invalid IPv4 mapped address - 0's after the mapped ffff indicator + "0:0:0:ffff::10.0.0.1" to null, + // Invalid IPv4 mapped address - 0's after the mapped ffff indicator + "0:0:ffff::10.0.0.1" to null, + // Invalid IPv4 mapped address - 0's after the mapped ffff indicator + "0:ffff::10.0.0.1" to null, + // Invalid IPv4 mapped address - 0's after the mapped ffff indicator + "ffff::10.0.0.1" to null, + // Invalid IPv4 mapped address - not all 0's before the mapped separator + "1:0:0:0:0:ffff:10.0.0.1" to null, + // Address that is similar to IPv4 mapped, but is invalid + "0:0:0:0:ffff:ffff:1.0.0.1" to null, + // Valid number of separators, but invalid IPv4 format + "::1:2:3:4:5:6.7.8.9" to null, + // Too many digits + "0:0:0:0:0:0:ffff:10.0.0.1" to null, + // Invalid IPv4 format + ":1.2.3.4" to null, + // Invalid IPv4 format + "::.2.3.4" to null, + // Invalid IPv4 format + "::ffff:0.1.2." to null) + + + private val ipv6ToAddressStrings = mapOf( + // From the RFC 5952 http://tools.ietf.org/html/rfc5952#section-4 + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1 + ) to "2001:db8::1", + + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 2, 0, 1 + ) to "2001:db8::2:1", + + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 1, + 0, 1, 0, 1, + 0, 1, 0, 1 + ) to "2001:db8:0:1:1:1:1:1", + + // Other examples + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 2, 0, 1 + ) to "2001:db8::2:1", + + byteArrayOf( + 32, 1, 0, 0, + 0, 0, 0, 1, + 0, 0, 0, 0, + 0, 0, 0, 1 + ) to "2001:0:0:1::1", + + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 1 + ) to "2001:db8::1:0:0:1", + + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 0, 0 + ) to "2001:db8:0:0:1::", + + byteArrayOf( + 32, 1, 13, -72, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 2, 0, 0 + ) to "2001:db8::2:0", + + byteArrayOf( + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1 + ) to "::1", + + byteArrayOf( + 0, 0, 0, 0, + 0, 0, 0, 1, + 0, 0, 0, 0, + 0, 0, 0, 1 + ) to "::1:0:0:0:1", + + byteArrayOf( + 0, 0, 0, 0, + 1, 0, 0, 1, + 0, 0, 0, 0, + 1, 0, 0, 0 + ) to "::100:1:0:0:100:0", + + byteArrayOf( + 32, 1, 0, 0, + 65, 54, -29, 120, + -128, 0, 99, -65, + 63, -1, -3, -46 + ) to "2001:0:4136:e378:8000:63bf:3fff:fdd2", + + byteArrayOf( + -86, -86, -69, -69, + -52, -52, -35, -35, + -18, -18, -1, -1, + 17, 17, 34, 34 + ) to "aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222", + + byteArrayOf( + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 + ) to "::" + ) + + + private val ipv4MappedToIPv6AddressStrings = mapOf( + // IPv4 addresses + "255.255.255.255" to "::ffff:255.255.255.255", + "0.0.0.0" to "::ffff:0.0.0.0", + "127.0.0.1" to "::ffff:127.0.0.1", + "1.2.3.4" to "::ffff:1.2.3.4", + "192.168.0.1" to "::ffff:192.168.0.1", + // IPv4 compatible addresses are deprecated [1], so we don't support outputting them, but we do support + // parsing them into IPv4 mapped addresses. These values are treated the same as a plain IPv4 address above. + // [1] https://tools.ietf.org/html/rfc4291#section-2.5.5.1 + "0:0:0:0:0:0:255.254.253.252" to "::ffff:255.254.253.252", + "0:0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4", + "::0:0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0::0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0::0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0::0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0:0::0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4", + "::0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0::0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0::0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0::0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4", + "::0:0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0::0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0::0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0::0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0:0::1.2.3.4" to "::ffff:1.2.3.4", + "::0:0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0::0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0::0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0:0::1.2.3.4" to "::ffff:1.2.3.4", + "::0:0:1.2.3.4" to "::ffff:1.2.3.4", + "0::0:1.2.3.4" to "::ffff:1.2.3.4", + "0:0::1.2.3.4" to "::ffff:1.2.3.4", + "::0:1.2.3.4" to "::ffff:1.2.3.4", + "::1.2.3.4" to "::ffff:1.2.3.4", + // IPv4 mapped (fully specified) + "0:0:0:0:0:ffff:1.2.3.4" to "::ffff:1.2.3.4", + // IPv6 addresses + // Fully specified + "2001:0:4136:e378:8000:63bf:3fff:fdd2" to "2001:0:4136:e378:8000:63bf:3fff:fdd2", + "aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222" to "aaaa:bbbb:cccc:dddd:eeee:ffff:1111:2222", + "0:0:0:0:0:0:0:0" to "::", + "0:0:0:0:0:0:0:1" to "::1", + // Compressing at the beginning + "::1:0:0:0:1" to "::1:0:0:0:1", + "::1:ffff:ffff" to "::1:ffff:ffff", + "::" to "::", + "::1" to "::1", + "::ffff" to "::ffff", + "::ffff:0" to "::ffff:0", + "::ffff:ffff" to "::ffff:ffff", + "::0987:9876:8765" to "::987:9876:8765", + "::0987:9876:8765:7654" to "::987:9876:8765:7654", + "::0987:9876:8765:7654:6543" to "::987:9876:8765:7654:6543", + "::0987:9876:8765:7654:6543:5432" to "::987:9876:8765:7654:6543:5432", + // Note the compression is removed (rfc 5952 section 4.2.2) + "::0987:9876:8765:7654:6543:5432:3210" to "0:987:9876:8765:7654:6543:5432:3210", + // Compressing at the end + // Note the compression is removed (rfc 5952 section 4.2.2) + "2001:db8:abcd:bcde:cdef:def1:ef12::" to "2001:db8:abcd:bcde:cdef:def1:ef12:0", + "2001:db8:abcd:bcde:cdef:def1::" to "2001:db8:abcd:bcde:cdef:def1::", + "2001:db8:abcd:bcde:cdef::" to "2001:db8:abcd:bcde:cdef::", + "2001:db8:abcd:bcde::" to "2001:db8:abcd:bcde::", + "2001:db8:abcd::" to "2001:db8:abcd::", + "2001:1234::" to "2001:1234::", + "2001::" to "2001::", + "0::" to "::", + // Compressing in the middle + "1234:2345::7890" to "1234:2345::7890", + "1234::2345:7890" to "1234::2345:7890", + "1234:2345:3456::7890" to "1234:2345:3456::7890", + "1234:2345::3456:7890" to "1234:2345::3456:7890", + "1234::2345:3456:7890" to "1234::2345:3456:7890", + "1234:2345:3456:4567::7890" to "1234:2345:3456:4567::7890", + "1234:2345:3456::4567:7890" to "1234:2345:3456::4567:7890", + "1234:2345::3456:4567:7890" to "1234:2345::3456:4567:7890", + "1234::2345:3456:4567:7890" to "1234::2345:3456:4567:7890", + "1234:2345:3456:4567:5678::7890" to "1234:2345:3456:4567:5678::7890", + "1234:2345:3456:4567::5678:7890" to "1234:2345:3456:4567::5678:7890", + "1234:2345:3456::4567:5678:7890" to "1234:2345:3456::4567:5678:7890", + "1234:2345::3456:4567:5678:7890" to "1234:2345::3456:4567:5678:7890", + "1234::2345:3456:4567:5678:7890" to "1234::2345:3456:4567:5678:7890", + // Note the compression is removed (rfc 5952 section 4.2.2) + "1234:2345:3456:4567:5678:6789::7890" to "1234:2345:3456:4567:5678:6789:0:7890", + // Note the compression is removed (rfc 5952 section 4.2.2) + "1234:2345:3456:4567:5678::6789:7890" to "1234:2345:3456:4567:5678:0:6789:7890", + // Note the compression is removed (rfc 5952 section 4.2.2) + "1234:2345:3456:4567::5678:6789:7890" to "1234:2345:3456:4567:0:5678:6789:7890", + // Note the compression is removed (rfc 5952 section 4.2.2) + "1234:2345:3456::4567:5678:6789:7890" to "1234:2345:3456:0:4567:5678:6789:7890", + // Note the compression is removed (rfc 5952 section 4.2.2) + "1234:2345::3456:4567:5678:6789:7890" to "1234:2345:0:3456:4567:5678:6789:7890", + // Note the compression is removed (rfc 5952 section 4.2.2) + "1234::2345:3456:4567:5678:6789:7890" to "1234:0:2345:3456:4567:5678:6789:7890", + // IPv4 mapped addresses + "::ffff:255.255.255.255" to "::ffff:255.255.255.255", + "::ffff:0.0.0.0" to "::ffff:0.0.0.0", + "::ffff:127.0.0.1" to "::ffff:127.0.0.1", + "::ffff:1.2.3.4" to "::ffff:1.2.3.4", + "::ffff:192.168.0.1" to "::ffff:192.168.0.1") + + + private fun assertHexDumpEquals(expected: String?, actual: ByteArray?, message: String) { + assertEquals(message, expected, if (actual == null) null else hex(actual)) + } + + private fun hex(value: ByteArray): String { + return Sys.bytesToHex(value).toLowerCase() + } + + private fun unhex(value: String?): ByteArray? { + return if (value != null) Sys.hexToBytes(value) else null + } + } + + @Test + fun testLocalhost() { + Assert.assertNotNull(IP.LOCALHOST) + } + + @Test + fun testLoopback() { + Assert.assertNotNull(IP.LOOPBACK_IF) + } + + @Test + fun testIsValidIpV4Address() { + for (host in validIpV4Hosts.keys) { + Assert.assertTrue(host, IPv4.isValid(host)) + } + for (host in invalidIpV4Hosts.keys) { + Assert.assertFalse(host, IPv4.isValid(host)) + } + } + + @Test + fun testIsValidIpV6Address() { + for (host in validIpV6Hosts.keys) { + Assert.assertTrue(host, IPv6.isValid(host)) + if (host[0] != '[' && !host.contains("%")) { + Assert.assertNotNull(host, IPv6.getByName(host, true)) + var hostMod = "[$host]" + Assert.assertTrue(hostMod, IPv6.isValid(hostMod)) + hostMod = "$host%" + Assert.assertTrue(hostMod, IPv6.isValid(hostMod)) + hostMod = "$host%eth1" + Assert.assertTrue(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host%]" + Assert.assertTrue(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host%1]" + Assert.assertTrue(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host]%" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host]%1" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + } + } + for (host in invalidIpV6Hosts.keys) { + Assert.assertFalse(host, IPv6.isValid(host)) + Assert.assertNull(host, IPv6.getByName(host)) + var hostMod = "[$host]" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "$host%" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "$host%eth1" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host%]" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host%1]" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host]%" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host]%1" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "$host]" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + hostMod = "[$host" + Assert.assertFalse(hostMod, IPv6.isValid(hostMod)) + } + } + + @Test + fun testCreateByteArrayFromIpAddressString() { + for ((ip, value) in validIpV4Hosts) { + assertHexDumpEquals(value, IPv4.toBytes(ip), ip) + } + for ((ip, value) in invalidIpV4Hosts) { + assertHexDumpEquals(value, IPv4.toBytesorNull(ip), ip) + } + for ((ip, value) in validIpV6Hosts) { + assertHexDumpEquals(value, IPv6.toBytes(ip), ip) + } + for ((ip, value) in invalidIpV6Hosts) { + assertHexDumpEquals(value, IPv6.toBytesOrNull(ip), ip) + } + } + + @Test + @Throws(UnknownHostException::class) + fun testBytesToIpAddress() { + for ((key) in validIpV4Hosts) { + assertEquals(key, IPv4.toString(IPv4.toBytes(key))) + assertEquals(key, IPv4.toString(IPv4.toBytes(key))) + } + for ((key, value) in ipv6ToAddressStrings) { + assertEquals(value, IPv6.toString(key)) + } + } + + @Test + @Throws(UnknownHostException::class) + fun testIp6AddressToString() { + for ((key, value) in ipv6ToAddressStrings) { + assertEquals(value, IPv6.toString(InetAddress.getByAddress(key))) + } + } + + @Test + @Throws(UnknownHostException::class) + fun testIp4AddressToString() { + for ((key, value) in validIpV4Hosts) { + assertEquals(key, IPv4.toString(InetAddress.getByAddress(unhex(value)))) + } + } + + @Test + fun testIpv4MappedIp6GetByName() { + for ((srcIp, dstIp) in ipv4MappedToIPv6AddressStrings) { + val inet6Address: Inet6Address? = IPv6.getByName(srcIp, true) + Assert.assertNotNull("$srcIp, $dstIp", inet6Address) + assertEquals(srcIp, dstIp, IPv6.toString(inet6Address!!, true)) + } + } + + @Test + fun testInvalidIpv4MappedIp6GetByName() { + for (host in invalidIpV4Hosts.keys) { + Assert.assertNull(host, IPv4.getByName(host)) + } + for (host in invalidIpV6Hosts.keys) { + Assert.assertNull(host, IPv6.getByName(host, true)) + } + } + + @Test + @Throws(UnknownHostException::class) + fun testIp6InetSocketAddressToString() { + for ((key, value) in ipv6ToAddressStrings) { + assertEquals("[$value]:9999", IP.toString(InetSocketAddress(InetAddress.getByAddress(key), 9999))) + } + } + + @Test + @Throws(UnknownHostException::class) + fun testIp4SocketAddressToString() { + for ((key, value) in validIpV4Hosts) { + assertEquals("$key:9999", IP.toString(InetSocketAddress(InetAddress.getByAddress(unhex(value)), 9999))) + } + } +// +// @Test +// fun testPing() { +// println(Ping().run()) +//// println(Executor().command("ping 1.1.1.1").readOutput().startAsShellBlocking().output.utf8()) +// } + + @Test + fun testIp4Range() { + assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.5"), IPv4.toInt("10.10.10.10"), 24)) + assertTrue("", IPv4.isInRange(IPv4.toInt("10.0.0.5"), IPv4.toInt("10.10.10.10"), 8)) + assertFalse("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 8)) + assertTrue("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 1)) + assertTrue("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 0)) + assertFalse("", IPv4.isInRange(IPv4.toInt("11.0.0.5"), IPv4.toInt("10.10.10.10"), 32)) + assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.10"), IPv4.toInt("10.10.10.10"), 32)) + assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.10"), IPv4.toInt("10.10.10.10"), 31)) + assertTrue("", IPv4.isInRange(IPv4.toInt("10.10.10.10"), IPv4.toInt("10.10.10.10"), 30)) + assertTrue("", IPv4.isInRange(IPv4.toInt("192.168.42.14"), IPv4.toInt("192.168.0.0"), 16)) + assertTrue("", IPv4.isInRange(IPv4.toInt("192.168.0.0"), IPv4.toInt("192.168.0.0"), 16)) + } +}