| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.fineract.integrationtests.client; |
| |
| import static org.junit.jupiter.api.Assertions.assertThrows; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import lombok.extern.slf4j.Slf4j; |
| import okhttp3.MediaType; |
| import okhttp3.MultipartBody; |
| import okhttp3.RequestBody; |
| import okhttp3.ResponseBody; |
| import org.apache.commons.io.IOUtils; |
| import org.apache.fineract.client.services.ImagesApi; |
| import org.apache.fineract.client.util.Parts; |
| import org.junit.jupiter.api.Order; |
| import org.junit.jupiter.api.Test; |
| import retrofit2.Call; |
| import retrofit2.Response; |
| import retrofit2.http.GET; |
| import retrofit2.http.Headers; |
| |
| /** |
| * Integration Test for /images API. |
| * |
| * @author Michael Vorburger.ch |
| */ |
| @Slf4j |
| public class ImageTest extends IntegrationTest { |
| |
| final File testImage = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile()); |
| |
| Long clientId = new ClientTest().getClientId(); |
| Long staffId = new StaffTest().getStaffId(); |
| |
| @Test |
| @Order(1) |
| void create() { |
| ok(fineract().images.create("staff", staffId, Parts.fromFile(testImage))); |
| ok(fineract().images.create("clients", clientId, Parts.fromFile(testImage))); |
| } |
| |
| @Test |
| @Order(2) |
| void getOriginalSize() throws IOException { |
| ResponseBody r = ok(fineract().images.get("staff", staffId, 3505, 1972, null)); |
| assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain")); |
| String encodedImage = r.string(); |
| assertThat(encodedImage).startsWith("data:image/jpeg;base64,"); |
| assertThat(encodedImage).hasLength(2846549); |
| assertThat(r.contentLength()).isEqualTo(-1); |
| } |
| |
| @Test |
| @Order(3) |
| void getSmallerSize() throws IOException { |
| ResponseBody r = ok(fineract().images.get("staff", staffId, 128, 128, null)); |
| assertThat(r.string()).hasLength(6591); |
| } |
| |
| @Test |
| @Order(4) |
| void getBiggerSize() throws IOException { |
| ResponseBody r = ok(fineract().images.get("staff", staffId, 9000, 6000, null)); |
| assertThat(r.string()).hasLength(2846549); |
| } |
| |
| @Test |
| @Order(5) |
| void getInlineOctetOutput() throws IOException { |
| // 3505x1972 is the exact original size of testFile |
| Response<ResponseBody> r = okR(fineract().images.get("staff", staffId, 3505, 1972, "inline_octet")); |
| try (ResponseBody body = r.body()) { |
| assertThat(body.contentType()).isEqualTo(MediaType.get("image/jpeg")); |
| assertThat(body.bytes().length).isEqualTo(testImage.length()); |
| assertThat(body.contentLength()).isEqualTo(testImage.length()); |
| } |
| |
| var staff = ok(fineract().staff.retrieveOne8(staffId)); |
| String expectedFileName = staff.getDisplayName() + "JPEG"; // without dot! |
| assertThat(Parts.fileName(r)).hasValue(expectedFileName); |
| } |
| |
| @Test |
| @Order(6) |
| void getOctetOutput() throws IOException { |
| ResponseBody r = ok(fineract().images.get("staff", staffId, 3505, 1972, "octet")); |
| assertThat(r.contentType()).isEqualTo(MediaType.get("image/jpeg")); |
| assertThat(r.bytes().length).isEqualTo(testImage.length()); |
| assertThat(r.contentLength()).isEqualTo(testImage.length()); |
| } |
| |
| @Test |
| @Order(7) |
| void getAnotherOutput() throws IOException { |
| ResponseBody r = ok(fineract().images.get("staff", staffId, 3505, 1972, "abcd")); |
| assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain")); |
| assertThat(r.string()).startsWith("data:image/jpeg;base64,"); |
| } |
| |
| @Test |
| @Order(8) |
| void getText() throws IOException { |
| ResponseBody r = ok(fineract().createService(ImagesApiWithHeadersForTest.class).getText("staff", staffId, 3505, 1972, null)); |
| assertThat(r.contentType()).isEqualTo(MediaType.get("text/plain")); |
| assertThat(r.string()).startsWith("data:image/jpeg;base64,"); |
| } |
| |
| @Test |
| @Order(9) |
| void getBytes() throws IOException { |
| ResponseBody r = ok(fineract().createService(ImagesApiWithHeadersForTest.class).getBytes("staff", staffId, 3505, 1972, null)); |
| assertThat(r.contentType()).isEqualTo(MediaType.get("image/jpeg")); |
| assertThat(r.bytes().length).isEqualTo(testImage.length()); |
| } |
| |
| @Test |
| @Order(50) |
| void update() { |
| ok(fineract().images.update("staff", staffId, Parts.fromFile(testImage))); |
| } |
| |
| @Test |
| @Order(99) |
| void delete() { |
| ok(fineract().images.delete("staff", staffId)); |
| ok(fineract().images.delete("clients", clientId)); |
| } |
| |
| @Test |
| @Order(100) |
| void pathTraversalJsp() { |
| final MultipartBody.Part part = createPart("image-text-wrong-content.jsp", |
| "../../../../../../../../../../tmp/image-text-wrong-content.jsp", "image/gif"); |
| |
| assertThat(part).isNotNull(); |
| |
| Exception exception = assertThrows(Exception.class, () -> { |
| ok(fineract().images.create("clients", clientId, part)); |
| }); |
| |
| assertThat(exception).isNotNull(); |
| |
| log.warn("Should not be able to upload a file that doesn't match the indicated content type: {}", exception.getMessage()); |
| } |
| |
| @Test |
| @Order(101) |
| void gifWithPngExtension() { |
| final MultipartBody.Part part = createPart("image-gif-wrong-extension.png", "image-gif-wrong-extension.png", "image/png"); |
| |
| assertThat(part).isNotNull(); |
| |
| Exception exception = assertThrows(Exception.class, () -> { |
| ok(fineract().images.create("clients", clientId, part)); |
| }); |
| |
| assertThat(exception).isNotNull(); |
| |
| log.warn("Should not be able to upload a gif by just renaming the file extension: {}", exception.getMessage()); |
| } |
| |
| @Test |
| @Order(102) |
| void gifImage() { |
| final MultipartBody.Part part = createPart("image-gif-correct-extension.gif", "image-gif-correct-extension.gif", "image/png"); |
| |
| assertThat(part).isNotNull(); |
| |
| Exception exception = assertThrows(Exception.class, () -> { |
| ok(fineract().images.create("clients", clientId, part)); |
| }); |
| |
| assertThat(exception).isNotNull(); |
| |
| log.warn("Should not be able to upload a gif it is not whitelisted: {}", exception.getMessage()); |
| } |
| |
| @Test |
| @Order(103) |
| void pathTraversalJpg() { |
| final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", |
| "../../../../../../../../../../tmp/michael.vorburger-crepes.jpg", "image/jpeg"); |
| |
| assertThat(part).isNotNull(); |
| |
| Exception exception = assertThrows(Exception.class, () -> { |
| ok(fineract().images.create("clients", clientId, part)); |
| }); |
| |
| assertThat(exception).isNotNull(); |
| |
| log.warn("Should not be able to upload a file with a forbidden name pattern: {}", exception.getMessage()); |
| } |
| |
| @Test |
| @Order(104) |
| void pathTraversalWithAbsolutePathJpg() { |
| final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", "../17/michael.vorburger-crepes.jpg", "image/jpeg"); |
| |
| assertThat(part).isNotNull(); |
| |
| Exception exception = assertThrows(Exception.class, () -> { |
| ok(fineract().images.create("clients", clientId, part)); |
| }); |
| |
| assertThat(exception).isNotNull(); |
| |
| log.warn("Should not be able to upload a file with a forbidden name pattern: {}", exception.getMessage()); |
| } |
| |
| @Test |
| @Order(105) |
| void pathTraversalWithAbsolutePathJpg2() { |
| final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", "..//17//michael.vorburger-crepes.jpg", "image/jpeg"); |
| |
| assertThat(part).isNotNull(); |
| |
| Exception exception = assertThrows(Exception.class, () -> { |
| ok(fineract().images.create("clients", clientId, part)); |
| }); |
| |
| assertThat(exception).isNotNull(); |
| |
| log.warn("Should not be able to upload a file with a forbidden name pattern: {}", exception.getMessage()); |
| } |
| |
| private MultipartBody.Part createPart(String fileResource, String fileName, String mediaType) { |
| try { |
| byte[] data = IOUtils.toByteArray(ImageTest.class.getClassLoader().getResourceAsStream(fileResource)); |
| RequestBody rb = RequestBody.create(data, MediaType.get(mediaType)); |
| return MultipartBody.Part.createFormData("file", fileName, rb); |
| } catch (Exception e) { |
| log.error("Error creating file part.", e); |
| } |
| |
| return null; |
| } |
| |
| interface ImagesApiWithHeadersForTest extends ImagesApi { |
| |
| @Headers("Accept: text/plain") |
| @GET("{entityType}/{entityId}/images") |
| Call<ResponseBody> getText(@retrofit2.http.Path("entityType") String entityType, @retrofit2.http.Path("entityId") Long entityId, |
| @retrofit2.http.Query("maxWidth") Integer maxWidth, @retrofit2.http.Query("maxHeight") Integer maxHeight, |
| @retrofit2.http.Query("output") String output); |
| |
| @Headers("Accept: application/octet-stream") |
| @GET("{entityType}/{entityId}/images") |
| Call<ResponseBody> getBytes(@retrofit2.http.Path("entityType") String entityType, @retrofit2.http.Path("entityId") Long entityId, |
| @retrofit2.http.Query("maxWidth") Integer maxWidth, @retrofit2.http.Query("maxHeight") Integer maxHeight, |
| @retrofit2.http.Query("output") String output); |
| } |
| } |