initial commit
This commit is contained in:
commit
57dc419fe3
89 changed files with 6594 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
data
|
||||||
|
work
|
||||||
|
logs
|
||||||
|
build
|
||||||
|
target
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
*.iml
|
||||||
|
/.settings
|
||||||
|
/.classpath
|
||||||
|
/.project
|
||||||
|
/.gradle
|
||||||
|
*~
|
||||||
|
*.orig
|
202
LICENSE.txt
Normal file
202
LICENSE.txt
Normal file
|
@ -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.
|
34
build.gradle
Normal file
34
build.gradle
Normal file
|
@ -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')
|
11
gradle.properties
Normal file
11
gradle.properties
Normal file
|
@ -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
|
34
gradle/compile/groovy.gradle
Normal file
34
gradle/compile/groovy.gradle
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
43
gradle/compile/java.gradle
Normal file
43
gradle/compile/java.gradle
Normal file
|
@ -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')
|
||||||
|
}
|
55
gradle/documentation/asciidoc.gradle
Normal file
55
gradle/documentation/asciidoc.gradle
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}*/
|
13
gradle/ide/idea.gradle
Normal file
13
gradle/ide/idea.gradle
Normal file
|
@ -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")
|
||||||
|
}
|
66
gradle/publishing/publication.gradle
Normal file
66
gradle/publishing/publication.gradle
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
gradle/publishing/sonatype.gradle
Normal file
11
gradle/publishing/sonatype.gradle
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
4
gradle/repositories/maven.gradle
Normal file
4
gradle/repositories/maven.gradle
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
27
gradle/test/junit5.gradle
Normal file
27
gradle/test/junit5.gradle
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -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
|
185
gradlew
vendored
Executable file
185
gradlew
vendored
Executable file
|
@ -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" "$@"
|
89
gradlew.bat
vendored
Normal file
89
gradlew.bat
vendored
Normal file
|
@ -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
|
18
groovy-crypt/CREDITS.txt
Normal file
18
groovy-crypt/CREDITS.txt
Normal file
|
@ -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
|
202
groovy-crypt/LICENSE.txt
Normal file
202
groovy-crypt/LICENSE.txt
Normal file
|
@ -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.
|
54
groovy-crypt/README.adoc
Normal file
54
groovy-crypt/README.adoc
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
|
684
groovy-crypt/src/docs/asciidoc/css/foundation.css
vendored
Normal file
684
groovy-crypt/src/docs/asciidoc/css/foundation.css
vendored
Normal file
|
@ -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; }
|
11
groovy-crypt/src/docs/asciidoc/index.adoc
Normal file
11
groovy-crypt/src/docs/asciidoc/index.adoc
Normal file
|
@ -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
|
105
groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Crypt.groovy
Normal file
105
groovy-crypt/src/main/groovy/org/xbib/groovy/crypt/Crypt.groovy
Normal file
|
@ -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<? extends Crypt> 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)
|
||||||
|
|
||||||
|
}
|
|
@ -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<? extends Crypt> providerClass
|
||||||
|
|
||||||
|
final String type
|
||||||
|
|
||||||
|
final String algorithm
|
||||||
|
|
||||||
|
private CryptType(String type, String algorithm, Class<? extends Crypt> 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<? extends Crypt> 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package org.xbib.groovy.crypt.random
|
||||||
|
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>{@link SeedGenerator} implementation that uses
|
||||||
|
* {@link java.security.SecureRandom} to generate random seed data.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*
|
||||||
|
* <p>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.</p>
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SecureRandomSeedGenerator implements SeedGenerator {
|
||||||
|
private static final SecureRandom INSTANCE = new SecureRandom()
|
||||||
|
|
||||||
|
byte[] generateSeed(int length) throws SeedException {
|
||||||
|
return INSTANCE.generateSeed(length)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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<numberOfLongBytes; ++i) {
|
||||||
|
array[pos + numberOfLongBytes-i-1] = (byte) (l >>> (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<NetworkInterface> 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
7
groovy-ftp/build.gradle
Normal file
7
groovy-ftp/build.gradle
Normal file
|
@ -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')}"
|
||||||
|
}
|
396
groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTP.java
Normal file
396
groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTP.java
Normal file
|
@ -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<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
||||||
|
PosixFilePermissions.fromString("rwxr-xr-x");
|
||||||
|
|
||||||
|
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
|
||||||
|
PosixFilePermissions.fromString("rw-r--r--");
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
private final Map<String, ?> env;
|
||||||
|
|
||||||
|
private FTP(String url, Map<String, ?> env) {
|
||||||
|
this.url = url;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTP newInstance() {
|
||||||
|
return newInstance("ftp://localhost:21");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTP newInstance(Map<String, ?> env) {
|
||||||
|
return newInstance("ftp://localhost:21", env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTP newInstance(String url) {
|
||||||
|
return newInstance(url, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTP newInstance(String url, Map<String, ?> 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<PosixFilePermission> permissions) throws Exception {
|
||||||
|
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<PosixFilePermission> 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<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) {
|
||||||
|
stream.forEach(closure::call);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void eachFilter(String path, DirectoryStream.Filter<Path> filter, Closure<?> closure) throws Exception {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
try (DirectoryStream<Path> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
|
||||||
|
// ignore user copy options
|
||||||
|
return EnumSet.of(StandardOpenOption.READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
|
||||||
|
Set<? extends OpenOption> 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> T performWithContext(WithContext<T> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTPContext.java
Normal file
27
groovy-ftp/src/main/java/org/xbib/groovy/ftp/FTPContext.java
Normal file
|
@ -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<String, ?> env) throws IOException {
|
||||||
|
this.provider = new FTPFileSystemProvider();
|
||||||
|
this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPEnvironment());
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
fileSystem.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.xbib.groovy.ftp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param <T> the context parameter
|
||||||
|
*/
|
||||||
|
public interface WithContext<T> {
|
||||||
|
T perform(FTPContext ctx) throws Exception;
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
groovy-ftp/src/test/resources/log4j2.xml
Normal file
13
groovy-ftp/src/test/resources/log4j2.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration status="OFF">
|
||||||
|
<appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
|
||||||
|
</Console>
|
||||||
|
</appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="debug">
|
||||||
|
<AppenderRef ref="Console" />
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</configuration>
|
7
groovy-ftps/build.gradle
Normal file
7
groovy-ftps/build.gradle
Normal file
|
@ -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')}"
|
||||||
|
}
|
395
groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPS.java
Normal file
395
groovy-ftps/src/main/java/org/xbib/groovy/ftps/FTPS.java
Normal file
|
@ -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<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
||||||
|
PosixFilePermissions.fromString("rwxr-xr-x");
|
||||||
|
|
||||||
|
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
|
||||||
|
PosixFilePermissions.fromString("rw-r--r--");
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
private final Map<String, ?> env;
|
||||||
|
|
||||||
|
private FTPS(String url, Map<String, ?> env) {
|
||||||
|
this.url = url;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTPS newInstance() {
|
||||||
|
return newInstance("ftps://localhost:21");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTPS newInstance(Map<String, ?> env) {
|
||||||
|
return newInstance("ftps://localhost:21", env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTPS newInstance(String url) {
|
||||||
|
return newInstance(url, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FTPS newInstance(String url, Map<String, ?> 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<PosixFilePermission> permissions) throws Exception {
|
||||||
|
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<PosixFilePermission> 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<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) {
|
||||||
|
stream.forEach(closure::call);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void eachFilter(String path, DirectoryStream.Filter<Path> filter, Closure<?> closure) throws Exception {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
try (DirectoryStream<Path> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
|
||||||
|
// ignore user copy options
|
||||||
|
return EnumSet.of(StandardOpenOption.READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
|
||||||
|
Set<? extends OpenOption> 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> T performWithContext(WithContext<T> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, ?> env) throws IOException {
|
||||||
|
this.provider = new FTPSFileSystemProvider();
|
||||||
|
this.fileSystem = provider.newFileSystem(uri, env != null ? env : new FTPSEnvironment());
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
fileSystem.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package org.xbib.groovy.ftps;
|
||||||
|
|
||||||
|
public interface WithContext<T> {
|
||||||
|
|
||||||
|
T perform(FTPSContext ctx) throws Exception;
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
groovy-ftps/src/test/resources/log4j2.xml
Normal file
13
groovy-ftps/src/test/resources/log4j2.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration status="OFF">
|
||||||
|
<appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
|
||||||
|
</Console>
|
||||||
|
</appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="all">
|
||||||
|
<AppenderRef ref="Console" />
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</configuration>
|
202
groovy-ldap/LICENSE.txt
Normal file
202
groovy-ldap/LICENSE.txt
Normal file
|
@ -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.
|
2
groovy-ldap/README.adoc
Normal file
2
groovy-ldap/README.adoc
Normal file
|
@ -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.
|
6
groovy-ldap/build.gradle
Normal file
6
groovy-ldap/build.gradle
Normal file
|
@ -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')}"
|
||||||
|
}
|
427
groovy-ldap/src/main/java/org/xbib/groovy/ldap/LDAP.java
Normal file
427
groovy-ldap/src/main/java/org/xbib/groovy/ldap/LDAP.java
Normal file
|
@ -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<String, Object> attributes) throws NamingException {
|
||||||
|
WithContext<Object> action = ctx -> {
|
||||||
|
BasicAttributes attrs = new BasicAttributes();
|
||||||
|
for (Map.Entry<String,Object> 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<Object> 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<Boolean> 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<String, Object> assertion) throws NamingException {
|
||||||
|
if (assertion.size() != 1) {
|
||||||
|
throw new IllegalArgumentException("Assertion may only include one attribute");
|
||||||
|
}
|
||||||
|
WithContext<Boolean> 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<SearchResult> 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<Object> 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<String, Object> 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<Object> action = ctx -> {
|
||||||
|
SearchControls ctls = new SearchControls();
|
||||||
|
ctls.setSearchScope(search.getScope().getValue());
|
||||||
|
ctls.setReturningAttributes(search.getAttrs());
|
||||||
|
ctls.setReturningObjFlag(true);
|
||||||
|
NamingEnumeration<SearchResult> 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<? extends Attribute> en = attrs.getAll();
|
||||||
|
Map<String, Object> 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<String, Object> attributes) throws NamingException {
|
||||||
|
modify(dn, ModificationType.valueOf(modType), attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void modify(String dn, ModificationType modType, Map<String, Object> attributes) throws NamingException {
|
||||||
|
List<ModificationItem> 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<Object> action = ctx -> {
|
||||||
|
ctx.modifyAttributes(dn, modItems);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
performWithContext(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void modify(String dn, List<List<Object>> modificationItem) throws NamingException {
|
||||||
|
List<ModificationItem> mods = new ArrayList<>();
|
||||||
|
for (List<Object> 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<String, Object> attributes = (Map<String, Object>) 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<Object> action = ctx -> {
|
||||||
|
ctx.modifyAttributes(dn, modItems);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
performWithContext(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> search(String filter) throws NamingException {
|
||||||
|
return search(new Search("", SearchScope.SUB, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> search(String base, SearchScope scope, String filter) throws NamingException {
|
||||||
|
return search(new Search(base, scope, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> search(Map<String, Object> searchParams) throws NamingException {
|
||||||
|
return search(new Search(searchParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> search(Search search) throws NamingException {
|
||||||
|
List<Map<String, Object>> result = new ArrayList<>();
|
||||||
|
WithContext<Object> action = ctx -> {
|
||||||
|
NamingEnumeration<SearchResult> 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<? extends Attribute> en = attrs.getAll();
|
||||||
|
Map<String, Object> 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<Object> 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 <T> parameter type
|
||||||
|
* @param action action
|
||||||
|
* @return an action result
|
||||||
|
* @throws NamingException naming exception
|
||||||
|
*/
|
||||||
|
private <T> T performWithContext(WithContext<T> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
117
groovy-ldap/src/main/java/org/xbib/groovy/ldap/Search.java
Normal file
117
groovy-ldap/src/main/java/org/xbib/groovy/ldap/Search.java
Normal file
|
@ -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<String, Object> 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> T[] toArray(Class<T> target, Object value) {
|
||||||
|
T[] values = null;
|
||||||
|
if (value.getClass().isArray()) {
|
||||||
|
values = (T[]) value;
|
||||||
|
} else if (value instanceof Collection) {
|
||||||
|
Collection<T> c = (Collection<T>) 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.xbib.groovy.ldap;
|
||||||
|
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.naming.ldap.LdapContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
public interface WithContext<T> {
|
||||||
|
T perform(LdapContext ctx) throws NamingException;
|
||||||
|
}
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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)
|
|
@ -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)
|
|
@ -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}
|
||||||
|
"""
|
|
@ -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)
|
||||||
|
}
|
|
@ -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})"
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
202
groovy-mail/LICENSE.txt
Normal file
202
groovy-mail/LICENSE.txt
Normal file
|
@ -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.
|
5
groovy-mail/build.gradle
Normal file
5
groovy-mail/build.gradle
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
apply from: rootProject.file('gradle/compile/groovy.gradle')
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api "com.sun.mail:javax.mail:${project.property('mail.version')}"
|
||||||
|
}
|
211
groovy-mail/src/main/java/org/xbib/groovy/imap/IMAP.java
Normal file
211
groovy-mail/src/main/java/org/xbib/groovy/imap/IMAP.java
Normal file
|
@ -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<Boolean> 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<Object> 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<Object> 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<Object> 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<Object> 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<Integer> 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<Object> 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<Object> 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<Object> 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<Object> 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> T performWithContext(WithContext<T> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.xbib.groovy.imap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param <T>
|
||||||
|
*/
|
||||||
|
public interface WithContext<T> {
|
||||||
|
|
||||||
|
T perform(ImapContext ctx) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Groovy IMAP support.
|
||||||
|
*/
|
||||||
|
package org.xbib.groovy.imap;
|
137
groovy-mail/src/main/java/org/xbib/groovy/smtp/SMTP.java
Normal file
137
groovy-mail/src/main/java/org/xbib/groovy/smtp/SMTP.java
Normal file
|
@ -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<Object> 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> T performWithContext(WithContext<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.xbib.groovy.smtp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Context for {@link SMTP}.
|
||||||
|
*
|
||||||
|
* @param <T> the type parameter
|
||||||
|
*/
|
||||||
|
public interface WithContext<T> {
|
||||||
|
|
||||||
|
T perform(SmtpContext ctx) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Groovy SMTP support.
|
||||||
|
*/
|
||||||
|
package org.xbib.groovy.smtp;
|
6
groovy-sshd/build.gradle
Normal file
6
groovy-sshd/build.gradle
Normal file
|
@ -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')}"
|
||||||
|
}
|
388
groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTP.java
Normal file
388
groovy-sshd/src/main/java/org/xbib/groovy/sshd/SFTP.java
Normal file
|
@ -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<PosixFilePermission> DEFAULT_DIR_PERMISSIONS =
|
||||||
|
PosixFilePermissions.fromString("rwxr-xr-x");
|
||||||
|
|
||||||
|
private static final Set<PosixFilePermission> DEFAULT_FILE_PERMISSIONS =
|
||||||
|
PosixFilePermissions.fromString("rw-r--r--");
|
||||||
|
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
private final Map<String, ?> env;
|
||||||
|
|
||||||
|
private SFTP(String url, Map<String, ?> env) {
|
||||||
|
this.url = url;
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SFTP newInstance() {
|
||||||
|
return newInstance("sftp://localhost:22");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SFTP newInstance(Map<String, ?> 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<String, ?> 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<PosixFilePermission> permissions) throws Exception {
|
||||||
|
performWithContext(ctx -> Files.setPosixFilePermissions(ctx.fileSystem.getPath(path), permissions));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<PosixFilePermission> 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<Path> stream = Files.newDirectoryStream(ctx.fileSystem.getPath(path))) {
|
||||||
|
stream.forEach(closure::call);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void eachFilter(String path, DirectoryStream.Filter<Path> filter, Closure<?> closure) throws Exception {
|
||||||
|
performWithContext(ctx -> {
|
||||||
|
try (DirectoryStream<Path> 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<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPermissions,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<PosixFilePermission> dirPerms,
|
||||||
|
Set<PosixFilePermission> 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<? extends OpenOption> prepareReadOptions(CopyOption... copyOptions) {
|
||||||
|
// ignore user copy options
|
||||||
|
return EnumSet.of(StandardOpenOption.READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<? extends OpenOption> prepareWriteOptions(CopyOption... copyOptions) {
|
||||||
|
Set<? extends OpenOption> 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> T performWithContext(WithContext<T> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, ?> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.xbib.groovy.sshd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param <T> the context parameter
|
||||||
|
*/
|
||||||
|
public interface WithContext<T> {
|
||||||
|
T perform(SFTPContext ctx) throws Exception;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* Groovy SSH/SFTP support.
|
||||||
|
*/
|
||||||
|
package org.xbib.groovy.sshd;
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
13
groovy-sshd/src/test/resources/log4j2.xml
Normal file
13
groovy-sshd/src/test/resources/log4j2.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration status="OFF">
|
||||||
|
<appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
|
||||||
|
</Console>
|
||||||
|
</appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="ALL">
|
||||||
|
<AppenderRef ref="Console" />
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</configuration>
|
7
settings.gradle
Normal file
7
settings.gradle
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
include 'groovy-ldap'
|
||||||
|
include 'groovy-crypt'
|
||||||
|
include 'groovy-mail'
|
||||||
|
include 'groovy-ftp'
|
||||||
|
include 'groovy-ftps'
|
||||||
|
include 'groovy-sshd'
|
Loading…
Reference in a new issue