├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── dependency-check.yml │ ├── publish-central.yml │ └── publish-github.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── vcs.xml ├── LICENSE.txt ├── README.md ├── pom.xml └── src ├── main └── java │ └── org │ └── cryptomator │ └── webdav │ └── core │ ├── filters │ ├── AcceptRangeFilter.java │ ├── HttpFilter.java │ ├── LoggingFilter.java │ ├── MacChunkedPutCompatibilityFilter.java │ ├── MkcolComplianceFilter.java │ ├── PostRequestBlockingFilter.java │ └── UnicodeResourcePathNormalizationFilter.java │ └── servlet │ ├── AbstractNioWebDavServlet.java │ ├── ByteRange.java │ ├── CopyingFileVisitor.java │ ├── DavFile.java │ ├── DavFileWithRange.java │ ├── DavFolder.java │ ├── DavLocatorFactoryImpl.java │ ├── DavLocatorImpl.java │ ├── DavNode.java │ ├── DavResourceFactoryImpl.java │ ├── DavSessionImpl.java │ ├── DavSessionProviderImpl.java │ ├── ExclusiveSharedLock.java │ ├── ExclusiveSharedLockManager.java │ ├── NioWebDavServlet.java │ ├── NullInputContext.java │ ├── OSUtil.java │ └── UncheckedDavException.java └── test └── java └── org └── cryptomator └── webdav └── core ├── filters ├── MacChunkedPutCompatibilityFilterTest.java └── UnicodeResourcePathNormalizationFilterTest.java └── servlet ├── ByteRangeTest.java ├── DavLocatorFactoryImplTest.java └── DavLocatorImplTest.java /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | groups: 8 | java-test-dependencies: 9 | patterns: 10 | - "org.junit.jupiter:*" 11 | - "org.mockito:*" 12 | - "org.hamcrest:*" 13 | maven-build-plugins: 14 | patterns: 15 | - "org.apache.maven.plugins:*" 16 | - "org.jacoco:jacoco-maven-plugin" 17 | - "org.owasp:dependency-check-maven" 18 | - "org.sonatype.plugins:nexus-staging-maven-plugin" 19 | java-production-dependencies: 20 | patterns: 21 | - "*" 22 | exclude-patterns: 23 | - "org.apache.maven.plugins:*" 24 | - "org.jacoco:jacoco-maven-plugin" 25 | - "org.owasp:dependency-check-maven" 26 | - "org.sonatype.plugins:nexus-staging-maven-plugin" 27 | - "org.junit.jupiter:*" 28 | - "org.mockito:*" 29 | - "org.hamcrest:*" 30 | 31 | 32 | - package-ecosystem: "github-actions" 33 | directory: "/" # even for `.github/workflows` 34 | schedule: 35 | interval: "monthly" 36 | groups: 37 | github-actions: 38 | patterns: 39 | - "*" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | pull_request_target: 5 | types: [labeled] 6 | jobs: 7 | build: 8 | name: Build and Test 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-java@v4 13 | with: 14 | java-version: 21 15 | distribution: 'temurin' 16 | cache: 'maven' 17 | - name: Ensure to use tagged version 18 | if: startsWith(github.ref, 'refs/tags/') 19 | run: mvn -B versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} 20 | - name: Build and Test 21 | id: buildAndTest 22 | run: mvn -B clean install jacoco:report -Pcoverage 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: artifacts 26 | path: target/*.jar 27 | - name: Create Release 28 | uses: softprops/action-gh-release@v2 29 | if: startsWith(github.ref, 'refs/tags/') 30 | with: 31 | prerelease: true 32 | token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} 33 | generate_release_notes: true 34 | -------------------------------------------------------------------------------- /.github/workflows/dependency-check.yml: -------------------------------------------------------------------------------- 1 | name: OWASP Maven Dependency Check 2 | on: 3 | schedule: 4 | - cron: '0 15 * * 0' 5 | push: 6 | branches: 7 | - 'release/**' 8 | workflow_dispatch: 9 | 10 | 11 | jobs: 12 | check-dependencies: 13 | uses: skymatic/workflows/.github/workflows/run-dependency-check.yml@v1 14 | with: 15 | runner-os: 'ubuntu-latest' 16 | java-distribution: 'temurin' 17 | java-version: 21 18 | secrets: 19 | nvd-api-key: ${{ secrets.NVD_API_KEY }} 20 | slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-central.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven Central 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-java@v4 14 | with: 15 | java-version: 21 16 | distribution: 'temurin' 17 | cache: 'maven' 18 | server-id: central 19 | server-username: MAVEN_CENTRAL_USERNAME 20 | server-password: MAVEN_CENTRAL_PASSWORD 21 | - name: Verify project version = ${{ github.event.release.tag_name }} 22 | run: | 23 | PROJECT_VERSION=$(mvn help:evaluate "-Dexpression=project.version" -q -DforceStdout) 24 | test "$PROJECT_VERSION" = "${{ github.event.release.tag_name }}" 25 | - name: Deploy 26 | run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress 27 | env: 28 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 29 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 30 | MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} 31 | MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} 32 | MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-github.yml: -------------------------------------------------------------------------------- 1 | name: Publish to GitHub Packages 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-java@v4 12 | with: 13 | java-version: 21 14 | distribution: 'temurin' 15 | cache: 'maven' 16 | - name: Enforce project version ${{ github.event.release.tag_name }} 17 | run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }} 18 | - name: Deploy 19 | run: mvn deploy -B -DskipTests -Psign,deploy-github --no-transfer-progress 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} 23 | MAVEN_GPG_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} 24 | MAVEN_GPG_KEY_FINGERPRINT: ${{ vars.RELEASES_GPG_KEY_FINGERPRINT }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | # Maven # 9 | target/ 10 | pom.xml.versionsBackup 11 | 12 | # IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # 13 | .idea/**/workspace.xml 14 | .idea/**/tasks.xml 15 | .idea/dictionaries 16 | .idea/**/libraries/ 17 | .idea/.name 18 | .idea/encodings.xml 19 | .idea/compiler.xml 20 | .idea/jarRepositories.xml 21 | *.iml 22 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/cryptomator/webdav-nio-adapter-servlet/workflows/Build/badge.svg)](https://github.com/cryptomator/webdav-nio-adapter-servlet/actions?query=workflow%3ABuild) 2 | [![Maven Central](https://img.shields.io/maven-central/v/org.cryptomator/webdav-nio-adapter-servlet.svg?maxAge=86400)](https://repo1.maven.org/maven2/org/cryptomator/webdav-nio-adapter-servlet/) 3 | [![Javadocs](http://www.javadoc.io/badge/org.cryptomator/webdav-nio-adapter-servlet.svg)](http://www.javadoc.io/doc/org.cryptomator/webdav-nio-adapter-servlet) 4 | 5 | # webdav-nio-adapter-servlet 6 | Serves directory contents specified by a `java.nio.file.Path` via a WebDAV servlet. Based on Apache Jackrabbit. 7 | 8 | ## Maven integration 9 | 10 | ```xml 11 | 12 | 13 | org.cryptomator 14 | webdav-nio-adapter-servlet 15 | 1.0.0 16 | 17 | 18 | ``` 19 | 20 | ## License 21 | 22 | This project is dual-licensed under the AGPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to use this library in applications, that are *not* licensed under the AGPL, feel free to contact our [support team](https://cryptomator.org/help/). 23 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.cryptomator 5 | webdav-nio-adapter-servlet 6 | 1.3.0-SNAPSHOT 7 | WebDAV-NIO Adapter Servlet 8 | Servlet serving NIO directory contents as WebDAV resources. 9 | https://github.com/cryptomator/webdav-nio-adapter-servlet 10 | 11 | 12 | scm:git:git@github.com:cryptomator/webdav-nio-adapter.git 13 | scm:git:git@github.com:cryptomator/webdav-nio-adapter.git 14 | git@github.com:cryptomator/webdav-nio-adapter-servlet.git 15 | 16 | 17 | 18 | UTF-8 19 | 20 | 21 | 4.0.1 22 | 2.22.0 23 | 33.4.0-jre 24 | 2.0.17 25 | 26 | 27 | 5.12.2 28 | 5.17.0 29 | 3.0 30 | 31 | 32 | 3.14.0 33 | 3.5.3 34 | 3.4.2 35 | 3.3.1 36 | 3.11.2 37 | 3.2.7 38 | 12.1.1 39 | 0.8.13 40 | 0.7.0 41 | 42 | 43 | 44 | 45 | GNU Affero General Public License (AGPL) version 3.0 46 | https://www.gnu.org/licenses/agpl.txt 47 | repo 48 | 49 | 50 | 51 | 52 | 53 | Sebastian Stenzel 54 | sebastian.stenzel@gmail.com 55 | +1 56 | cryptomator.org 57 | http://cryptomator.org 58 | 59 | 60 | 61 | 62 | 63 | 64 | javax.servlet 65 | javax.servlet-api 66 | ${servlet.version} 67 | provided 68 | 69 | 70 | org.apache.jackrabbit 71 | jackrabbit-webdav 72 | ${jackrabbit.version} 73 | 74 | 75 | 77 | org.apache.httpcomponents 78 | httpclient 79 | 80 | 81 | 82 | org.slf4j 83 | jcl-over-slf4j 84 | 85 | 86 | 87 | 88 | 89 | 90 | com.google.guava 91 | guava 92 | ${guava.version} 93 | 94 | 95 | 96 | 97 | org.slf4j 98 | slf4j-api 99 | ${slf4j.version} 100 | 101 | 102 | 103 | 104 | org.junit.jupiter 105 | junit-jupiter 106 | ${junit.jupiter.version} 107 | test 108 | 109 | 110 | org.mockito 111 | mockito-core 112 | ${mockito.version} 113 | test 114 | 115 | 116 | org.hamcrest 117 | hamcrest 118 | ${hamcrest.version} 119 | test 120 | 121 | 122 | org.slf4j 123 | slf4j-simple 124 | ${slf4j.version} 125 | test 126 | 127 | 128 | 129 | 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-compiler-plugin 134 | ${mvn-compiler.version} 135 | 136 | 11 137 | true 138 | 139 | 140 | 141 | maven-surefire-plugin 142 | ${mvn-surefire.version} 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-jar-plugin 147 | ${mvn-jar.version} 148 | 149 | 150 | 151 | org.cryptomator.frontend.webdav.servlet 152 | 153 | 154 | 155 | 156 | 157 | maven-source-plugin 158 | ${mvn-source.version} 159 | 160 | 161 | attach-sources 162 | 163 | jar-no-fork 164 | 165 | 166 | 167 | 168 | 169 | maven-javadoc-plugin 170 | ${mvn-javadoc.version} 171 | 172 | 173 | attach-javadocs 174 | 175 | jar 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | dependency-check 186 | 187 | 188 | 189 | org.owasp 190 | dependency-check-maven 191 | ${dependency-check.version} 192 | 193 | 24 194 | 6 195 | true 196 | true 197 | NVD_API_KEY 198 | 199 | 200 | 201 | 202 | check 203 | 204 | validate 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | coverage 214 | 215 | 216 | 217 | org.jacoco 218 | jacoco-maven-plugin 219 | ${jacoco.version} 220 | 221 | 222 | prepare-agent 223 | 224 | prepare-agent 225 | 226 | 227 | surefire.jacoco.args 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | sign 238 | 239 | 240 | 241 | maven-gpg-plugin 242 | ${mvn-gpg.version} 243 | 244 | 245 | sign-artifacts 246 | verify 247 | 248 | sign 249 | 250 | 251 | bc 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | deploy-central 262 | 263 | 264 | 265 | org.sonatype.central 266 | central-publishing-maven-plugin 267 | ${central-publishing.version} 268 | true 269 | 270 | central 271 | true 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | deploy-github 280 | 281 | 282 | github 283 | GitHub Packages 284 | https://maven.pkg.github.com/cryptomator/webdav-nio-adapter-servlet 285 | 286 | 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/AcceptRangeFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.FilterConfig; 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | 18 | /** 19 | * Adds an Accept-Range: bytes header to all GET requests. 20 | */ 21 | public class AcceptRangeFilter implements HttpFilter { 22 | 23 | private static final String METHOD_GET = "GET"; 24 | private static final String HEADER_ACCEPT_RANGES = "Accept-Ranges"; 25 | private static final String HEADER_ACCEPT_RANGE_VALUE = "bytes"; 26 | 27 | @Override 28 | public void init(FilterConfig filterConfig) throws ServletException { 29 | // no-op 30 | } 31 | 32 | @Override 33 | public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 34 | if (METHOD_GET.equalsIgnoreCase(request.getMethod())) { 35 | response.addHeader(HEADER_ACCEPT_RANGES, HEADER_ACCEPT_RANGE_VALUE); 36 | } 37 | chain.doFilter(request, response); 38 | } 39 | 40 | @Override 41 | public void destroy() { 42 | // no-op 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/HttpFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import javax.servlet.*; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | interface HttpFilter extends Filter { 17 | 18 | @Override 19 | default void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 20 | if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { 21 | doFilterHttp((HttpServletRequest) request, (HttpServletResponse) response, chain); 22 | } else { 23 | chain.doFilter(request, response); 24 | } 25 | } 26 | 27 | @Override 28 | default void init(FilterConfig filterConfig) throws ServletException { 29 | // no-op 30 | } 31 | 32 | @Override 33 | default void destroy() { 34 | // no-op 35 | } 36 | 37 | void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException; 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/LoggingFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE file. 5 | *******************************************************************************/ 6 | package org.cryptomator.webdav.core.filters; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import javax.servlet.FilterChain; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | import java.util.Enumeration; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | 19 | public class LoggingFilter implements HttpFilter { 20 | 21 | private static final Logger LOG = LoggerFactory.getLogger(LoggingFilter.class); 22 | private final AtomicLong REQUEST_ID_GEN = new AtomicLong(); 23 | 24 | @Override 25 | public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 26 | if (LOG.isDebugEnabled()) { 27 | long requestId = REQUEST_ID_GEN.getAndIncrement(); 28 | LOG.debug("REQUEST {}:\n{} {} {}\n{}", requestId, request.getMethod(), request.getRequestURI(), request.getProtocol(), headers(request)); 29 | chain.doFilter(request, response); 30 | LOG.debug("RESPONSE {}:\n{}\n{}", requestId, response.getStatus(), headers(response)); 31 | } else { 32 | chain.doFilter(request, response); 33 | } 34 | } 35 | 36 | private String headers(HttpServletResponse response) { 37 | StringBuilder result = new StringBuilder(); 38 | for (String headerName : response.getHeaderNames()) { 39 | for (String value : response.getHeaders(headerName)) { 40 | result.append(headerName).append(": ").append(value).append('\n'); 41 | } 42 | } 43 | return result.toString(); 44 | } 45 | 46 | private String headers(HttpServletRequest request) { 47 | StringBuilder result = new StringBuilder(); 48 | Enumeration headerNames = request.getHeaderNames(); 49 | while (headerNames.hasMoreElements()) { 50 | String headerName = headerNames.nextElement(); 51 | Enumeration values = request.getHeaders(headerName); 52 | while (values.hasMoreElements()) { 53 | result.append(headerName).append(": ").append(values.nextElement()).append('\n'); 54 | } 55 | } 56 | return result.toString(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/MacChunkedPutCompatibilityFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import com.google.common.io.ByteStreams; 12 | 13 | import javax.servlet.FilterChain; 14 | import javax.servlet.ReadListener; 15 | import javax.servlet.ServletException; 16 | import javax.servlet.ServletInputStream; 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletRequestWrapper; 19 | import javax.servlet.http.HttpServletResponse; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | 23 | /** 24 | * If a PUT request with chunked transfer encoding and a X-Expected-Entity-Length header field is sent, 25 | * the input stream will return EOF after the number of bytes stated in this header has been read. 26 | * 27 | * This filter ensures compatibility of the Mac OS X WebDAV client, as Macs don't terminate chunked transfers normally (by sending a 0-byte-chunk). 28 | */ 29 | public class MacChunkedPutCompatibilityFilter implements HttpFilter { 30 | 31 | private static final String METHOD_PUT = "PUT"; 32 | private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding"; 33 | private static final String HEADER_X_EXPECTED_ENTITIY_LENGTH = "X-Expected-Entity-Length"; 34 | private static final String HEADER_TRANSFER_ENCODING_CHUNKED = "chunked"; 35 | 36 | @Override 37 | public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 38 | final String expectedEntitiyLengthHeader = request.getHeader(HEADER_X_EXPECTED_ENTITIY_LENGTH); 39 | if (METHOD_PUT.equalsIgnoreCase(request.getMethod()) // 40 | && HEADER_TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(request.getHeader(HEADER_TRANSFER_ENCODING)) // 41 | && expectedEntitiyLengthHeader != null) { 42 | long expectedEntitiyLength; 43 | try { 44 | expectedEntitiyLength = Long.valueOf(expectedEntitiyLengthHeader); 45 | } catch (NumberFormatException e) { 46 | response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid X-Expected-Entity-Length"); 47 | return; 48 | } 49 | chain.doFilter(new PutRequestWithBoundedInputStream(request, expectedEntitiyLength), response); 50 | } else { 51 | chain.doFilter(request, response); 52 | } 53 | } 54 | 55 | private static class PutRequestWithBoundedInputStream extends HttpServletRequestWrapper { 56 | 57 | private final long inputStreamLimit; 58 | 59 | public PutRequestWithBoundedInputStream(HttpServletRequest request, long inputStreamLimit) { 60 | super(request); 61 | this.inputStreamLimit = inputStreamLimit; 62 | } 63 | 64 | @Override 65 | public ServletInputStream getInputStream() throws IOException { 66 | return new BoundedServletInputStream(super.getInputStream(), inputStreamLimit); 67 | } 68 | 69 | } 70 | 71 | /** 72 | * A ServletInputStream with limited number of bytes that can be read. 73 | */ 74 | private static class BoundedServletInputStream extends ServletInputStream { 75 | 76 | private final InputStream boundedIn; 77 | private final ServletInputStream servletIn; 78 | private boolean reachedEof = false; 79 | private ReadListener readListener; 80 | 81 | public BoundedServletInputStream(ServletInputStream delegate, long limit) { 82 | this.boundedIn = ByteStreams.limit(delegate, limit); 83 | this.servletIn = delegate; 84 | } 85 | 86 | private void reachedEof() throws IOException { 87 | reachedEof = true; 88 | if (readListener != null) { 89 | readListener.onAllDataRead(); 90 | } 91 | } 92 | 93 | /* BoundedInputStream */ 94 | 95 | @Override 96 | public long skip(long n) throws IOException { 97 | return boundedIn.skip(n); 98 | } 99 | 100 | @Override 101 | public int available() throws IOException { 102 | return boundedIn.available(); 103 | } 104 | 105 | @Override 106 | public int read(byte[] b, int off, int len) throws IOException { 107 | int read = boundedIn.read(b, off, len); 108 | if (read == -1) { 109 | reachedEof(); 110 | } 111 | return read; 112 | } 113 | 114 | @Override 115 | public int read() throws IOException { 116 | int aByte = boundedIn.read(); 117 | if (aByte == -1) { 118 | reachedEof(); 119 | } 120 | return aByte; 121 | } 122 | 123 | /* ServletInputStream */ 124 | 125 | @Override 126 | public boolean isFinished() { 127 | return reachedEof || servletIn.isFinished(); 128 | } 129 | 130 | @Override 131 | public boolean isReady() { 132 | return !reachedEof && servletIn.isReady(); 133 | } 134 | 135 | @Override 136 | public void setReadListener(ReadListener readListener) { 137 | servletIn.setReadListener(readListener); 138 | this.readListener = readListener; 139 | } 140 | 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/MkcolComplianceFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import javax.servlet.FilterChain; 15 | import javax.servlet.ServletException; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | 20 | /** 21 | * Responds with status code 415, if an attempt is made to create a collection with a body. 22 | * 23 | * See https://tools.ietf.org/html/rfc2518#section-8.3.1: 24 | * "If the server receives a MKCOL request entity type it does not support or understand 25 | * it MUST respond with a 415 (Unsupported Media Type) status code." 26 | */ 27 | public class MkcolComplianceFilter implements HttpFilter { 28 | 29 | private static final Logger LOG = LoggerFactory.getLogger(MkcolComplianceFilter.class); 30 | private static final String METHOD_MKCOL = "MKCOL"; 31 | private static final String HEADER_TRANSFER_ENCODING = "Transfer-Encoding"; 32 | 33 | @Override 34 | public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 35 | boolean hasBody = request.getContentLengthLong() > 0 || request.getHeader(HEADER_TRANSFER_ENCODING) != null; 36 | if (METHOD_MKCOL.equalsIgnoreCase(request.getMethod()) && hasBody) { 37 | LOG.warn("Blocked invalid MKCOL request to {}", request.getRequestURI()); 38 | response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, "MKCOL with body not supported."); 39 | } else { 40 | chain.doFilter(request, response); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/PostRequestBlockingFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import javax.servlet.FilterChain; 15 | import javax.servlet.ServletException; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import javax.servlet.http.HttpServletResponseWrapper; 19 | import java.io.IOException; 20 | 21 | import static java.util.Arrays.stream; 22 | import static java.util.function.Predicate.isEqual; 23 | import static java.util.stream.Collectors.joining; 24 | 25 | /** 26 | * Blocks all post requests. 27 | */ 28 | public class PostRequestBlockingFilter implements HttpFilter { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(PostRequestBlockingFilter.class); 31 | private static final String POST_METHOD = "POST"; 32 | 33 | @Override 34 | public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 35 | if (isPost(request)) { 36 | LOG.warn("Blocked POST request to {}", request.getRequestURI()); 37 | response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); 38 | } else { 39 | chain.doFilter(request, new FilteredResponse(response)); 40 | } 41 | } 42 | 43 | private boolean isPost(HttpServletRequest request) { 44 | return POST_METHOD.equalsIgnoreCase(request.getMethod()); 45 | } 46 | 47 | private static class FilteredResponse extends HttpServletResponseWrapper { 48 | 49 | public FilteredResponse(HttpServletResponse response) { 50 | super(response); 51 | } 52 | 53 | @Override 54 | public void addHeader(String name, String value) { 55 | if (isAllowHeader(name)) { 56 | super.setHeader(name, removePost(value)); 57 | } else { 58 | super.addHeader(name, value); 59 | } 60 | } 61 | 62 | @Override 63 | public void setHeader(String name, String value) { 64 | if (isAllowHeader(name)) { 65 | super.setHeader(name, removePost(value)); 66 | } else { 67 | super.setHeader(name, value); 68 | } 69 | } 70 | 71 | private String removePost(String value) { 72 | return stream(value.split("\\s*,\\s*")).filter(isEqual("POST").negate()).collect(joining(", ")); 73 | } 74 | 75 | private boolean isAllowHeader(String name) { 76 | return "allow".equalsIgnoreCase(name); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/filters/UnicodeResourcePathNormalizationFilter.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE file. 5 | *******************************************************************************/ 6 | package org.cryptomator.webdav.core.filters; 7 | 8 | import com.google.common.collect.ImmutableSet; 9 | import org.apache.jackrabbit.webdav.DavServletResponse; 10 | import org.apache.jackrabbit.webdav.util.EncodeUtil; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import javax.servlet.FilterChain; 15 | import javax.servlet.ServletException; 16 | import javax.servlet.ServletOutputStream; 17 | import javax.servlet.WriteListener; 18 | import javax.servlet.http.HttpServletRequest; 19 | import javax.servlet.http.HttpServletRequestWrapper; 20 | import javax.servlet.http.HttpServletResponse; 21 | import javax.servlet.http.HttpServletResponseWrapper; 22 | import javax.xml.namespace.QName; 23 | import javax.xml.stream.*; 24 | import java.io.*; 25 | import java.net.URI; 26 | import java.net.URISyntaxException; 27 | import java.nio.charset.StandardCharsets; 28 | import java.text.Normalizer; 29 | import java.text.Normalizer.Form; 30 | import java.util.Set; 31 | 32 | /** 33 | * Makes sure, all resource paths containing special unicode characters are composed of characters in {@link Form#NFC Normalization Form C}. 34 | * Multistatus responses will be transformed back to {@link Form#NFD NFD} for certain user agents known to expect it. 35 | */ 36 | public class UnicodeResourcePathNormalizationFilter implements HttpFilter { 37 | 38 | private static final Logger LOG = LoggerFactory.getLogger(UnicodeResourcePathNormalizationFilter.class); 39 | private static final String PROPFIND_METHOD = "PROPFIND"; 40 | private static final String USER_AGENT_HEADER = "User-Agent"; 41 | private static final Set USER_AGENTS_EXPECTING_NFD = ImmutableSet.of("WebDAVFS"); 42 | 43 | @Override 44 | public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { 45 | HttpServletResponse filteredResponse; 46 | if (PROPFIND_METHOD.equalsIgnoreCase(request.getMethod()) && isUserAgentExpectingNfdResponses(request)) { 47 | // response will probably be a multi status xml response, we need to filter it, too: 48 | filteredResponse = new NormalizedMultiStatusResponse(response, Form.NFD); 49 | } else { 50 | // otherwise don't intercept the response 51 | filteredResponse = response; 52 | } 53 | chain.doFilter(new NormalizedRequest(request), filteredResponse); 54 | } 55 | 56 | private boolean isUserAgentExpectingNfdResponses(HttpServletRequest request) { 57 | String userAgent = request.getHeader(USER_AGENT_HEADER); 58 | return USER_AGENTS_EXPECTING_NFD.contains(userAgent); 59 | } 60 | 61 | /** 62 | * Encodes any paths in this requests to {@link Form#NFC}. 63 | */ 64 | private static class NormalizedRequest extends HttpServletRequestWrapper { 65 | 66 | private static final String DESTINATION_HEADER = "Destination"; 67 | 68 | public NormalizedRequest(HttpServletRequest request) { 69 | super(request); 70 | } 71 | 72 | @Override 73 | public String getPathInfo() { 74 | int lengthContextPath = this.getContextPath().length(); 75 | String path = getNormalizedRequestURI().getPath(); 76 | assert (path.length() >= lengthContextPath); 77 | return path.substring(lengthContextPath); 78 | } 79 | 80 | @Override 81 | public String getRequestURI() { 82 | return getNormalizedRequestURI().toString(); 83 | } 84 | 85 | private URI getNormalizedRequestURI() { 86 | try { 87 | URI uri = URI.create(super.getRequestURI()); 88 | if (!uri.getPath().startsWith(this.getContextPath())) { 89 | throw new IllegalStateException("URI does not match to Context Path."); 90 | } 91 | String normedUri = Normalizer.normalize(uri.getPath(), Form.NFC); 92 | return new URI(null, null, normedUri, null); 93 | } catch (URISyntaxException e) { 94 | throw new IllegalStateException(e); 95 | } 96 | } 97 | 98 | @Override 99 | public String getHeader(String name) { 100 | if (DESTINATION_HEADER.equalsIgnoreCase(name)) { 101 | String origDestHeader = super.getHeader(DESTINATION_HEADER); 102 | if (origDestHeader == null) { 103 | return null; 104 | } 105 | try { 106 | // header value contains RFC 2396 absolute uri 107 | URI orig = URI.create(origDestHeader); 108 | String normalizedPath = Normalizer.normalize(orig.getPath(), Form.NFC); 109 | return new URI(orig.getScheme(), orig.getUserInfo(), orig.getHost(), orig.getPort(), normalizedPath, orig.getQuery(), orig.getFragment()).toString(); 110 | } catch (URISyntaxException e) { 111 | throw new IllegalStateException("URI constructed from valid URI can not be invalid.", e); 112 | } 113 | } else { 114 | return super.getHeader(name); 115 | } 116 | } 117 | 118 | @Override 119 | public StringBuffer getRequestURL() { 120 | StringBuffer url = new StringBuffer(); 121 | url.append(getScheme()).append("://"); 122 | url.append(getServerName()); 123 | if ((getScheme().equals("http") && getServerPort() != 80) || (getScheme().equals("https") && getServerPort() != 443)) { 124 | url.append(':').append(getServerPort()); 125 | } 126 | url.append(getRequestURI()); 127 | return url; 128 | } 129 | 130 | } 131 | 132 | /** 133 | * Whenever the http status code is 207 and a fixed-length body is expected, this ServletResponse will return a filtered outputstream. 134 | */ 135 | private static class NormalizedMultiStatusResponse extends HttpServletResponseWrapper { 136 | 137 | private boolean isMultiStatus = true; 138 | private int contentLength = -1; 139 | private final Form normalizationForm; 140 | 141 | public NormalizedMultiStatusResponse(HttpServletResponse response, Form normalizationForm) { 142 | super(response); 143 | this.normalizationForm = normalizationForm; 144 | } 145 | 146 | @Override 147 | public void setStatus(int sc) { 148 | super.setStatus(sc); 149 | isMultiStatus = sc == DavServletResponse.SC_MULTI_STATUS; 150 | } 151 | 152 | @Override 153 | public void setContentLength(int len) { 154 | contentLength = len; 155 | } 156 | 157 | @Override 158 | public void setContentLengthLong(long len) { 159 | if (len <= Integer.MAX_VALUE) { 160 | contentLength = (int) len; 161 | } else { 162 | // we do not want to intercept a 4gb+ response. just stream the original, unfiltered response. 163 | contentLength = -1; 164 | } 165 | } 166 | 167 | @Override 168 | public ServletOutputStream getOutputStream() throws IOException { 169 | if (isMultiStatus && contentLength != -1) { 170 | return new NormalizedServletOutputStream(super.getOutputStream(), contentLength, normalizationForm); 171 | } else { 172 | LOG.warn("Response not a Multi Status response, thus output encoding will not be normalized."); 173 | return super.getOutputStream(); 174 | } 175 | } 176 | 177 | } 178 | 179 | /** 180 | * Buffers all bytes up to a pre-defined threshold (the content length). 181 | * When it is reached, the buffer will be transformed using a {@link MultistatusHrefNormalizer}. 182 | */ 183 | private static class NormalizedServletOutputStream extends ServletOutputStream { 184 | 185 | private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 186 | private final ServletOutputStream delegate; 187 | private final int contentLength; 188 | private final Form normalizationForm; 189 | 190 | public NormalizedServletOutputStream(ServletOutputStream delegate, int contentLength, Form normalizationForm) { 191 | if (contentLength < 0) { 192 | throw new IllegalArgumentException("contentLength must be a positive integer"); 193 | } 194 | this.delegate = delegate; 195 | this.contentLength = contentLength; 196 | this.normalizationForm = normalizationForm; 197 | } 198 | 199 | @Override 200 | public boolean isReady() { 201 | return delegate.isReady(); 202 | } 203 | 204 | @Override 205 | public void setWriteListener(WriteListener writeListener) { 206 | delegate.setWriteListener(writeListener); 207 | } 208 | 209 | @Override 210 | public void write(int b) throws IOException { 211 | buffer.write(b); 212 | } 213 | 214 | @Override 215 | public void write(byte b[], int off, int len) throws IOException { 216 | buffer.write(b, off, len); 217 | if (buffer.size() >= contentLength) { 218 | normalize(); 219 | } 220 | } 221 | 222 | private void normalize() throws IOException { 223 | try (InputStream in = new ByteArrayInputStream(buffer.toByteArray(), 0, contentLength); // 224 | MultistatusHrefNormalizer transformer = new MultistatusHrefNormalizer(in, delegate, normalizationForm)) { 225 | transformer.transform(); 226 | } catch (XMLStreamException e) { 227 | LOG.error("Error processing XML.", e); 228 | throw new IOException(e); 229 | } 230 | } 231 | 232 | } 233 | 234 | /** 235 | * Parses XML from a given input stream and replicates it to the given output stream, except for any href tags whose contents will be interpreted as URIs 236 | * and adjusted to the specified Unicode Normalization Form. 237 | */ 238 | // visible for testing 239 | static class MultistatusHrefNormalizer implements AutoCloseable { 240 | 241 | private final XMLStreamReader reader; 242 | private final XMLStreamWriter writer; 243 | private boolean isParsingHref = false; 244 | private final Form normalizationForm; 245 | 246 | public MultistatusHrefNormalizer(InputStream in, OutputStream out, Form normalizationForm) { 247 | try { 248 | XMLInputFactory inputFactory = XMLInputFactory.newInstance(); 249 | inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); 250 | this.reader = inputFactory.createXMLStreamReader(in); 251 | XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); 252 | this.writer = outputFactory.createXMLStreamWriter(out, StandardCharsets.UTF_8.name()); 253 | } catch (XMLStreamException | FactoryConfigurationError e) { 254 | throw new IllegalStateException("Failed to set up XML reader/writer", e); 255 | } 256 | this.normalizationForm = normalizationForm; 257 | } 258 | 259 | public void transform() throws XMLStreamException { 260 | writer.writeStartDocument(); 261 | while (reader.hasNext()) { 262 | int xmlEvent = reader.next(); 263 | switch (xmlEvent) { 264 | case XMLStreamReader.START_ELEMENT: 265 | QName qname = reader.getName(); 266 | writer.writeStartElement(qname.getPrefix(), qname.getLocalPart(), qname.getNamespaceURI()); 267 | for (int i = 0; i < reader.getNamespaceCount(); i++) { 268 | writer.writeNamespace(reader.getNamespacePrefix(i), reader.getNamespaceURI(i)); 269 | } 270 | isParsingHref = qname.getLocalPart().equalsIgnoreCase("href"); 271 | break; 272 | case XMLStreamReader.CHARACTERS: 273 | if (isParsingHref) { 274 | writer.writeCharacters(transformHref(reader.getText())); 275 | } else { 276 | writer.writeCharacters(reader.getText()); 277 | } 278 | break; 279 | case XMLStreamReader.END_ELEMENT: 280 | writer.writeEndElement(); 281 | isParsingHref = false; 282 | break; 283 | default: 284 | break; 285 | } 286 | } 287 | writer.writeEndDocument(); 288 | writer.flush(); 289 | } 290 | 291 | private String transformHref(String originalHref) { 292 | URI uri = URI.create(originalHref); // should be a valid RFC 2396 URI 293 | String normalizedPath = Normalizer.normalize(uri.getPath(), normalizationForm); 294 | String escapedPath = EncodeUtil.escapePath(normalizedPath); 295 | return uri.getScheme() + "://" + uri.getRawAuthority() + escapedPath; 296 | } 297 | 298 | @Override 299 | public void close() throws XMLStreamException { 300 | reader.close(); 301 | writer.close(); 302 | } 303 | 304 | } 305 | 306 | } 307 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/AbstractNioWebDavServlet.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.base.Predicates; 12 | import com.google.common.collect.ImmutableSet; 13 | import com.google.common.collect.Iterators; 14 | import org.apache.jackrabbit.webdav.*; 15 | import org.apache.jackrabbit.webdav.header.IfHeader; 16 | import org.apache.jackrabbit.webdav.lock.ActiveLock; 17 | import org.apache.jackrabbit.webdav.lock.Scope; 18 | import org.apache.jackrabbit.webdav.lock.Type; 19 | import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import javax.servlet.ServletException; 24 | import java.io.IOException; 25 | import java.nio.file.Path; 26 | import java.util.Set; 27 | 28 | public abstract class AbstractNioWebDavServlet extends AbstractWebdavServlet { 29 | 30 | private static final String NO_LOCK = "DAV:no-lock"; 31 | private static final Logger LOG = LoggerFactory.getLogger(AbstractNioWebDavServlet.class); 32 | 33 | private final DavSessionProvider davSessionProvider = new DavSessionProviderImpl(); 34 | private final DavLocatorFactory davLocatorFactory = new DavLocatorFactoryImpl(); 35 | private final DavResourceFactoryImpl davResourceFactory = new DavResourceFactoryImpl(this::resolveUrl); 36 | 37 | /** 38 | * @param relativeUrl An url 39 | * @return A path 40 | * @throws IllegalArgumentException If no path could be found for the given url. 41 | */ 42 | protected abstract Path resolveUrl(String relativeUrl) throws IllegalArgumentException; 43 | 44 | @Override 45 | protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) { 46 | IfHeader ifHeader = new IfHeader(request); 47 | if (ifHeader.hasValue() && Iterators.all(ifHeader.getAllTokens(), Predicates.equalTo(NO_LOCK))) { 48 | // https://tools.ietf.org/html/rfc4918#section-10.4.8: 49 | // "DAV:no-lock" is known to never represent a current lock token. 50 | return false; 51 | } else if (ifHeader.hasValue() && Iterators.any(ifHeader.getAllNotTokens(), Predicates.equalTo(NO_LOCK))) { 52 | // by applying "Not" to a state token that is known not to be current, the Condition always evaluates to true. 53 | return true; 54 | } else { 55 | return request.matchesIfHeader(resource); 56 | } 57 | } 58 | 59 | @Override 60 | public DavSessionProvider getDavSessionProvider() { 61 | return davSessionProvider; 62 | } 63 | 64 | @Override 65 | public void setDavSessionProvider(DavSessionProvider davSessionProvider) { 66 | throw new UnsupportedOperationException("Setting davSessionProvider not supported."); 67 | } 68 | 69 | @Override 70 | public DavLocatorFactory getLocatorFactory() { 71 | return davLocatorFactory; 72 | } 73 | 74 | @Override 75 | public void setLocatorFactory(DavLocatorFactory locatorFactory) { 76 | throw new UnsupportedOperationException("Setting locatorFactory not supported."); 77 | } 78 | 79 | @Override 80 | public DavResourceFactory getResourceFactory() { 81 | return davResourceFactory; 82 | } 83 | 84 | @Override 85 | public void setResourceFactory(DavResourceFactory resourceFactory) { 86 | throw new UnsupportedOperationException("Setting resourceFactory not supported."); 87 | } 88 | 89 | /* Unchecked DAV exception rewrapping and logging */ 90 | 91 | @Override 92 | protected boolean execute(WebdavRequest request, WebdavResponse response, int method, DavResource resource) throws ServletException, IOException, DavException { 93 | try { 94 | try { 95 | return super.execute(request, response, method, resource); 96 | } catch (UncheckedDavException e) { 97 | throw e.toDavException(); 98 | } 99 | } catch (DavException e) { 100 | if (e.getErrorCode() == DavServletResponse.SC_INTERNAL_SERVER_ERROR) { 101 | LOG.error("Unexpected DavException.", e); 102 | } 103 | throw e; 104 | } 105 | } 106 | 107 | /* GET stuff */ 108 | 109 | @Override 110 | protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException { 111 | super.doGet(request, response, resource); 112 | } 113 | 114 | /* LOCK stuff */ 115 | 116 | @Override 117 | protected int validateDestination(DavResource destResource, WebdavRequest request, boolean checkHeader) throws DavException { 118 | if (isLocked(destResource) && !hasCorrectLockTokens(request.getDavSession(), destResource)) { 119 | throw new DavException(DavServletResponse.SC_LOCKED, "The destination resource is locked"); 120 | } 121 | return super.validateDestination(destResource, request, checkHeader); 122 | } 123 | 124 | @Override 125 | protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException { 126 | if (isLocked(resource) && !hasCorrectLockTokens(request.getDavSession(), resource)) { 127 | throw new DavException(DavServletResponse.SC_LOCKED, "The resource is locked"); 128 | } 129 | super.doPut(request, response, resource); 130 | } 131 | 132 | @Override 133 | protected void doDelete(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException { 134 | if (isLocked(resource) && !hasCorrectLockTokens(request.getDavSession(), resource)) { 135 | throw new DavException(DavServletResponse.SC_LOCKED, "The resource is locked"); 136 | } 137 | super.doDelete(request, response, resource); 138 | } 139 | 140 | @Override 141 | protected void doMove(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException { 142 | if (isLocked(resource) && !hasCorrectLockTokens(request.getDavSession(), resource)) { 143 | throw new DavException(DavServletResponse.SC_LOCKED, "The source resource is locked"); 144 | } 145 | super.doMove(request, response, resource); 146 | } 147 | 148 | @Override 149 | protected void doPropPatch(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException { 150 | if (isLocked(resource) && !hasCorrectLockTokens(request.getDavSession(), resource)) { 151 | throw new DavException(DavServletResponse.SC_LOCKED, "The resource is locked"); 152 | } 153 | super.doPropPatch(request, response, resource); 154 | } 155 | 156 | private boolean hasCorrectLockTokens(DavSession session, DavResource resource) { 157 | boolean access = false; 158 | 159 | final Set providedLockTokens = ImmutableSet.copyOf(session.getLockTokens()); 160 | for (ActiveLock lock : resource.getLocks()) { 161 | access |= providedLockTokens.contains(lock.getToken()); 162 | } 163 | return access; 164 | } 165 | 166 | private boolean isLocked(DavResource resource) { 167 | return resource.hasLock(Type.WRITE, Scope.EXCLUSIVE) || resource.hasLock(Type.WRITE, Scope.SHARED); 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/ByteRange.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE file. 5 | *******************************************************************************/ 6 | package org.cryptomator.webdav.core.servlet; 7 | 8 | import com.google.common.base.CharMatcher; 9 | import com.google.common.base.Splitter; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * Parsed HTTP range header field (RFC 7233)
15 | * 16 | * Valid ranges:
17 | * 18 | * bytes=100-200
19 | * bytes=-500
20 | * bytes=1000- 21 | *
22 | */ 23 | class ByteRange { 24 | 25 | private static final String RANGE_BYTE_PREFIX = "bytes="; 26 | private static final char RANGE_SET_SEP = ','; 27 | private static final char RANGE_SEP = '-'; 28 | 29 | private final Long firstByte; 30 | private final Long lastByte; 31 | 32 | private ByteRange(Long firstByte, Long lastByte) throws MalformedByteRangeException { 33 | if (firstByte == null && lastByte == null || // 34 | firstByte != null && lastByte != null && firstByte > lastByte) { 35 | throw new MalformedByteRangeException(); 36 | } 37 | this.firstByte = firstByte; 38 | this.lastByte = lastByte; 39 | } 40 | 41 | /** 42 | * @param headerValue The raw HTTP header value (i.e. without the key, e.g. bytes=100-200) 43 | * @throws UnsupportedRangeException thrown if the range is not supported by this implementation (range header should be ignored) 44 | * @throws MalformedByteRangeException thrown if the range is syntactically malformed (client should be informed about a bad request) 45 | */ 46 | public static ByteRange parse(String headerValue) throws UnsupportedRangeException, MalformedByteRangeException { 47 | if (!headerValue.startsWith(RANGE_BYTE_PREFIX)) { 48 | throw new UnsupportedRangeException(); 49 | } 50 | final String byteRangeStr = getSingleByteRange(headerValue); 51 | return getPositions(byteRangeStr); 52 | } 53 | 54 | private static String getSingleByteRange(String headerValue) throws UnsupportedRangeException, MalformedByteRangeException { 55 | final String byteRangeSet = headerValue.substring(RANGE_BYTE_PREFIX.length()); 56 | if (CharMatcher.whitespace().matchesAllOf(byteRangeSet)) { 57 | throw new MalformedByteRangeException(); // empty string 58 | } 59 | List byteRanges = Splitter.on(RANGE_SET_SEP).omitEmptyStrings().splitToList(byteRangeSet); 60 | if (byteRanges.size() == 1) { 61 | return byteRanges.get(0); 62 | } else { 63 | throw new UnsupportedRangeException(); // only a single range is expected 64 | } 65 | } 66 | 67 | private static ByteRange getPositions(String byteRangeStr) throws MalformedByteRangeException { 68 | final List bytePos = Splitter.on(RANGE_SEP).splitToList(byteRangeStr); 69 | if (bytePos.size() != 2) { 70 | throw new MalformedByteRangeException(); 71 | } 72 | try { 73 | Long first = bytePos.get(0).isEmpty() ? null : Long.valueOf(bytePos.get(0)); 74 | Long last = bytePos.get(1).isEmpty() ? null : Long.valueOf(bytePos.get(1)); 75 | return new ByteRange(first, last); 76 | } catch (NumberFormatException e) { 77 | throw new MalformedByteRangeException(); 78 | } 79 | } 80 | 81 | /** 82 | * @param contentLength Total size of the resource of which a range is requested. 83 | * @return Index of first byte to be served bounded to [0, contentLength) 84 | */ 85 | public long getEffectiveFirstByte(long contentLength) { 86 | if (firstByte == null) { 87 | // bytes=-500 88 | assert lastByte != null; 89 | return Math.max(0, contentLength - lastByte); 90 | } else { 91 | // bytes=100-200 92 | // bytes=1000- 93 | return firstByte; 94 | } 95 | } 96 | 97 | /** 98 | * @param contentLength Total size of the resource of which a range is requested. 99 | * @return Index of last byte to be served bounded to [0, contentLength) 100 | */ 101 | public long getEffectiveLastByte(long contentLength) { 102 | if (firstByte == null || lastByte == null) { 103 | // bytes=-500 104 | // bytes=1000- 105 | return contentLength - 1; 106 | } else { 107 | // bytes=100-200 108 | return Math.min(lastByte, contentLength - 1); 109 | } 110 | } 111 | 112 | /** 113 | * Indicates that a byte range is not understood or supported by this implementation. 114 | * "An origin server MUST ignore a Range header field that contains a range unit it does not understand." 115 | * - RFC 7233 Section 3.1 116 | */ 117 | public static class UnsupportedRangeException extends Exception { 118 | } 119 | 120 | /** 121 | * Indicates a malformed range header, which should be reported as client error. 122 | */ 123 | public static class MalformedByteRangeException extends Exception { 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/CopyingFileVisitor.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.*; 13 | import java.nio.file.attribute.BasicFileAttributes; 14 | 15 | class CopyingFileVisitor extends SimpleFileVisitor { 16 | 17 | private final Path srcDir; 18 | private final Path dstDir; 19 | private final CopyOption[] options; 20 | 21 | public CopyingFileVisitor(Path srcDir, Path dstDir, CopyOption... options) { 22 | this.srcDir = srcDir; 23 | this.dstDir = dstDir; 24 | this.options = options; 25 | } 26 | 27 | @Override 28 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 29 | Path relativePath = srcDir.relativize(dir); 30 | Path resolvedPath = dstDir.resolve(relativePath); 31 | Files.copy(dir, resolvedPath, options); 32 | return FileVisitResult.CONTINUE; 33 | } 34 | 35 | @Override 36 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 37 | Path relativePath = srcDir.relativize(file); 38 | Path resolvedPath = dstDir.resolve(relativePath); 39 | Files.copy(file, resolvedPath, options); 40 | return FileVisitResult.CONTINUE; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavFile.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.base.Strings; 12 | import org.apache.jackrabbit.webdav.*; 13 | import org.apache.jackrabbit.webdav.io.InputContext; 14 | import org.apache.jackrabbit.webdav.io.OutputContext; 15 | import org.apache.jackrabbit.webdav.lock.ActiveLock; 16 | import org.apache.jackrabbit.webdav.lock.LockInfo; 17 | import org.apache.jackrabbit.webdav.lock.LockManager; 18 | import org.apache.jackrabbit.webdav.property.DavProperty; 19 | import org.apache.jackrabbit.webdav.property.DavPropertyName; 20 | import org.apache.jackrabbit.webdav.property.DavPropertySet; 21 | import org.apache.jackrabbit.webdav.property.DefaultDavProperty; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.io.IOException; 26 | import java.io.UncheckedIOException; 27 | import java.nio.file.FileSystemException; 28 | import java.nio.file.Files; 29 | import java.nio.file.Path; 30 | import java.nio.file.StandardCopyOption; 31 | import java.nio.file.attribute.BasicFileAttributes; 32 | import java.util.Optional; 33 | 34 | class DavFile extends DavNode { 35 | 36 | public static final Logger LOG = LoggerFactory.getLogger(DavFile.class); 37 | 38 | protected static final String CONTENT_TYPE_VALUE = "application/octet-stream"; 39 | protected static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition"; 40 | protected static final String CONTENT_DISPOSITION_VALUE = "attachment"; 41 | protected static final String X_CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options"; 42 | protected static final String X_CONTENT_TYPE_OPTIONS_VALUE = "nosniff"; 43 | 44 | public DavFile(DavResourceFactoryImpl factory, LockManager lockManager, DavLocatorImpl locator, Path path, Optional attr, DavSession session) { 45 | super(factory, lockManager, locator, path, attr, session); 46 | } 47 | 48 | @Override 49 | public boolean isCollection() { 50 | return false; 51 | } 52 | 53 | @Override 54 | public void spool(OutputContext outputContext) throws IOException { 55 | assert exists(); 56 | outputContext.setModificationTime(attr.get().lastModifiedTime().toMillis()); 57 | if (!outputContext.hasStream()) { 58 | return; 59 | } 60 | outputContext.setContentType(CONTENT_TYPE_VALUE); 61 | outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE); 62 | outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE); 63 | outputContext.setContentLength(attr.get().size()); 64 | Files.copy(path, outputContext.getOutputStream()); 65 | } 66 | 67 | @Override 68 | public void addMember(DavResource resource, InputContext inputContext) throws DavException { 69 | throw new UnsupportedOperationException(); 70 | } 71 | 72 | @Override 73 | public DavResourceIterator getMembers() { 74 | throw new UnsupportedOperationException(); 75 | } 76 | 77 | @Override 78 | public void removeMember(DavResource member) throws DavException { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | @Override 83 | public void move(DavResource destination) throws DavException { 84 | if (destination instanceof DavNode) { 85 | DavFile dst = (DavFile) destination; 86 | if (!Files.isDirectory(dst.path.getParent())) { 87 | throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); 88 | } 89 | try { 90 | // Overwrite header already checked by AbstractWebdavServlet#validateDestination 91 | Files.move(path, dst.path, StandardCopyOption.REPLACE_EXISTING); 92 | } catch (FileSystemException e) { 93 | String reason = Strings.nullToEmpty(e.getReason()); 94 | if (reason.contains("too long")) { 95 | // Status code 414 not applictable for things other than request uris. 96 | // If Destination header is too long, return status code 400: 97 | // https://tools.ietf.org/html/rfc4918#section-10.3 98 | throw new DavException(DavServletResponse.SC_BAD_REQUEST); 99 | } else { 100 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 101 | } 102 | } catch (IOException e) { 103 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 104 | } 105 | } else { 106 | throw new IllegalArgumentException("Destination not a DavNode: " + destination.getClass().getName()); 107 | } 108 | } 109 | 110 | @Override 111 | public void copy(DavResource destination, boolean shallow) throws DavException { 112 | if (destination instanceof DavNode) { 113 | DavFile dst = (DavFile) destination; 114 | if (!Files.isDirectory(dst.path.getParent())) { 115 | throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); 116 | } 117 | try { 118 | // Overwrite header already checked by AbstractWebdavServlet#validateDestination 119 | Files.copy(path, dst.path, StandardCopyOption.REPLACE_EXISTING); 120 | } catch (FileSystemException e) { 121 | String reason = Strings.nullToEmpty(e.getReason()); 122 | if (reason.contains("path too long")) { 123 | // Status code 414 not applictable for things other than request uris. 124 | // If Destination header is too long, return status code 400: 125 | // https://tools.ietf.org/html/rfc4918#section-10.3 126 | throw new DavException(DavServletResponse.SC_BAD_REQUEST); 127 | } else { 128 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 129 | } 130 | } catch (IOException e) { 131 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 132 | } 133 | } else { 134 | throw new IllegalArgumentException("Destination not a DavFile: " + destination.getClass().getName()); 135 | } 136 | } 137 | 138 | @Override 139 | public DavProperty getProperty(DavPropertyName name) { 140 | if (DavPropertyName.GETCONTENTLENGTH.equals(name)) { 141 | return sizeProperty().orElse(null); 142 | } else { 143 | return super.getProperty(name); 144 | } 145 | } 146 | 147 | @Override 148 | public DavPropertySet getProperties() { 149 | final DavPropertySet result = super.getProperties(); 150 | if (!result.contains(DavPropertyName.GETCONTENTLENGTH)) { 151 | sizeProperty().ifPresent(result::add); 152 | } 153 | return result; 154 | } 155 | 156 | private Optional> sizeProperty() { 157 | return attr.map(a -> new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, a.size())); 158 | } 159 | 160 | @Override 161 | public ActiveLock lock(LockInfo reqLockInfo) throws DavException { 162 | ActiveLock lock = super.lock(reqLockInfo); 163 | if (!exists()) { 164 | // locking non-existing resources must create a non-collection resource: 165 | // https://tools.ietf.org/html/rfc4918#section-9.10.4 166 | DavFolder parentFolder = getCollection(); 167 | assert parentFolder != null : "File always has a folder."; 168 | parentFolder.addMember(this, new NullInputContext()); 169 | } 170 | return lock; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavFileWithRange.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016, 2017 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.io.ByteStreams; 12 | import org.apache.jackrabbit.webdav.DavServletResponse; 13 | import org.apache.jackrabbit.webdav.DavSession; 14 | import org.apache.jackrabbit.webdav.io.OutputContext; 15 | import org.apache.jackrabbit.webdav.lock.LockManager; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | import java.nio.channels.Channels; 21 | import java.nio.channels.SeekableByteChannel; 22 | import java.nio.file.Files; 23 | import java.nio.file.Path; 24 | import java.nio.file.StandardOpenOption; 25 | import java.nio.file.attribute.BasicFileAttributes; 26 | import java.util.Objects; 27 | import java.util.Optional; 28 | 29 | /** 30 | * Delivers only the requested range of bytes from a file. 31 | * 32 | * @see RFC 7233 Section 4 33 | */ 34 | class DavFileWithRange extends DavFile { 35 | 36 | private static final String CONTENT_RANGE_HEADER = "Content-Range"; 37 | 38 | private final ByteRange reqRange; 39 | 40 | public DavFileWithRange(DavResourceFactoryImpl factory, LockManager lockManager, DavLocatorImpl locator, Path path, BasicFileAttributes attr, DavSession session, ByteRange byteRange) { 41 | super(factory, lockManager, locator, path, Optional.of(attr), session); 42 | this.reqRange = Objects.requireNonNull(byteRange); 43 | } 44 | 45 | @Override 46 | public void spool(OutputContext outputContext) throws IOException { 47 | assert exists(); 48 | outputContext.setModificationTime(attr.get().lastModifiedTime().toMillis()); 49 | if (!outputContext.hasStream()) { 50 | return; 51 | } 52 | final long contentLength = attr.get().size(); 53 | final long firstByte = reqRange.getEffectiveFirstByte(contentLength); 54 | final long lastByte = reqRange.getEffectiveLastByte(contentLength); 55 | final long rangeLength = lastByte - firstByte + 1; 56 | assert firstByte >= 0; 57 | assert lastByte < contentLength; 58 | if (firstByte >= contentLength) { 59 | outputContext.setProperty(CONTENT_RANGE_HEADER, "bytes */" + contentLength); 60 | throw new UncheckedDavException(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE, "Valid Range would be in [0, " + contentLength + "]"); 61 | } 62 | outputContext.setContentLength(rangeLength); 63 | outputContext.setProperty(CONTENT_RANGE_HEADER, contentRangeResponseHeader(firstByte, lastByte, contentLength)); 64 | outputContext.setContentType(CONTENT_TYPE_VALUE); 65 | outputContext.setProperty(CONTENT_DISPOSITION_HEADER, CONTENT_DISPOSITION_VALUE); 66 | outputContext.setProperty(X_CONTENT_TYPE_OPTIONS_HEADER, X_CONTENT_TYPE_OPTIONS_VALUE); 67 | try (SeekableByteChannel src = Files.newByteChannel(path, StandardOpenOption.READ); OutputStream out = outputContext.getOutputStream()) { 68 | src.position(firstByte); 69 | InputStream limitedIn = ByteStreams.limit(Channels.newInputStream(src), rangeLength); 70 | ByteStreams.copy(limitedIn, out); 71 | } 72 | } 73 | 74 | private String contentRangeResponseHeader(long firstByte, long lastByte, long completeLength) { 75 | return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavFolder.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.base.Strings; 12 | import com.google.common.collect.Iterables; 13 | import com.google.common.io.ByteStreams; 14 | import com.google.common.io.MoreFiles; 15 | import com.google.common.io.RecursiveDeleteOption; 16 | import org.apache.jackrabbit.webdav.*; 17 | import org.apache.jackrabbit.webdav.io.InputContext; 18 | import org.apache.jackrabbit.webdav.io.OutputContext; 19 | import org.apache.jackrabbit.webdav.lock.ActiveLock; 20 | import org.apache.jackrabbit.webdav.lock.LockManager; 21 | import org.apache.jackrabbit.webdav.property.DavProperty; 22 | import org.apache.jackrabbit.webdav.property.DavPropertyName; 23 | import org.apache.jackrabbit.webdav.property.DefaultDavProperty; 24 | import org.apache.jackrabbit.webdav.property.ResourceType; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.io.IOException; 29 | import java.io.InputStream; 30 | import java.io.UncheckedIOException; 31 | import java.nio.channels.Channels; 32 | import java.nio.channels.ReadableByteChannel; 33 | import java.nio.channels.WritableByteChannel; 34 | import java.nio.file.*; 35 | import java.nio.file.attribute.BasicFileAttributeView; 36 | import java.nio.file.attribute.BasicFileAttributes; 37 | import java.util.ArrayList; 38 | import java.util.Arrays; 39 | import java.util.List; 40 | import java.util.Optional; 41 | 42 | class DavFolder extends DavNode { 43 | 44 | private static final Logger LOG = LoggerFactory.getLogger(DavFolder.class); 45 | private static final DavPropertyName PROPERTY_QUOTA_AVAILABLE = DavPropertyName.create("quota-available-bytes"); 46 | private static final DavPropertyName PROPERTY_QUOTA_USED = DavPropertyName.create("quota-used-bytes"); 47 | 48 | public DavFolder(DavResourceFactoryImpl factory, LockManager lockManager, DavLocatorImpl locator, Path path, Optional optional, DavSession session) { 49 | super(factory, lockManager, locator, path, optional, session); 50 | properties.add(new ResourceType(ResourceType.COLLECTION)); 51 | properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); 52 | } 53 | 54 | @Override 55 | public boolean isCollection() { 56 | return true; 57 | } 58 | 59 | @Override 60 | public void spool(OutputContext outputContext) throws IOException { 61 | // no-op 62 | } 63 | 64 | @Override 65 | public void addMember(DavResource resource, InputContext inputContext) throws DavException { 66 | if (resource instanceof DavFolder) { 67 | addMemberFolder((DavFolder) resource); 68 | } else if (resource instanceof DavFile) { 69 | assert inputContext.hasStream(); 70 | addMemberFile((DavFile) resource, inputContext.getInputStream()); 71 | } else { 72 | throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName()); 73 | } 74 | } 75 | 76 | private void addMemberFolder(DavFolder memberFolder) throws DavException { 77 | try { 78 | Files.createDirectory(memberFolder.path); 79 | } catch (FileSystemException e) { 80 | String reason = Strings.nullToEmpty(e.getReason()); 81 | if (reason.contains("path too long")) { 82 | throw new DavException(DavServletResponse.SC_REQUEST_URI_TOO_LONG); 83 | } else { 84 | throw new UncheckedIOException(e); 85 | } 86 | } catch (IOException e) { 87 | throw new UncheckedIOException(e); 88 | } 89 | } 90 | 91 | private void addMemberFile(DavFile memberFile, InputStream inputStream) throws DavException { 92 | try (ReadableByteChannel src = Channels.newChannel(inputStream); // 93 | WritableByteChannel dst = Files.newByteChannel(memberFile.path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { 94 | ByteStreams.copy(src, dst); 95 | } catch (FileSystemException e) { 96 | String reason = Strings.nullToEmpty(e.getReason()); 97 | if (reason.contains("path too long")) { 98 | throw new DavException(DavServletResponse.SC_REQUEST_URI_TOO_LONG); 99 | } else { 100 | throw new UncheckedIOException(e); 101 | } 102 | } catch (IOException e) { 103 | throw new UncheckedIOException(e); 104 | } 105 | } 106 | 107 | @Override 108 | public DavResourceIterator getMembers() { 109 | try (DirectoryStream stream = Files.newDirectoryStream(path)) { 110 | List children = new ArrayList<>(); 111 | for (Path childPath : stream) { 112 | try { 113 | BasicFileAttributes childAttr = Files.readAttributes(childPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); 114 | DavLocatorImpl childLocator = locator.resolveChild(childPath.getFileName().toString()); 115 | if (childAttr.isDirectory()) { 116 | DavFolder childFolder = factory.createFolder(childLocator, childPath, Optional.of(childAttr), session); 117 | children.add(childFolder); 118 | } else if (childAttr.isRegularFile()) { 119 | DavFile childFile = factory.createFile(childLocator, childPath, Optional.of(childAttr), session); 120 | children.add(childFile); 121 | } else { 122 | LOG.warn("encountered unsupported node: {}", childPath); 123 | } 124 | } catch (IOException e) { 125 | LOG.error("Exception while reading attributes of {}. Skipping file in listing.", childPath, e); 126 | } 127 | } 128 | return new DavResourceIteratorImpl(children); 129 | } catch (IOException e) { 130 | throw new UncheckedIOException(e); 131 | } 132 | } 133 | 134 | @Override 135 | public void removeMember(DavResource member) throws DavException { 136 | for (ActiveLock lock : member.getLocks()) { 137 | member.unlock(lock.getToken()); 138 | } 139 | if (member instanceof DavNode) { 140 | removeMemberInternal((DavNode) member); 141 | } 142 | } 143 | 144 | public void removeMemberInternal(DavNode member) throws DavException { 145 | try { 146 | // The DELETE method on a collection must act as if a "Depth: infinity" header was used on it 147 | MoreFiles.deleteRecursively(member.path, RecursiveDeleteOption.ALLOW_INSECURE); 148 | } catch (NoSuchFileException e) { 149 | throw new DavException(DavServletResponse.SC_NOT_FOUND); 150 | } catch (IOException e) { 151 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 152 | } 153 | } 154 | 155 | @Override 156 | public void move(DavResource destination) throws DavException { 157 | if (!exists()) { 158 | throw new DavException(DavServletResponse.SC_NOT_FOUND); 159 | } else if (destination instanceof DavNode) { 160 | this.moveInternal((DavNode) destination); 161 | } else { 162 | throw new IllegalArgumentException("Destination not a DavFolder: " + destination.getClass().getName()); 163 | } 164 | } 165 | 166 | private void moveInternal(DavNode destination) throws DavException { 167 | if (Files.isDirectory(destination.path.getParent())) { 168 | try { 169 | Files.move(path, destination.path, StandardCopyOption.REPLACE_EXISTING); 170 | } catch (FileSystemException e) { 171 | String reason = Strings.nullToEmpty(e.getReason()); 172 | if (reason.contains("path too long")) { 173 | // Status code 414 not applictable for things other than request uris. 174 | // If Destination header is too long, return status code 400: 175 | // https://tools.ietf.org/html/rfc4918#section-10.3 176 | throw new DavException(DavServletResponse.SC_BAD_REQUEST); 177 | } else { 178 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 179 | } 180 | } catch (IOException e) { 181 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 182 | } 183 | } else { 184 | throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); 185 | } 186 | } 187 | 188 | @Override 189 | public void copy(DavResource destination, boolean shallow) throws DavException { 190 | if (!exists()) { 191 | throw new DavException(DavServletResponse.SC_NOT_FOUND); 192 | } else if (destination instanceof DavNode) { 193 | copyInternal((DavNode) destination, shallow); 194 | } else { 195 | throw new IllegalArgumentException("Destination not a DavNode: " + destination.getClass().getName()); 196 | } 197 | } 198 | 199 | private void copyInternal(DavNode destination, boolean shallow) throws DavException { 200 | assert exists(); 201 | assert attr.isPresent(); 202 | if (!Files.isDirectory(destination.path.getParent())) { 203 | throw new DavException(DavServletResponse.SC_CONFLICT, "Destination's parent doesn't exist."); 204 | } 205 | 206 | try { 207 | if (shallow && destination instanceof DavFolder) { 208 | // http://www.webdav.org/specs/rfc2518.html#copy.for.collections 209 | Files.createDirectory(destination.path); 210 | BasicFileAttributeView attrView = Files.getFileAttributeView(destination.path, BasicFileAttributeView.class); 211 | if (attrView != null) { 212 | BasicFileAttributes a = attr.get(); 213 | attrView.setTimes(a.lastModifiedTime(), a.lastAccessTime(), a.creationTime()); 214 | } 215 | } else { 216 | Files.walkFileTree(path, new CopyingFileVisitor(path, destination.path, StandardCopyOption.REPLACE_EXISTING)); 217 | } 218 | } catch (FileSystemException e) { 219 | String reason = Strings.nullToEmpty(e.getReason()); 220 | if (reason.contains("path too long")) { 221 | // Status code 414 not applictable for things other than request uris. 222 | // If Destination header is too long, return status code 400: 223 | // https://tools.ietf.org/html/rfc4918#section-10.3 224 | throw new DavException(DavServletResponse.SC_BAD_REQUEST); 225 | } else { 226 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 227 | } 228 | } catch (IOException e) { 229 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 230 | } 231 | } 232 | 233 | @Override 234 | public DavPropertyName[] getPropertyNames() { 235 | List list = Arrays.asList(super.getPropertyNames()); 236 | list.add(PROPERTY_QUOTA_AVAILABLE); 237 | list.add(PROPERTY_QUOTA_USED); 238 | return Iterables.toArray(list, DavPropertyName.class); 239 | } 240 | 241 | @Override 242 | public DavProperty getProperty(DavPropertyName name) { 243 | if (PROPERTY_QUOTA_AVAILABLE.equals(name)) { 244 | if (OSUtil.isMacOS15_4orNewer()) { 245 | // macOS 15.4+ has a bug that causes the file system to mount with a 90s delay 246 | return null; 247 | } 248 | try { 249 | long availableBytes = Files.getFileStore(path).getUsableSpace(); 250 | return new DefaultDavProperty(name, availableBytes); 251 | } catch (IOException e) { 252 | return null; 253 | } 254 | } else if (PROPERTY_QUOTA_USED.equals(name)) { 255 | if (OSUtil.isMacOS15_4orNewer()) { 256 | // macOS 15.4+ has a bug that causes the file system to mount with a 90s delay 257 | return null; 258 | } 259 | try { 260 | long availableBytes = Files.getFileStore(path).getTotalSpace(); 261 | long freeBytes = Files.getFileStore(path).getUsableSpace(); 262 | long usedBytes = availableBytes - freeBytes; 263 | return new DefaultDavProperty(name, usedBytes); 264 | } catch (IOException e) { 265 | return null; 266 | } 267 | } else { 268 | return super.getProperty(name); 269 | } 270 | } 271 | 272 | } 273 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavLocatorFactoryImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.DavLocatorFactory; 12 | import org.apache.jackrabbit.webdav.util.EncodeUtil; 13 | 14 | class DavLocatorFactoryImpl implements DavLocatorFactory { 15 | 16 | @Override 17 | public DavLocatorImpl createResourceLocator(String prefix, String href) { 18 | final String canonicalPrefix = prefix.endsWith("/") ? prefix : prefix + "/"; 19 | final String canonicalHref = href.startsWith("/") ? href.substring(1) : href; 20 | final String hrefWithoutPrefix = canonicalHref.startsWith(canonicalPrefix) ? canonicalHref.substring(canonicalPrefix.length()) : canonicalHref; 21 | final String resourcePath = EncodeUtil.unescape(hrefWithoutPrefix); 22 | return createResourceLocator(canonicalPrefix, null, resourcePath); 23 | } 24 | 25 | @Override 26 | public DavLocatorImpl createResourceLocator(String prefix, String workspacePath, String resourcePath) { 27 | return new DavLocatorImpl(this, prefix, resourcePath); 28 | } 29 | 30 | @Override 31 | public DavLocatorImpl createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { 32 | // ignore isResourcePath. This impl doesn't distinguish resourcePath and repositoryPath. 33 | return createResourceLocator(prefix, workspacePath, path); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavLocatorImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.base.CharMatcher; 12 | import org.apache.jackrabbit.webdav.DavResourceLocator; 13 | import org.apache.jackrabbit.webdav.util.EncodeUtil; 14 | 15 | import java.util.Objects; 16 | 17 | class DavLocatorImpl implements DavResourceLocator { 18 | 19 | private final DavLocatorFactoryImpl factory; 20 | private final String prefix; 21 | private final String resourcePath; 22 | 23 | /** 24 | * Behold, this is a constructor. It constructs constructions. Didn't see that coming, did you? 25 | * 26 | * @param factory Locator factory. 27 | * @param prefix Must end on "/". 28 | * @param resourcePath Must be an relative path, i.e. must not start with "/". 29 | */ 30 | public DavLocatorImpl(DavLocatorFactoryImpl factory, String prefix, String resourcePath) { 31 | if (!prefix.endsWith("/")) { 32 | throw new IllegalArgumentException("prefix must end on '/' but was: " + prefix); 33 | } 34 | if (resourcePath.startsWith("/")) { 35 | throw new IllegalArgumentException("resourcePath must not start with '/' but was: " + resourcePath); 36 | } 37 | this.factory = Objects.requireNonNull(factory); 38 | this.prefix = prefix; 39 | this.resourcePath = CharMatcher.is('/').trimTrailingFrom(resourcePath); 40 | } 41 | 42 | public DavLocatorImpl resolveChild(String childName) { 43 | if (isRootLocation()) { 44 | return factory.createResourceLocator(prefix, null, childName); 45 | } else { 46 | return factory.createResourceLocator(prefix, null, resourcePath + "/" + childName); 47 | } 48 | } 49 | 50 | public DavLocatorImpl resolveParent() { 51 | if (isRootLocation()) { 52 | // root does not have a parent: 53 | return null; 54 | } else if (resourcePath.contains("/")) { 55 | // parent is a directory: 56 | String parentResourcePath = resourcePath.substring(0, CharMatcher.is('/').lastIndexIn(resourcePath)); 57 | return factory.createResourceLocator(prefix, null, parentResourcePath); 58 | } else { 59 | // parent is root: 60 | return factory.createResourceLocator(prefix, null, ""); 61 | } 62 | } 63 | 64 | @Override 65 | public String getPrefix() { 66 | return prefix; 67 | } 68 | 69 | @Override 70 | public String getResourcePath() { 71 | return resourcePath; 72 | } 73 | 74 | @Override 75 | public String getWorkspacePath() { 76 | // TODO overheadhunter: what defines a workspace? same servlet? 77 | return null; 78 | } 79 | 80 | @Override 81 | public String getWorkspaceName() { 82 | // TODO overheadhunter: what defines a workspace? same servlet? 83 | return null; 84 | } 85 | 86 | @Override 87 | public boolean isSameWorkspace(DavResourceLocator locator) { 88 | // TODO overheadhunter: what defines a workspace? same servlet? 89 | return false; 90 | } 91 | 92 | @Override 93 | public boolean isSameWorkspace(String workspaceName) { 94 | // TODO overheadhunter: what defines a workspace? same servlet? 95 | return false; 96 | } 97 | 98 | @Override 99 | public String getHref(boolean isCollection) { 100 | String href = getHref(); 101 | if (isCollection) { 102 | return href.endsWith("/") ? href : href + "/"; 103 | } else { 104 | return CharMatcher.is('/').trimTrailingFrom(href); 105 | } 106 | } 107 | 108 | private String getHref() { 109 | return prefix + EncodeUtil.escapePath(resourcePath); 110 | } 111 | 112 | @Override 113 | public boolean isRootLocation() { 114 | return resourcePath.isEmpty(); 115 | } 116 | 117 | @Override 118 | public DavLocatorFactoryImpl getFactory() { 119 | return factory; 120 | } 121 | 122 | @Override 123 | public String getRepositoryPath() { 124 | return getResourcePath(); 125 | } 126 | 127 | @Override 128 | public int hashCode() { 129 | return Objects.hash(factory, prefix, resourcePath); 130 | } 131 | 132 | @Override 133 | public boolean equals(Object obj) { 134 | if (obj instanceof DavLocatorImpl) { 135 | DavLocatorImpl other = (DavLocatorImpl) obj; 136 | assert this.factory != null // 137 | && this.prefix != null // 138 | && this.resourcePath != null; 139 | return this.factory.equals(other.factory) // 140 | && this.prefix.equals(other.prefix) // 141 | && this.resourcePath.equals(other.resourcePath); 142 | } else { 143 | return false; 144 | } 145 | } 146 | 147 | @Override 148 | public String toString() { 149 | return this.factory + ": " + this.prefix + this.resourcePath; 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavNode.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.base.Splitter; 12 | import com.google.common.collect.Iterables; 13 | import org.apache.jackrabbit.webdav.*; 14 | import org.apache.jackrabbit.webdav.lock.*; 15 | import org.apache.jackrabbit.webdav.property.*; 16 | 17 | import java.io.IOException; 18 | import java.io.UncheckedIOException; 19 | import java.nio.file.Files; 20 | import java.nio.file.NoSuchFileException; 21 | import java.nio.file.Path; 22 | import java.nio.file.attribute.BasicFileAttributeView; 23 | import java.nio.file.attribute.BasicFileAttributes; 24 | import java.nio.file.attribute.FileTime; 25 | import java.time.Instant; 26 | import java.time.OffsetDateTime; 27 | import java.time.ZoneOffset; 28 | import java.time.format.DateTimeFormatter; 29 | import java.util.Arrays; 30 | import java.util.List; 31 | import java.util.Objects; 32 | import java.util.Optional; 33 | import java.util.stream.Stream; 34 | 35 | abstract class DavNode implements DavResource { 36 | 37 | private static final String DAV_COMPLIANCE_CLASSES = "1, 2"; 38 | private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavConstants.PROPERTY_CREATIONDATE, "Win32CreationTime"}; 39 | private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavConstants.PROPERTY_GETLASTMODIFIED, "Win32LastModifiedTime"}; 40 | 41 | protected final DavResourceFactoryImpl factory; 42 | protected final LockManager lockManager; 43 | protected final DavLocatorImpl locator; 44 | protected final Path path; 45 | protected final Optional attr; 46 | protected final DavSession session; 47 | protected final DavPropertySet properties; 48 | 49 | public DavNode(DavResourceFactoryImpl factory, LockManager lockManager, DavLocatorImpl locator, Path path, Optional attr, DavSession session) { 50 | this.factory = factory; 51 | this.lockManager = lockManager; 52 | this.locator = locator; 53 | this.path = path; 54 | this.attr = attr; 55 | this.session = session; 56 | this.properties = new DavPropertySet(); 57 | } 58 | 59 | @Override 60 | public String getComplianceClass() { 61 | return DAV_COMPLIANCE_CLASSES; 62 | } 63 | 64 | @Override 65 | public String getSupportedMethods() { 66 | return METHODS; 67 | } 68 | 69 | @Override 70 | public boolean exists() { 71 | return attr.isPresent(); 72 | } 73 | 74 | @Override 75 | public String getDisplayName() { 76 | return Iterables.getLast(Splitter.on('/').omitEmptyStrings().split(getResourcePath())); 77 | } 78 | 79 | @Override 80 | public DavLocatorImpl getLocator() { 81 | return locator; 82 | } 83 | 84 | @Override 85 | public String getResourcePath() { 86 | return locator.getResourcePath(); 87 | } 88 | 89 | @Override 90 | public String getHref() { 91 | return locator.getHref(isCollection()); 92 | } 93 | 94 | @Override 95 | public long getModificationTime() { 96 | return attr.map(BasicFileAttributes::lastModifiedTime).map(FileTime::toInstant).map(Instant::toEpochMilli).orElse(-1l); 97 | } 98 | 99 | protected void setModificationTime(Instant instant) throws DavException { 100 | BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class); 101 | if (attrView != null) { 102 | try { 103 | attrView.setTimes(FileTime.from(instant), null, null); 104 | } catch (IOException e) { 105 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 106 | } 107 | } 108 | } 109 | 110 | protected void setCreationTime(Instant instant) throws DavException { 111 | BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class); 112 | if (attrView != null) { 113 | try { 114 | attrView.setTimes(null, null, FileTime.from(instant)); 115 | } catch (IOException e) { 116 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 117 | } 118 | } 119 | } 120 | 121 | @Override 122 | public DavPropertyName[] getPropertyNames() { 123 | return getProperties().getPropertyNames(); 124 | } 125 | 126 | @Override 127 | public DavProperty getProperty(DavPropertyName name) { 128 | final String namespacelessPropertyName = name.getName(); 129 | if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName)) { 130 | return creationDateProperty(name).orElse(null); 131 | } else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName)) { 132 | return lastModifiedDateProperty(name).orElse(null); 133 | } else { 134 | return properties.get(name); 135 | } 136 | } 137 | 138 | /** 139 | * Returns a current snapshot of all available properties. 140 | */ 141 | @Override 142 | public DavPropertySet getProperties() { 143 | creationDateProperty(DavPropertyName.CREATIONDATE).ifPresent(properties::add); 144 | lastModifiedDateProperty(DavPropertyName.GETLASTMODIFIED).ifPresent(properties::add); 145 | return properties; 146 | } 147 | 148 | private Optional> lastModifiedDateProperty(DavPropertyName name) { 149 | return attr.map(BasicFileAttributes::lastModifiedTime) // 150 | .map(FileTime::toInstant) // 151 | .map(creationTime -> OffsetDateTime.ofInstant(creationTime, ZoneOffset.UTC)) // 152 | .map(creationDate -> new DefaultDavProperty<>(name, DateTimeFormatter.RFC_1123_DATE_TIME.format(creationDate))); 153 | } 154 | 155 | private Optional> creationDateProperty(DavPropertyName name) { 156 | return attr.map(BasicFileAttributes::creationTime) // 157 | .map(FileTime::toInstant) // 158 | .map(creationTime -> OffsetDateTime.ofInstant(creationTime, ZoneOffset.UTC)) // 159 | .map(creationDate -> new DefaultDavProperty<>(name, DateTimeFormatter.RFC_1123_DATE_TIME.format(creationDate))); 160 | } 161 | 162 | @Override 163 | public void setProperty(DavProperty property) throws DavException { 164 | final String namespacelessPropertyName = property.getName().getName(); 165 | if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) { 166 | String createDateStr = (String) property.getValue(); 167 | OffsetDateTime creationDate = OffsetDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(createDateStr)); 168 | this.setCreationTime(creationDate.toInstant()); 169 | } else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) { 170 | String lastModifiedDateStr = (String) property.getValue(); 171 | OffsetDateTime lastModifiedDate = OffsetDateTime.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(lastModifiedDateStr)); 172 | this.setModificationTime(lastModifiedDate.toInstant()); 173 | } 174 | properties.add(property); 175 | } 176 | 177 | @Override 178 | public void removeProperty(DavPropertyName propertyName) { 179 | getProperties().remove(propertyName); 180 | } 181 | 182 | @Override 183 | public MultiStatusResponse alterProperties(List changeList) throws DavException { 184 | final DavPropertyNameSet names = new DavPropertyNameSet(); 185 | for (final PropEntry entry : changeList) { 186 | if (entry instanceof DavProperty) { 187 | final DavProperty prop = (DavProperty) entry; 188 | this.setProperty(prop); 189 | names.add(prop.getName()); 190 | } else if (entry instanceof DavPropertyName) { 191 | final DavPropertyName name = (DavPropertyName) entry; 192 | this.removeProperty(name); 193 | names.add(name); 194 | } 195 | } 196 | return new MultiStatusResponse(this, names); 197 | } 198 | 199 | @Override 200 | public DavFolder getCollection() { 201 | DavLocatorImpl parentLocator = locator.resolveParent(); 202 | if (parentLocator == null) { 203 | return null; 204 | } else { 205 | Path parentPath = path.getParent(); 206 | BasicFileAttributes parentAttr; 207 | try { 208 | parentAttr = Files.readAttributes(parentPath, BasicFileAttributes.class); 209 | } catch (NoSuchFileException e) { 210 | parentAttr = null; 211 | } catch (IOException e) { 212 | throw new UncheckedIOException(e); 213 | } 214 | return factory.createFolder(parentLocator, parentPath, Optional.ofNullable(parentAttr), session); 215 | } 216 | } 217 | 218 | @Override 219 | public boolean isLockable(Type type, Scope scope) { 220 | return Type.WRITE.equals(type) && Scope.EXCLUSIVE.equals(scope) || Scope.SHARED.equals(scope); 221 | } 222 | 223 | @Override 224 | public boolean hasLock(Type type, Scope scope) { 225 | return getLock(type, scope) != null; 226 | } 227 | 228 | @Override 229 | public ActiveLock getLock(Type type, Scope scope) { 230 | return lockManager.getLock(type, scope, this); 231 | } 232 | 233 | @Override 234 | public ActiveLock[] getLocks() { 235 | final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE); 236 | final ActiveLock sharedWriteLock = getLock(Type.WRITE, Scope.SHARED); 237 | return Stream.of(exclusiveWriteLock, sharedWriteLock).filter(Objects::nonNull).toArray(ActiveLock[]::new); 238 | } 239 | 240 | @Override 241 | public ActiveLock lock(LockInfo reqLockInfo) throws DavException { 242 | return lockManager.createLock(reqLockInfo, this); 243 | } 244 | 245 | @Override 246 | public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken) throws DavException { 247 | return lockManager.refreshLock(reqLockInfo, lockToken, this); 248 | } 249 | 250 | @Override 251 | public void unlock(String lockToken) throws DavException { 252 | lockManager.releaseLock(lockToken, this); 253 | } 254 | 255 | @Override 256 | public void addLockManager(LockManager lockmgr) { 257 | throw new UnsupportedOperationException("Locks are managed"); 258 | } 259 | 260 | @Override 261 | public DavResourceFactoryImpl getFactory() { 262 | return factory; 263 | } 264 | 265 | @Override 266 | public DavSession getSession() { 267 | return session; 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavResourceFactoryImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016, 2017 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import com.google.common.collect.ImmutableSet; 12 | import org.apache.jackrabbit.webdav.*; 13 | import org.apache.jackrabbit.webdav.lock.LockManager; 14 | 15 | import java.io.IOException; 16 | import java.nio.file.Files; 17 | import java.nio.file.LinkOption; 18 | import java.nio.file.NoSuchFileException; 19 | import java.nio.file.Path; 20 | import java.nio.file.attribute.BasicFileAttributes; 21 | import java.time.Instant; 22 | import java.time.format.DateTimeFormatter; 23 | import java.time.format.DateTimeParseException; 24 | import java.util.Optional; 25 | import java.util.function.Function; 26 | 27 | class DavResourceFactoryImpl implements DavResourceFactory { 28 | 29 | private static final String RANGE_HEADER = "Range"; 30 | private static final String IF_RANGE_HEADER = "If-Range"; 31 | 32 | private final LockManager lockManager = new ExclusiveSharedLockManager(); 33 | private final Function urlResolver; 34 | 35 | public DavResourceFactoryImpl(Function urlResolver) { 36 | this.urlResolver = urlResolver; 37 | } 38 | 39 | private Path resolveUrl(String relativeUrl) throws DavException { 40 | try { 41 | return urlResolver.apply(relativeUrl); 42 | } catch (IllegalArgumentException e) { 43 | throw new DavException(DavServletResponse.SC_NOT_FOUND, e.getMessage(), e, null); 44 | } 45 | } 46 | 47 | @Override 48 | public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { 49 | if (locator instanceof DavLocatorImpl && locator.equals(request.getRequestLocator())) { 50 | return createRequestResource((DavLocatorImpl) locator, request, response); 51 | } else if (locator instanceof DavLocatorImpl && locator.equals(request.getDestinationLocator())) { 52 | return createDestinationResource((DavLocatorImpl) locator, request, response); 53 | } else { 54 | throw new IllegalArgumentException("Unsupported locator of type " + locator.getClass()); 55 | } 56 | } 57 | 58 | private DavResource createRequestResource(DavLocatorImpl locator, DavServletRequest request, DavServletResponse response) throws DavException { 59 | assert locator.equals(request.getRequestLocator()); 60 | Path p = resolveUrl(locator.getResourcePath()); 61 | Optional attr = readBasicFileAttributes(p); 62 | if (DavMethods.METHOD_PUT.equals(request.getMethod())) { 63 | checkPreconditionsForPut(p, attr); 64 | return createFile(locator, p, Optional.empty(), request.getDavSession()); 65 | } else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) { 66 | checkPreconditionsForMkcol(p, attr); 67 | return createFolder(locator, p, Optional.empty(), request.getDavSession()); 68 | } else if (!attr.isPresent() && DavMethods.METHOD_LOCK.equals(request.getMethod())) { 69 | // locking non-existing resources must create a non-collection resource: 70 | // https://tools.ietf.org/html/rfc4918#section-9.10.4 71 | // See also: DavFile#lock(...) 72 | return createFile(locator, p, Optional.empty(), request.getDavSession()); 73 | } else if (!attr.isPresent()) { 74 | throw new DavException(DavServletResponse.SC_NOT_FOUND); 75 | } else if (attr.get().isDirectory()) { 76 | return createFolder(locator, p, attr, request.getDavSession()); 77 | } else if (attr.get().isRegularFile() && DavMethods.METHOD_GET.equals(request.getMethod()) && request.getHeader(RANGE_HEADER) != null) { 78 | return createFileRange(locator, p, attr.get(), request.getDavSession(), request, response); 79 | } else if (attr.get().isRegularFile()) { 80 | return createFile(locator, p, attr, request.getDavSession()); 81 | } else { 82 | throw new DavException(DavServletResponse.SC_NOT_FOUND, "Node not a file or directory: " + p); 83 | } 84 | } 85 | 86 | private void checkPreconditionsForPut(Path p, Optional attr) throws DavException { 87 | if (attr.isPresent() && !attr.get().isRegularFile()) { 88 | throw new DavException(DavServletResponse.SC_CONFLICT, p + " already exists."); 89 | } 90 | } 91 | 92 | private void checkPreconditionsForMkcol(Path p, Optional attr) throws DavException { 93 | if (attr.isPresent()) { 94 | // status code 405 required by https://tools.ietf.org/html/rfc2518#section-8.3.2 95 | throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED, p + " already exists."); 96 | } 97 | } 98 | 99 | private DavResource createDestinationResource(DavLocatorImpl locator, DavServletRequest request, DavServletResponse response) throws DavException { 100 | assert locator.equals(request.getDestinationLocator()); 101 | assert ImmutableSet.of(DavMethods.METHOD_MOVE, DavMethods.METHOD_COPY).contains(request.getMethod()); 102 | Path srcP = resolveUrl(request.getRequestLocator().getResourcePath()); 103 | Path dstP = resolveUrl(locator.getResourcePath()); 104 | Optional srcAttr = readBasicFileAttributes(srcP); 105 | Optional dstAttr = readBasicFileAttributes(dstP); 106 | if (!srcAttr.isPresent()) { 107 | throw new DavException(DavServletResponse.SC_NOT_FOUND); 108 | } else if (srcAttr.get().isDirectory()) { 109 | return createFolder(locator, dstP, dstAttr, request.getDavSession()); 110 | } else { 111 | return createFile(locator, dstP, dstAttr, request.getDavSession()); 112 | } 113 | } 114 | 115 | @Override 116 | public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { 117 | if (locator instanceof DavLocatorImpl) { 118 | return createResourceInternal((DavLocatorImpl) locator, session); 119 | } else { 120 | throw new IllegalArgumentException("Unsupported locator of type " + locator.getClass()); 121 | } 122 | } 123 | 124 | private DavResource createResourceInternal(DavLocatorImpl locator, DavSession session) throws DavException { 125 | Path p = resolveUrl(locator.getResourcePath()); 126 | Optional attr = readBasicFileAttributes(p); 127 | if (!attr.isPresent()) { 128 | throw new DavException(DavServletResponse.SC_NOT_FOUND); 129 | } else if (attr.get().isDirectory()) { 130 | return createFolder(locator, p, attr, session); 131 | } else { 132 | return createFile(locator, p, attr, session); 133 | } 134 | } 135 | 136 | /** 137 | * @return BasicFileAttributes or {@link Optional#empty()} if the file/folder for the given path does not exist. 138 | * @throws DavException If an {@link IOException} occured during {@link Files#readAttributes(Path, Class, java.nio.file.LinkOption...)}. 139 | */ 140 | private Optional readBasicFileAttributes(Path path) throws DavException { 141 | try { 142 | return Optional.of(Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)); 143 | } catch (NoSuchFileException e) { 144 | return Optional.empty(); 145 | } catch (IOException e) { 146 | throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); 147 | } 148 | } 149 | 150 | DavFolder createFolder(DavLocatorImpl locator, Path path, Optional attr, DavSession session) { 151 | return new DavFolder(this, lockManager, locator, path, attr, session); 152 | } 153 | 154 | DavFile createFile(DavLocatorImpl locator, Path path, Optional attr, DavSession session) { 155 | return new DavFile(this, lockManager, locator, path, attr, session); 156 | } 157 | 158 | private DavFile createFileRange(DavLocatorImpl locator, Path path, BasicFileAttributes attr, DavSession session, DavServletRequest request, DavServletResponse response) throws DavException { 159 | // 200 for "normal" resources, if if-range is not satisified: 160 | final String ifRangeHeader = request.getHeader(IF_RANGE_HEADER); 161 | if (!isIfRangeHeaderSatisfied(attr, ifRangeHeader)) { 162 | return createFile(locator, path, Optional.of(attr), session); 163 | } 164 | 165 | final String rangeHeader = request.getHeader(RANGE_HEADER); 166 | try { 167 | // 206 for ranged resources: 168 | final ByteRange byteRange = ByteRange.parse(rangeHeader); 169 | response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT); 170 | return new DavFileWithRange(this, lockManager, locator, path, attr, session, byteRange); 171 | } catch (ByteRange.UnsupportedRangeException ex) { 172 | return createFile(locator, path, Optional.of(attr), session); 173 | } catch (ByteRange.MalformedByteRangeException e) { 174 | throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Malformed range header: " + rangeHeader); 175 | } 176 | } 177 | 178 | /** 179 | * @return true if a partial response should be generated according to an If-Range precondition. 180 | */ 181 | private boolean isIfRangeHeaderSatisfied(BasicFileAttributes attr, String ifRangeHeader) throws DavException { 182 | if (ifRangeHeader == null) { 183 | // no header set -> satisfied implicitly 184 | return true; 185 | } else { 186 | try { 187 | Instant expectedTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(ifRangeHeader)); 188 | Instant actualTime = attr.lastModifiedTime().toInstant(); 189 | return expectedTime.compareTo(actualTime) == 0; 190 | } catch (DateTimeParseException e) { 191 | throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader); 192 | } 193 | } 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavSessionImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.DavSession; 12 | 13 | import java.util.HashSet; 14 | 15 | class DavSessionImpl implements DavSession { 16 | 17 | private final HashSet lockTokens = new HashSet(); 18 | private final HashSet references = new HashSet(); 19 | 20 | @Override 21 | public void addReference(Object reference) { 22 | references.add(reference); 23 | } 24 | 25 | @Override 26 | public void removeReference(Object reference) { 27 | references.remove(reference); 28 | } 29 | 30 | @Override 31 | public void addLockToken(String token) { 32 | lockTokens.add(token); 33 | } 34 | 35 | @Override 36 | public String[] getLockTokens() { 37 | return lockTokens.toArray(new String[lockTokens.size()]); 38 | } 39 | 40 | @Override 41 | public void removeLockToken(String token) { 42 | lockTokens.remove(token); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/DavSessionProviderImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.DavException; 12 | import org.apache.jackrabbit.webdav.DavSession; 13 | import org.apache.jackrabbit.webdav.DavSessionProvider; 14 | import org.apache.jackrabbit.webdav.WebdavRequest; 15 | 16 | class DavSessionProviderImpl implements DavSessionProvider { 17 | 18 | 19 | @Override 20 | public boolean attachSession(WebdavRequest request) throws DavException { 21 | // every request gets a new session 22 | final DavSession session = new DavSessionImpl(); 23 | session.addReference(request); 24 | request.setDavSession(session); 25 | return true; 26 | } 27 | 28 | @Override 29 | public void releaseSession(WebdavRequest request) { 30 | final DavSession session = request.getDavSession(); 31 | if (session != null) { 32 | session.removeReference(request); 33 | request.setDavSession(null); 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/ExclusiveSharedLock.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.DavConstants; 12 | import org.apache.jackrabbit.webdav.lock.AbstractActiveLock; 13 | import org.apache.jackrabbit.webdav.lock.LockInfo; 14 | import org.apache.jackrabbit.webdav.lock.Scope; 15 | import org.apache.jackrabbit.webdav.lock.Type; 16 | 17 | class ExclusiveSharedLock extends AbstractActiveLock { 18 | 19 | private final String token; 20 | private final Type type; 21 | private final Scope scope; 22 | private String owner; 23 | private boolean isDeep = true; // deep by default 24 | private long expirationTime = DavConstants.INFINITE_TIMEOUT; // never expires by default; 25 | 26 | ExclusiveSharedLock(String token, LockInfo lockInfo) { 27 | this.token = token; 28 | this.type = lockInfo.getType(); 29 | this.scope = lockInfo.getScope(); 30 | this.owner = lockInfo.getOwner(); 31 | this.isDeep = lockInfo.isDeep(); 32 | setTimeout(lockInfo.getTimeout()); 33 | } 34 | 35 | @Override 36 | public boolean isLockedByToken(String lockToken) { 37 | return token.equals(lockToken); 38 | } 39 | 40 | @Override 41 | public boolean isExpired() { 42 | return System.currentTimeMillis() > expirationTime; 43 | } 44 | 45 | @Override 46 | public String getToken() { 47 | return token; 48 | } 49 | 50 | @Override 51 | public String getOwner() { 52 | return owner; 53 | } 54 | 55 | @Override 56 | public void setOwner(String owner) { 57 | this.owner = owner; 58 | } 59 | 60 | @Override 61 | public long getTimeout() { 62 | return expirationTime - System.currentTimeMillis(); 63 | } 64 | 65 | @Override 66 | public void setTimeout(long timeout) { 67 | if (timeout > 0) { 68 | expirationTime = System.currentTimeMillis() + timeout; 69 | } 70 | } 71 | 72 | @Override 73 | public boolean isDeep() { 74 | return isDeep; 75 | } 76 | 77 | @Override 78 | public void setIsDeep(boolean isDeep) { 79 | this.isDeep = isDeep; 80 | } 81 | 82 | @Override 83 | public Type getType() { 84 | return type; 85 | } 86 | 87 | @Override 88 | public Scope getScope() { 89 | return scope; 90 | } 91 | 92 | /* HASHCODE / EQUALS */ 93 | 94 | @Override 95 | public int hashCode() { 96 | return getToken().hashCode(); 97 | } 98 | 99 | @Override 100 | public boolean equals(Object obj) { 101 | if (obj instanceof ExclusiveSharedLock) { 102 | ExclusiveSharedLock other = (ExclusiveSharedLock) obj; 103 | return this.getToken().equals(other.getToken()); 104 | } else { 105 | return false; 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/ExclusiveSharedLockManager.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.*; 12 | import org.apache.jackrabbit.webdav.lock.*; 13 | 14 | import java.util.*; 15 | import java.util.Map.Entry; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.concurrent.ConcurrentMap; 18 | 19 | class ExclusiveSharedLockManager implements LockManager { 20 | 21 | private final ConcurrentMap> lockedResources = new ConcurrentHashMap<>(); 22 | 23 | @Override 24 | public ActiveLock createLock(LockInfo lockInfo, DavResource resource) throws DavException { 25 | Objects.requireNonNull(lockInfo); 26 | Objects.requireNonNull(resource); 27 | if (resource instanceof DavNode) { 28 | return createLockInternal(lockInfo, (DavNode) resource); 29 | } else { 30 | throw new IllegalArgumentException("Unsupported resource type " + resource.getClass()); 31 | } 32 | } 33 | 34 | private synchronized ActiveLock createLockInternal(LockInfo lockInfo, DavNode resource) throws DavException { 35 | DavLocatorImpl locator = resource.getLocator(); 36 | removedExpiredLocksInLocatorHierarchy(locator); 37 | 38 | // look for existing locks on this resource or its ancestors: 39 | ActiveLock existingExclusiveLock = getLock(lockInfo.getType(), Scope.EXCLUSIVE, resource); 40 | ActiveLock existingSharedLock = getLock(lockInfo.getType(), Scope.SHARED, resource); 41 | boolean hasExclusiveLock = existingExclusiveLock != null; 42 | boolean hasSharedLock = existingSharedLock != null; 43 | boolean isLocked = hasExclusiveLock || hasSharedLock; 44 | if ((Scope.EXCLUSIVE.equals(lockInfo.getScope()) && isLocked) || (Scope.SHARED.equals(lockInfo.getScope()) && hasExclusiveLock)) { 45 | throw new DavException(DavServletResponse.SC_LOCKED, "Resource (or parent resource) already locked."); 46 | } 47 | 48 | // look for locked children: 49 | for (Entry> potentialChild : lockedResources.entrySet()) { 50 | final DavLocatorImpl childLocator = potentialChild.getKey(); 51 | final Collection childLocks = potentialChild.getValue().values(); 52 | if (isChild(locator, childLocator) && isAffectedByChildLocks(lockInfo, locator, childLocks, childLocator)) { 53 | throw new DavException(DavServletResponse.SC_CONFLICT, "Subresource already locked. " + childLocator); 54 | } 55 | } 56 | 57 | String token = DavConstants.OPAQUE_LOCK_TOKEN_PREFIX + UUID.randomUUID(); 58 | Map lockMap = Objects.requireNonNull(lockedResources.computeIfAbsent(locator, loc -> new HashMap<>())); 59 | return lockMap.computeIfAbsent(token, t -> new ExclusiveSharedLock(t, lockInfo)); 60 | } 61 | 62 | private void removedExpiredLocksInLocatorHierarchy(DavLocatorImpl locator) { 63 | Objects.requireNonNull(locator); 64 | lockedResources.getOrDefault(locator, Collections.emptyMap()).values().removeIf(ActiveLock::isExpired); 65 | if (!locator.isRootLocation()) { 66 | this.removedExpiredLocksInLocatorHierarchy(locator.resolveParent()); 67 | } 68 | } 69 | 70 | private boolean isChild(DavResourceLocator parent, DavResourceLocator child) { 71 | return child.getResourcePath().startsWith(parent.getResourcePath()); 72 | } 73 | 74 | private boolean isAffectedByChildLocks(LockInfo parentLockInfo, DavLocatorImpl parentLocator, Collection childLocks, DavLocatorImpl childLocator) { 75 | for (ActiveLock lock : childLocks) { 76 | if (Scope.SHARED.equals(lock.getScope()) && Scope.SHARED.equals(parentLockInfo.getScope())) { 77 | continue; 78 | } else if (parentLockInfo.isDeep() || childLocator.resolveParent().equals(parentLocator)) { 79 | return true; 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | @Override 86 | public ActiveLock refreshLock(LockInfo lockInfo, String lockToken, DavResource resource) throws DavException { 87 | ActiveLock lock = getLock(lockInfo.getType(), lockInfo.getScope(), resource); 88 | if (lock == null) { 89 | throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED); 90 | } else if (!lock.isLockedByToken(lockToken)) { 91 | throw new DavException(DavServletResponse.SC_LOCKED); 92 | } 93 | lock.setTimeout(lockInfo.getTimeout()); 94 | return lock; 95 | } 96 | 97 | @Override 98 | public synchronized void releaseLock(String lockToken, DavResource resource) throws DavException { 99 | if (resource instanceof DavNode) { 100 | try { 101 | releaseLockInternal(lockToken, (DavNode) resource); 102 | } catch (UncheckedDavException e) { 103 | throw e.toDavException(); 104 | } 105 | } else { 106 | throw new IllegalArgumentException("Unsupported resource type " + resource.getClass()); 107 | } 108 | } 109 | 110 | private synchronized void releaseLockInternal(String lockToken, DavNode resource) throws UncheckedDavException { 111 | lockedResources.compute(resource.getLocator(), (loc, locks) -> { 112 | if (locks == null || locks.isEmpty()) { 113 | // no lock exists, nothing needs to change. 114 | return null; 115 | } else if (!locks.containsKey(lockToken)) { 116 | throw new UncheckedDavException(DavServletResponse.SC_LOCKED, "Resource locked with different token."); 117 | } else { 118 | locks.remove(lockToken); 119 | return locks.isEmpty() ? null : locks; 120 | } 121 | }); 122 | } 123 | 124 | @Override 125 | public ActiveLock getLock(Type type, Scope scope, DavResource resource) { 126 | if (resource instanceof DavNode) { 127 | DavNode node = (DavNode) resource; 128 | return getLockInternal(type, scope, node.getLocator(), 0); 129 | } else { 130 | throw new IllegalArgumentException("Unsupported resource type " + resource.getClass()); 131 | } 132 | } 133 | 134 | private ActiveLock getLockInternal(Type type, Scope scope, DavLocatorImpl locator, int depth) { 135 | // try to find a lock directly on this resource: 136 | if (lockedResources.containsKey(locator)) { 137 | for (ActiveLock lock : lockedResources.get(locator).values()) { 138 | if (type.equals(lock.getType()) && scope.equals(lock.getScope()) && (depth == 0 || lock.isDeep())) { 139 | return lock; 140 | } 141 | } 142 | } 143 | // or otherwise look for parent locks (if there is a parent): 144 | if (locator.isRootLocation()) { 145 | return null; 146 | } else { 147 | return getLockInternal(type, scope, locator.resolveParent(), depth + 1); 148 | } 149 | } 150 | 151 | @Override 152 | public boolean hasLock(String lockToken, DavResource resource) { 153 | if (resource instanceof DavNode) { 154 | DavNode node = (DavNode) resource; 155 | return lockedResources.getOrDefault(node.getLocator(), Collections.emptyMap()).containsKey(lockToken); 156 | } else { 157 | throw new IllegalArgumentException("Unsupported resource type " + resource.getClass()); 158 | } 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/NioWebDavServlet.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.webdav.core.servlet; 2 | 3 | import javax.servlet.ServletException; 4 | import java.nio.file.Path; 5 | import java.nio.file.Paths; 6 | 7 | public class NioWebDavServlet extends AbstractNioWebDavServlet { 8 | 9 | public static final String INIT_PARAM_ROOT_PATH = "rootPath"; 10 | private Path rootPath; 11 | 12 | @Override 13 | public void init() throws ServletException { 14 | super.init(); 15 | rootPath = Paths.get(getInitParameter(INIT_PARAM_ROOT_PATH)); 16 | } 17 | 18 | @Override 19 | protected Path resolveUrl(String relativeUrl) throws IllegalArgumentException { 20 | return rootPath.resolve(relativeUrl); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/NullInputContext.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.io.InputContext; 12 | 13 | import java.io.ByteArrayInputStream; 14 | import java.io.InputStream; 15 | 16 | class NullInputContext implements InputContext { 17 | 18 | @Override 19 | public boolean hasStream() { 20 | return true; 21 | } 22 | 23 | @Override 24 | public InputStream getInputStream() { 25 | return new ByteArrayInputStream(new byte[0]); 26 | } 27 | 28 | @Override 29 | public long getModificationTime() { 30 | return 0; 31 | } 32 | 33 | @Override 34 | public String getContentLanguage() { 35 | return null; 36 | } 37 | 38 | @Override 39 | public long getContentLength() { 40 | return 0; 41 | } 42 | 43 | @Override 44 | public String getContentType() { 45 | return null; 46 | } 47 | 48 | @Override 49 | public String getProperty(String propertyName) { 50 | return null; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/OSUtil.java: -------------------------------------------------------------------------------- 1 | package org.cryptomator.webdav.core.servlet; 2 | 3 | final class OSUtil { 4 | 5 | static boolean isMacOS15_4orNewer() { 6 | var osName = System.getProperty("os.name").toLowerCase(); 7 | if (osName.contains("mac")) { 8 | var osVersion = System.getProperty("os.version").split("\\."); 9 | if (osVersion.length >= 2) { 10 | try { 11 | var majorVersion = Integer.parseInt(osVersion[0]); 12 | var minorVersion = Integer.parseInt(osVersion[1]); 13 | return majorVersion == 15 && minorVersion >= 4 || majorVersion > 16; 14 | } catch (NumberFormatException e) { 15 | //no-op 16 | } 17 | } 18 | } 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/cryptomator/webdav/core/servlet/UncheckedDavException.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.DavException; 12 | 13 | public class UncheckedDavException extends RuntimeException { 14 | 15 | private final int errorCode; 16 | 17 | public UncheckedDavException(int errorCode, String message) { 18 | this(errorCode, message, null); 19 | } 20 | 21 | public UncheckedDavException(int errorCode, String message, Throwable cause) { 22 | super(message, cause); 23 | this.errorCode = errorCode; 24 | } 25 | 26 | public DavException toDavException() { 27 | return new DavException(errorCode, getMessage(), getCause(), null); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/webdav/core/filters/MacChunkedPutCompatibilityFilterTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.mockito.ArgumentCaptor; 15 | import org.mockito.Mockito; 16 | 17 | import javax.servlet.FilterChain; 18 | import javax.servlet.ServletException; 19 | import javax.servlet.ServletInputStream; 20 | import javax.servlet.ServletResponse; 21 | import javax.servlet.http.HttpServletRequest; 22 | import javax.servlet.http.HttpServletResponse; 23 | import java.io.IOException; 24 | 25 | public class MacChunkedPutCompatibilityFilterTest { 26 | 27 | private MacChunkedPutCompatibilityFilter filter; 28 | private FilterChain chain; 29 | private HttpServletRequest request; 30 | private HttpServletResponse response; 31 | 32 | @BeforeEach 33 | public void setup() { 34 | filter = new MacChunkedPutCompatibilityFilter(); 35 | chain = Mockito.mock(FilterChain.class); 36 | request = Mockito.mock(HttpServletRequest.class); 37 | response = Mockito.mock(HttpServletResponse.class); 38 | } 39 | 40 | @Test 41 | public void testUnfilteredGetRequest() throws IOException, ServletException { 42 | Mockito.when(request.getMethod()).thenReturn("GET"); 43 | filter.doFilter(request, response, chain); 44 | 45 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 46 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 47 | Assertions.assertSame(request, wrappedReq.getValue()); 48 | } 49 | 50 | @Test 51 | public void testUnfilteredPutRequest1() throws IOException, ServletException { 52 | Mockito.when(request.getMethod()).thenReturn("PUT"); 53 | Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn(null); 54 | filter.doFilter(request, response, chain); 55 | 56 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 57 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 58 | Assertions.assertSame(request, wrappedReq.getValue()); 59 | } 60 | 61 | @Test 62 | public void testUnfilteredPutRequest2() throws IOException, ServletException { 63 | Mockito.when(request.getMethod()).thenReturn("PUT"); 64 | Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn("chunked"); 65 | Mockito.when(request.getHeader("X-Expected-Entity-Length")).thenReturn(null); 66 | filter.doFilter(request, response, chain); 67 | 68 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 69 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 70 | Assertions.assertSame(request, wrappedReq.getValue()); 71 | } 72 | 73 | @Test 74 | public void testMalformedXExpectedEntityLengthHeader() throws IOException, ServletException { 75 | Mockito.when(request.getMethod()).thenReturn("PUT"); 76 | Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn("chunked"); 77 | Mockito.when(request.getHeader("X-Expected-Entity-Length")).thenReturn("NaN"); 78 | filter.doFilter(request, response, chain); 79 | 80 | Mockito.verify(response).sendError(Mockito.eq(HttpServletResponse.SC_BAD_REQUEST), Mockito.anyString()); 81 | Mockito.verifyNoMoreInteractions(chain); 82 | } 83 | 84 | /* actual input stream testing */ 85 | 86 | @Test 87 | public void testBoundedInputStream() throws IOException, ServletException { 88 | ServletInputStream in = Mockito.mock(ServletInputStream.class); 89 | 90 | Mockito.when(request.getMethod()).thenReturn("PUT"); 91 | Mockito.when(request.getHeader("Transfer-Encoding")).thenReturn("chunked"); 92 | Mockito.when(request.getHeader("X-Expected-Entity-Length")).thenReturn("5"); 93 | Mockito.when(request.getInputStream()).thenReturn(in); 94 | filter.doFilter(request, response, chain); 95 | 96 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 97 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 98 | ServletInputStream wrappedIn = wrappedReq.getValue().getInputStream(); 99 | 100 | Mockito.when(in.isFinished()).thenReturn(false); 101 | Assertions.assertFalse(wrappedIn.isFinished()); 102 | 103 | Mockito.when(in.isReady()).thenReturn(true); 104 | Assertions.assertTrue(wrappedIn.isReady()); 105 | 106 | Mockito.when(in.read()).thenReturn(0xFF); 107 | Assertions.assertEquals(0xFF, wrappedIn.read()); 108 | 109 | Mockito.when(in.available()).thenReturn(100); 110 | Assertions.assertEquals(4, wrappedIn.available()); 111 | 112 | Mockito.when(in.skip(2)).thenReturn(2l); 113 | Assertions.assertEquals(2, wrappedIn.skip(2)); 114 | 115 | Mockito.when(in.read(Mockito.any(), Mockito.eq(0), Mockito.eq(100))).thenReturn(100); 116 | Mockito.when(in.read(Mockito.any(), Mockito.eq(0), Mockito.eq(2))).thenReturn(2); 117 | Assertions.assertEquals(2, wrappedIn.read(new byte[100], 0, 100)); 118 | 119 | Mockito.when(in.read()).thenReturn(0xFF); 120 | Assertions.assertEquals(-1, wrappedIn.read()); 121 | 122 | Mockito.when(in.isFinished()).thenReturn(false); 123 | Assertions.assertTrue(wrappedIn.isFinished()); 124 | 125 | Mockito.when(in.isReady()).thenReturn(true); 126 | Assertions.assertFalse(wrappedIn.isReady()); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/webdav/core/filters/UnicodeResourcePathNormalizationFilterTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.filters; 10 | 11 | import org.cryptomator.webdav.core.filters.UnicodeResourcePathNormalizationFilter.MultistatusHrefNormalizer; 12 | import org.hamcrest.CoreMatchers; 13 | import org.hamcrest.MatcherAssert; 14 | import org.junit.jupiter.api.Assertions; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.Nested; 17 | import org.junit.jupiter.api.Test; 18 | import org.mockito.ArgumentCaptor; 19 | import org.mockito.Mockito; 20 | 21 | import javax.servlet.*; 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.servlet.http.HttpServletResponse; 24 | import javax.xml.stream.XMLStreamException; 25 | import java.io.ByteArrayInputStream; 26 | import java.io.ByteArrayOutputStream; 27 | import java.io.IOException; 28 | import java.text.Normalizer.Form; 29 | 30 | import static java.nio.charset.StandardCharsets.UTF_8; 31 | 32 | public class UnicodeResourcePathNormalizationFilterTest { 33 | 34 | private UnicodeResourcePathNormalizationFilter filter; 35 | private FilterChain chain; 36 | private HttpServletRequest request; 37 | private HttpServletResponse response; 38 | 39 | @BeforeEach 40 | public void setup() { 41 | filter = new UnicodeResourcePathNormalizationFilter(); 42 | chain = Mockito.mock(FilterChain.class); 43 | request = Mockito.mock(HttpServletRequest.class); 44 | response = Mockito.mock(HttpServletResponse.class); 45 | } 46 | 47 | @Nested 48 | public class NormalizedRequestTest { 49 | 50 | @BeforeEach 51 | public void setup() { 52 | Mockito.when(request.getScheme()).thenReturn("http"); 53 | Mockito.when(request.getServerName()).thenReturn("example.com"); 54 | Mockito.when(request.getServerPort()).thenReturn(80); 55 | Mockito.when(request.getContextPath()).thenReturn("/foo"); 56 | } 57 | 58 | @Test 59 | public void testRequestWithNormalizedResourceUri() throws IOException, ServletException { 60 | Mockito.when(request.getRequestURI()).thenReturn("/foo/bar"); 61 | filter.doFilter(request, response, chain); 62 | 63 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 64 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 65 | Mockito.verify(request, Mockito.never()).getPathInfo(); 66 | Assertions.assertEquals("/foo/bar", wrappedReq.getValue().getRequestURI()); 67 | Assertions.assertEquals("http://example.com/foo/bar", wrappedReq.getValue().getRequestURL().toString()); 68 | } 69 | 70 | @Test 71 | public void testRequestWithSemicolonInURI() throws IOException, ServletException { 72 | Mockito.when(request.getRequestURI()).thenReturn("/foo/bar;foo"); 73 | filter.doFilter(request, response, chain); 74 | 75 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 76 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 77 | Mockito.verify(request, Mockito.never()).getPathInfo(); 78 | Assertions.assertEquals("/foo/bar;foo", wrappedReq.getValue().getRequestURI()); 79 | Assertions.assertEquals("http://example.com/foo/bar;foo", wrappedReq.getValue().getRequestURL().toString()); 80 | } 81 | 82 | @Test 83 | public void testRequestWithNonNormalizedResourceUri1() throws IOException, ServletException { 84 | Mockito.when(request.getRequestURI()).thenReturn("/foo/\u0041\u030A"); 85 | filter.doFilter(request, response, chain); 86 | 87 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 88 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 89 | Mockito.verify(request, Mockito.never()).getPathInfo(); 90 | Assertions.assertEquals("/\u00C5", wrappedReq.getValue().getPathInfo()); 91 | Assertions.assertEquals("/foo/\u00C5", wrappedReq.getValue().getRequestURI()); 92 | Assertions.assertEquals("http://example.com/foo/\u00C5", wrappedReq.getValue().getRequestURL().toString()); 93 | } 94 | 95 | @Test 96 | public void testRequestWithNonNormalizedResourceUri2() throws IOException, ServletException { 97 | Mockito.when(request.getRequestURI()).thenReturn("/foo/O\u0308"); 98 | filter.doFilter(request, response, chain); 99 | 100 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 101 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 102 | Assertions.assertEquals("http://example.com/foo/Ö", wrappedReq.getValue().getRequestURL().toString()); 103 | } 104 | 105 | @Test 106 | public void testRequestWithNonNormalizedDestinationUri() throws IOException, ServletException { 107 | Mockito.when(request.getHeader("Destination")).thenReturn("http://example.com/bar/\u0041\u030A"); 108 | filter.doFilter(request, response, chain); 109 | 110 | ArgumentCaptor wrappedReq = ArgumentCaptor.forClass(HttpServletRequest.class); 111 | Mockito.verify(chain).doFilter(wrappedReq.capture(), Mockito.any(ServletResponse.class)); 112 | Assertions.assertEquals("http://example.com/bar/\u00C5", wrappedReq.getValue().getHeader("Destination")); 113 | } 114 | 115 | } 116 | 117 | @Nested 118 | public class NormalizedResponseTest { 119 | 120 | private ServletOutputStream out; 121 | private HttpServletResponse res; 122 | 123 | @BeforeEach 124 | public void setup() throws IOException, ServletException { 125 | out = Mockito.mock(ServletOutputStream.class); 126 | 127 | Mockito.when(response.getOutputStream()).thenReturn(out); 128 | Mockito.when(request.getMethod()).thenReturn("PROPFIND"); 129 | Mockito.when(request.getHeader("User-Agent")).thenReturn("WebDAVFS"); 130 | 131 | filter.doFilter(request, response, chain); 132 | 133 | ArgumentCaptor wrappedRes = ArgumentCaptor.forClass(HttpServletResponse.class); 134 | Mockito.verify(chain).doFilter(Mockito.any(ServletRequest.class), wrappedRes.capture()); 135 | res = wrappedRes.getValue(); 136 | } 137 | 138 | @Test 139 | public void testUnmodifiedNonMultistatusResponseBody() throws IOException { 140 | res.setStatus(200); 141 | Assertions.assertSame(out, res.getOutputStream()); 142 | } 143 | 144 | @Test 145 | public void testNfdUrlsInMultistatusResponseBody() throws IOException { 146 | ByteArrayOutputStream nfdBody = new ByteArrayOutputStream(); 147 | Mockito.doAnswer(invocation -> { 148 | int b = invocation.getArgument(0); 149 | nfdBody.write(b); 150 | return null; 151 | }).when(out).write(Mockito.anyInt()); 152 | 153 | byte[] nfcBody = "http://example.com/%C3%BC/".getBytes(UTF_8); 154 | res.setStatus(207); 155 | res.setContentLength(nfcBody.length); 156 | res.getOutputStream().write(nfcBody); 157 | 158 | MatcherAssert.assertThat(nfdBody.toString(UTF_8), CoreMatchers.containsString("http://example.com/u%cc%88/")); 159 | } 160 | 161 | } 162 | 163 | @Nested 164 | public class MultistatusHrefNormalizerTest { 165 | 166 | @Test 167 | public void testPreservesXmlStructure() throws XMLStreamException { 168 | ByteArrayInputStream in = new ByteArrayInputStream("barhttp://example.com/ascii/".getBytes(UTF_8)); 169 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 170 | try (MultistatusHrefNormalizer transformer = new MultistatusHrefNormalizer(in, out, Form.NFD)) { 171 | transformer.transform(); 172 | } 173 | String transformed = out.toString(UTF_8); 174 | Assertions.assertTrue(transformed.startsWith("")); 176 | Assertions.assertTrue(transformed.contains("bar")); 177 | Assertions.assertTrue(transformed.contains("http://example.com/ascii/")); 178 | Assertions.assertTrue(transformed.endsWith("")); 179 | } 180 | 181 | @Test 182 | public void testNfcToNfd() throws XMLStreamException { 183 | ByteArrayInputStream in = new ByteArrayInputStream("\u00fchttp://example.com/%C3%BC/".getBytes(UTF_8)); 184 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 185 | try (MultistatusHrefNormalizer transformer = new MultistatusHrefNormalizer(in, out, Form.NFD)) { 186 | transformer.transform(); 187 | } 188 | String transformed = out.toString(UTF_8); 189 | Assertions.assertTrue(transformed.contains("\u00fc")); 190 | Assertions.assertTrue(transformed.contains("http://example.com/u%cc%88/")); 191 | } 192 | 193 | @Test 194 | public void testNfdToNfc() throws XMLStreamException { 195 | ByteArrayInputStream in = new ByteArrayInputStream("u\u0308http://example.com/u%CC%88/".getBytes(UTF_8)); 196 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 197 | try (MultistatusHrefNormalizer transformer = new MultistatusHrefNormalizer(in, out, Form.NFC)) { 198 | transformer.transform(); 199 | } 200 | String transformed = out.toString(UTF_8); 201 | Assertions.assertTrue(transformed.contains("u\u0308")); 202 | Assertions.assertTrue(transformed.contains("http://example.com/%c3%bc/")); 203 | } 204 | 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/webdav/core/servlet/ByteRangeTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE file. 5 | *******************************************************************************/ 6 | package org.cryptomator.webdav.core.servlet; 7 | 8 | import org.cryptomator.webdav.core.servlet.ByteRange.MalformedByteRangeException; 9 | import org.cryptomator.webdav.core.servlet.ByteRange.UnsupportedRangeException; 10 | import org.junit.jupiter.api.Assertions; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.ValueSource; 14 | 15 | public class ByteRangeTest { 16 | 17 | @Test 18 | public void testConstructionWithUnsupportedRangeType() { 19 | Assertions.assertThrows(UnsupportedRangeException.class, () -> { 20 | ByteRange.parse("cats=2-3"); 21 | }); 22 | } 23 | 24 | @Test 25 | public void testConstructionWithMultipleRanges() { 26 | Assertions.assertThrows(UnsupportedRangeException.class, () -> { 27 | ByteRange.parse("bytes=2-3,7-8"); 28 | }); 29 | } 30 | 31 | @ParameterizedTest 32 | @ValueSource(strings = {"bytes=2-3-4", "bytes=3-2", "bytes=-", "bytes=", "bytes=2-z"}) 33 | public void testConstructionWithMalformedSingleRange1(String str) { 34 | Assertions.assertThrows(MalformedByteRangeException.class, () -> { 35 | ByteRange.parse(str); 36 | }); 37 | } 38 | 39 | @Test 40 | public void testConstructionWithSingleClosedRange() throws UnsupportedRangeException, MalformedByteRangeException { 41 | ByteRange range = ByteRange.parse("bytes=2-3"); 42 | Assertions.assertEquals(2, range.getEffectiveFirstByte(1000)); 43 | Assertions.assertEquals(3, range.getEffectiveLastByte(1000)); 44 | } 45 | 46 | @Test 47 | public void testConstructionWithSingleOpenRange1() throws UnsupportedRangeException, MalformedByteRangeException { 48 | ByteRange range = ByteRange.parse("bytes=2-"); 49 | Assertions.assertEquals(2, range.getEffectiveFirstByte(1000)); 50 | Assertions.assertEquals(999, range.getEffectiveLastByte(1000)); 51 | } 52 | 53 | @Test 54 | public void testConstructionWithSingleOpenRange2() throws UnsupportedRangeException, MalformedByteRangeException { 55 | ByteRange range = ByteRange.parse("bytes=-2"); 56 | Assertions.assertEquals(998, range.getEffectiveFirstByte(1000)); 57 | Assertions.assertEquals(999, range.getEffectiveLastByte(1000)); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/webdav/core/servlet/DavLocatorFactoryImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.apache.jackrabbit.webdav.DavResourceLocator; 12 | import org.junit.jupiter.api.Assertions; 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class DavLocatorFactoryImplTest { 16 | 17 | @Test 18 | public void testCreateResourceLocatorFromHref() { 19 | DavLocatorFactoryImpl factory = new DavLocatorFactoryImpl(); 20 | DavResourceLocator loc1 = factory.createResourceLocator("http://localhost:123/contextPath/", "http://localhost:123/contextPath/foo/foo%20bar.txt"); 21 | Assertions.assertEquals("foo/foo bar.txt", loc1.getResourcePath()); 22 | 23 | DavResourceLocator loc2 = factory.createResourceLocator("http://localhost:123/contextPath/", "relative/path.txt"); 24 | Assertions.assertEquals("relative/path.txt", loc2.getResourcePath()); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/org/cryptomator/webdav/core/servlet/DavLocatorImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Copyright (c) 2016 Sebastian Stenzel and others. 3 | * All rights reserved. This program and the accompanying materials 4 | * are made available under the terms of the accompanying LICENSE.txt. 5 | * 6 | * Contributors: 7 | * Sebastian Stenzel - initial API and implementation 8 | *******************************************************************************/ 9 | package org.cryptomator.webdav.core.servlet; 10 | 11 | import org.junit.jupiter.api.Assertions; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | import org.mockito.Mockito; 15 | 16 | public class DavLocatorImplTest { 17 | 18 | private DavLocatorFactoryImpl factory; 19 | private DavLocatorImpl locator; 20 | 21 | @BeforeEach 22 | public void setup() { 23 | factory = Mockito.mock(DavLocatorFactoryImpl.class); 24 | locator = new DavLocatorImpl(factory, "http://localhost/contextPath/", "foo/foo bar.txt"); 25 | } 26 | 27 | @Test 28 | public void testConstructionWithTrailingSlash() { 29 | DavLocatorImpl locator = new DavLocatorImpl(factory, "http://localhost/contextPath/", "foo/bar/baz/"); 30 | Assertions.assertEquals("foo/bar/baz", locator.getResourcePath()); 31 | } 32 | 33 | @Test 34 | public void testConstructionWithInvalidPrefix() { 35 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 36 | new DavLocatorImpl(factory, "http://localhost/contextPath", "foo/foo bar.txt"); 37 | }); 38 | } 39 | 40 | @Test 41 | public void testConstructionWithInvalidPath() { 42 | Assertions.assertThrows(IllegalArgumentException.class, () -> { 43 | new DavLocatorImpl(factory, "http://localhost/contextPath/", "/foo/foo bar.txt"); 44 | }); 45 | } 46 | 47 | @Test 48 | public void testGetResourcePath() { 49 | Assertions.assertEquals("foo/foo bar.txt", locator.getResourcePath()); 50 | } 51 | 52 | @Test 53 | public void testResolveParent1() { 54 | DavLocatorImpl fooBarBaz = new DavLocatorImpl(factory, "http://localhost/contextPath/", "foo/bar/baz"); 55 | DavLocatorImpl fooBar = new DavLocatorImpl(factory, "http://localhost/contextPath/", "foo/bar"); 56 | Mockito.when(factory.createResourceLocator("http://localhost/contextPath/", null, "foo/bar")).thenReturn(fooBar); 57 | DavLocatorImpl result = fooBarBaz.resolveParent(); 58 | Assertions.assertEquals(fooBar, result); 59 | } 60 | 61 | @Test 62 | public void testResolveParent2() { 63 | DavLocatorImpl foo = new DavLocatorImpl(factory, "http://localhost/contextPath/", "foo"); 64 | DavLocatorImpl root = new DavLocatorImpl(factory, "http://localhost/contextPath/", ""); 65 | Mockito.when(factory.createResourceLocator("http://localhost/contextPath/", null, "")).thenReturn(root); 66 | DavLocatorImpl result = foo.resolveParent(); 67 | Assertions.assertEquals(root, result); 68 | } 69 | 70 | @Test 71 | public void testResolveParent3() { 72 | DavLocatorImpl root = new DavLocatorImpl(factory, "http://localhost/contextPath/", ""); 73 | Assertions.assertNull(root.resolveParent()); 74 | } 75 | 76 | @Test 77 | public void testGetFactory() { 78 | Assertions.assertSame(factory, locator.getFactory()); 79 | } 80 | 81 | } 82 | --------------------------------------------------------------------------------