From 57dc419fe312eb7a8485861acb7899c462f64368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 4 Nov 2021 16:28:10 +0100 Subject: [PATCH] initial commit --- .gitignore | 14 + LICENSE.txt | 202 ++++++ build.gradle | 34 + gradle.properties | 11 + gradle/compile/groovy.gradle | 34 + gradle/compile/java.gradle | 43 ++ gradle/documentation/asciidoc.gradle | 55 ++ gradle/ide/idea.gradle | 13 + gradle/publishing/publication.gradle | 66 ++ gradle/publishing/sonatype.gradle | 11 + gradle/repositories/maven.gradle | 4 + gradle/test/junit5.gradle | 27 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 +++++ gradlew.bat | 89 +++ groovy-crypt/CREDITS.txt | 18 + groovy-crypt/LICENSE.txt | 202 ++++++ groovy-crypt/README.adoc | 54 ++ .../src/docs/asciidoc/css/foundation.css | 684 ++++++++++++++++++ groovy-crypt/src/docs/asciidoc/index.adoc | 11 + .../groovy/org/xbib/groovy/crypt/Crypt.groovy | 105 +++ .../org/xbib/groovy/crypt/CryptType.groovy | 69 ++ .../org/xbib/groovy/crypt/CryptUtil.groovy | 113 +++ .../org/xbib/groovy/crypt/DesCrypt.groovy | 390 ++++++++++ .../org/xbib/groovy/crypt/Encoder.groovy | 26 + .../org/xbib/groovy/crypt/Md5Crypt.groovy | 107 +++ .../groovy/org/xbib/groovy/crypt/Salt.groovy | 78 ++ .../org/xbib/groovy/crypt/Sha256Crypt.groovy | 28 + .../org/xbib/groovy/crypt/Sha2Crypt.groovy | 145 ++++ .../org/xbib/groovy/crypt/Sha512Crypt.groovy | 39 + .../random/DevRandomSeedGenerator.groovy | 53 ++ .../xbib/groovy/crypt/random/MTRandom.groovy | 147 ++++ .../crypt/random/MersenneTwisterRandom.groovy | 118 +++ .../groovy/crypt/random/RandomUtil.groovy | 14 + .../random/SecureRandomSeedGenerator.groovy | 25 + .../groovy/crypt/random/SeedException.groovy | 16 + .../groovy/crypt/random/SeedGenerator.groovy | 14 + .../groovy/crypt/random/UUIDGenerator.groovy | 116 +++ .../xbib/groovy/crypt/test/CryptTest.groovy | 172 +++++ .../xbib/groovy/crypt/test/RandomTest.groovy | 19 + groovy-ftp/build.gradle | 7 + .../main/java/org/xbib/groovy/ftp/FTP.java | 396 ++++++++++ .../java/org/xbib/groovy/ftp/FTPContext.java | 27 + .../java/org/xbib/groovy/ftp/WithContext.java | 9 + .../groovy/org/xbib/groovy/ftp/FTPTest.groovy | 22 + groovy-ftp/src/test/resources/log4j2.xml | 13 + groovy-ftps/build.gradle | 7 + .../main/java/org/xbib/groovy/ftps/FTPS.java | 395 ++++++++++ .../org/xbib/groovy/ftps/FTPSContext.java | 24 + .../org/xbib/groovy/ftps/WithContext.java | 6 + .../java/org/xbib/groovy/ftps/FTPSTest.groovy | 39 + groovy-ftps/src/test/resources/log4j2.xml | 13 + groovy-ldap/LICENSE.txt | 202 ++++++ groovy-ldap/README.adoc | 2 + groovy-ldap/build.gradle | 6 + .../main/java/org/xbib/groovy/ldap/LDAP.java | 427 +++++++++++ .../xbib/groovy/ldap/ModificationType.java | 23 + .../java/org/xbib/groovy/ldap/Search.java | 117 +++ .../org/xbib/groovy/ldap/SearchScope.java | 23 + .../org/xbib/groovy/ldap/WithContext.java | 12 + .../org/xbib/groovy/ldap/AddTest.groovy | 14 + .../org/xbib/groovy/ldap/CompareTest.groovy | 19 + .../org/xbib/groovy/ldap/DeleteTest.groovy | 6 + .../org/xbib/groovy/ldap/ModifyDNTest.groovy | 22 + .../org/xbib/groovy/ldap/ModifyTest.groovy | 18 + .../org/xbib/groovy/ldap/ReadTest.groovy | 12 + .../groovy/ldap/SearchAndDeleteTest.groovy | 7 + .../xbib/groovy/ldap/SearchClosureTest.groovy | 8 + .../org/xbib/groovy/ldap/SearchTest.groovy | 30 + .../org/xbib/groovy/ldap/JavaSearchTest.java | 14 + groovy-mail/LICENSE.txt | 202 ++++++ groovy-mail/build.gradle | 5 + .../main/java/org/xbib/groovy/imap/IMAP.java | 211 ++++++ .../org/xbib/groovy/imap/ImapContext.java | 21 + .../org/xbib/groovy/imap/WithContext.java | 10 + .../org/xbib/groovy/imap/package-info.java | 4 + .../main/java/org/xbib/groovy/smtp/SMTP.java | 137 ++++ .../org/xbib/groovy/smtp/SmtpContext.java | 15 + .../org/xbib/groovy/smtp/WithContext.java | 11 + .../org/xbib/groovy/smtp/package-info.java | 4 + groovy-sshd/build.gradle | 6 + .../main/java/org/xbib/groovy/sshd/SFTP.java | 388 ++++++++++ .../org/xbib/groovy/sshd/SFTPContext.java | 42 ++ .../org/xbib/groovy/sshd/WithContext.java | 9 + .../org/xbib/groovy/sshd/package-info.java | 4 + .../org/xbib/groovy/sshd/SFTPTest.groovy | 29 + groovy-sshd/src/test/resources/log4j2.xml | 13 + settings.gradle | 7 + 89 files changed, 6594 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/compile/groovy.gradle create mode 100644 gradle/compile/java.gradle create mode 100644 gradle/documentation/asciidoc.gradle create mode 100644 gradle/ide/idea.gradle create mode 100644 gradle/publishing/publication.gradle create mode 100644 gradle/publishing/sonatype.gradle create mode 100644 gradle/repositories/maven.gradle create mode 100644 gradle/test/junit5.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 groovy-crypt/CREDITS.txt create mode 100644 groovy-crypt/LICENSE.txt create mode 100644 groovy-crypt/README.adoc create mode 100644 groovy-crypt/src/docs/asciidoc/css/foundation.css create mode 100644 groovy-crypt/src/docs/asciidoc/index.adoc create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Crypt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptType.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/DesCrypt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Encoder.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Md5Crypt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Salt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha256Crypt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha2Crypt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha512Crypt.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/DevRandomSeedGenerator.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MTRandom.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MersenneTwisterRandom.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/RandomUtil.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SecureRandomSeedGenerator.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedException.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedGenerator.groovy create mode 100644 groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/UUIDGenerator.groovy create mode 100644 groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy create mode 100644 groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/RandomTest.groovy create mode 100644 groovy-ftp/build.gradle create mode 100644 groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTP.java create mode 100644 groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTPContext.java create mode 100644 groovy-ftp/src/main/java/org/xbib/groovy/ftp/WithContext.java create mode 100644 groovy-ftp/src/test/groovy/org/xbib/groovy/ftp/FTPTest.groovy create mode 100644 groovy-ftp/src/test/resources/log4j2.xml create mode 100644 groovy-ftps/build.gradle create mode 100644 groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPS.java create mode 100644 groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPSContext.java create mode 100644 groovy-ftps/src/main/java/org/xbib/groovy/ftps/WithContext.java create mode 100644 groovy-ftps/src/test/java/org/xbib/groovy/ftps/FTPSTest.groovy create mode 100644 groovy-ftps/src/test/resources/log4j2.xml create mode 100644 groovy-ldap/LICENSE.txt create mode 100644 groovy-ldap/README.adoc create mode 100644 groovy-ldap/build.gradle create mode 100644 groovy-ldap/src/main/java/org/xbib/groovy/ldap/LDAP.java create mode 100644 groovy-ldap/src/main/java/org/xbib/groovy/ldap/ModificationType.java create mode 100644 groovy-ldap/src/main/java/org/xbib/groovy/ldap/Search.java create mode 100644 groovy-ldap/src/main/java/org/xbib/groovy/ldap/SearchScope.java create mode 100644 groovy-ldap/src/main/java/org/xbib/groovy/ldap/WithContext.java create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/AddTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/CompareTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/DeleteTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyDNTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ReadTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchAndDeleteTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchClosureTest.groovy create mode 100644 groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchTest.groovy create mode 100644 groovy-ldap/src/test/java/org/xbib/groovy/ldap/JavaSearchTest.java create mode 100644 groovy-mail/LICENSE.txt create mode 100644 groovy-mail/build.gradle create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/imap/IMAP.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/imap/ImapContext.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/imap/WithContext.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/imap/package-info.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/smtp/SMTP.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/smtp/SmtpContext.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/smtp/WithContext.java create mode 100644 groovy-mail/src/main/java/org/xbib/groovy/smtp/package-info.java create mode 100644 groovy-sshd/build.gradle create mode 100644 groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTP.java create mode 100644 groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTPContext.java create mode 100644 groovy-sshd/src/main/java/org/xbib/groovy/sshd/WithContext.java create mode 100644 groovy-sshd/src/main/java/org/xbib/groovy/sshd/package-info.java create mode 100644 groovy-sshd/src/test/groovy/org/xbib/groovy/sshd/SFTPTest.groovy create mode 100644 groovy-sshd/src/test/resources/log4j2.xml create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acc234e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +data +work +logs +build +target +/.idea +.DS_Store +*.iml +/.settings +/.classpath +/.project +/.gradle +*~ +*.orig diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. + + "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. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6c2cdde --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id "de.marcphilipp.nexus-publish" version "0.4.0" + id "io.codearte.nexus-staging" version "0.21.1" +} + +wrapper { + gradleVersion = "${project.property('gradle.wrapper.version')}" + distributionType = Wrapper.DistributionType.ALL +} + +ext { + user = 'jprante' + name = 'groovy-extensions' + description = 'Groovy extensions' + inceptionYear = '2021' + url = 'https://github.com/' + user + '/' + name + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:ssh://git@github.com:' + user + '/' + name + '.git' + issueManagementSystem = 'Github' + issueManagementUrl = ext.scmUrl + '/issues' + licenseName = 'The Apache License, Version 2.0' + licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' +} + +subprojects { + apply plugin: 'java-library' + apply from: rootProject.file('gradle/ide/idea.gradle') + apply from: rootProject.file('gradle/compile/java.gradle') + apply from: rootProject.file('gradle/test/junit5.gradle') + apply from: rootProject.file('gradle/repositories/maven.gradle') + apply from: rootProject.file('gradle/publishing/publication.gradle') +} +apply from: rootProject.file('gradle/publishing/sonatype.gradle') diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c617bba --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +group = org.xbib +name = groovy-extensions +version = 0.0.1 + +groovy.version = 2.5.12 +gradle.wrapper.version = 6.6.1 +ftp.version = 2.6.0 +mail.version = 1.6.2 +sshd.version = 2.6.0.0 +log4j.version = 2.14.0 +junit4.version = 4.13 diff --git a/gradle/compile/groovy.gradle b/gradle/compile/groovy.gradle new file mode 100644 index 0000000..1abf883 --- /dev/null +++ b/gradle/compile/groovy.gradle @@ -0,0 +1,34 @@ +apply plugin: 'groovy' + +dependencies { + implementation "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy" +} + +compileGroovy { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileTestGroovy { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(GroovyCompile) { + options.compilerArgs + if (!options.compilerArgs.contains("-processor")) { + options.compilerArgs << '-proc:none' + } + groovyOptions.optimizationOptions.indy = true +} + +task groovydocJar(type: Jar, dependsOn: 'groovydoc') { + from groovydoc.destinationDir + archiveClassifier.set('javadoc') +} + +configurations.all { + resolutionStrategy { + force "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy" + } +} diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle new file mode 100644 index 0000000..c9bba7f --- /dev/null +++ b/gradle/compile/java.gradle @@ -0,0 +1,43 @@ + +apply plugin: 'java-library' + +java { + modularity.inferModulePath.set(true) +} + +compileJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +jar { + manifest { + attributes('Implementation-Version': project.version) + } +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' +} + +artifacts { + archives sourcesJar, javadocJar +} + +tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:all,-fallthrough' +} + +javadoc { + options.addStringOption('Xdoclint:none', '-quiet') +} diff --git a/gradle/documentation/asciidoc.gradle b/gradle/documentation/asciidoc.gradle new file mode 100644 index 0000000..87ba22e --- /dev/null +++ b/gradle/documentation/asciidoc.gradle @@ -0,0 +1,55 @@ +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' + +configurations { + asciidoclet +} + +dependencies { + asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" +} + + +asciidoctor { + backends 'html5' + outputDir = file("${rootProject.projectDir}/docs") + separateOutputDirs = false + attributes 'source-highlighter': 'coderay', + idprefix: '', + idseparator: '-', + toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css" +} + + +/*javadoc { +options.docletpath = configurations.asciidoclet.files.asType(List) +options.doclet = 'org.asciidoctor.Asciidoclet' +//options.overview = "src/docs/asciidoclet/overview.adoc" +options.addStringOption "-base-dir", "${projectDir}" +options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" +configure(options) { + noTimestamp = true +} +}*/ + + +/*javadoc { + options.docletpath = configurations.asciidoclet.files.asType(List) + options.doclet = 'org.asciidoctor.Asciidoclet' + options.overview = "${rootProject.projectDir}/src/docs/asciidoclet/overview.adoc" + options.addStringOption "-base-dir", "${projectDir}" + options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/xbib/${project.name}" + options.destinationDirectory(file("${projectDir}/docs/javadoc")) + configure(options) { + noTimestamp = true + } +}*/ diff --git a/gradle/ide/idea.gradle b/gradle/ide/idea.gradle new file mode 100644 index 0000000..64e2167 --- /dev/null +++ b/gradle/ide/idea.gradle @@ -0,0 +1,13 @@ +apply plugin: 'idea' + +idea { + module { + outputDir file('build/classes/java/main') + testOutputDir file('build/classes/java/test') + } +} + +if (project.convention.findPlugin(JavaPluginConvention)) { + //sourceSets.main.output.classesDirs = file("build/classes/java/main") + //sourceSets.test.output.classesDirs = file("build/classes/java/test") +} diff --git a/gradle/publishing/publication.gradle b/gradle/publishing/publication.gradle new file mode 100644 index 0000000..2e2b2c0 --- /dev/null +++ b/gradle/publishing/publication.gradle @@ -0,0 +1,66 @@ + +apply plugin: "de.marcphilipp.nexus-publish" + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + pom { + name = project.name + description = rootProject.ext.description + url = rootProject.ext.url + inceptionYear = rootProject.ext.inceptionYear + packaging = 'jar' + organization { + name = 'xbib' + url = 'https://xbib.org' + } + developers { + developer { + id = 'jprante' + name = 'Jörg Prante' + email = 'joergprante@gmail.com' + url = 'https://github.com/jprante' + } + } + scm { + url = rootProject.ext.scmUrl + connection = rootProject.ext.scmConnection + developerConnection = rootProject.ext.scmDeveloperConnection + } + issueManagement { + system = rootProject.ext.issueManagementSystem + url = rootProject.ext.issueManagementUrl + } + licenses { + license { + name = rootProject.ext.licenseName + url = rootProject.ext.licenseUrl + distribution = 'repo' + } + } + } + } + } +} + +if (project.hasProperty("signing.keyId")) { + apply plugin: 'signing' + signing { + sign publishing.publications.mavenJava + } +} + +if (project.hasProperty("ossrhUsername")) { + nexusPublishing { + repositories { + sonatype { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } + } + } +} diff --git a/gradle/publishing/sonatype.gradle b/gradle/publishing/sonatype.gradle new file mode 100644 index 0000000..e1813f3 --- /dev/null +++ b/gradle/publishing/sonatype.gradle @@ -0,0 +1,11 @@ + +if (project.hasProperty('ossrhUsername') && project.hasProperty('ossrhPassword')) { + + apply plugin: 'io.codearte.nexus-staging' + + nexusStaging { + username = project.property('ossrhUsername') + password = project.property('ossrhPassword') + packageGroup = "org.xbib" + } +} diff --git a/gradle/repositories/maven.gradle b/gradle/repositories/maven.gradle new file mode 100644 index 0000000..ec58acb --- /dev/null +++ b/gradle/repositories/maven.gradle @@ -0,0 +1,4 @@ +repositories { + mavenLocal() + mavenCentral() +} diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle new file mode 100644 index 0000000..cfef972 --- /dev/null +++ b/gradle/test/junit5.gradle @@ -0,0 +1,27 @@ + +def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.6.2' +def hamcrestVersion = project.hasProperty('hamcrest.version')?project.property('hamcrest.version'):'2.2' + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" + testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" + testImplementation "org.hamcrest:hamcrest-library:${hamcrestVersion}" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" +} + +test { + useJUnitPlatform() + failFast = true + testLogging { + events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' + } + afterSuite { desc, result -> + if (!desc.parent) { + println "\nTest result: ${result.resultType}" + println "Test summary: ${result.testCount} tests, " + + "${result.successfulTestCount} succeeded, " + + "${result.failedTestCount} failed, " + + "${result.skippedTestCount} skipped" + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..33682bb --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## 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 or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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 Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +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 execute + +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 + +: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 %* + +: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/groovy-crypt/CREDITS.txt b/groovy-crypt/CREDITS.txt new file mode 100644 index 0000000..5b8d20d --- /dev/null +++ b/groovy-crypt/CREDITS.txt @@ -0,0 +1,18 @@ + +Credits go to Carl Harris (soulwing) who created crpyt4j + +http://www.solewing.org/blog/2014/02/unix-compatible-password-encryption-for-java/ + +and released it under Apache Software License 2.0. + +This software was converted to Groovy code, with HMAC methods added. + +Credits to Apache Commons UnixCrypt + +https://github.com/apache/commons-codec/blob/master/src/main/java/org/apache/commons/codec/digest/UnixCrypt.java + +Daniel Dyer (https://maths.uncommons.org/) for the Java port of Mersenne Twister Random + +Flake IDs, used for UUID generation, are based on + +http://boundary.com/blog/2012/01/12/flake-a-decentralized-k-ordered-unique-id-generator-in-erlang diff --git a/groovy-crypt/LICENSE.txt b/groovy-crypt/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/groovy-crypt/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. + + "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. diff --git a/groovy-crypt/README.adoc b/groovy-crypt/README.adoc new file mode 100644 index 0000000..8a8c0d6 --- /dev/null +++ b/groovy-crypt/README.adoc @@ -0,0 +1,54 @@ + += Groovy crypt library + +image:https://api.travis-ci.org/xbib/groovy-crypt.svg[title="Build status", link="https://travis-ci.org/xbib/groovy-crypt/"] +image:https://maven-badges.herokuapp.com/maven-central/org.xbib.groovy/groovy-crypt/badge.svg[title="Maven Central", link="http://search.maven.org/#search%7Cga%7C1%7Cxbib%20groovy-crypt"] +image:https://img.shields.io/badge/License-Apache%202.0-blue.svg[title="Apache License 2.0", link="https://opensource.org/licenses/Apache-2.0"] +image:https://img.shields.io/twitter/url/https/twitter.com/xbib.svg?style=social&label=Follow%20%40xbib[title="Twitter", link="https://twitter.com/xbib"] + +image:https://sonarqube.com/api/badges/gate?key=org.xbib.groovy:groovy-crypt[title="Quality Gate", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"] +image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=coverage[title="Coverage", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"] +image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=vulnerabilities[title="Vulnerabilities", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"] +image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=bugs[title="Bugs", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"] +image:https://sonarqube.com/api/badges/measure?key=org.xbib.groovy:groovy-crypt&metric=sqale_debt_ratio[title="Technical debt ratio", link="https://sonarqube.com/dashboard/index?id=org.xbib.groovy%3Agroovy-crypt"] + +This Groovy crypt implementation of the `crypt(3)` function provided in the GNU C library (glibc) +was derived from crypt4j by Carl Harris https://github.com/soulwing/crypt4j + +This implementation supports the MD5, SHA, SHA-256, and SHA-512 variants. +Additionally, it supports legacy DES and HMAC. + +It is useful for LDAP passwords or secure cookie handling. + += Usage + + void testHMAC() { + String s = "Hello World" + String secret = "secret" + String code = CryptUtil.hmac(s, secret, "HmacSHA1") + assertEquals("858da8837b87f04b052c0f6e954c3f7bbe081164", code) + } + + void testSHA() { + String plaintext = 'geheim' + String code = CryptUtil.sha(plaintext) + assertEquals('SHA algorithm', + '{sha}kGByAB793z4R5tK1eC9Hd/4Dhzk=', code) + } + + void testSSHA256() { + String plaintext = 'geheim' + byte[] salt = "467dd5b71e8d0f9e".decodeHex() + String code = CryptUtil.ssha256(plaintext, salt) + assertEquals('test SSHA-256 method', + '{ssha256}9yT5rYItjXK+mY8sKNBcKsKSnlY6ysTg8wbDVmAguTFGfdW3Ho0Png==', code) + } + + void testSSHA512() { + String plaintext = 'geheim' + byte[] salt = "3c68f1f47f41d82f".decodeHex() + String code = CryptUtil.ssha512(plaintext, salt) + assertEquals('test SSHA-512 method', + '{ssha512}jeWuCXRjsvKh/vK548GP9ZCs4q9Sh1u700C8eONyV+EL/P810C8vlx9Eu4vRjHq/TDoGW8FE1l/P2KG3w9lHITxo8fR/Qdgv', code) + } + diff --git a/groovy-crypt/src/docs/asciidoc/css/foundation.css b/groovy-crypt/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/groovy-crypt/src/docs/asciidoc/css/foundation.css @@ -0,0 +1,684 @@ +/*! normalize.css v2.1.2 | MIT License | git.io/normalize */ +/* ========================================================================== HTML5 display definitions ========================================================================== */ +/** Correct `block` display not defined in IE 8/9. */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; } + +/** Correct `inline-block` display not defined in IE 8/9. */ +audio, canvas, video { display: inline-block; } + +/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */ +audio:not([controls]) { display: none; height: 0; } + +/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */ +[hidden], template { display: none; } + +script { display: none !important; } + +/* ========================================================================== Base ========================================================================== */ +/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */ +html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } + +/** Remove default margin. */ +body { margin: 0; } + +/* ========================================================================== Links ========================================================================== */ +/** Remove the gray background color from active links in IE 10. */ +a { background: transparent; } + +/** Address `outline` inconsistency between Chrome and other browsers. */ +a:focus { outline: thin dotted; } + +/** Improve readability when focused and also mouse hovered in all browsers. */ +a:active, a:hover { outline: 0; } + +/* ========================================================================== Typography ========================================================================== */ +/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */ +h1 { font-size: 2em; margin: 0.67em 0; } + +/** Address styling not present in IE 8/9, Safari 5, and Chrome. */ +abbr[title] { border-bottom: 1px dotted; } + +/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */ +b, strong { font-weight: bold; } + +/** Address styling not present in Safari 5 and Chrome. */ +dfn { font-style: italic; } + +/** Address differences between Firefox and other browsers. */ +hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; } + +/** Address styling not present in IE 8/9. */ +mark { background: #ff0; color: #000; } + +/** Correct font family set oddly in Safari 5 and Chrome. */ +code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; } + +/** Improve readability of pre-formatted text in all browsers. */ +pre { white-space: pre-wrap; } + +/** Set consistent quote types. */ +q { quotes: "\201C" "\201D" "\2018" "\2019"; } + +/** Address inconsistent and variable font size in all browsers. */ +small { font-size: 80%; } + +/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */ +sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } + +sup { top: -0.5em; } + +sub { bottom: -0.25em; } + +/* ========================================================================== Embedded content ========================================================================== */ +/** Remove border when inside `a` element in IE 8/9. */ +img { border: 0; } + +/** Correct overflow displayed oddly in IE 9. */ +svg:not(:root) { overflow: hidden; } + +/* ========================================================================== Figures ========================================================================== */ +/** Address margin not present in IE 8/9 and Safari 5. */ +figure { margin: 0; } + +/* ========================================================================== Forms ========================================================================== */ +/** Define consistent border, margin, and padding. */ +fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } + +/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */ +legend { border: 0; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */ +button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ } + +/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */ +button, input { line-height: normal; } + +/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */ +button, select { text-transform: none; } + +/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */ +button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } + +/** Re-set default cursor for disabled elements. */ +button[disabled], html input[disabled] { cursor: default; } + +/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */ +input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } + +/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */ +input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; } + +/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */ +input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + +/** Remove inner padding and border in Firefox 4+. */ +button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } + +/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */ +textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ } + +/* ========================================================================== Tables ========================================================================== */ +/** Remove most spacing between table cells. */ +table { border-collapse: collapse; border-spacing: 0; } + +meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; } + +meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; } + +meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; } + +*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } + +html, body { font-size: 100%; } + +body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; } + +a:hover { cursor: pointer; } + +img, object, embed { max-width: 100%; height: auto; } + +object, embed { height: 100%; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; } + +.left { float: left !important; } + +.right { float: right !important; } + +.text-left { text-align: left !important; } + +.text-right { text-align: right !important; } + +.text-center { text-align: center !important; } + +.text-justify { text-align: justify !important; } + +.hide { display: none; } + +.antialiased { -webkit-font-smoothing: antialiased; } + +img { display: inline-block; vertical-align: middle; } + +textarea { height: auto; min-height: 50px; } + +select { width: 100%; } + +object, svg { display: inline-block; vertical-align: middle; } + +.center { margin-left: auto; margin-right: auto; } + +.spread { width: 100%; } + +p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; } + +.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; } + +/* Typography resets */ +div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; } + +/* Default Link Styles */ +a { color: #2ba6cb; text-decoration: none; line-height: inherit; } +a:hover, a:focus { color: #2795b6; } +a img { border: none; } + +/* Default paragraph styles */ +p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; } +p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; } + +/* Default header styles */ +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; } +h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; } + +h1 { font-size: 2.125em; } + +h2 { font-size: 1.6875em; } + +h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; } + +h4 { font-size: 1.125em; } + +h5 { font-size: 1.125em; } + +h6 { font-size: 1em; } + +hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; } + +/* Helpful Typography Defaults */ +em, i { font-style: italic; line-height: inherit; } + +strong, b { font-weight: bold; line-height: inherit; } + +small { font-size: 60%; line-height: inherit; } + +code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; } + +/* Lists */ +ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; } + +ul, ol { margin-left: 1.5em; } +ul.no-bullet, ol.no-bullet { margin-left: 1.5em; } + +/* Unordered Lists */ +ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ } +ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; } +ul.square { list-style-type: square; } +ul.circle { list-style-type: circle; } +ul.disc { list-style-type: disc; } +ul.no-bullet { list-style: none; } + +/* Ordered Lists */ +ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; } + +/* Definition Lists */ +dl dt { margin-bottom: 0.3125em; font-weight: bold; } +dl dd { margin-bottom: 1.25em; } + +/* Abbreviations */ +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; } + +abbr { text-transform: none; } + +/* Blockquotes */ +blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; } +blockquote cite { display: block; font-size: 0.8125em; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a, blockquote cite a:visited { color: #555555; } + +blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; } + +/* Microformats */ +.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; } +.vcard li { margin: 0; display: block; } +.vcard .fn { font-weight: bold; font-size: 0.9375em; } + +.vevent .summary { font-weight: bold; } +.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; } + +@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + h1 { font-size: 2.75em; } + h2 { font-size: 2.3125em; } + h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; } + h4 { font-size: 1.4375em; } } +/* Tables */ +table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; } +table thead, table tfoot { background: whitesmoke; font-weight: bold; } +table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; } +table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; } +table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; } +table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; } + +body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; } + +h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; } + +.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; } +.clearfix:after, .float-group:after { clear: both; } + +*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; } +*:not(pre) > code.nobreak { word-wrap: normal; } +*:not(pre) > code.nowrap { white-space: nowrap; } + +pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; } + +em em { font-style: normal; } + +strong strong { font-weight: normal; } + +.keyseq { color: #555555; } + +kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; } + +.keyseq kbd:first-child { margin-left: 0; } + +.keyseq kbd:last-child { margin-right: 0; } + +.menuseq, .menu { color: #090909; } + +b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; } + +b.button:before { content: "["; padding: 0 3px 0 2px; } + +b.button:after { content: "]"; padding: 0 2px 0 3px; } + +#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; } +#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; } +#header:after, #content:after, #footnotes:after, #footer:after { clear: both; } + +#content { margin-top: 1.25em; } + +#content:before { content: none; } + +#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; } +#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; } +#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; } +#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; } +#header .details span:first-child { margin-left: -0.125em; } +#header .details span.email a { color: #6f6f6f; } +#header .details br { display: none; } +#header .details br + span:before { content: "\00a0\2013\00a0"; } +#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; } +#header .details br + span#revremark:before { content: "\00a0|\00a0"; } +#header #revnumber { text-transform: capitalize; } +#header #revnumber:after { content: "\00a0"; } + +#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; } + +#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; } +#toc > ul { margin-left: 0.125em; } +#toc ul.sectlevel0 > li > a { font-style: italic; } +#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; } +#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; } +#toc li { line-height: 1.3334; margin-top: 0.3334em; } +#toc a { text-decoration: none; } +#toc a:active { text-decoration: underline; } + +#toctitle { color: #6f6f6f; font-size: 1.2em; } + +@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; } + body.toc2 { padding-left: 15em; padding-right: 0; } + #toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; } + #toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; } + #toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; } + #toc.toc2 ul ul { margin-left: 0; padding-left: 1em; } + #toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; } + body.toc2.toc-right { padding-left: 0; padding-right: 15em; } + body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } } +@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; } + #toc.toc2 { width: 20em; } + #toc.toc2 #toctitle { font-size: 1.375em; } + #toc.toc2 > ul { font-size: 0.95em; } + #toc.toc2 ul ul { padding-left: 1.25em; } + body.toc2.toc-right { padding-left: 0; padding-right: 20em; } } +#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +#content #toc > :first-child { margin-top: 0; } +#content #toc > :last-child { margin-bottom: 0; } + +#footer { max-width: 100%; background-color: #222222; padding: 1.25em; } + +#footer-text { color: #dddddd; line-height: 1.44; } + +.sect1 { padding-bottom: 0.625em; } + +@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } } +.sect1 + .sect1 { border-top: 1px solid #dddddd; } + +#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; } +#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; } +#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; } +#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; } +#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; } + +.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; } + +.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; } + +table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; } + +.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; } + +table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; } + +.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; } +.admonitionblock > table td.icon { text-align: center; width: 80px; } +.admonitionblock > table td.icon img { max-width: initial; } +.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; } +.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; } +.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; } + +.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; } +.exampleblock > .content > :first-child { margin-top: 0; } +.exampleblock > .content > :last-child { margin-bottom: 0; } + +.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; } +.sidebarblock > :first-child { margin-top: 0; } +.sidebarblock > :last-child { margin-bottom: 0; } +.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; } + +.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; } + +.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; } +.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; } + +.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; } +.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; } +@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } } +@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } } + +.literalblock.output pre { color: #eeeeee; background-color: black; } + +.listingblock pre.highlightjs { padding: 0; } +.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; } + +.listingblock > .content { position: relative; } + +.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; } + +.listingblock:hover code[data-lang]:before { display: block; } + +.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; } + +.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; } + +table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; } + +table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; } + +table.pyhltable td.code { padding-left: .75em; padding-right: 0; } + +pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; } + +pre.pygments .lineno { display: inline-block; margin-right: .25em; } + +table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; } + +.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; } +.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; } +.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; } +.quoteblock blockquote { margin: 0; padding: 0; border: 0; } +.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } +.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; } +.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; } +.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; } +.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; } +.quoteblock .quoteblock blockquote:before { display: none; } + +.verseblock { margin: 0 1em 1.25em 1em; } +.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; } +.verseblock pre strong { font-weight: 400; } +.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; } + +.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; } +.quoteblock .attribution br, .verseblock .attribution br { display: none; } +.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; } + +.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; } +.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; } +.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; } + +table.tableblock { max-width: 100%; border-collapse: separate; } +table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; } + +table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; } + +table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; } + +table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; } + +table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; } + +table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; } + +table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; } + +table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; } + +table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; } + +table.frame-all { border-width: 1px; } + +table.frame-sides { border-width: 0 1px; } + +table.frame-topbot { border-width: 1px 0; } + +th.halign-left, td.halign-left { text-align: left; } + +th.halign-right, td.halign-right { text-align: right; } + +th.halign-center, td.halign-center { text-align: center; } + +th.valign-top, td.valign-top { vertical-align: top; } + +th.valign-bottom, td.valign-bottom { vertical-align: bottom; } + +th.valign-middle, td.valign-middle { vertical-align: middle; } + +table thead th, table tfoot th { font-weight: bold; } + +tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; } + +tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; } + +p.tableblock > code:only-child { background: none; padding: 0; } + +p.tableblock { font-size: 1em; } + +td > div.verse { white-space: pre; } + +ol { margin-left: 1.75em; } + +ul li ol { margin-left: 1.5em; } + +dl dd { margin-left: 1.125em; } + +dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; } + +ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; } + +ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; } + +ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; } + +ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; } + +ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; } + +ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; } +ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; } +ul.inline > li > * { display: block; } + +.unstyled dl dt { font-weight: normal; font-style: normal; } + +ol.arabic { list-style-type: decimal; } + +ol.decimal { list-style-type: decimal-leading-zero; } + +ol.loweralpha { list-style-type: lower-alpha; } + +ol.upperalpha { list-style-type: upper-alpha; } + +ol.lowerroman { list-style-type: lower-roman; } + +ol.upperroman { list-style-type: upper-roman; } + +ol.lowergreek { list-style-type: lower-greek; } + +.hdlist > table, .colist > table { border: 0; background: none; } +.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; } + +td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; } + +td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; } + +.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; } + +.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; } +.colist > table tr > td:first-of-type img { max-width: initial; } +.colist > table tr > td:last-of-type { padding: 0.25em 0; } + +.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; } + +.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; } +.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; } +.imageblock > .title { margin-bottom: 0; } +.imageblock.thumb, .imageblock.th { border-width: 6px; } +.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; } + +.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; } +.image.left { margin-right: 0.625em; } +.image.right { margin-left: 0.625em; } + +a.image { text-decoration: none; display: inline-block; } +a.image object { pointer-events: none; } + +sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; } +sup.footnote a, sup.footnoteref a { text-decoration: none; } +sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; } + +#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; } +#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; } +#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; } +#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; } +#footnotes .footnote:last-of-type { margin-bottom: 0; } +#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; } + +.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; } +.gist .file-data > table td.line-data { width: 99%; } + +div.unbreakable { page-break-inside: avoid; } + +.big { font-size: larger; } + +.small { font-size: smaller; } + +.underline { text-decoration: underline; } + +.overline { text-decoration: overline; } + +.line-through { text-decoration: line-through; } + +.aqua { color: #00bfbf; } + +.aqua-background { background-color: #00fafa; } + +.black { color: black; } + +.black-background { background-color: black; } + +.blue { color: #0000bf; } + +.blue-background { background-color: #0000fa; } + +.fuchsia { color: #bf00bf; } + +.fuchsia-background { background-color: #fa00fa; } + +.gray { color: #606060; } + +.gray-background { background-color: #7d7d7d; } + +.green { color: #006000; } + +.green-background { background-color: #007d00; } + +.lime { color: #00bf00; } + +.lime-background { background-color: #00fa00; } + +.maroon { color: #600000; } + +.maroon-background { background-color: #7d0000; } + +.navy { color: #000060; } + +.navy-background { background-color: #00007d; } + +.olive { color: #606000; } + +.olive-background { background-color: #7d7d00; } + +.purple { color: #600060; } + +.purple-background { background-color: #7d007d; } + +.red { color: #bf0000; } + +.red-background { background-color: #fa0000; } + +.silver { color: #909090; } + +.silver-background { background-color: #bcbcbc; } + +.teal { color: #006060; } + +.teal-background { background-color: #007d7d; } + +.white { color: #bfbfbf; } + +.white-background { background-color: #fafafa; } + +.yellow { color: #bfbf00; } + +.yellow-background { background-color: #fafa00; } + +span.icon > .fa { cursor: default; } + +.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; } +.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; } +.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; } +.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; } +.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; } +.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; } + +.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; } +.conum[data-value] * { color: #fff !important; } +.conum[data-value] + b { display: none; } +.conum[data-value]:after { content: attr(data-value); } +pre .conum[data-value] { position: relative; top: -0.125em; } + +b.conum * { color: inherit !important; } + +.conum:not([data-value]):empty { display: none; } + +.literalblock pre, .listingblock pre { background: #eeeeee; } diff --git a/groovy-crypt/src/docs/asciidoc/index.adoc b/groovy-crypt/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..d75678f --- /dev/null +++ b/groovy-crypt/src/docs/asciidoc/index.adoc @@ -0,0 +1,11 @@ += Groovy Crypt library +Jörg Prante +Version 1.0.0 +:sectnums: +:toc: preamble +:toclevels: 4 +:!toc-title: Content +:experimental: +:description: Crypt library for Groovy +:keywords: Groovy, crypt, des, md5, sha, sha2, sha256, sha512, hmac +:icons: font diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Crypt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Crypt.groovy new file mode 100644 index 0000000..fc1ccb4 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Crypt.groovy @@ -0,0 +1,105 @@ +package org.xbib.groovy.crypt + +import java.lang.reflect.Constructor +import java.lang.reflect.InvocationTargetException +import java.security.NoSuchAlgorithmException + +/** + * A utility class that encrypts password strings using algorithms that are + * compatible with {@code crypt(3)} from the GNU C library. + */ +abstract class Crypt { + + protected final CryptType type + + protected Crypt(CryptType type) { + this.type = type + } + + /** + * Encrypts (digests) the given password using the algorithm identified by the + * given salt. + * @param password the password to encrypt + * @param salt algorithm identifier, parameters, and salt text + * @return the encrypted (digested) password + * @throws NoSuchAlgorithmException if the desired algorithm is not supported + * on this platform + * @throws UnsupportedEncodingException if UTF-8 encoding is not available not + * the platform + * + */ + static String crypt(String password, String salt) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + Salt s = new Salt(salt) + newInstance(CryptType.forSalt(s)).doCrypt(password, s) + } + + /** + * Constructs a new instance of the specified type. + * @param type crypt type + * @return new crypt object + */ + private static Crypt newInstance(CryptType type) throws NoSuchAlgorithmException { + try { + Constructor constructor = type.providerClass.getConstructor(CryptType) + constructor.newInstance(type) + } catch (NoSuchMethodException ex) { + throw new RuntimeException(ex) + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex) + } catch (InvocationTargetException ex) { + throw new RuntimeException(ex) + } catch (InstantiationException ex) { + throw new RuntimeException(ex) + } + } + + /** + * Converts the encrypted password to a crypt output string. + * @param password the encrypted password + * @param salt salt + * @param maxSaltLength maximum allowable length for the salt + * @param params subclass-specific parameters (these will be passed to + * {@link #encodeParameters(Object ...)} + * @return crypt output string + */ + protected String passwordToString(byte[] password, Salt salt, int maxSaltLength, Object... params) { + StringBuilder sb = new StringBuilder() + if (salt.type == 0) { + return encodePassword(password) + } + sb.append('$').append(salt.type).append('$') + if (params != null && params.length != 0) { + String encodedParameters = encodeParameters(params) + if (encodedParameters != null) { + sb.append(encodedParameters).append('$') + } + } + sb.append(salt.getText(maxSaltLength)).append('$') + sb.append(encodePassword(password)) + sb.toString() + } + + protected abstract String encodeParameters(Object... params) + + /** + * Performs the password encryption operation. + * @param password the password to encrypt + * @param salt salt for the encryption + * @return formatted crypt output string + * @throws NoSuchAlgorithmException if the specified encryption type cannot be + * supported on the platform + * @throws UnsupportedEncodingException if the password character encoding + * cannot be supported on the platform + */ + protected abstract String doCrypt(String password, Salt salt) + throws NoSuchAlgorithmException, UnsupportedEncodingException + + /** + * Encodes the password for use in the crypt output string. + * @param password the password to encode + * @return string encoding of {@code password} + */ + protected abstract String encodePassword(byte[] password) + +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptType.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptType.groovy new file mode 100644 index 0000000..ab8a77c --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptType.groovy @@ -0,0 +1,69 @@ +package org.xbib.groovy.crypt + +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * A enumeration of supported encryption types. + */ +enum CryptType { + + DES("0", "DES", DesCrypt), + MD5("1", "MD5", Md5Crypt), + /* Avoid the following: + 2 - the original BCrypt, which has been deprecated because of a security issue a long time before BCrypt became popular. + 2a - the official BCrypt algorithm and a insecure implementation in crypt_blowfish http://seclists.org/oss-sec/2011/q2/632 + 2x - suggested for hashes created by the insecure algorithm for compatibility + 2y - suggested new marker for the fixed crypt_blowfish + */ + SHA256("5", "SHA-256", Sha256Crypt), + SHA512("6", "SHA-512", Sha512Crypt) + + private final Class providerClass + + final String type + + final String algorithm + + private CryptType(String type, String algorithm, Class providerClass) { + this.type = type + this.algorithm = algorithm + this.providerClass = providerClass + } + + /** + * Gets the type instance that corresponds to the type specified by the + * given salt. + * + * @param salt the subject salt + * @return type instance + * @throws NoSuchAlgorithmException if no type corresponds to the given salt + */ + static CryptType forSalt(Salt salt) throws NoSuchAlgorithmException { + for (CryptType type : values()) { + if (type.type == salt.type) { + return type + } + } + throw new NoSuchAlgorithmException("unsupported type") + } + + /** + * Gets the {@code providerClass} property. + * + * @return + */ + Class getProviderClass() { + providerClass + } + + /** + * Creates a new digest for the algorithm specified for this type. + * + * @return message digest + * @throws NoSuchAlgorithmException + */ + MessageDigest newDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(algorithm) + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy new file mode 100644 index 0000000..715e653 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/CryptUtil.groovy @@ -0,0 +1,113 @@ +package org.xbib.groovy.crypt + +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom + +/** + * A utility class for invoking encryption methods and returning LDAP password string, + * using {@link java.security.MessageDigest} and {@link javax.crypto.Mac}. + */ +class CryptUtil { + + private static final Random random = new SecureRandom() + + static String hexDigest(String plainText, String algo, String prefix) throws NoSuchAlgorithmException { + if (plainText == null) { + return null + } + MessageDigest digest = MessageDigest.getInstance(algo) + digest.update(plainText.getBytes(StandardCharsets.UTF_8)) + "{${prefix}}${digest.digest().encodeHex()}" + } + + static String base64Digest(String plainText, String algo, String prefix) throws NoSuchAlgorithmException { + if (plainText == null) { + return null + } + MessageDigest digest = MessageDigest.getInstance(algo) + digest.update(plainText.getBytes(StandardCharsets.UTF_8)) + "{${prefix}}${digest.digest().encodeBase64()}" + } + + static String base64Digest(String plainText, byte[] salt, String algo, String prefix) throws NoSuchAlgorithmException { + if (plainText == null) { + return null + } + MessageDigest digest = MessageDigest.getInstance(algo) + digest.update(plainText.getBytes(StandardCharsets.UTF_8)) + digest.update(salt) + byte[] hash = digest.digest() + byte[] code = new byte[salt.length + hash.length] + System.arraycopy(hash, 0, code, 0, hash.length) + System.arraycopy(salt, 0, code, hash.length, salt.length) + "{${prefix}}${code.encodeBase64()}" + } + + static String crypt(String plainText, String salt) { + "{crypt}${Crypt.crypt(plainText, salt)}" + } + + static String md5(String plainText) { + base64Digest(plainText, 'MD5', 'md5') + } + + static String sha(String plainText) { + base64Digest(plainText, 'SHA', 'sha') + } + + static String sha256(String plainText) { + base64Digest(plainText, 'SHA-256', 'sha256') + } + + static String sha512(String plainText) { + base64Digest(plainText, 'SHA-512', 'sha512') + } + + static String ssha(String plainText, byte[] salt) { + base64Digest(plainText, salt, 'SHA1', 'ssha') + } + + static String ssha256(String plainText, byte[] salt) { + base64Digest(plainText, salt, 'SHA-256', 'ssha256') + } + + static String ssha512(String plainText, byte[] salt) { + base64Digest(plainText, salt, 'SHA-512', 'ssha512') + } + + static String hmacSHA1(String plainText, String secret) { + hmac(plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1") + } + + static String hmacSHA1(byte[] plainText, String secret) { + hmac(plainText, secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1") + } + + static String hmacSHA1(byte[] plainText, byte[] secret) { + hmac(plainText, secret, "HmacSHA1") + } + + static String hmac(String plainText, String secret, String algo) { + hmac(plainText.getBytes(StandardCharsets.UTF_8), secret.getBytes(StandardCharsets.UTF_8), algo) + } + + static String hmac(byte[] plainText, String secret, String algo) { + hmac(plainText, secret.getBytes(StandardCharsets.UTF_8), algo) + } + + static String hmac(byte[] plainText, byte[] secret, String algo) throws NoSuchAlgorithmException { + Mac mac = Mac.getInstance(algo) + mac.init(new SecretKeySpec(secret, algo)) + mac.doFinal(plainText).encodeHex() + } + + static String random(int length) { + byte[] b = new byte[length] + random.nextBytes(b) + b.encodeHex() + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/DesCrypt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/DesCrypt.groovy new file mode 100644 index 0000000..3e0734e --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/DesCrypt.groovy @@ -0,0 +1,390 @@ +package org.xbib.groovy.crypt + +import java.nio.charset.StandardCharsets +import java.security.NoSuchAlgorithmException + +/** + * The DES algorithm. + */ +class DesCrypt extends Crypt { + + private static final int[] CON_SALT = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 36, 37, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 0, 0, 0, 0, 0] + + private static final int[] COV2CHAR = [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, + 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122] + + private static + final char[] SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./".toCharArray() + + private static final boolean[] SHIFT2 = [false, false, true, true, true, true, true, true, false, true, true, + true, true, true, true, false] + + private static final int[][] SKB = [ + [0, 16, 0x20000000, 0x20000010, 0x10000, 0x10010, 0x20010000, 0x20010010, 2048, 2064, 0x20000800, + 0x20000810, 0x10800, 0x10810, 0x20010800, 0x20010810, 32, 48, 0x20000020, 0x20000030, 0x10020, + 0x10030, 0x20010020, 0x20010030, 2080, 2096, 0x20000820, 0x20000830, 0x10820, 0x10830, 0x20010820, + 0x20010830, 0x80000, 0x80010, 0x20080000, 0x20080010, 0x90000, 0x90010, 0x20090000, 0x20090010, + 0x80800, 0x80810, 0x20080800, 0x20080810, 0x90800, 0x90810, 0x20090800, 0x20090810, 0x80020, + 0x80030, 0x20080020, 0x20080030, 0x90020, 0x90030, 0x20090020, 0x20090030, 0x80820, 0x80830, + 0x20080820, 0x20080830, 0x90820, 0x90830, 0x20090820, 0x20090830], + [0, 0x2000000, 8192, 0x2002000, 0x200000, 0x2200000, 0x202000, 0x2202000, 4, 0x2000004, 8196, 0x2002004, + 0x200004, 0x2200004, 0x202004, 0x2202004, 1024, 0x2000400, 9216, 0x2002400, 0x200400, 0x2200400, + 0x202400, 0x2202400, 1028, 0x2000404, 9220, 0x2002404, 0x200404, 0x2200404, 0x202404, 0x2202404, + 0x10000000, 0x12000000, 0x10002000, 0x12002000, 0x10200000, 0x12200000, 0x10202000, 0x12202000, + 0x10000004, 0x12000004, 0x10002004, 0x12002004, 0x10200004, 0x12200004, 0x10202004, 0x12202004, + 0x10000400, 0x12000400, 0x10002400, 0x12002400, 0x10200400, 0x12200400, 0x10202400, 0x12202400, + 0x10000404, 0x12000404, 0x10002404, 0x12002404, 0x10200404, 0x12200404, 0x10202404, 0x12202404], + [0, 1, 0x40000, 0x40001, 0x1000000, 0x1000001, 0x1040000, 0x1040001, 2, 3, 0x40002, 0x40003, 0x1000002, + 0x1000003, 0x1040002, 0x1040003, 512, 513, 0x40200, 0x40201, 0x1000200, 0x1000201, 0x1040200, + 0x1040201, 514, 515, 0x40202, 0x40203, 0x1000202, 0x1000203, 0x1040202, 0x1040203, 0x8000000, + 0x8000001, 0x8040000, 0x8040001, 0x9000000, 0x9000001, 0x9040000, 0x9040001, 0x8000002, 0x8000003, + 0x8040002, 0x8040003, 0x9000002, 0x9000003, 0x9040002, 0x9040003, 0x8000200, 0x8000201, 0x8040200, + 0x8040201, 0x9000200, 0x9000201, 0x9040200, 0x9040201, 0x8000202, 0x8000203, 0x8040202, 0x8040203, + 0x9000202, 0x9000203, 0x9040202, 0x9040203], + [0, 0x100000, 256, 0x100100, 8, 0x100008, 264, 0x100108, 4096, 0x101000, 4352, 0x101100, 4104, 0x101008, + 4360, 0x101108, 0x4000000, 0x4100000, 0x4000100, 0x4100100, 0x4000008, 0x4100008, 0x4000108, + 0x4100108, 0x4001000, 0x4101000, 0x4001100, 0x4101100, 0x4001008, 0x4101008, 0x4001108, 0x4101108, + 0x20000, 0x120000, 0x20100, 0x120100, 0x20008, 0x120008, 0x20108, 0x120108, 0x21000, 0x121000, + 0x21100, 0x121100, 0x21008, 0x121008, 0x21108, 0x121108, 0x4020000, 0x4120000, 0x4020100, + 0x4120100, 0x4020008, 0x4120008, 0x4020108, 0x4120108, 0x4021000, 0x4121000, 0x4021100, 0x4121100, + 0x4021008, 0x4121008, 0x4021108, 0x4121108], + [0, 0x10000000, 0x10000, 0x10010000, 4, 0x10000004, 0x10004, 0x10010004, 0x20000000, 0x30000000, + 0x20010000, 0x30010000, 0x20000004, 0x30000004, 0x20010004, 0x30010004, 0x100000, 0x10100000, + 0x110000, 0x10110000, 0x100004, 0x10100004, 0x110004, 0x10110004, 0x20100000, 0x30100000, + 0x20110000, 0x30110000, 0x20100004, 0x30100004, 0x20110004, 0x30110004, 4096, 0x10001000, 0x11000, + 0x10011000, 4100, 0x10001004, 0x11004, 0x10011004, 0x20001000, 0x30001000, 0x20011000, 0x30011000, + 0x20001004, 0x30001004, 0x20011004, 0x30011004, 0x101000, 0x10101000, 0x111000, 0x10111000, + 0x101004, 0x10101004, 0x111004, 0x10111004, 0x20101000, 0x30101000, 0x20111000, 0x30111000, + 0x20101004, 0x30101004, 0x20111004, 0x30111004], + [0, 0x8000000, 8, 0x8000008, 1024, 0x8000400, 1032, 0x8000408, 0x20000, 0x8020000, 0x20008, 0x8020008, + 0x20400, 0x8020400, 0x20408, 0x8020408, 1, 0x8000001, 9, 0x8000009, 1025, 0x8000401, 1033, + 0x8000409, 0x20001, 0x8020001, 0x20009, 0x8020009, 0x20401, 0x8020401, 0x20409, 0x8020409, + 0x2000000, 0xa000000, 0x2000008, 0xa000008, 0x2000400, 0xa000400, 0x2000408, 0xa000408, 0x2020000, + 0xa020000, 0x2020008, 0xa020008, 0x2020400, 0xa020400, 0x2020408, 0xa020408, 0x2000001, 0xa000001, + 0x2000009, 0xa000009, 0x2000401, 0xa000401, 0x2000409, 0xa000409, 0x2020001, 0xa020001, 0x2020009, + 0xa020009, 0x2020401, 0xa020401, 0x2020409, 0xa020409], + [0, 256, 0x80000, 0x80100, 0x1000000, 0x1000100, 0x1080000, 0x1080100, 16, 272, 0x80010, 0x80110, + 0x1000010, 0x1000110, 0x1080010, 0x1080110, 0x200000, 0x200100, 0x280000, 0x280100, 0x1200000, + 0x1200100, 0x1280000, 0x1280100, 0x200010, 0x200110, 0x280010, 0x280110, 0x1200010, 0x1200110, + 0x1280010, 0x1280110, 512, 768, 0x80200, 0x80300, 0x1000200, 0x1000300, 0x1080200, 0x1080300, 528, + 784, 0x80210, 0x80310, 0x1000210, 0x1000310, 0x1080210, 0x1080310, 0x200200, 0x200300, 0x280200, + 0x280300, 0x1200200, 0x1200300, 0x1280200, 0x1280300, 0x200210, 0x200310, 0x280210, 0x280310, + 0x1200210, 0x1200310, 0x1280210, 0x1280310], + [0, 0x4000000, 0x40000, 0x4040000, 2, 0x4000002, 0x40002, 0x4040002, 8192, 0x4002000, 0x42000, 0x4042000, + 8194, 0x4002002, 0x42002, 0x4042002, 32, 0x4000020, 0x40020, 0x4040020, 34, 0x4000022, 0x40022, + 0x4040022, 8224, 0x4002020, 0x42020, 0x4042020, 8226, 0x4002022, 0x42022, 0x4042022, 2048, + 0x4000800, 0x40800, 0x4040800, 2050, 0x4000802, 0x40802, 0x4040802, 10240, 0x4002800, 0x42800, + 0x4042800, 10242, 0x4002802, 0x42802, 0x4042802, 2080, 0x4000820, 0x40820, 0x4040820, 2082, + 0x4000822, 0x40822, 0x4040822, 10272, 0x4002820, 0x42820, 0x4042820, 10274, 0x4002822, 0x42822, + 0x4042822]] as int[][] + + private static final int[][] SPTRANS = [ + [0x820200, 0x20000, 0x80800000, 0x80820200, 0x800000, 0x80020200, 0x80020000, 0x80800000, 0x80020200, + 0x820200, 0x820000, 0x80000200, 0x80800200, 0x800000, 0, 0x80020000, 0x20000, 0x80000000, + 0x800200, 0x20200, 0x80820200, 0x820000, 0x80000200, 0x800200, 0x80000000, 512, 0x20200, + 0x80820000, 512, 0x80800200, 0x80820000, 0, 0, 0x80820200, 0x800200, 0x80020000, 0x820200, + 0x20000, 0x80000200, 0x800200, 0x80820000, 512, 0x20200, 0x80800000, 0x80020200, 0x80000000, + 0x80800000, 0x820000, 0x80820200, 0x20200, 0x820000, 0x80800200, 0x800000, 0x80000200, 0x80020000, + 0, 0x20000, 0x800000, 0x80800200, 0x820200, 0x80000000, 0x80820000, 512, 0x80020200], + [0x10042004, 0, 0x42000, 0x10040000, 0x10000004, 8196, 0x10002000, 0x42000, 8192, 0x10040004, 4, + 0x10002000, 0x40004, 0x10042000, 0x10040000, 4, 0x40000, 0x10002004, 0x10040004, 8192, 0x42004, + 0x10000000, 0, 0x40004, 0x10002004, 0x42004, 0x10042000, 0x10000004, 0x10000000, 0x40000, 8196, + 0x10042004, 0x40004, 0x10042000, 0x10002000, 0x42004, 0x10042004, 0x40004, 0x10000004, 0, + 0x10000000, 8196, 0x40000, 0x10040004, 8192, 0x10000000, 0x42004, 0x10002004, 0x10042000, 8192, 0, + 0x10000004, 4, 0x10042004, 0x42000, 0x10040000, 0x10040004, 0x40000, 8196, 0x10002000, 0x10002004, + 4, 0x10040000, 0x42000], + [0x41000000, 0x1010040, 64, 0x41000040, 0x40010000, 0x1000000, 0x41000040, 0x10040, 0x1000040, 0x10000, + 0x1010000, 0x40000000, 0x41010040, 0x40000040, 0x40000000, 0x41010000, 0, 0x40010000, 0x1010040, + 64, 0x40000040, 0x41010040, 0x10000, 0x41000000, 0x41010000, 0x1000040, 0x40010040, 0x1010000, + 0x10040, 0, 0x1000000, 0x40010040, 0x1010040, 64, 0x40000000, 0x10000, 0x40000040, 0x40010000, + 0x1010000, 0x41000040, 0, 0x1010040, 0x10040, 0x41010000, 0x40010000, 0x1000000, 0x41010040, + 0x40000000, 0x40010040, 0x41000000, 0x1000000, 0x41010040, 0x10000, 0x1000040, 0x41000040, + 0x10040, 0x1000040, 0, 0x41010000, 0x40000040, 0x41000000, 0x40010040, 64, 0x1010000], + [0x100402, 0x4000400, 2, 0x4100402, 0, 0x4100000, 0x4000402, 0x100002, 0x4100400, 0x4000002, 0x4000000, + 1026, 0x4000002, 0x100402, 0x100000, 0x4000000, 0x4100002, 0x100400, 1024, 2, 0x100400, 0x4000402, + 0x4100000, 1024, 1026, 0, 0x100002, 0x4100400, 0x4000400, 0x4100002, 0x4100402, 0x100000, + 0x4100002, 1026, 0x100000, 0x4000002, 0x100400, 0x4000400, 2, 0x4100000, 0x4000402, 0, 1024, + 0x100002, 0, 0x4100002, 0x4100400, 1024, 0x4000000, 0x4100402, 0x100402, 0x100000, 0x4100402, 2, + 0x4000400, 0x100402, 0x100002, 0x100400, 0x4100000, 0x4000402, 1026, 0x4000000, 0x4000002, + 0x4100400], + [0x2000000, 16384, 256, 0x2004108, 0x2004008, 0x2000100, 16648, 0x2004000, 16384, 8, 0x2000008, 16640, + 0x2000108, 0x2004008, 0x2004100, 0, 16640, 0x2000000, 16392, 264, 0x2000100, 16648, 0, 0x2000008, + 8, 0x2000108, 0x2004108, 16392, 0x2004000, 256, 264, 0x2004100, 0x2004100, 0x2000108, 16392, + 0x2004000, 16384, 8, 0x2000008, 0x2000100, 0x2000000, 16640, 0x2004108, 0, 16648, 0x2000000, 256, + 16392, 0x2000108, 256, 0, 0x2004108, 0x2004008, 0x2004100, 264, 16384, 16640, 0x2004008, + 0x2000100, 264, 8, 16648, 0x2004000, 0x2000008], + [0x20000010, 0x80010, 0, 0x20080800, 0x80010, 2048, 0x20000810, 0x80000, 2064, 0x20080810, 0x80800, + 0x20000000, 0x20000800, 0x20000010, 0x20080000, 0x80810, 0x80000, 0x20000810, 0x20080010, 0, 2048, + 16, 0x20080800, 0x20080010, 0x20080810, 0x20080000, 0x20000000, 2064, 16, 0x80800, 0x80810, + 0x20000800, 2064, 0x20000000, 0x20000800, 0x80810, 0x20080800, 0x80010, 0, 0x20000800, 0x20000000, + 2048, 0x20080010, 0x80000, 0x80010, 0x20080810, 0x80800, 16, 0x20080810, 0x80800, 0x80000, + 0x20000810, 0x20000010, 0x20080000, 0x80810, 0, 2048, 0x20000010, 0x20000810, 0x20080800, + 0x20080000, 2064, 16, 0x20080010], + [4096, 128, 0x400080, 0x400001, 0x401081, 4097, 4224, 0, 0x400000, 0x400081, 129, 0x401000, 1, 0x401080, + 0x401000, 129, 0x400081, 4096, 4097, 0x401081, 0, 0x400080, 0x400001, 4224, 0x401001, 4225, + 0x401080, 1, 4225, 0x401001, 128, 0x400000, 4225, 0x401000, 0x401001, 129, 4096, 128, 0x400000, + 0x401001, 0x400081, 4225, 4224, 0, 128, 0x400001, 1, 0x400080, 0, 0x400081, 0x400080, 4224, 129, + 4096, 0x401081, 0x400000, 0x401080, 1, 4097, 0x401081, 0x400001, 0x401080, 0x401000, 4097], + [0x8200020, 0x8208000, 32800, 0, 0x8008000, 0x200020, 0x8200000, 0x8208020, 32, 0x8000000, 0x208000, + 32800, 0x208020, 0x8008020, 0x8000020, 0x8200000, 32768, 0x208020, 0x200020, 0x8008000, 0x8208020, + 0x8000020, 0, 0x208000, 0x8000000, 0x200000, 0x8008020, 0x8200020, 0x200000, 32768, 0x8208000, 32, + 0x200000, 32768, 0x8000020, 0x8208020, 32800, 0x8000000, 0, 0x208000, 0x8200020, 0x8008020, + 0x8008000, 0x200020, 0x8208000, 32, 0x200020, 0x8008000, 0x8208020, 0x200000, 0x8200000, + 0x8000020, 0x208000, 32800, 0x8008020, 0x8200000, 32, 0x8208000, 0x208020, 0, 0x8000000, + 0x8200020, 32768, 0x208020]] as int[][] + + static final String B64T = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + + DesCrypt(CryptType type) { + super(type) + } + + @Override + protected String doCrypt(String password, Salt salt) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + return crypt(password.getBytes(StandardCharsets.UTF_8), salt.getText(2)) + } + + @Override + protected String encodeParameters(Object... params) { + throw new UnsupportedOperationException() + } + + @Override + protected String encodePassword(byte[] password) { + throw new UnsupportedOperationException() + } + + static String crypt(byte[] original) { + crypt(original, null) + } + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + *

+ * Using unspecified characters as salt results incompatible hash values. + * + * @param original + * plaintext password + * @param salt + * a two character string drawn from [a-zA-Z0-9./] or null for a random one + * @return a 13 character string starting with the salt string + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + */ + static String crypt(byte[] original, String salt) { + if (salt == null) { + Random randomGenerator = new Random() + int numSaltChars = SALT_CHARS.length + salt = "" + SALT_CHARS[randomGenerator.nextInt(numSaltChars)] + SALT_CHARS[randomGenerator.nextInt(numSaltChars)] + } else if (!salt.matches('^[' + B64T + ']{2,}$')) { + throw new IllegalArgumentException('Invalid salt value: ' + salt) + } + StringBuilder sb = new StringBuilder(" ") + char charZero = salt.charAt(0) + char charOne = salt.charAt(1) + sb.setCharAt(0, charZero) + sb.setCharAt(1, charOne) + int eSwap0 = CON_SALT[charZero as int] + int eSwap1 = CON_SALT[charOne as int] << 4 + byte[] key = new byte[8] + for (int i = 0; i < key.length; i++) { + key[i] = 0 + } + for (int i = 0; i < key.length && i < original.length; i++) { + int iChar = original[i] + key[i] = (byte) (iChar.leftShift(1)) + } + int[] schedule = desSetKey(key) + int[] out = body(schedule, eSwap0, eSwap1) + byte[] b = new byte[9] + intToFourBytes(out[0], b, 0) + intToFourBytes(out[1], b, 4) + b[8] = 0 + int i = 2 + int y = 0 + int u = 128 + for (; i < 13; i++) { + int j = 0 + int c = 0 + for (; j < 6; j++) { + c <<= 1 + if ((b[y] & u) != 0) { + c |= 0x1 + } + u >>>= 1 + if (u == 0) { + y++ + u = 128 + } + sb.setCharAt(i, COV2CHAR[c] as char) + } + } + sb.toString() + } + + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + * + * As no salt is given, a random one is used. + * + * @param original + * plaintext password + * @return a 13 character string starting with the salt string + */ + static String crypt(String original) { + crypt(original.getBytes(StandardCharsets.UTF_8)) + } + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + * + * @param original + * plaintext password + * @param salt + * a two character string drawn from [a-zA-Z0-9./] or null for a random one + * @return a 13 character string starting with the salt string + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + */ + static String crypt(String original, String salt) { + crypt(original.getBytes(StandardCharsets.UTF_8), salt) + } + + private static int[] body(int[] schedule, int eSwap0, int eSwap1) { + int left = 0 + int right = 0 + int t + for (int j = 0; j < 25; j++) { + for (int i = 0; i < 32; i += 4) { + left = dEncrypt(left, right, i, eSwap0, eSwap1, schedule) + right = dEncrypt(right, left, i + 2, eSwap0, eSwap1, schedule) + } + t = left + left = right + right = t + } + t = right + right = left >>> 1 | left << 31 + left = t >>> 1 | t << 31 + int[] results = new int[2] + permOp(right, left, 1, 0x55555555, results) + right = results[0] + left = results[1] + permOp(left, right, 8, 0xff00ff, results) + left = results[0] + right = results[1] + permOp(right, left, 2, 0x33333333, results) + right = results[0] + left = results[1] + permOp(left, right, 16, 65535, results) + left = results[0] + right = results[1] + permOp(right, left, 4, 0xf0f0f0f, results) + right = results[0] + left = results[1] + int[] out = new int[2] + out[0] = left + out[1] = right + return out + } + + private static int byteToUnsigned(byte b) { + int value = b + return value < 0 ? value + 256 : value + } + + private static int dEncrypt(int el, int r, int s, int e0, int e1, int[] sArr) { + int v = r ^ r >>> 16 + int u = v & e0 + v &= e1 + u = u ^ u << 16 ^ r ^ sArr[s] + int t = v ^ v << 16 ^ r ^ sArr[s + 1] + t = t >>> 4 | t << 28 + el ^= SPTRANS[1][t & 0x3f] | SPTRANS[3][t >>> 8 & 0x3f] | SPTRANS[5][t >>> 16 & 0x3f] | + SPTRANS[7][t >>> 24 & 0x3f] | SPTRANS[0][u & 0x3f] | SPTRANS[2][u >>> 8 & 0x3f] | + SPTRANS[4][u >>> 16 & 0x3f] | SPTRANS[6][u >>> 24 & 0x3f] + return el + } + + private static int[] desSetKey(byte[] key) { + int[] schedule = new int[32] + int c = fourBytesToInt(key, 0) + int d = fourBytesToInt(key, 4) + int[] results = new int[2] + permOp(d, c, 4, 0xf0f0f0f, results) + d = results[0] + c = results[1] + c = hPermOp(c, -2, 0xcccc0000) + d = hPermOp(d, -2, 0xcccc0000) + permOp(d, c, 1, 0x55555555, results) + d = results[0] + c = results[1] + permOp(c, d, 8, 0xff00ff, results) + c = results[0] + d = results[1] + permOp(d, c, 1, 0x55555555, results) + d = results[0] + c = results[1] + d = ((d & 0xff) << 16) | (d & 0xff00) | ((d & 0xff0000) >>> 16) as int | ((c & 0xf0000000) >>> 4) as int + c &= 0xfffffff + int j = 0 + for (int i = 0; i < 16; i++) { + if (SHIFT2[i]) { + c = c >>> 2 | c << 26 + d = d >>> 2 | d << 26 + } else { + c = c >>> 1 | c << 27 + d = d >>> 1 | d << 27 + } + c &= 0xfffffff + d &= 0xfffffff + int s = SKB[0][c & 0x3f] | SKB[1][c >>> 6 & 0x3 | c >>> 7 & 0x3c] | + SKB[2][c >>> 13 & 0xf | c >>> 14 & 0x30] | + SKB[3][c >>> 20 & 0x1 | c >>> 21 & 0x6 | c >>> 22 & 0x38] + int t = SKB[4][d & 0x3f] | SKB[5][d >>> 7 & 0x3 | d >>> 8 & 0x3c] | SKB[6][d >>> 15 & 0x3f] | + SKB[7][d >>> 21 & 0xf | d >>> 22 & 0x30] + schedule[j++] = (t << 16 | s & 0xffff) + s = ((s >>> 16) | (t & 0xffff0000)) as int + s = s << 4 | s >>> 28 + schedule[j++] = s + } + return schedule + } + + private static int fourBytesToInt(byte[] b, int offset) { + int value = byteToUnsigned(b[offset++]) + value |= byteToUnsigned(b[offset++]) << 8 + value |= byteToUnsigned(b[offset++]) << 16 + value |= byteToUnsigned(b[offset]) << 24 + return value + } + + private static int hPermOp(int a, int n, long m) { + int t = (a << 16 - n ^ a) & (m as int) + a = a ^ t ^ t >>> 16 - n + return a + } + + private static void intToFourBytes(int iValue, byte[] b, int offset) { + b[offset++] = (byte) (iValue & 0xff) + b[offset++] = (byte) (iValue >>> 8 & 0xff) + b[offset++] = (byte) (iValue >>> 16 & 0xff) + b[offset] = (byte) (iValue >>> 24 & 0xff) + } + + private static void permOp(int a, int b, int n, int m, int[] results) { + int t = (a >>> n ^ b) & m + a ^= t << n + b ^= t + results[0] = a + results[1] = b + } + +} \ No newline at end of file diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Encoder.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Encoder.groovy new file mode 100644 index 0000000..48789fe --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Encoder.groovy @@ -0,0 +1,26 @@ +package org.xbib.groovy.crypt + +class Encoder { + + private static final String BASE64_SET = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + /** + * Encodes a 24-bit value as a character array containing base 64. + * @param b2 high-order 8 bits + * @param b1 middle 8 bits + * @param b0 low-order 8 bits + * @param n number of characters to encode + * @return character array of length {@code n} containing the base 64 + * representation of the input value + */ + static char[] base64(byte b2, byte b1, byte b0, int n) { + char[] buf = new char[n]; + int i = 0; + int w = (((int) b2 & 0xff) << 16) | (((int) b1 & 0xff) << 8) | ((int) b0 & 0xff) + while (i < n) { + buf[i++] = BASE64_SET.charAt(w & 0x3f) + w >>>= 6 + } + return buf + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Md5Crypt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Md5Crypt.groovy new file mode 100644 index 0000000..daa33a2 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Md5Crypt.groovy @@ -0,0 +1,107 @@ +package org.xbib.groovy.crypt + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * MD5 crypt implementation. + */ +class Md5Crypt extends Crypt { + + private static final String SALT_PREFIX = '$1$' + + private static final int MAX_SALT_LENGTH = 8 + + private static final int ROUNDS = 1000 + + /** + * Constructs a new instance. + * @param type + */ + Md5Crypt(CryptType type) { + super(type) + } + + @Override + protected String doCrypt(String password, Salt salt) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + byte[] encrypted = doCrypt(password.getBytes(StandardCharsets.UTF_8), + salt.getBytes(MAX_SALT_LENGTH, StandardCharsets.UTF_8)) + return passwordToString(encrypted, salt, MAX_SALT_LENGTH) + } + + protected byte[] doCrypt(byte[] password, byte[] salt) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + final MessageDigest a = type.newDigest() + final int digestLength = a.getDigestLength() + a.update(password) + a.update(SALT_PREFIX.getBytes(StandardCharsets.UTF_8)) + a.update(salt) + final MessageDigest b = type.newDigest() + b.update(password) + b.update(salt) + b.update(password) + final byte[] sumB = b.digest() + int max = (password.length / digestLength) as int + for (int i = 0; i < max; i++) { + a.update(sumB) + } + a.update(sumB, 0, password.length % digestLength) + + /* The original implementation now does something weird: for every 1 + * bit in the key the first 0 is added to the buffer, for every 0 bit + * the first character of the key. This does not seem to be what was + * intended but we have to follow this to be compatible. + */ + final byte[] zero = [0] + for (int length = password.length; length != 0; length >>>= 1) { + if ((length & 1) != 0) { + a.update(zero) + } else { + a.update(password, 0, 1) + } + } + final byte[] sumA = a.digest() + byte[] ac = sumA + for (int i = 0; i < ROUNDS; i++) { + final MessageDigest c = type.newDigest() + if (i % 2 != 0) { + c.update(password) + } else { + c.update(ac) + } + if (i % 3 != 0) { + c.update(salt) + } + if (i % 7 != 0) { + c.update(password) + } + if (i % 2 != 0) { + c.update(ac) + } else { + c.update(password) + } + ac = c.digest() + } + ac + } + + @Override + protected String encodeParameters(Object... params) { + throw new UnsupportedOperationException() + } + + @Override + protected String encodePassword(byte[] password) { + StringBuilder sb = new StringBuilder() + sb.append(Encoder.base64(password[0], password[6], password[12], 4)) + sb.append(Encoder.base64(password[1], password[7], password[13], 4)) + sb.append(Encoder.base64(password[2], password[8], password[14], 4)) + sb.append(Encoder.base64(password[3], password[9], password[15], 4)) + sb.append(Encoder.base64(password[4], password[10], password[5], 4)) + sb.append(Encoder.base64((byte) 0, (byte) 0, password[11], 2)) + sb.toString() + } + +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Salt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Salt.groovy new file mode 100644 index 0000000..6572576 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Salt.groovy @@ -0,0 +1,78 @@ +package org.xbib.groovy.crypt + +import java.nio.charset.Charset + +/** + * Salt for encryption. + */ +class Salt { + + String type + + String params + + String text + + Salt(String salt) { + if (salt.charAt(0) != '$') { + this.type = "0" + this.params = null + this.text = salt + return + } + int index = 1 + int extent = salt.indexOf('$', index) + if (extent == -1) { + throw new IllegalArgumentException("illegal salt format") + } + this.type = salt.substring(index, extent) + index = extent + 1 + if (index > salt.length()) { + throw new IllegalArgumentException("illegal salt format") + } + extent = salt.indexOf('$', index) + if (extent == -1 || salt.substring(index, extent).indexOf('=') == -1) { + this.params = null + } else { + this.params = salt.substring(index, extent) + index = extent + 1 + if (index > salt.length()) { + throw new IllegalArgumentException("illegal salt format") + } + } + extent = salt.indexOf('$', index) + if (extent == -1) { + extent = salt.length() + } + this.text = salt.substring(index, extent) + } + + /** + * Gets the salt text truncated to a given maximum length. + * @param maxLength maximum length + * @return truncated salt text + */ + String getText(int maxLength) { + text.substring(0, Math.min(text.length(), maxLength)) + } + + /** + * Gets the salt text as an array of bytes of a given character encoding. + * @param charset character set name + * @return byte array + * @throws UnsupportedEncodingException + */ + byte[] getBytes(int maxLength, Charset charset) + throws UnsupportedEncodingException { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + Writer writer = new OutputStreamWriter(outputStream, charset) + writer.write(text, 0, Math.min(text.length(), maxLength)) + writer.close() + return outputStream.toByteArray() + } catch (IOException ex) { + throw new RuntimeException(ex) + } + } + +} \ No newline at end of file diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha256Crypt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha256Crypt.groovy new file mode 100644 index 0000000..16a5127 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha256Crypt.groovy @@ -0,0 +1,28 @@ +package org.xbib.groovy.crypt + +/** + * The SHA-256 encryption algorithm. + */ +class Sha256Crypt extends Sha2Crypt { + + Sha256Crypt(CryptType type) { + super(type) + } + + @Override + protected String encodePassword(byte[] password) { + StringBuilder sb = new StringBuilder() + .append(Encoder.base64(password[0], password[10], password[20], 4)) + .append(Encoder.base64(password[21], password[1], password[11], 4)) + .append(Encoder.base64(password[12], password[22], password[2], 4)) + .append(Encoder.base64(password[3], password[13], password[23], 4)) + .append(Encoder.base64(password[24], password[4], password[14], 4)) + .append(Encoder.base64(password[15], password[25], password[5], 4)) + .append(Encoder.base64(password[6], password[16], password[26], 4)) + .append(Encoder.base64(password[27], password[7], password[17], 4)) + .append(Encoder.base64(password[18], password[28], password[8], 4)) + .append(Encoder.base64(password[9], password[19], password[29], 4)) + .append(Encoder.base64((byte) 0, password[31], password[30], 3)) + sb.toString() + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha2Crypt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha2Crypt.groovy new file mode 100644 index 0000000..252815e --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha2Crypt.groovy @@ -0,0 +1,145 @@ +package org.xbib.groovy.crypt + +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +/** + * An abstract base for password encryption that uses one of the SHA-2 + * variants (SHA-256, SHA-512). + */ +abstract class Sha2Crypt extends Crypt { + + private static final String ROUNDS_PARAM = "rounds=" + + private static final int MIN_ROUNDS = 1000 + + private static final int MAX_ROUNDS = 999999999 + + private static final int DEFAULT_ROUNDS = 5000 + + private static final int MAX_SALT_LENGTH = 16 + + protected Sha2Crypt(CryptType type) { + super(type) + } + + @Override + protected String doCrypt(String password, Salt salt) + throws NoSuchAlgorithmException, UnsupportedEncodingException { + Integer rounds = rounds(salt) + byte[] encrypted = doCrypt(password.getBytes(StandardCharsets.UTF_8), + salt.getBytes(MAX_SALT_LENGTH, StandardCharsets.UTF_8), + rounds == null ? DEFAULT_ROUNDS : rounds) + passwordToString(encrypted, salt, MAX_SALT_LENGTH, rounds) + } + + @Override + protected String encodeParameters(Object... params) { + params[0] == null ? null : ROUNDS_PARAM + params[0] + } + + /** + * Gets the number of rounds explicitly requested in the given salt + * @param salt the subject salt + * @return number of rounds requested or {@code null} if the salt does not + * specify the number of rounds + */ + private static Integer rounds(Salt salt) { + String params = salt.params + if (params == null || !params.startsWith(ROUNDS_PARAM)) { + return null + } + Integer rounds = Integer.valueOf(params.substring(ROUNDS_PARAM.length())) + rounds = Math.max(MIN_ROUNDS, rounds) + rounds = Math.min(rounds, MAX_ROUNDS) + rounds + } + + /** + * Encrypts the given password. + * @param password the password to encrypt + * @param salt salt for the encryption + * @param rounds number of rounds requested + * @return byte array with encrypted value. + * @throws NoSuchAlgorithmException + */ + protected byte[] doCrypt(byte[] password, byte[] salt, int rounds) + throws NoSuchAlgorithmException { + final MessageDigest a = type.newDigest() + final int digestLength = a.getDigestLength() + a.update(password) + a.update(salt) + final MessageDigest b = type.newDigest() + b.update(password) + b.update(salt) + b.update(password) + final byte[] sumB = b.digest() + int max = (password.length / digestLength) as int + for (int i = 0; i < max; i++) { + a.update(sumB) + } + a.update(sumB, 0, password.length % digestLength) + for (int length = password.length; length > 0; length >>>= 1) { + if ((length & 1) != 0) { + a.update(sumB) + } else { + a.update(password) + } + } + final byte[] sumA = a.digest() + final MessageDigest dp = type.newDigest() + for (int i = 0; i < password.length; i++) { + dp.update(password) + } + final byte[] sumDP = dp.digest() + final byte[] seqP = makeSequence(sumDP, password.length, digestLength) + final MessageDigest ds = type.newDigest() + max = 16 + ((int) sumA[0] & 0xff) + for (int i = 0; i < max; i++) { + ds.update(salt) + } + final byte[] sumDS = ds.digest() + final byte[] seqS = makeSequence(sumDS, salt.length, digestLength) + byte[] ac = sumA + max = rounds + for (int i = 0; i < max; i++) { + final MessageDigest c = type.newDigest() + if (i % 2 != 0) { + c.update(seqP) + } else { + c.update(ac) + } + if (i % 3 != 0) { + c.update(seqS) + } + if (i % 7 != 0) { + c.update(seqP) + } + if (i % 2 != 0) { + c.update(ac) + } else { + c.update(seqP) + } + ac = c.digest() + } + ac + } + + /** + * Makes a sequence as described in steps 16 and 20 of the algorithm. + * @param sum the intermediate sum to place into the sequence + * @param length length of the sequence in bytes + * @param digestLength length of the digest in bytes + * @return sequence + */ + private static byte[] makeSequence(byte[] sum, int length, final int digestLength) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + int max = (length / digestLength) as int + for (int i = 0; i < max; i++) { + outputStream.write(sum) + } + outputStream.write(sum, 0, length % digestLength) + outputStream.toByteArray() + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha512Crypt.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha512Crypt.groovy new file mode 100644 index 0000000..2540d30 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Sha512Crypt.groovy @@ -0,0 +1,39 @@ +package org.xbib.groovy.crypt + +/** + * The SHA-512 encryption algorithm. + */ +class Sha512Crypt extends Sha2Crypt { + + Sha512Crypt(CryptType type) { + super(type) + } + + @Override + protected String encodePassword(byte[] password) { + StringBuilder sb = new StringBuilder() + .append(Encoder.base64(password[0], password[21], password[42], 4)) + .append(Encoder.base64(password[22], password[43], password[1], 4)) + .append(Encoder.base64(password[44], password[2], password[23], 4)) + .append(Encoder.base64(password[3], password[24], password[45], 4)) + .append(Encoder.base64(password[25], password[46], password[4], 4)) + .append(Encoder.base64(password[47], password[5], password[26], 4)) + .append(Encoder.base64(password[6], password[27], password[48], 4)) + .append(Encoder.base64(password[28], password[49], password[7], 4)) + .append(Encoder.base64(password[50], password[8], password[29], 4)) + .append(Encoder.base64(password[9], password[30], password[51], 4)) + .append(Encoder.base64(password[31], password[52], password[10], 4)) + .append(Encoder.base64(password[53], password[11], password[32], 4)) + .append(Encoder.base64(password[12], password[33], password[54], 4)) + .append(Encoder.base64(password[34], password[55], password[13], 4)) + .append(Encoder.base64(password[56], password[14], password[35], 4)) + .append(Encoder.base64(password[15], password[36], password[57], 4)) + .append(Encoder.base64(password[37], password[58], password[16], 4)) + .append(Encoder.base64(password[59], password[17], password[38], 4)) + .append(Encoder.base64(password[18], password[39], password[60], 4)) + .append(Encoder.base64(password[40], password[61], password[19], 4)) + .append(Encoder.base64(password[62], password[20], password[41], 4)) + .append(Encoder.base64((byte) 0, (byte) 0, password[63], 2)) + sb.toString() + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/DevRandomSeedGenerator.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/DevRandomSeedGenerator.groovy new file mode 100644 index 0000000..5a43af2 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/DevRandomSeedGenerator.groovy @@ -0,0 +1,53 @@ +package org.xbib.groovy.crypt.random + +/** + * RNG seed strategy that gets data from {@literal /dev/random} on systems + * that provide it (e.g. Solaris/Linux). If {@literal /dev/random} does not + * exist or is not accessible, a {@link SeedException} is thrown. + */ +class DevRandomSeedGenerator implements SeedGenerator { + + private static final File DEV_RANDOM = new File("/dev/random") + + /** + * Generate seed. + * @return The requested number of random bytes, read directly from + * {@literal /dev/random}. + * @throws SeedException If {@literal /dev/random} does not exist or is + * not accessible + */ + @Override + byte[] generateSeed(int length) throws SeedException { + FileInputStream file = null + try { + file = new FileInputStream(DEV_RANDOM) + byte[] randomSeed = new byte[length] + int count = 0 + while (count < length) { + int bytesRead = file.read(randomSeed, count, length - count) + if (bytesRead == -1) { + throw new SeedException("end-of-file while reading random data") + } + count += bytesRead + } + return randomSeed + } + catch (IOException ex) { + throw new SeedException("failed reading from " + DEV_RANDOM.getName(), ex) + } + catch (SecurityException ex) { + throw new SeedException("security prevented access to " + DEV_RANDOM.getName(), ex) + } + finally { + if (file != null) { + try { + file.close() + } + catch (IOException ex) { + // ignore + } + } + } + } + +} \ No newline at end of file diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MTRandom.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MTRandom.groovy new file mode 100644 index 0000000..0e1cf01 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MTRandom.groovy @@ -0,0 +1,147 @@ +package org.xbib.groovy.crypt.random + +/** + * + */ +class MTRandom extends Random { + + private final static int UPPER_MASK = 0x80000000 as int + private final static int LOWER_MASK = 0x7fffffff + private final static int N = 624 + private final static int M = 397 + private final static int[] MAGIC = [0x0, 0x9908b0df] as int[] + private final static int MAGIC_FACTOR1 = 1812433253 + private final static int MAGIC_FACTOR2 = 1664525 + private final static int MAGIC_FACTOR3 = 1566083941 + private final static int MAGIC_MASK1 = 0x9d2c5680 as int + private final static int MAGIC_MASK2 = 0xefc60000 as int + private final static int MAGIC_SEED = 19650218 + private final static int DEFAULT_SEED = 5489L + private transient int[] mt + private transient int mti + private transient boolean compat = false + private transient int[] ibuf + + MTRandom() { + } + + MTRandom(boolean compatible) { + super(0L) + compat = compatible + long l = compat ? DEFAULT_SEED : System.currentTimeMillis() + setSeed(l) + } + + MTRandom(long seed) { + super(seed) + } + + MTRandom(byte[] buf) { + super(0L) + setSeed(buf) + } + + MTRandom(int[] buf) { + super(0L) + setSeed(buf) + } + + void setSeed(int seed) { + if (mt == null) { + mt = new int[N] + } + mt[0] = seed + for (mti = 1; mti < N; mti++) { + mt[mti] = (MAGIC_FACTOR1 * (mt[mti - 1] ^ (mt[mti - 1] >>> 30)) + mti) + } + } + + synchronized void setSeed(long seed) { + if (compat) { + setSeed((int) seed) + } else { + if (ibuf == null) { + ibuf = new int[2] + } + ibuf[0] = (int) seed + ibuf[1] = (int) (seed >>> 32) + setSeed(ibuf) + } + } + + void setSeed(byte[] buf) { + setSeed(pack(buf)) + } + + synchronized void setSeed(int[] buf) { + int length = buf.length + if (length == 0) { + throw new IllegalArgumentException("Seed buffer may not be empty") + } + int i = 1 + int j = 0 + int k = (N > length ? N : length) + setSeed(MAGIC_SEED) + for (; k > 0; k--) { + mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * MAGIC_FACTOR2)) + buf[j] + j + i++ + j++ + if (i >= N) { + mt[0] = mt[N - 1] + i = 1 + } + if (j >= length) { + j = 0 + } + } + for (k = N - 1; k > 0; k--) { + mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * MAGIC_FACTOR3)) - i + i++ + if (i >= N) { + mt[0] = mt[N - 1] + i = 1 + } + } + mt[0] = UPPER_MASK + } + protected synchronized int next(int bits) { + int y + int kk + if (mti >= N) { + for (kk = 0; kk < N - M; kk++) { + y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK) + mt[kk] = mt[kk + M] ^ (y >>> 1) ^ MAGIC[y & 0x1] + } + for (; kk < N - 1; kk++) { + y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK) + mt[kk] = mt[kk + (M - N)] ^ (y >>> 1) ^ MAGIC[y & 0x1] + } + y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK) + mt[N - 1] = mt[M - 1] ^ (y >>> 1) ^ MAGIC[y & 0x1] + mti = 0 + } + y = mt[mti++] + y ^= (y >>> 11) + y ^= (y << 7) & MAGIC_MASK1 + y ^= (y << 15) & MAGIC_MASK2 + y ^= (y >>> 18) + y >>> (32 - bits) + } + + private static int[] pack(byte[] buf) { + int k + int blen = buf.length + int ilen = ((buf.length + 3) >>> 2) + int[] ibuf = new int[ilen] + for (int n = 0; n < ilen; n++) { + int m = (n + 1) << 2 + if (m > blen) { + m = blen + } + for (k = buf[--m] & 0xff; (m & 0x3) != 0; k = (k << 8) | buf[--m] & 0xff) { + ibuf[n] = k + } + } + ibuf + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MersenneTwisterRandom.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MersenneTwisterRandom.groovy new file mode 100644 index 0000000..ae43b20 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/MersenneTwisterRandom.groovy @@ -0,0 +1,118 @@ +package org.xbib.groovy.crypt.random + +import java.util.concurrent.locks.ReentrantLock + +class MersenneTwisterRandom extends Random { + + private static final int SEED_SIZE_BYTES = 16 + private static final int N = 624 + private static final int M = 397 + private static final int[] MAG01 = [0, 0x9908b0df] + private static final int UPPER_MASK = 0x80000000 as int + private static final int LOWER_MASK = 0x7fffffff as int + private static final int BOOTSTRAP_SEED = 19650218 + private static final int BOOTSTRAP_FACTOR = 1812433253 + private static final int SEED_FACTOR1 = 1664525 + private static final int SEED_FACTOR2 = 1566083941 + private static final int GENERATE_MASK1 = 0x9d2c5680 as int + private static final int GENERATE_MASK2 = 0xefc60000 as int + private static final SeedGenerator[] GENERATORS = [new SecureRandomSeedGenerator(), new DevRandomSeedGenerator()] + private static final int BITWISE_BYTE_TO_INT = 0x000000FF + private final ReentrantLock lock = new ReentrantLock() + private final int[] mt = new int[N] + private byte[] seed + private int mtIndex = 0 + + MersenneTwisterRandom() { + this.seed = generateSeed(SEED_SIZE_BYTES) + if (seed == null || seed.length != SEED_SIZE_BYTES) { + throw new IllegalArgumentException("Mersenne Twister requires a 128-bit seed") + } + int[] seedInts = convertBytesToInts(this.seed) + mt[0] = BOOTSTRAP_SEED + for (mtIndex = 1; mtIndex < N; mtIndex++) { + mt[mtIndex] = BOOTSTRAP_FACTOR * (mt[mtIndex - 1] ^ (mt[mtIndex - 1] >>> 30)) + mtIndex + } + int i = 1 + int j = 0 + for (int k = Math.max(N, seedInts.length); k > 0; k--) { + mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * SEED_FACTOR1)) + seedInts[j] + j + i++ + j++ + if (i >= N) { + mt[0] = mt[N - 1] + i = 1 + } + if (j >= seedInts.length) { + j = 0 + } + } + for (int k = N - 1; k > 0; k--) { + mt[i] = (mt[i] ^ ((mt[i - 1] ^ (mt[i - 1] >>> 30)) * SEED_FACTOR2)) - i + i++ + if (i >= N) { + mt[0] = mt[N - 1] + i = 1 + } + } + mt[0] = UPPER_MASK + } + + @Override + protected final int next(int bits) { + int y = 0 + try { + lock.lock() + if (mtIndex >= N) { + int kk + for (kk = 0; kk < N - M; kk++) { + y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK) + mt[kk] = mt[kk + M] ^ (y >>> 1) ^ MAG01[y & 0x1] + } + for (; kk < N - 1; kk++) { + y = (mt[kk] & UPPER_MASK) | (mt[kk + 1] & LOWER_MASK) + mt[kk] = mt[kk + (M - N)] ^ (y >>> 1) ^ MAG01[y & 0x1] + } + y = (mt[N - 1] & UPPER_MASK) | (mt[0] & LOWER_MASK) + mt[N - 1] = mt[M - 1] ^ (y >>> 1) ^ MAG01[y & 0x1] + mtIndex = 0 + } + y = mt[mtIndex++] + } + finally { + lock.unlock() + } + y ^= (y >>> 11) + y ^= (y << 7) & GENERATE_MASK1 + y ^= (y << 15) & GENERATE_MASK2 + y ^= (y >>> 18) + return y >>> (32 - bits) + } + + private static byte[] generateSeed(int length) { + for (SeedGenerator generator : GENERATORS) { + try { + return generator.generateSeed(length) + } + catch (SeedException ex) { + // ignore + } + } + throw new IllegalStateException("no seed generator available") + } + + private static int[] convertBytesToInts(byte[] bytes) { + if (bytes.length % 4 != 0) { + throw new IllegalArgumentException("number of input bytes must be a multiple of 4") + } + int[] ints = new int[bytes.length / 4] + for (int i = 0; i < ints.length; i++) { + ints[i] = convertBytesToInt(bytes, i * 4) + } + return ints + } + + private static int convertBytesToInt(byte[] bytes, int offset) { + (BITWISE_BYTE_TO_INT & bytes[offset + 3]) | ((BITWISE_BYTE_TO_INT & bytes[offset + 2]) << 8) | ((BITWISE_BYTE_TO_INT & bytes[offset + 1]) << 16) | ((BITWISE_BYTE_TO_INT & bytes[offset]) << 24) + } +} \ No newline at end of file diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/RandomUtil.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/RandomUtil.groovy new file mode 100644 index 0000000..222e660 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/RandomUtil.groovy @@ -0,0 +1,14 @@ +package org.xbib.groovy.crypt.random + +import java.security.SecureRandom + +class RandomUtil { + + static final SecureRandom secureRandom = new SecureRandom() + + static String randomString(int len) { + byte[] b = new byte[len] + secureRandom.nextBytes(b) + b.encodeHex().toString() + } +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SecureRandomSeedGenerator.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SecureRandomSeedGenerator.groovy new file mode 100644 index 0000000..80e4232 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SecureRandomSeedGenerator.groovy @@ -0,0 +1,25 @@ +package org.xbib.groovy.crypt.random + +import java.security.SecureRandom + +/** + *

{@link SeedGenerator} implementation that uses + * {@link java.security.SecureRandom} to generate random seed data.

+ * + *

The advantage of using SecureRandom for seeding but not as the + * primary random number generator is that we can use it to seed random number generators + * that are much faster than SecureRandom.

+ * + *

This is the only seeding strategy that is guaranteed to work on all + * platforms and therefore is provided as a fall-back option should + * none of the other provided {@link SeedGenerator} implementations be + * useable.

+ */ + +class SecureRandomSeedGenerator implements SeedGenerator { + private static final SecureRandom INSTANCE = new SecureRandom() + + byte[] generateSeed(int length) throws SeedException { + return INSTANCE.generateSeed(length) + } +} \ No newline at end of file diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedException.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedException.groovy new file mode 100644 index 0000000..b5abb8e --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedException.groovy @@ -0,0 +1,16 @@ +package org.xbib.groovy.crypt.random + +/** + * Exception thrown by {@link SeedGenerator} implementations when + * they are unable to generate a new seed for a randon number generator. + */ +class SeedException extends Exception { + + SeedException(String message) { + super(message); + } + + SeedException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedGenerator.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedGenerator.groovy new file mode 100644 index 0000000..1d546a5 --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/SeedGenerator.groovy @@ -0,0 +1,14 @@ +package org.xbib.groovy.crypt.random + +/** + * Strategy interface for seeding random number generators. + */ +interface SeedGenerator { + /** + * Generate a seed value for a random number generator. + * @param length The length of the seed to generate (in bytes). + * @return A byte array containing the seed data. + * @throws SeedException If a seed cannot be generated for any reason. + */ + byte[] generateSeed(int length) throws SeedException +} diff --git a/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/UUIDGenerator.groovy b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/UUIDGenerator.groovy new file mode 100644 index 0000000..6b9e6fe --- /dev/null +++ b/groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/random/UUIDGenerator.groovy @@ -0,0 +1,116 @@ +package org.xbib.groovy.crypt.random + +import java.nio.charset.StandardCharsets +import java.util.concurrent.atomic.AtomicInteger + +/** + * UUID generator. The UUIDs are essentially flake ids + * but we use 6 (not 8) bytes for timestamp and 3 (not 2) bytes for sequence number. + */ +class UUIDGenerator { + + private final static byte[] secureAddress = getSecureAddress() + + private final AtomicInteger sequenceNumber = new AtomicInteger(RandomUtil.secureRandom.nextInt()) + + private long lastTimestamp + + String getBase16UUID() { + StringBuilder sb = new StringBuilder() + getUUIDBytes().each { + sb.append(Integer.toHexString((int) it & 0xFF)) + } + sb.toString() + } + + String getBase64UUID() { + byte[] encoded = getBase64UUIDBytes() + new String(encoded, 0, encoded.length, StandardCharsets.US_ASCII) + } + + private static void putLong(byte[] array, long l, int pos, int numberOfLongBytes) { + for (int i=0; i>> (i*8)) + } + } + + private static byte[] getSecureAddress() { + byte[] address = null + try { + address = getMacAddress() + } catch (Throwable t) { + } + if (!isValidAddress(address)) { + address = constructDummyMulticastAddress() + } + byte[] bytes = new byte[6] + RandomUtil.secureRandom.nextBytes(bytes) + for (int i = 0; i < 6; ++i) { + bytes[i] = (bytes[i] ^ address[i]) as byte + } + bytes + } + + private static byte[] constructDummyMulticastAddress() { + byte[] bytes = new byte[6] + RandomUtil.secureRandom.nextBytes(bytes) + bytes[0] = (bytes[0] | 0x01) as byte + bytes + } + + private static byte[] getMacAddress() throws SocketException { + Enumeration en = NetworkInterface.getNetworkInterfaces() + if (en != null) { + while (en.hasMoreElements()) { + NetworkInterface nint = en.nextElement() + if (!nint.isLoopback()) { + byte[] address = nint.getHardwareAddress() + if (isValidAddress(address)) { + return address + } + } + } + } + null + } + + private static boolean isValidAddress(byte[] address) { + if (address == null || address.length != 6) { + return false + } + for (byte b : address) { + if (b != 0x00 as byte) { + return true + } + } + false + } + + private byte[] getUUIDBytes() { + int sequenceId = sequenceNumber.incrementAndGet() & 0xffffff + long timestamp = System.currentTimeMillis() + synchronized (this) { + timestamp = Math.max(lastTimestamp, timestamp) + if (sequenceId == 0) { + timestamp++ + } + lastTimestamp = timestamp + } + byte[] uuidBytes = new byte[15] + putLong(uuidBytes, timestamp, 0, 6) + System.arraycopy(secureAddress, 0, uuidBytes, 6, secureAddress.length) + putLong(uuidBytes, sequenceId, 12, 3) + uuidBytes + } + + private byte[] getBase64UUIDBytes() { + byte[] uuidBytes = getUUIDBytes() + byte[] encoded + try { + encoded = Base64.encoder.encode(uuidBytes) + } catch (IOException e) { + throw new IllegalStateException("should not be thrown", e) + } + encoded + } +} diff --git a/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy b/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy new file mode 100644 index 0000000..c30fd84 --- /dev/null +++ b/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/CryptTest.groovy @@ -0,0 +1,172 @@ +package org.xbib.groovy.crypt.test + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.function.Executable +import org.xbib.groovy.crypt.Crypt +import org.xbib.groovy.crypt.CryptUtil +import org.xbib.groovy.crypt.DesCrypt + +import java.security.NoSuchAlgorithmException + +import static org.junit.jupiter.api.Assertions.assertEquals +import static org.junit.jupiter.api.Assertions.assertNotSame +import static org.junit.jupiter.api.Assertions.assertTrue + +class CryptTest { + + @Test + void testWithNonExistentType() throws Exception { + Assertions.assertThrows(NoSuchAlgorithmException.class, new Executable() { + @Override + void execute() throws Throwable { + Crypt.crypt("password", '$99$XX') + } + }) + } + + @Test + void testUnixCryptStrings() { + assertEquals("xxWAum7tHdIUw", Crypt.crypt("secret", "xx")) + assertEquals("12UFlHxel6uMM", Crypt.crypt("", "12")) + assertEquals("12FJgqDtVOg7Q", Crypt.crypt("secret", "12")) + assertEquals("12FJgqDtVOg7Q", Crypt.crypt("secret", "12345678")) + } + + @Test + void testUnixCryptExplicitCall() { + // A call to crypt() with an empty salt would result in a "$6$" hash. + // Using unixCrypt() explicitly results in a random salt. + assertTrue(DesCrypt.crypt("secret".getBytes()).matches('^[a-zA-Z0-9./]{13}$')) + assertTrue(DesCrypt.crypt("secret".getBytes(), null).matches('^[a-zA-Z0-9./]{13}$')) + } + + /** + * Single character salts are illegal! + * E.g. with glibc 2.13, crypt("secret", "x") = "xxZREZpkHZpkI" but + * crypt("secret", "xx") = "xxWAum7tHdIUw" which makes it unverifyable. + */ + @Test + void testUnixCryptWithHalfSalt() { + Assertions.assertThrows(IllegalArgumentException.class, new Executable() { + @Override + void execute() throws Throwable { + DesCrypt.crypt("secret", "x") + } + }) + } + + /** + * Unimplemented "$foo$" salt prefixes would be threated as UnixCrypt salt. + */ + @Test + void testUnicCryptInvalidSalt() { + Assertions.assertThrows(IllegalArgumentException.class, new Executable() { + @Override + void execute() throws Throwable { + DesCrypt.crypt("secret", '$a') + } + }) + } + + @Test + void testUnixCryptWithEmptySalt() { + Assertions.assertThrows(IllegalArgumentException.class, new Executable() { + @Override + void execute() throws Throwable { + DesCrypt.crypt("secret", "") + } + }) + } + + @Test + void testUnixCryptWithoutSalt() { + final String hash = DesCrypt.crypt("foo"); + assertTrue(hash.matches('^[a-zA-Z0-9./]{13}$')) + final String hash2 = DesCrypt.crypt("foo") + assertNotSame(hash, hash2) + } + + @Test + void testCrypt() { + assertEquals('saszt8mUri4AI', + Crypt.crypt('Hello world!', 'saltstring'), + 'DES with simple password and salt') + assertEquals('$1$saltstri$YMyguxXMBpd2TEZ.vS/3q1', + Crypt.crypt('Hello world!', '$1$saltstring'), + 'MD5 with password and excessive salt') + assertEquals('$5$saltstring$5B8vYYiY.CVt1RlTTf8KbXBH3hsxY/GNooZaBBGWEc5', + Crypt.crypt('Hello world!', '$5$saltstring'), + 'SHA-256 with password < digest and salt < max') + assertEquals('$5$rounds=10000$saltstringsaltst$3xv.VbSHBb41AL9AvLeujZkZRBAwqFMz2.opqey6IcA', + Crypt.crypt('Hello world!', '$5$rounds=10000$saltstringsaltstring'), + 'SHA-256 with excessive salt, explicit rounds') + assertEquals('$5$rounds=5000$toolongsaltstrin$Un/5jzAHMgOGZ5.mWJpuVolil07guHPvOW8mGRcvxa5', + Crypt.crypt('This is just a test', '$5$rounds=5000$toolongsaltstring'), + 'SHA-256 with excessive salt and explicit default rounds param',) + assertEquals('$5$rounds=1400$anotherlongsalts$Rx.j8H.h8HjEDGomFU8bDkXm3XIUnzyxf12oP84Bnq1', + Crypt.crypt('a very much longer text to encrypt. This one even stretches over morethan one line.', '$5$rounds=1400$anotherlongsaltstring'), + 'SHA-256 with password > digest, excessive salt, explicit rounds') + assertEquals('$5$rounds=77777$short$JiO1O3ZpDAxGJeaDIuqCoEFysAe1mZNJRs3pw0KQRd/', + Crypt.crypt('we have a short salt string but not a short password', '$5$rounds=77777$short'), + 'SHA-256 with short salt, long password, explicit rounds') + assertEquals('$5$rounds=123456$asaltof16chars..$gP3VQ/6X7UUEW3HkBn2w1/Ptq2jxPyzV/cZKmF/wJvD', + Crypt.crypt('a short string', '$5$rounds=123456$asaltof16chars..'), + 'SHA-256 with maximal salt, short password, large explicit round count',) + assertEquals('$5$rounds=1000$roundstoolow$yfvwcWrQ8l/K0DAWyuPMDNHpIVlTQebY9l/gL972bIC', + Crypt.crypt('the minimum number is still observed', '$5$rounds=10$roundstoolow'), + 'SHA-256 with rounds below mininum',) + assertEquals('$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1', + Crypt.crypt('Hello world!', '$6$saltstring'), + 'SHA-512 with password.length < digest.length and salt < max',) + assertEquals('$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.', + Crypt.crypt('Hello world!', '$6$rounds=10000$saltstringsaltstring'), + 'SHA-512 with excessive salt, explicit rounds',) + assertEquals('$6$rounds=5000$toolongsaltstrin$lQ8jolhgVRVhY4b5pZKaysCLi0QBxGoNeKQzQ3glMhwllF7oGDZxUhx1yxdYcz/e1JSbq3y6JMxxl8audkUEm0', + Crypt.crypt('This is just a test', '$6$rounds=5000$toolongsaltstring'), + 'SHA-512 with excessive salt and explicit default rounds param',) + assertEquals('$6$rounds=1400$anotherlongsalts$POfYwTEok97VWcjxIiSOjiykti.o/pQs.wPvMxQ6Fm7I6IoYN3CmLs66x9t0oSwbtEW7o7UmJEiDwGqd8p4ur1', + Crypt.crypt('a very much longer text to encrypt. This one even stretches over morethan one line.', '$6$rounds=1400$anotherlongsaltstring'), + 'SHA-512 with password > digest, excessive salt, explicit rounds',) + assertEquals('$6$rounds=77777$short$WuQyW2YR.hBNpjjRhpYD/ifIw05xdfeEyQoMxIXbkvr0gge1a1x3yRULJ5CCaUeOxFmtlcGZelFl5CxtgfiAc0', + Crypt.crypt('we have a short salt string but not a short password', '$6$rounds=77777$short'), + 'SHA-512 with short salt, long password, explicit rounds',) + assertEquals('$6$rounds=123456$asaltof16chars..$BtCwjqMJGx5hrJhZywWvt0RLE8uZ4oPwcelCjmw2kSYu.Ec6ycULevoBK25fs2xXgMNrCzIMVcgEJAstJeonj1', + Crypt.crypt('a short string', '$6$rounds=123456$asaltof16chars..'), + 'SHA-512 with maximal salt, short password, large explicit round count') + assertEquals('$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1xhLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX.', + Crypt.crypt('the minimum number is still observed', '$6$rounds=10$roundstoolow'), + 'SHA-512 with rounds below mininum') + } + + @Test + void testHMAC() { + String s = "Hello World" + String secret = "secret" + String code = CryptUtil.hmac(s, secret, "HmacSHA1") + assertEquals("858da8837b87f04b052c0f6e954c3f7bbe081164", code) + } + + @Test + void testSHA() { + String plaintext = 'geheim' + String code = CryptUtil.sha(plaintext) + assertEquals('{sha}kGByAB793z4R5tK1eC9Hd/4Dhzk=', code, 'SHA algorithm') + } + + @Test + void testSSHA256() { + String plaintext = 'geheim' + byte[] salt = "467dd5b71e8d0f9e".decodeHex() + String code = CryptUtil.ssha256(plaintext, salt) + assertEquals('{ssha256}9yT5rYItjXK+mY8sKNBcKsKSnlY6ysTg8wbDVmAguTFGfdW3Ho0Png==', code, 'test SSHA-256 method') + } + + @Test + void testSSHA512() { + String plaintext = 'geheim' + byte[] salt = "3c68f1f47f41d82f".decodeHex() + String code = CryptUtil.ssha512(plaintext, salt) + assertEquals('{ssha512}jeWuCXRjsvKh/vK548GP9ZCs4q9Sh1u700C8eONyV+EL/P810C8vlx9Eu4vRjHq/TDoGW8FE1l/P2KG3w9lHITxo8fR/Qdgv', code,'test SSHA-512 method') + } +} diff --git a/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/RandomTest.groovy b/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/RandomTest.groovy new file mode 100644 index 0000000..33ce4b7 --- /dev/null +++ b/groovy-crypt/src/test/groovy/org/xbib/groovy/crypt/test/RandomTest.groovy @@ -0,0 +1,19 @@ +package org.xbib.groovy.crypt.test + +import org.junit.jupiter.api.Test +import org.xbib.groovy.crypt.random.RandomUtil + +import static org.junit.jupiter.api.Assertions.assertNotEquals + +class RandomTest { + + @Test + void testRandom() { + String s1 = RandomUtil.randomString(16) + String s2 = RandomUtil.randomString(16) + String s3 = RandomUtil.randomString(16) + assertNotEquals(s1, s2) + assertNotEquals(s1, s3) + assertNotEquals(s2, s3) + } +} diff --git a/groovy-ftp/build.gradle b/groovy-ftp/build.gradle new file mode 100644 index 0000000..32eaa77 --- /dev/null +++ b/groovy-ftp/build.gradle @@ -0,0 +1,7 @@ +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + api "org.xbib:ftp-fs:${project.property('ftp.version')}" + testImplementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" + testImplementation "org.apache.logging.log4j:log4j-jul:${project.property('log4j.version')}" +} diff --git a/groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTP.java b/groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTP.java new file mode 100644 index 0000000..d88ee0c --- /dev/null +++ b/groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTP.java @@ -0,0 +1,396 @@ +package org.xbib.groovy.ftp; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +import groovy.lang.Closure; + +public class FTP { + + private static final int READ_BUFFER_SIZE = 128 * 1024; + + private static final int WRITE_BUFFER_SIZE = 128 * 1024; + + private static final Set DEFAULT_DIR_PERMISSIONS = + PosixFilePermissions.fromString("rwxr-xr-x"); + + private static final Set DEFAULT_FILE_PERMISSIONS = + PosixFilePermissions.fromString("rw-r--r--"); + + private final String url; + + private final Map env; + + private FTP(String url, Map env) { + this.url = url; + this.env = env; + } + + public static FTP newInstance() { + return newInstance("ftp://localhost:21"); + } + + public static FTP newInstance(Map env) { + return newInstance("ftp://localhost:21", env); + } + + public static FTP newInstance(String url) { + return newInstance(url, null); + } + + public static FTP newInstance(String url, Map env) { + return new FTP(url, env); + } + + public Boolean exists(String path) throws Exception { + return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path))); + } + + public Boolean isExecutable(String path) throws Exception { + return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path))); + } + + public Boolean isDirectory(String path) throws Exception { + return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path))); + } + + public Boolean isRegularFile(String path) throws Exception { + return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path))); + } + + public Boolean isHidden(String path) throws Exception { + return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path))); + } + + public Boolean isSameFile(String path1, String path2) throws Exception { + return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2))); + } + + public Boolean isSymbolicLink(String path) throws Exception { + return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path))); + } + + public Boolean isReadable(String path) throws Exception { + return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path))); + } + + public Boolean isWritable(String path) throws Exception { + return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path))); + } + + public void createFile(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes)); + } + + public void createDirectory(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes)); + } + + public void createDirectories(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes)); + } + + public void setAttribute(String path, String attribute, Object value) throws Exception { + performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value)); + } + + public Object getAttribute(String path, String attribute) throws Exception { + return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute)); + } + + public void setPermissions(String path, Set permissions) throws Exception { + performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions)); + } + + public Set getPermissions(String path) throws Exception { + return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path))); + } + + public void setLastModifiedTime(String path, FileTime fileTime) throws Exception { + performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime)); + } + + public FileTime getLastModified(String path) throws Exception{ + return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path))); + } + + public void setOwner(String path, UserPrincipal userPrincipal) throws Exception { + performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal)); + } + + public UserPrincipal getOwner(String path) throws Exception { + return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); + } + + public void each(String path, Closure closure) throws Exception { + performWithContext(ctx -> { + try (DirectoryStream stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) { + stream.forEach(closure::call); + } + return null; + }); + } + + public void eachFilter(String path, DirectoryStream.Filter filter, Closure closure) throws Exception { + performWithContext(ctx -> { + try (DirectoryStream stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)) { + stream.forEach(closure::call); + } + return null; + }); + } + + public void upload(Path source, Path target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(Path source, String target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(InputStream source, Path target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(InputStream source, String target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void download(Path source, Path target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(String source, Path target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(Path source, OutputStream target) throws Exception { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE); + return null; + }); + } + + public void download(String source, OutputStream target) throws Exception { + performWithContext(ctx -> { + Files.copy(ctx.fileSystem.getPath(source), target); + return null; + }); + } + + public void copy(String source, String target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions); + return null; + }); + } + + public void rename(String source, String target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions); + return null; + }); + } + + public void remove(String source) throws Exception { + performWithContext(ctx -> { + Files.deleteIfExists(ctx.fileSystem.getPath(source)); + return null; + }); + } + + private void upload(FTPContext ctx, + ReadableByteChannel source, + Path target, + int bufferSize, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void download(FTPContext ctx, + Path source, + OutputStream outputStream, + int bufferSize) throws Exception { + download(ctx, source, Channels.newChannel(outputStream), bufferSize); + } + + private void download(FTPContext ctx, + Path source, + WritableByteChannel writableByteChannel, + int bufferSize) throws Exception { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel, + bufferSize); + } + + private void download(FTPContext ctx, + Path source, + Path target, + int bufferSize, + CopyOption... copyOptions) throws Exception { + prepareForWrite(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void prepareForWrite(Path path) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + } + if (!Files.exists(path)) { + Files.createFile(path); + } + } + + private void prepareForWrite(Path path, + Set dirPerms, + Set filePerms) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(parent, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(dirPerms); + } + if (!Files.exists(path)) { + Files.createFile(path); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(path, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(filePerms); + } + + private Set prepareReadOptions(CopyOption... copyOptions) { + // ignore user copy options + return EnumSet.of(StandardOpenOption.READ); + } + + private Set prepareWriteOptions(CopyOption... copyOptions) { + Set options = null; + for (CopyOption copyOption : copyOptions) { + if (copyOption == StandardCopyOption.REPLACE_EXISTING) { + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + if (options == null) { + // we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile() + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } + return options; + } + + private void transfer(ReadableByteChannel readableByteChannel, + WritableByteChannel writableByteChannel, + int bufferSize) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + int read; + while ((read = readableByteChannel.read(buffer)) > 0) { + buffer.flip(); + while (read > 0) { + read -= writableByteChannel.write(buffer); + } + buffer.clear(); + } + } + + private T performWithContext(WithContext action) throws Exception { + FTPContext ctx = null; + try { + if (url != null) { + ctx = new FTPContext(URI.create(url), env); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } +} diff --git a/groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTPContext.java b/groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTPContext.java new file mode 100644 index 0000000..7d94f53 --- /dev/null +++ b/groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTPContext.java @@ -0,0 +1,27 @@ +package org.xbib.groovy.ftp; + +import org.xbib.io.ftp.fs.FTPEnvironment; +import org.xbib.io.ftp.fs.FTPFileSystemProvider; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.util.Map; + +/** + */ +class FTPContext { + + final FTPFileSystemProvider provider; + + final FileSystem fileSystem; + + FTPContext(URI uri, Map env) throws IOException { + this.provider = new FTPFileSystemProvider(); + this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment()); + } + + void close() throws IOException { + fileSystem.close(); + } +} diff --git a/groovy-ftp/src/main/java/org/xbib/groovy/ftp/WithContext.java b/groovy-ftp/src/main/java/org/xbib/groovy/ftp/WithContext.java new file mode 100644 index 0000000..9d9efad --- /dev/null +++ b/groovy-ftp/src/main/java/org/xbib/groovy/ftp/WithContext.java @@ -0,0 +1,9 @@ +package org.xbib.groovy.ftp; + +/** + * + * @param the context parameter + */ +public interface WithContext { + T perform(FTPContext ctx) throws Exception; +} diff --git a/groovy-ftp/src/test/groovy/org/xbib/groovy/ftp/FTPTest.groovy b/groovy-ftp/src/test/groovy/org/xbib/groovy/ftp/FTPTest.groovy new file mode 100644 index 0000000..afbd9a7 --- /dev/null +++ b/groovy-ftp/src/test/groovy/org/xbib/groovy/ftp/FTPTest.groovy @@ -0,0 +1,22 @@ +package org.xbib.groovy.ftp + +import groovy.util.logging.Log4j2 +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +import java.nio.file.Files +import java.nio.file.Path + +@Log4j2 +class FTPTest { + + @Disabled + @Test + void testFTP() { + FTP ftp = FTP.newInstance("ftp://demo.wftpserver.com:21", [username: 'demo', password: 'demo'.toCharArray()]) + log.info ftp.exists('/') + ftp.each('/') { Path path -> + log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path) + } + } +} diff --git a/groovy-ftp/src/test/resources/log4j2.xml b/groovy-ftp/src/test/resources/log4j2.xml new file mode 100644 index 0000000..1258d7f --- /dev/null +++ b/groovy-ftp/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/groovy-ftps/build.gradle b/groovy-ftps/build.gradle new file mode 100644 index 0000000..32eaa77 --- /dev/null +++ b/groovy-ftps/build.gradle @@ -0,0 +1,7 @@ +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + api "org.xbib:ftp-fs:${project.property('ftp.version')}" + testImplementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" + testImplementation "org.apache.logging.log4j:log4j-jul:${project.property('log4j.version')}" +} diff --git a/groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPS.java b/groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPS.java new file mode 100644 index 0000000..bb77afb --- /dev/null +++ b/groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPS.java @@ -0,0 +1,395 @@ +package org.xbib.groovy.ftps; + +import groovy.lang.Closure; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +public class FTPS { + + private static final int READ_BUFFER_SIZE = 128 * 1024; + + private static final int WRITE_BUFFER_SIZE = 128 * 1024; + + private static final Set DEFAULT_DIR_PERMISSIONS = + PosixFilePermissions.fromString("rwxr-xr-x"); + + private static final Set DEFAULT_FILE_PERMISSIONS = + PosixFilePermissions.fromString("rw-r--r--"); + + private final String url; + + private final Map env; + + private FTPS(String url, Map env) { + this.url = url; + this.env = env; + } + + public static FTPS newInstance() { + return newInstance("ftps://localhost:21"); + } + + public static FTPS newInstance(Map env) { + return newInstance("ftps://localhost:21", env); + } + + public static FTPS newInstance(String url) { + return newInstance(url, null); + } + + public static FTPS newInstance(String url, Map env) { + return new FTPS(url, env); + } + + public Boolean exists(String path) throws Exception { + return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path))); + } + + public Boolean isExecutable(String path) throws Exception { + return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path))); + } + + public Boolean isDirectory(String path) throws Exception { + return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path))); + } + + public Boolean isRegularFile(String path) throws Exception { + return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path))); + } + + public Boolean isHidden(String path) throws Exception { + return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path))); + } + + public Boolean isSameFile(String path1, String path2) throws Exception { + return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2))); + } + + public Boolean isSymbolicLink(String path) throws Exception { + return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path))); + } + + public Boolean isReadable(String path) throws Exception { + return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path))); + } + + public Boolean isWritable(String path) throws Exception { + return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path))); + } + + public void createFile(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes)); + } + + public void createDirectory(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes)); + } + + public void createDirectories(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes)); + } + + public void setAttribute(String path, String attribute, Object value) throws Exception { + performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value)); + } + + public Object getAttribute(String path, String attribute) throws Exception { + return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute)); + } + + public void setPermissions(String path, Set permissions) throws Exception { + performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions)); + } + + public Set getPermissions(String path) throws Exception { + return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path))); + } + + public void setLastModifiedTime(String path, FileTime fileTime) throws Exception { + performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime)); + } + + public FileTime getLastModified(String path) throws Exception{ + return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path))); + } + + public void setOwner(String path, UserPrincipal userPrincipal) throws Exception { + performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal)); + } + + public UserPrincipal getOwner(String path) throws Exception { + return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); + } + + public void each(String path, Closure closure) throws Exception { + performWithContext(ctx -> { + try (DirectoryStream stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) { + stream.forEach(closure::call); + } + return null; + }); + } + + public void eachFilter(String path, DirectoryStream.Filter filter, Closure closure) throws Exception { + performWithContext(ctx -> { + try (DirectoryStream stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)) { + stream.forEach(closure::call); + } + return null; + }); + } + + public void upload(Path source, Path target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(Path source, String target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(InputStream source, Path target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, Path target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void upload(InputStream source, String target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, String target, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPerms, filePerms, copyOptions); + return null; + }); + } + + public void download(Path source, Path target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(String source, Path target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(Path source, OutputStream target) throws Exception { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE); + return null; + }); + } + + public void download(String source, OutputStream target) throws Exception { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE); + return null; + }); + } + + public void copy(String source, String target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions); + return null; + }); + } + + public void rename(String source, String target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions); + return null; + }); + } + + public void remove(String source) throws Exception { + performWithContext(ctx -> { + Files.deleteIfExists(ctx.fileSystem.getPath(source)); + return null; + }); + } + + private void upload(FTPSContext ctx, + ReadableByteChannel source, + Path target, + int bufferSize, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void download(FTPSContext ctx, + Path source, + OutputStream outputStream, + int bufferSize) throws Exception { + download(ctx, source, Channels.newChannel(outputStream), bufferSize); + } + + private void download(FTPSContext ctx, + Path source, + WritableByteChannel writableByteChannel, + int bufferSize) throws Exception { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel, + bufferSize); + } + + private void download(FTPSContext ctx, + Path source, + Path target, + int bufferSize, + CopyOption... copyOptions) throws Exception { + prepareForWrite(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void prepareForWrite(Path path) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + } + if (!Files.exists(path)) { + Files.createFile(path); + } + } + + private void prepareForWrite(Path path, + Set dirPerms, + Set filePerms) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(parent, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(dirPerms); + } + if (!Files.exists(path)) { + Files.createFile(path); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(path, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(filePerms); + } + + private Set prepareReadOptions(CopyOption... copyOptions) { + // ignore user copy options + return EnumSet.of(StandardOpenOption.READ); + } + + private Set prepareWriteOptions(CopyOption... copyOptions) { + Set options = null; + for (CopyOption copyOption : copyOptions) { + if (copyOption == StandardCopyOption.REPLACE_EXISTING) { + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + if (options == null) { + // we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile() + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } + return options; + } + + private void transfer(ReadableByteChannel readableByteChannel, + WritableByteChannel writableByteChannel, + int bufferSize) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + int read; + while ((read = readableByteChannel.read(buffer)) > 0) { + buffer.flip(); + while (read > 0) { + read -= writableByteChannel.write(buffer); + } + buffer.clear(); + } + } + + private T performWithContext(WithContext action) throws Exception { + FTPSContext ctx = null; + try { + if (url != null) { + ctx = new FTPSContext(URI.create(url), env); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } +} diff --git a/groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPSContext.java b/groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPSContext.java new file mode 100644 index 0000000..7cb7136 --- /dev/null +++ b/groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPSContext.java @@ -0,0 +1,24 @@ +package org.xbib.groovy.ftps; + +import org.xbib.io.ftp.fs.FTPSEnvironment; +import org.xbib.io.ftp.fs.FTPSFileSystemProvider; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.util.Map; + +public class FTPSContext { + + final FTPSFileSystemProvider provider; + + final FileSystem fileSystem; + + FTPSContext(URI uri, Map env) throws IOException { + this.provider = new FTPSFileSystemProvider(); + this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPSEnvironment()); + } + + void close() throws IOException { + fileSystem.close(); + } +} diff --git a/groovy-ftps/src/main/java/org/xbib/groovy/ftps/WithContext.java b/groovy-ftps/src/main/java/org/xbib/groovy/ftps/WithContext.java new file mode 100644 index 0000000..ed47f77 --- /dev/null +++ b/groovy-ftps/src/main/java/org/xbib/groovy/ftps/WithContext.java @@ -0,0 +1,6 @@ +package org.xbib.groovy.ftps; + +public interface WithContext { + + T perform(FTPSContext ctx) throws Exception; +} diff --git a/groovy-ftps/src/test/java/org/xbib/groovy/ftps/FTPSTest.groovy b/groovy-ftps/src/test/java/org/xbib/groovy/ftps/FTPSTest.groovy new file mode 100644 index 0000000..d1b394e --- /dev/null +++ b/groovy-ftps/src/test/java/org/xbib/groovy/ftps/FTPSTest.groovy @@ -0,0 +1,39 @@ +package org.xbib.groovy.ftps + +import groovy.util.logging.Log4j2 +import org.junit.Test +import org.xbib.io.ftp.fs.SecurityMode + +import java.nio.file.Files +import java.nio.file.Path + +@Log4j2 +class FTPSTest { + + @Test + void testExplicitFTPS() { + Map env = [ + username: 'demo', + password: 'password'.toCharArray() + ] + FTPS ftps = FTPS.newInstance("ftps://test.rebex.net:21", env) + log.info ftps.exists('/') + ftps.each('/') { Path path -> + log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path) + } + } + + @Test + void testImplicitFTPS() { + Map env = [ + username: 'demo', + password: 'password'.toCharArray(), + securityMode: SecurityMode.IMPLICIT + ] + FTPS ftps = FTPS.newInstance("ftps://test.rebex.net:990", env) + log.info ftps.exists('/') + ftps.each('/') { Path path -> + log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path) + } + } +} diff --git a/groovy-ftps/src/test/resources/log4j2.xml b/groovy-ftps/src/test/resources/log4j2.xml new file mode 100644 index 0000000..6b15c7a --- /dev/null +++ b/groovy-ftps/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/groovy-ldap/LICENSE.txt b/groovy-ldap/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/groovy-ldap/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. + + "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. diff --git a/groovy-ldap/README.adoc b/groovy-ldap/README.adoc new file mode 100644 index 0000000..1ede2e5 --- /dev/null +++ b/groovy-ldap/README.adoc @@ -0,0 +1,2 @@ +This is a Java 8 / Gradle version of https://svn.apache.org/repos/asf/directory/sandbox/szoerner/groovyldap/ +with some modifications. \ No newline at end of file diff --git a/groovy-ldap/build.gradle b/groovy-ldap/build.gradle new file mode 100644 index 0000000..3e524c4 --- /dev/null +++ b/groovy-ldap/build.gradle @@ -0,0 +1,6 @@ +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + testImplementation "org.codehaus.groovy:groovy:${project.property('groovy.version')}" + testImplementation "junit:junit:${project.property('junit4.version')}" +} diff --git a/groovy-ldap/src/main/java/org/xbib/groovy/ldap/LDAP.java b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/LDAP.java new file mode 100644 index 0000000..439e67e --- /dev/null +++ b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/LDAP.java @@ -0,0 +1,427 @@ +package org.xbib.groovy.ldap; + +import groovy.lang.Closure; + +import javax.naming.Context; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.ModificationItem; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import javax.naming.ldap.LdapName; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A wrapper class which provides LDAP functionality to Groovy. + */ +public class LDAP { + + private static final Logger logger = Logger.getLogger(LDAP.class.getName()); + + private static final String DEFAULT_URL = "ldap://localhost:389/"; + + private final String url; + + private final String bindUser; + + private final String bindPassword; + + private LDAP(String url, String bindUser, String bindPassword) { + this.url = url; + this.bindUser = bindUser; + this.bindPassword = bindPassword; + } + + public static LDAP newInstance() { + return new LDAP(DEFAULT_URL, null, null); + } + + public static LDAP newInstance(String url) { + return new LDAP(url, null, null); + } + + public static LDAP newInstance(String url, String bindUser, String bindPassword) { + return new LDAP(url, bindUser, bindPassword); + } + + /** + * LDAP add operation. Adds a new entry to the directory. The attributes have to be provided as a map. + * + * @param dn DN of the entry + * @param attributes attributes of the entry + * @throws NamingException if DN can not be resolved + */ + public void add(final String dn, final Map attributes) throws NamingException { + WithContext action = ctx -> { + BasicAttributes attrs = new BasicAttributes(); + for (Map.Entry entry : attributes.entrySet()) { + logger.log(Level.FINE, MessageFormat.format("entry {0} {1}", entry, entry.getValue().getClass())); + Attribute attr = createAttribute(entry.getKey(), entry.getValue()); + logger.log(Level.FINE, MessageFormat.format("attr {0} {1}", attr, attr.get().getClass())); + attrs.put(attr); + } + ctx.createSubcontext(dn, attrs); + return null; + }; + performWithContext(action); + } + + /** + * LDAP delete operation. Deletes an entry from the directory. + * + * @param dn DN of the entry + * @throws NamingException if DN can not be resolved + */ + public void delete(final String dn) throws NamingException { + if (!exists(dn)) { + throw new NameNotFoundException("Entry " + dn + " does not exist!"); + } + WithContext action = ctx -> { + ctx.destroySubcontext(dn); + return null; + }; + performWithContext(action); + } + + /** + * Reads an entry by its DN. + * @param dn distinguished name + * @return object + * @throws NamingException if DN can not be resolved + */ + public Object read(final String dn) throws NamingException { + return performWithContext(ctx -> ctx.lookup(dn)); + } + + /** + * Check whether an entry with the given DN exists. The method performs a search to check this, which is not so + * efficient than just reading the entry. + * @param dn distinguished name + * @return true if exists + * @throws NamingException if DN can not be resolved + */ + public Boolean exists(final String dn) throws NamingException { + WithContext action = ctx -> { + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); + searchControls.setReturningAttributes(new String[0]); + searchControls.setReturningObjFlag(false); + try { + ctx.search(dn, "(objectClass=*)", searchControls); + return true; + } catch (NameNotFoundException e) { + logger.log(Level.FINEST, e.getMessage(), e); + } + return false; + }; + return performWithContext(action); + } + + /** + * LDAP compare operation. + * + * @param dn Distinguished name of the entry. + * @param assertion attribute assertion. + * @return true is comparison matches + * @throws NamingException if DN can not be resolved + */ + public boolean compare(final String dn, final Map assertion) throws NamingException { + if (assertion.size() != 1) { + throw new IllegalArgumentException("Assertion may only include one attribute"); + } + WithContext action = ctx -> { + SearchControls searchControls = new SearchControls(); + searchControls.setReturningAttributes(new String[0]); + searchControls.setSearchScope(SearchControls.OBJECT_SCOPE); + searchControls.setReturningObjFlag(false); + String attrName = assertion.keySet().iterator().next(); + String filter = "(" + attrName + "={0})"; + Object value = assertion.get(attrName); + NamingEnumeration enumeration = ctx.search(dn, filter, new Object[]{value}, searchControls); + return enumeration.hasMore(); + }; + return performWithContext(action); + } + + /** + * LDAP modify DN operation. + * + * @param dn Distinguished name of the entry. + * @param newRDN new realtive distinguished name of the entry. + * @param deleteOldRDN if old relative distinguished name should be deleted + * @param newSuperior new superior DN + * @throws NamingException if DN can not be resolved + */ + public void modifyDn(final String dn, final String newRDN, final boolean deleteOldRDN, final String newSuperior) + throws NamingException { + WithContext action = ctx -> { + LdapName source = new LdapName(dn); + LdapName target = new LdapName(newSuperior); + target.add(newRDN); + ctx.addToEnvironment("java.naming.ldap.deleteRDN", Boolean.toString(deleteOldRDN)); + ctx.rename(source, target); + return null; + }; + performWithContext(action); + } + + public void eachEntry(String filter, String base, SearchScope scope, Closure closure) throws NamingException { + eachEntry(new Search(base, scope, filter), closure); + } + + public void eachEntry(Map searchParams, Closure closure) throws NamingException { + eachEntry(new Search(searchParams), closure); + } + + public void eachEntry(String filter, Closure closure) throws NamingException { + eachEntry(filter, "", SearchScope.SUB, closure); + } + + public void eachEntry(Search search, Closure closure) throws NamingException { + WithContext action = ctx -> { + SearchControls ctls = new SearchControls(); + ctls.setSearchScope(search.getScope().getValue()); + ctls.setReturningAttributes(search.getAttrs()); + ctls.setReturningObjFlag(true); + NamingEnumeration results = ctx.search(search.getBase(), search.getFilter(), search + .getFilterArgs(), ctls); + while (results != null && results.hasMore()) { + SearchResult sr = results.next(); + String dn = sr.getNameInNamespace(); + Attributes attrs = sr.getAttributes(); + NamingEnumeration en = attrs.getAll(); + Map map = new LinkedHashMap<>(); + map.put("dn", dn); + while (en.hasMore()) { + Attribute attr = en.next(); + String key = attr.getID(); + map.put(key, attr.get(0).toString()); + } + closure.call(map); + } + return null; + }; + performWithContext(action); + } + + public void modify(String dn, String modType, Map attributes) throws NamingException { + modify(dn, ModificationType.valueOf(modType), attributes); + } + + public void modify(String dn, ModificationType modType, Map attributes) throws NamingException { + List mods = new ArrayList<>(); + for (String key : attributes.keySet()) { + Attribute attr = createAttribute(key, attributes.get(key)); + ModificationItem item = new ModificationItem(modType.getValue(), attr); + mods.add(item); + } + ModificationItem[] modItems = mods.toArray(new ModificationItem[mods.size()]); + WithContext action = ctx -> { + ctx.modifyAttributes(dn, modItems); + return null; + }; + performWithContext(action); + } + + public void modify(String dn, List> modificationItem) throws NamingException { + List mods = new ArrayList<>(); + for (List pair : modificationItem) { + if (pair.size() != 2) { + throw new IllegalArgumentException("parameter 2 is not a list of pairs"); + } + Object oModType = pair.get(0); + ModificationType modType; + if (oModType instanceof ModificationType) { + modType = (ModificationType) oModType; + } else if (oModType instanceof String) { + modType = ModificationType.valueOf((String) oModType); + } else { + throw new IllegalArgumentException("parameter is not o valid ModificationType: " + oModType); + } + if (pair.get(1) instanceof Map) { + @SuppressWarnings("unchecked") + Map attributes = (Map) pair.get(1); + for (String key : attributes.keySet()) { + Attribute attr = createAttribute(key, attributes.get(key)); + ModificationItem item = new ModificationItem(modType.getValue(), attr); + mods.add(item); + } + } + } + ModificationItem[] modItems = mods.toArray(new ModificationItem[mods.size()]); + WithContext action = ctx -> { + ctx.modifyAttributes(dn, modItems); + return null; + }; + performWithContext(action); + } + + public List> search(String filter) throws NamingException { + return search(new Search("", SearchScope.SUB, filter)); + } + + public List> search(String base, SearchScope scope, String filter) throws NamingException { + return search(new Search(base, scope, filter)); + } + + public List> search(Map searchParams) throws NamingException { + return search(new Search(searchParams)); + } + + public List> search(Search search) throws NamingException { + List> result = new ArrayList<>(); + WithContext action = ctx -> { + NamingEnumeration results = + ctx.search(search.getBase(), search.getFilter(), search.getFilterArgs(), search.getSearchControls()); + while (results != null && results.hasMore()) { + SearchResult sr = results.next(); + String dn = sr.getNameInNamespace(); + Attributes attrs = sr.getAttributes(); + NamingEnumeration en = attrs.getAll(); + Map map = new LinkedHashMap<>(); + map.put("dn", dn); + while (en.hasMore()) { + Attribute attr = en.next(); + String key = attr.getID(); + if (attr.size() == 1) { + map.put(key, attr.get()); + } else { + List l = new ArrayList<>(); + for (int i = 0; i < attr.size(); ++i) { + l.add(attr.get(i)); + } + map.put(key, l); + } + } + result.add(map); + } + return null; + }; + performWithContext(action); + return result; + } + + public void bind(String bindUser, String bindPassword) throws NamingException { + LdapContext ctx = null; + try { + ctx = new InitialLdapContext(createEnvironment(url, bindUser, bindPassword), null); + } finally { + try { + if (ctx != null) { + ctx.close(); + } + } catch (NamingException e) { + logger.log(Level.FINEST, e.getMessage(), e); + } + } + } + + /** + * Open an LDAP context and perform a given task within this context. + * + * @param parameter type + * @param action action + * @return an action result + * @throws NamingException naming exception + */ + private T performWithContext(WithContext action) throws NamingException { + LdapContext ctx = null; + try { + if (url != null) { + ctx = new InitialLdapContext(createEnvironment(url, bindUser, bindPassword), null); + return action.perform(ctx); + } else { + return null; + } + } finally { + try { + if (ctx != null) { + ctx.close(); + } + } catch (NamingException e) { + logger.log(Level.FINEST, e.getMessage(), e); + } + } + } + + private static Properties createEnvironment(String url, String bindUser, String bindPassword) { + Properties env = new Properties(); + env.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.setProperty(Context.PROVIDER_URL, url); + if (bindUser != null) { + env.setProperty(Context.SECURITY_PRINCIPAL, bindUser); + env.setProperty(Context.SECURITY_CREDENTIALS, bindPassword); + } + return env; + } + + private static Attribute createAttribute(String name, Object value) { + Attribute attr = new BasicAttribute(name); + if (value instanceof Collection) { + Collection values = (Collection) value; + for (Object val : values) { + attr.add(val); + } + } else { + attr.add(value); + } + return attr; + } + + public static String escapeValue(String filter) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < filter.length(); i++) { + switch (filter.charAt(i)) { + case '\\': + sb.append("\\5c"); + break; + case '!': + sb.append("\\21"); + break; + case '&': + sb.append("\\26"); + break; + case '*': + sb.append("\\2a"); + break; + case ':': + sb.append("\\3a"); + break; + case '(': + sb.append("\\28"); + break; + case ')': + sb.append("\\29"); + break; + case '|': + sb.append("\\7c"); + break; + case '~': + sb.append("\\7e"); + break; + case '\u0000': + sb.append("\\00"); + break; + default: + sb.append(filter.charAt(i)); + } + } + return sb.toString(); + } + +} diff --git a/groovy-ldap/src/main/java/org/xbib/groovy/ldap/ModificationType.java b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/ModificationType.java new file mode 100644 index 0000000..21f6b30 --- /dev/null +++ b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/ModificationType.java @@ -0,0 +1,23 @@ +package org.xbib.groovy.ldap; + +import javax.naming.directory.DirContext; + +/** + * Modification types for LDAP attributes. + */ +public enum ModificationType { + + ADD(DirContext.ADD_ATTRIBUTE), + DELETE(DirContext.REMOVE_ATTRIBUTE), + REPLACE(DirContext.REPLACE_ATTRIBUTE); + + private final int value; + + ModificationType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/groovy-ldap/src/main/java/org/xbib/groovy/ldap/Search.java b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/Search.java new file mode 100644 index 0000000..2dd7cb6 --- /dev/null +++ b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/Search.java @@ -0,0 +1,117 @@ +package org.xbib.groovy.ldap; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; + +import javax.naming.directory.SearchControls; + +/** + * Contains all parameters for an LDAP search. + */ +public class Search { + + public static final int DEFAULT_TIME_LIMIT = 5000; + + public static final int DEFAULT_COUNT_LIMIT = 20000; + + private final SearchControls searchControls; + + private final String base; + + private final SearchScope scope; + + private final String filter; + + private final Object[] filterArgs; + + private final String[] attrs; + + private final int timeLimit; + + private final int countLimit; + + public Search() { + this("", SearchScope.SUB, "(objectClass=*)", DEFAULT_TIME_LIMIT, DEFAULT_COUNT_LIMIT); + } + + public Search(String base, SearchScope scope, String filter) { + this(base, scope, filter, DEFAULT_TIME_LIMIT, DEFAULT_COUNT_LIMIT); + } + + public Search(String base, SearchScope scope, String filter, int timeLimit, int countLimit) { + this.base = base; + this.scope = scope; + this.filter = filter; + this.filterArgs = null; + this.attrs = null; + this.timeLimit = timeLimit; + this.countLimit = countLimit; + this.searchControls = getSearchControls(scope, null, timeLimit, countLimit); + } + + public Search(Map map) { + this.base = map.containsKey("base") ? map.get("base").toString() : ""; + this.scope = map.containsKey("scope") ? SearchScope.valueOf(map.get("scope").toString()) : SearchScope.SUB; + this.filter = map.containsKey("filter") ? map.get("filter").toString() : "(objectClass=*)"; + this.filterArgs = map.containsKey("filterArgs") ? toArray(Object.class, map.get("filterArgs")) : null; + this.attrs = map.containsKey("attrs") ? toArray(String.class, map.get("attrs")) : null; + this.timeLimit = map.containsKey("timeLimit") ? + Integer.parseInt((String) map.get("timeLimit")) : DEFAULT_TIME_LIMIT; + this.countLimit = map.containsKey("countLimit") ? + Integer.parseInt((String) map.get("countLimit")) : DEFAULT_COUNT_LIMIT; + this.searchControls = getSearchControls(scope, attrs, timeLimit, countLimit); + } + + @SuppressWarnings("unchecked") + private static T[] toArray(Class target, Object value) { + T[] values = null; + if (value.getClass().isArray()) { + values = (T[]) value; + } else if (value instanceof Collection) { + Collection c = (Collection) value; + values = c.toArray((T[]) Array.newInstance(target, c.size())); + } else { + values = (T[]) Array.newInstance(target, 1); + values[0] = (T) value; + } + return values; + } + + public String[] getAttrs() { + return attrs; + } + + public String getBase() { + return base; + } + + public String getFilter() { + return filter; + } + + public Object[] getFilterArgs() { + return filterArgs; + } + + public SearchScope getScope() { + return scope; + } + + public SearchControls getSearchControls() { + return searchControls; + } + + private static SearchControls getSearchControls(SearchScope searchScope, + String[] attrs, + int timeLimit, + int countLimit) { + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(searchScope.getValue()); + searchControls.setReturningAttributes(attrs); + searchControls.setReturningObjFlag(true); + searchControls.setTimeLimit(timeLimit); + searchControls.setCountLimit(countLimit); + return searchControls; + } +} diff --git a/groovy-ldap/src/main/java/org/xbib/groovy/ldap/SearchScope.java b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/SearchScope.java new file mode 100644 index 0000000..6fcd2f9 --- /dev/null +++ b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/SearchScope.java @@ -0,0 +1,23 @@ +package org.xbib.groovy.ldap; + +import javax.naming.directory.SearchControls; + +/** + * Enumeration for the search scope options. To be used in LDAP search operations. + */ +public enum SearchScope { + + BASE(SearchControls.OBJECT_SCOPE), + ONE(SearchControls.ONELEVEL_SCOPE), + SUB(SearchControls.SUBTREE_SCOPE); + + private final int value; + + SearchScope(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} diff --git a/groovy-ldap/src/main/java/org/xbib/groovy/ldap/WithContext.java b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/WithContext.java new file mode 100644 index 0000000..68ffbbb --- /dev/null +++ b/groovy-ldap/src/main/java/org/xbib/groovy/ldap/WithContext.java @@ -0,0 +1,12 @@ +package org.xbib.groovy.ldap; + +import javax.naming.NamingException; +import javax.naming.ldap.LdapContext; + +/** + * + * @param + */ +public interface WithContext { + T perform(LdapContext ctx) throws NamingException; +} diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/AddTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/AddTest.groovy new file mode 100644 index 0000000..cf89dc4 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/AddTest.groovy @@ -0,0 +1,14 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://localhost:389', 'uid=admin,ou=system' ,'secret') + +assert ! ldap.exists('cn=Joe Doe,dc=example,dc=com') + +attrs = [ + objectclass: ['top', 'person'], + sn: 'Doe', + cn: 'Joe DOe' +] +ldap.add('cn=Joe Doe,dc=example,dc=com', attrs) + +assert ldap.exists('cn=Joe Doe,dc=example,dc=com') \ No newline at end of file diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/CompareTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/CompareTest.groovy new file mode 100644 index 0000000..3624a21 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/CompareTest.groovy @@ -0,0 +1,19 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://localhost:389', 'uid=admin,ou=system' ,'secret') + +assert ! ldap.exists('cn=Joe Doe,dc=example,dc=com') + +attrs = [ + objectclass: ['top', 'person'], + sn: 'Doe', + cn: 'Joe Doe', + userPassword: 'secret' +] +ldap.add('cn=Joe Doe,dc=example,dc=com', attrs) +assert ldap.exists('cn=Joe Doe,dc=example,dc=com') +assert ldap.compare('cn=Joe Doe,dc=example,dc=com', [cn: 'Joe Doe'] ) +assert ldap.compare('cn=Joe Doe,dc=example,dc=com', [cn: 'JOE DOE'] ) +assert ldap.compare('cn=Joe Doe,dc=example,dc=com', [userPassword: 'secret'] ) +assert ! ldap.compare('cn=Joe Doe,dc=example,dc=com', [userPassword: 'SECRET'] ) +ldap.delete('cn=Joe Doe,dc=example,dc=com') \ No newline at end of file diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/DeleteTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/DeleteTest.groovy new file mode 100644 index 0000000..3fb3658 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/DeleteTest.groovy @@ -0,0 +1,6 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://localhost:389', 'uid=admin,ou=system' ,'secret') + +ldap.delete('cn=Joe Doe,dc=example,dc=com') +assert !ldap.exists('cn=Joe Doe,dc=example,dc=com') \ No newline at end of file diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyDNTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyDNTest.groovy new file mode 100644 index 0000000..b6fac3f --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyDNTest.groovy @@ -0,0 +1,22 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://zanzibar:10389', 'uid=admin,ou=system' ,'secret') + +superior='dc=example,dc=com' +dn = 'cn=Myra Ellen Amos,dc=example,dc=com' +newRdn = 'cn=Tori Amos' +newDn = 'cn=Tori Amos,dc=example,dc=com' + +assert !ldap.exists(dn) +attrs = [ + objectclass: ['top', 'person'], + sn: 'Amos', + cn: ['Tori Amos', 'Myra Ellen Amos'], +] +ldap.add(dn, attrs) +assert ldap.exists(dn) +ldap.modifyDn(dn, newRdn, true, superior) +assert ldap.exists(newDn) +tori = ldap.read(newDn) +assert tori.cn == 'Tori Amos' +ldap.delete(newDn) \ No newline at end of file diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyTest.groovy new file mode 100644 index 0000000..3c1c280 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ModifyTest.groovy @@ -0,0 +1,18 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://zanzibar:10389', 'uid=admin,ou=system' ,'secret') + +dn = 'cn=Heather Nova,dc=example,dc=com' + +// Adding a single attribute +// +descr = [ description: 'a singer-songwriter' ] +ldap.modify(dn, 'ADD', descr) + +// performing two operations atomically +// +mods = [ + [ 'REPLACE', [description: 'a singer-songwriter, born in Bermuda'] ], + [ 'ADD', [userPassword: 'secret'] ] +] +ldap.modify(dn, mods) diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ReadTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ReadTest.groovy new file mode 100644 index 0000000..1a0fa78 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/ReadTest.groovy @@ -0,0 +1,12 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance("ldap://zanzibar:10389") + +// Simple entry lookup via dn +heather = ldap.read('cn=Heather Nova,dc=example,dc=com') + +print """ +DN: ${heather.dn} +Common name: ${heather.cn} +Object classes: ${heather.objectclass} +""" diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchAndDeleteTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchAndDeleteTest.groovy new file mode 100644 index 0000000..990f836 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchAndDeleteTest.groovy @@ -0,0 +1,7 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://zanzibar:10389', 'uid=admin,ou=system' ,'secret') + +ldap.eachEntry(base:'dc=example,dc=com', filter:'(objectClass=person)', scope:'ONE') { entry -> + ldap.delete(entry.dn) +} diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchClosureTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchClosureTest.groovy new file mode 100644 index 0000000..b08f81c --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchClosureTest.groovy @@ -0,0 +1,8 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://zanzibar:10389/dc=example,dc=com') + +ldap.eachEntry ('(objectClass=person)') { person -> + println "${person.cn} (${person.dn})" +} + diff --git a/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchTest.groovy b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchTest.groovy new file mode 100644 index 0000000..6cc0fb2 --- /dev/null +++ b/groovy-ldap/src/test/groovy/org/xbib/groovy/ldap/SearchTest.groovy @@ -0,0 +1,30 @@ +package org.xbib.groovy.ldap + +ldap = LDAP.newInstance('ldap://zanzibar:10389/') + +results = ldap.search('dc=example,dc=com', SearchScope.ONE, '(objectClass=person)') +println " ${results.size} entries found ".center(40,'-') +for (entry in results) { + println entry.dn +} + +println "" + +results = ldap.search(filter: '(objectClass=person)', base: 'dc=example,dc=com', scope: 'ONE') +println " ${results.size} entries found ".center(40,'-') +for (entry in results) { + println entry.dn +} + +println "" + +def params = new JavaSearchTest() +params.filter='(objectClass=person)' +params.base='dc=example,dc=com' +params.scope=SearchScope.ONE + +results = ldap.search(params) +println " ${results.size} entries found ".center(40,'-') +for (entry in results) { + println entry.dn +} \ No newline at end of file diff --git a/groovy-ldap/src/test/java/org/xbib/groovy/ldap/JavaSearchTest.java b/groovy-ldap/src/test/java/org/xbib/groovy/ldap/JavaSearchTest.java new file mode 100644 index 0000000..7281def --- /dev/null +++ b/groovy-ldap/src/test/java/org/xbib/groovy/ldap/JavaSearchTest.java @@ -0,0 +1,14 @@ +package org.xbib.groovy.ldap; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class JavaSearchTest { + + @Test + public void defaultConstructor() { + Search search = new Search(); + assertEquals(SearchScope.SUB, search.getScope()); + } +} diff --git a/groovy-mail/LICENSE.txt b/groovy-mail/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/groovy-mail/LICENSE.txt @@ -0,0 +1,202 @@ + + 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. + + "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. diff --git a/groovy-mail/build.gradle b/groovy-mail/build.gradle new file mode 100644 index 0000000..f37d65f --- /dev/null +++ b/groovy-mail/build.gradle @@ -0,0 +1,5 @@ +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + api "com.sun.mail:javax.mail:${project.property('mail.version')}" +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/imap/IMAP.java b/groovy-mail/src/main/java/org/xbib/groovy/imap/IMAP.java new file mode 100644 index 0000000..4e72cb2 --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/imap/IMAP.java @@ -0,0 +1,211 @@ +package org.xbib.groovy.imap; + +import groovy.lang.Closure; + +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.search.FlagTerm; +import javax.mail.search.SearchTerm; +import java.net.URI; +import java.util.Properties; + +/** + * A wrapper class for IMAP functionality to Groovy. + */ +public class IMAP { + + private static final String DEFAULT_URL = "imap://localhost:143/"; + + private final String url; + + private final String username; + + private final String password; + + private IMAP(String url, String username, String password) { + this.url = url; + this.username = username; + this.password = password; + } + + public static IMAP newInstance() { + return new IMAP(DEFAULT_URL, null, null); + } + + public static IMAP newInstance(String url) { + return new IMAP(url, null, null); + } + + public static IMAP newInstance(String url, String username, String password) { + return new IMAP(url, username, password); + } + + public Boolean exist(String folderName) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + return folder.exists(); + }; + return performWithContext(action); + } + + public void eachFolder(String folderName, String folderPattern, Closure closure) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.open(Folder.READ_ONLY); + Folder[] folders = folder.list(folderPattern); + for (Folder f : folders) { + closure.call(f); + } + folder.close(false); + } + return null; + }; + performWithContext(action); + } + + public void create(String folderName) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (!folder.exists()) { + folder.create(Folder.HOLDS_MESSAGES | Folder.READ_WRITE); + } + folder.close(false); + return null; + }; + performWithContext(action); + } + + public void delete(String folderName) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.delete(true); + } + folder.close(true); + return null; + }; + performWithContext(action); + } + + public void expunge(String folderName) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.expunge(); + } + folder.close(false); + return null; + }; + performWithContext(action); + } + + public Integer messageCount(String folderName) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + return folder.getMessageCount(); + } + return null; + }; + return performWithContext(action); + } + + public void eachMessage(String folderName, Closure closure) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.open(Folder.READ_ONLY); + Message[] messages = folder.getMessages(); + for (Message message : messages) { + closure.call(message); + } + folder.close(false); + } + return null; + }; + performWithContext(action); + } + + public void eachMessage(String folderName, int start, int end, Closure closure) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.open(Folder.READ_ONLY); + Message[] messages = folder.getMessages(start, end); + for (Message message : messages) { + closure.call(message); + } + folder.close(false); + } + return null; + }; + performWithContext(action); + } + + public void eachSearchedMessage(String folderName, Flags flags, Closure closure) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.open(Folder.READ_ONLY); + FlagTerm flagTerm = new FlagTerm(flags, false); + Message[] messages = folder.search(flagTerm); + for (Message message : messages) { + closure.call(message); + } + folder.close(false); + } + return null; + }; + performWithContext(action); + } + + public void eachSearchedMessage(String folderName, SearchTerm searchTerm, Closure closure) throws Exception { + WithContext action = ctx -> { + Folder folder = ctx.store.getFolder(folderName); + if (folder.exists()) { + folder.open(Folder.READ_ONLY); + Message[] messages = folder.search(searchTerm); + for (Message message : messages) { + closure.call(message); + } + folder.close(false); + } + return null; + }; + performWithContext(action); + } + + private T performWithContext(WithContext action) throws Exception { + ImapContext ctx = null; + try { + if (url != null) { + ctx = new ImapContext(); + ctx.properties = createEnvironment(url); + ctx.session = Session.getDefaultInstance(ctx.properties, null); + ctx.store = ctx.session.getStore("imap"); + ctx.store.connect(username, password); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } + + private static Properties createEnvironment(String urlSpec) { + URI uri = URI.create(urlSpec); + Properties env = new Properties(); + env.setProperty("mail.store.protocol", "imap"); + env.setProperty("mail.imap.host", uri.getHost()); + env.setProperty("mail.imap.port", Integer.toString(uri.getPort())); + boolean secure = uri.getScheme().equals("imaps") || 993 == uri.getPort(); + env.setProperty("mail.imap.ssl.enable", secure ? "true" : "false"); + return env; + } +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/imap/ImapContext.java b/groovy-mail/src/main/java/org/xbib/groovy/imap/ImapContext.java new file mode 100644 index 0000000..ee246c4 --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/imap/ImapContext.java @@ -0,0 +1,21 @@ +package org.xbib.groovy.imap; + +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Store; +import java.util.Properties; + +public class ImapContext { + + Properties properties; + + Session session; + + Store store; + + void close() throws MessagingException { + if (store != null) { + store.close(); + } + } +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/imap/WithContext.java b/groovy-mail/src/main/java/org/xbib/groovy/imap/WithContext.java new file mode 100644 index 0000000..702395a --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/imap/WithContext.java @@ -0,0 +1,10 @@ +package org.xbib.groovy.imap; + +/** + * + * @param + */ +public interface WithContext { + + T perform(ImapContext ctx) throws Exception; +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/imap/package-info.java b/groovy-mail/src/main/java/org/xbib/groovy/imap/package-info.java new file mode 100644 index 0000000..e5d40e2 --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/imap/package-info.java @@ -0,0 +1,4 @@ +/** + * Groovy IMAP support. + */ +package org.xbib.groovy.imap; diff --git a/groovy-mail/src/main/java/org/xbib/groovy/smtp/SMTP.java b/groovy-mail/src/main/java/org/xbib/groovy/smtp/SMTP.java new file mode 100644 index 0000000..03909af --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/smtp/SMTP.java @@ -0,0 +1,137 @@ +package org.xbib.groovy.smtp; + +import javax.mail.Address; +import javax.mail.Authenticator; +import javax.mail.Message; +import javax.mail.Multipart; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import java.net.URI; +import java.util.Date; +import java.util.Properties; + +/** + */ +public class SMTP { + + private static final String DEFAULT_URL = "smtp://localhost:25/"; + + private final String url; + + private final String username; + + private final String password; + + private SMTP(String url, String username, String password) { + this.url = url; + this.username = username; + this.password = password; + } + + public static SMTP newInstance() { + return new SMTP(DEFAULT_URL, null, null); + } + + public static SMTP newInstance(String url) { + return new SMTP(url, null, null); + } + + public static SMTP newInstance(String url, String username, String password) { + return new SMTP(url, username, password); + } + + public String getURL() { + return url; + } + + public void send(String subject, String from, String to, String text) throws Exception { + Address[] toAddr = { new InternetAddress(to) }; + send(subject, new InternetAddress(from), null, toAddr, null, null, text); + } + + public void send(String subject, Address from, Address[] to, String text) throws Exception { + send(subject, from, null, to, null, null, text); + } + + public void send(String subject, + Address from, Address[] replyTo, + Address[] to, Address[] cc, Address[] bcc, + String text) throws Exception { + Multipart multipart = new MimeMultipart("mixed"); + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setHeader("Content-Transfer-Encoding", "base64"); + mimeBodyPart.setText(text); + multipart.addBodyPart(mimeBodyPart); + send(subject, from, replyTo, to, cc, bcc, multipart); + } + + public void send(String subject, + Address from, Address[] replyTo, + Address[] to, Address[] cc, Address[] bcc, + Multipart multipart) throws Exception { + WithContext action = ctx -> { + Message message = new MimeMessage(ctx.session); + message.setSentDate(new Date()); + message.setFrom(from); + message.setSubject(subject); + if (replyTo != null) { + message.setReplyTo(replyTo); + } + message.setRecipients(Message.RecipientType.TO, to); + if (cc != null) { + message.setRecipients(Message.RecipientType.CC, cc); + } + if (bcc != null) { + message.setRecipients(Message.RecipientType.BCC, bcc); + } + message.setContent(multipart); + Transport.send(message); + return null; + }; + performWithContext(action); + } + + private T performWithContext(WithContext action) throws Exception { + SmtpContext ctx = null; + try { + if (url != null) { + ctx = new SmtpContext(); + ctx.properties = createEnvironment(url); + ctx.session = username != null ? + Session.getDefaultInstance(ctx.properties, new SMTPAuthenticator()) : + Session.getDefaultInstance(ctx.properties); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } + + private static Properties createEnvironment(String urlSpec) { + URI uri = URI.create(urlSpec); + Properties env = new Properties(); + env.setProperty("mail.smtp.auth", "false"); + env.setProperty("mail.smtp.host", uri.getHost()); + env.setProperty("mail.smtp.port", Integer.toString(uri.getPort())); + boolean secure = uri.getScheme().equals("smtps") || 995 == uri.getPort(); + env.setProperty("mail.smtp.ssl.enable", secure ? "true" : "false"); + env.setProperty("mail.debug", "true"); + return env; + } + + private class SMTPAuthenticator extends Authenticator { + @Override + public PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + } +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/smtp/SmtpContext.java b/groovy-mail/src/main/java/org/xbib/groovy/smtp/SmtpContext.java new file mode 100644 index 0000000..5591dac --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/smtp/SmtpContext.java @@ -0,0 +1,15 @@ +package org.xbib.groovy.smtp; + +import javax.mail.MessagingException; +import javax.mail.Session; +import java.util.Properties; + +public class SmtpContext { + + Properties properties; + + Session session; + + void close() throws MessagingException { + } +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/smtp/WithContext.java b/groovy-mail/src/main/java/org/xbib/groovy/smtp/WithContext.java new file mode 100644 index 0000000..40ec28c --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/smtp/WithContext.java @@ -0,0 +1,11 @@ +package org.xbib.groovy.smtp; + +/** + * The Context for {@link SMTP}. + * + * @param the type parameter + */ +public interface WithContext { + + T perform(SmtpContext ctx) throws Exception; +} diff --git a/groovy-mail/src/main/java/org/xbib/groovy/smtp/package-info.java b/groovy-mail/src/main/java/org/xbib/groovy/smtp/package-info.java new file mode 100644 index 0000000..7240d17 --- /dev/null +++ b/groovy-mail/src/main/java/org/xbib/groovy/smtp/package-info.java @@ -0,0 +1,4 @@ +/** + * Groovy SMTP support. + */ +package org.xbib.groovy.smtp; diff --git a/groovy-sshd/build.gradle b/groovy-sshd/build.gradle new file mode 100644 index 0000000..c0f2f12 --- /dev/null +++ b/groovy-sshd/build.gradle @@ -0,0 +1,6 @@ +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + api "org.xbib:sshd-fs:${project.property('sshd.version')}" + testImplementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" +} diff --git a/groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTP.java b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTP.java new file mode 100644 index 0000000..02fa69f --- /dev/null +++ b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTP.java @@ -0,0 +1,388 @@ +package org.xbib.groovy.sshd; + +import groovy.lang.Closure; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.CopyOption; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.nio.file.attribute.UserPrincipal; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +public class SFTP { + + private static final int READ_BUFFER_SIZE = 128 * 1024; + + private static final int WRITE_BUFFER_SIZE = 128 * 1024; + + private static final Set DEFAULT_DIR_PERMISSIONS = + PosixFilePermissions.fromString("rwxr-xr-x"); + + private static final Set DEFAULT_FILE_PERMISSIONS = + PosixFilePermissions.fromString("rw-r--r--"); + + private final String url; + + private final Map env; + + private SFTP(String url, Map env) { + this.url = url; + this.env = env; + } + + public static SFTP newInstance() { + return newInstance("sftp://localhost:22"); + } + + public static SFTP newInstance(Map env) { + return newInstance("sftp://localhost:22", env); + } + + public static SFTP newInstance(String url) { + return newInstance(url, Collections.emptyMap()); + } + + public static SFTP newInstance(String url, Map env) { + return new SFTP(url, env); + } + + public Boolean exists(String path) throws Exception { + return performWithContext(ctx -> Files.exists(ctx.fileSystem.getPath(path))); + } + + public Boolean isExecutable(String path) throws Exception { + return performWithContext(ctx -> Files.isExecutable(ctx.fileSystem.getPath(path))); + } + + public Boolean isDirectory(String path) throws Exception { + return performWithContext(ctx -> Files.isDirectory(ctx.fileSystem.getPath(path))); + } + + public Boolean isRegularFile(String path) throws Exception { + return performWithContext(ctx -> Files.isRegularFile(ctx.fileSystem.getPath(path))); + } + + public Boolean isHidden(String path) throws Exception { + return performWithContext(ctx -> Files.isHidden(ctx.fileSystem.getPath(path))); + } + + public Boolean isSameFile(String path1, String path2) throws Exception { + return performWithContext(ctx -> Files.isSameFile(ctx.fileSystem.getPath(path1), ctx.fileSystem.getPath(path2))); + } + + public Boolean isSymbolicLink(String path) throws Exception { + return performWithContext(ctx -> Files.isSymbolicLink(ctx.fileSystem.getPath(path))); + } + + public Boolean isReadable(String path) throws Exception { + return performWithContext(ctx -> Files.isReadable(ctx.fileSystem.getPath(path))); + } + + public Boolean isWritable(String path) throws Exception { + return performWithContext(ctx -> Files.isWritable(ctx.fileSystem.getPath(path))); + } + + public void createFile(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createFile(ctx.fileSystem.getPath(path), attributes)); + } + + public void createDirectory(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createDirectory(ctx.fileSystem.getPath(path), attributes)); + } + + public void createDirectories(String path, FileAttribute... attributes) throws Exception { + performWithContext(ctx -> Files.createDirectories(ctx.fileSystem.getPath(path), attributes)); + } + + public void setAttribute(String path, String attribute, Object value) throws Exception { + performWithContext(ctx -> Files.setAttribute(ctx.fileSystem.getPath(path), attribute, value)); + } + + public Object getAttribute(String path, String attribute) throws Exception { + return performWithContext(ctx -> Files.getAttribute(ctx.fileSystem.getPath(path), attribute)); + } + + public void setPermissions(String path, Set permissions) throws Exception { + performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions)); + } + + public Set getPermissions(String path) throws Exception { + return performWithContext(ctx -> Files.getPosixFilePermissions(ctx.fileSystem.getPath(path))); + } + + public void setLastModifiedTime(String path, FileTime fileTime) throws Exception { + performWithContext(ctx -> Files.setLastModifiedTime(ctx.fileSystem.getPath(path), fileTime)); + } + + public FileTime getLastModified(String path) throws Exception{ + return performWithContext(ctx -> Files.getLastModifiedTime(ctx.fileSystem.getPath(path))); + } + + public void setOwner(String path, UserPrincipal userPrincipal) throws Exception { + performWithContext(ctx -> Files.setOwner(ctx.fileSystem.getPath(path), userPrincipal)); + } + + public UserPrincipal getOwner(String path) throws Exception { + return performWithContext(ctx -> Files.getOwner(ctx.fileSystem.getPath(path))); + } + + public void each(String path, Closure closure) throws Exception { + performWithContext(ctx -> { + try (DirectoryStream stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) { + stream.forEach(closure::call); + } + return null; + }); + } + + public void eachFilter(String path, DirectoryStream.Filter filter, Closure closure) throws Exception { + performWithContext(ctx -> { + try (DirectoryStream stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path), filter)) { + stream.forEach(closure::call); + } + return null; + }); + } + + public void upload(Path source, Path target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), target, WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void upload(Path source, String target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(Path source, String target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Files.newByteChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void upload(InputStream source, Path target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, Path target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), target, WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void upload(InputStream source, String target, CopyOption... copyOptions) throws Exception { + upload(source, target, DEFAULT_DIR_PERMISSIONS, DEFAULT_FILE_PERMISSIONS, copyOptions); + } + + public void upload(InputStream source, String target, + Set dirPermissions, + Set filePermissions, + CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + upload(ctx, Channels.newChannel(source), ctx.fileSystem.getPath(target), WRITE_BUFFER_SIZE, + dirPermissions, filePermissions, copyOptions); + return null; + }); + } + + public void download(Path source, Path target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(String source, Path target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE, copyOptions); + return null; + }); + } + + public void download(Path source, OutputStream target) throws Exception { + performWithContext(ctx -> { + download(ctx, source, target, READ_BUFFER_SIZE); + return null; + }); + } + + public void download(String source, OutputStream target) throws Exception { + performWithContext(ctx -> { + download(ctx, ctx.fileSystem.getPath(source), target, READ_BUFFER_SIZE); + return null; + }); + } + + public void copy(String source, String target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> Files.copy(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions)); + } + + public void rename(String source, String target, CopyOption... copyOptions) throws Exception { + performWithContext(ctx -> Files.move(ctx.fileSystem.getPath(source), ctx.fileSystem.getPath(target), copyOptions)); + } + + public void remove(String source) throws Exception { + performWithContext(ctx -> Files.deleteIfExists(ctx.fileSystem.getPath(source))); + } + + private void upload(SFTPContext ctx, + ReadableByteChannel source, + Path target, + int bufferSize, + Set dirPerms, + Set filePerms, + CopyOption... copyOptions) throws Exception { + prepareForWrite(target, dirPerms, filePerms); + transfer(source, ctx.provider.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void download(SFTPContext ctx, + Path source, + OutputStream outputStream, + int bufferSize) throws Exception { + download(ctx, source, Channels.newChannel(outputStream), bufferSize); + } + + private void download(SFTPContext ctx, + Path source, + WritableByteChannel writableByteChannel, + int bufferSize) throws Exception { + transfer(ctx.provider.newByteChannel(source, prepareReadOptions()), writableByteChannel, + bufferSize); + } + + private void download(SFTPContext ctx, + Path source, + Path target, + int bufferSize, + CopyOption... copyOptions) throws Exception { + prepareForRead(target); + transfer(ctx.provider.newByteChannel(source, prepareReadOptions(copyOptions)), + Files.newByteChannel(target, prepareWriteOptions(copyOptions)), bufferSize); + } + + private void prepareForRead(Path path) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + } + if (!Files.exists(path)) { + Files.createFile(path); + } + } + + private void prepareForWrite(Path path, + Set dirPerms, + Set filePerms) throws IOException { + if (path == null) { + return; + } + Path parent = path.getParent(); + if (parent != null) { + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(parent, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(dirPerms); + } + if (!Files.exists(path)) { + Files.createFile(path); + } + PosixFileAttributeView posixFileAttributeView = + Files.getFileAttributeView(path, PosixFileAttributeView.class); + posixFileAttributeView.setPermissions(filePerms); + } + + private Set prepareReadOptions(CopyOption... copyOptions) { + // ignore user copy options + return EnumSet.of(StandardOpenOption.READ); + } + + private Set prepareWriteOptions(CopyOption... copyOptions) { + Set options = null; + for (CopyOption copyOption : copyOptions) { + if (copyOption == StandardCopyOption.REPLACE_EXISTING) { + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); + } + } + if (options == null) { + // we can not use CREATE_NEW, file is already there because of prepareForWrite() -> Files.createFile() + options = EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE); + } + return options; + } + + private void transfer(ReadableByteChannel readableByteChannel, + WritableByteChannel writableByteChannel, + int bufferSize) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(bufferSize); + int read; + while ((read = readableByteChannel.read(buffer)) > 0) { + buffer.flip(); + while (read > 0) { + read -= writableByteChannel.write(buffer); + } + buffer.clear(); + } + } + + private T performWithContext(WithContext action) throws Exception { + SFTPContext ctx = null; + try { + if (url != null) { + ctx = new SFTPContext(URI.create(url), env); + return action.perform(ctx); + } else { + return null; + } + } finally { + if (ctx != null) { + ctx.close(); + } + } + } +} diff --git a/groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTPContext.java b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTPContext.java new file mode 100644 index 0000000..02b2c91 --- /dev/null +++ b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTPContext.java @@ -0,0 +1,42 @@ +package org.xbib.groovy.sshd; + +import org.apache.sshd.client.ClientBuilder; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.fs.SftpFileSystem; +import org.apache.sshd.fs.SftpFileSystemProvider; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +/** + */ +class SFTPContext { + + private final SshClient sshClient; + + final SftpFileSystemProvider provider; + + final SftpFileSystem fileSystem; + + SFTPContext(URI uri, Map env) throws IOException { + this.sshClient = ClientBuilder.builder().build(); + Object object = env.get("workers"); + if (object instanceof Integer) { + sshClient.setNioWorkers((Integer) object); + } else if (object instanceof String) { + sshClient.setNioWorkers(Integer.parseInt((String) object)); + } else { + // we do not require a vast pool of threads + sshClient.setNioWorkers(1); + } + sshClient.start(); + this.provider = new SftpFileSystemProvider(sshClient); + this.fileSystem = provider.newFileSystem(uri, env); + } + + void close() throws IOException { + sshClient.stop(); + fileSystem.close(); + } +} diff --git a/groovy-sshd/src/main/java/org/xbib/groovy/sshd/WithContext.java b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/WithContext.java new file mode 100644 index 0000000..545c262 --- /dev/null +++ b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/WithContext.java @@ -0,0 +1,9 @@ +package org.xbib.groovy.sshd; + +/** + * + * @param the context parameter + */ +public interface WithContext { + T perform(SFTPContext ctx) throws Exception; +} diff --git a/groovy-sshd/src/main/java/org/xbib/groovy/sshd/package-info.java b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/package-info.java new file mode 100644 index 0000000..69eca5f --- /dev/null +++ b/groovy-sshd/src/main/java/org/xbib/groovy/sshd/package-info.java @@ -0,0 +1,4 @@ +/** + * Groovy SSH/SFTP support. + */ +package org.xbib.groovy.sshd; diff --git a/groovy-sshd/src/test/groovy/org/xbib/groovy/sshd/SFTPTest.groovy b/groovy-sshd/src/test/groovy/org/xbib/groovy/sshd/SFTPTest.groovy new file mode 100644 index 0000000..6484dba --- /dev/null +++ b/groovy-sshd/src/test/groovy/org/xbib/groovy/sshd/SFTPTest.groovy @@ -0,0 +1,29 @@ +package org.xbib.groovy.sshd + +import groovy.util.logging.Log4j2 +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.xbib.io.sshd.eddsa.EdDSASecurityProvider + +import java.nio.file.Files +import java.nio.file.Path +import java.security.Security + +@Log4j2 +class SFTPTest { + + static { + Security.addProvider(new EdDSASecurityProvider()); + } + + @Disabled + @Test + void testSFTP() { + SFTP sftp = SFTP.newInstance("sftp://demo.wftpserver.com:2222",[username: 'demo', password: 'demo'.toCharArray()]) + log.info sftp.exists('/') + sftp.each('/') { Path path -> + log.info "{} {} {}", path, Files.isDirectory(path), Files.getLastModifiedTime(path) + } + } + +} diff --git a/groovy-sshd/src/test/resources/log4j2.xml b/groovy-sshd/src/test/resources/log4j2.xml new file mode 100644 index 0000000..b04550a --- /dev/null +++ b/groovy-sshd/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..68fd158 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,7 @@ + +include 'groovy-ldap' +include 'groovy-crypt' +include 'groovy-mail' +include 'groovy-ftp' +include 'groovy-ftps' +include 'groovy-sshd'