Files

93 lines
4.2 KiB
Java

package com.ankurm.filetransfer;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import com.ankurm.filetransfer.Cap.Part;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FilenameAndDownloadTest {
static final String F = "02-filename.txt";
static final String G = "03-download.txt";
@LocalServerPort int port;
@Autowired FileStore store;
@BeforeAll
static void clean() {
Cap.reset(F);
Cap.reset(G);
}
String base() {
return "http://localhost:" + port;
}
String postRaw(String filename, String contentType, byte[] content) throws Exception {
String boundary = "----ft" + System.nanoTime();
byte[] body = Cap.multipart(boundary, List.of(new Part("file", filename, contentType, content)));
HttpRequest req = HttpRequest.newBuilder(URI.create(base() + "/files"))
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(body)).build();
return Cap.HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
@Test
void whatTheServerSeesIsWhatTheClientSent() throws Exception {
String evil = "../../etc/cron.d/evil.txt";
String json = postRaw(evil, "image/png", "hello".getBytes(StandardCharsets.UTF_8));
Cap.say(F, "client sent filename=" + evil + " Content-Type=image/png");
Cap.say(F, "server response: " + json);
Path root = store.root();
boolean escapes = !root.resolve(evil).normalize().startsWith(root);
Cap.say(F, "root.resolve(originalFilename).normalize() stays inside the storage dir: " + !escapes);
assertThat(json).contains("\"displayName\":\"evil.txt\"");
assertThat(escapes).isTrue();
String win = postRaw("..\\..\\windows\\system32\\evil.dll", "text/plain", "x".getBytes(StandardCharsets.UTF_8));
Cap.say(F, "client sent a Windows-style path; server response: " + win);
}
@Test
void downloadSupportsRangeAndEncodesNames() throws Exception {
String json = postRaw("résumé 2026.txt", "text/plain", "ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(StandardCharsets.UTF_8));
String id = json.replaceAll(".*\"id\":\"([^\"]+)\".*", "$1");
HttpResponse<String> full = Cap.HTTP.send(HttpRequest.newBuilder(URI.create(base() + "/files/" + id)).build(),
HttpResponse.BodyHandlers.ofString());
Cap.say(G, "GET /files/{id}");
Cap.say(G, " status=" + full.statusCode() + " body=" + full.body());
for (String h : List.of("content-type", "content-length", "content-disposition", "accept-ranges", "x-content-type-options")) {
Cap.say(G, " " + h + ": " + full.headers().firstValue(h).orElse("(absent)"));
}
HttpResponse<String> part = Cap.HTTP.send(HttpRequest.newBuilder(URI.create(base() + "/files/" + id))
.header("Range", "bytes=10-19").build(), HttpResponse.BodyHandlers.ofString());
Cap.say(G, "GET /files/{id} with Range: bytes=10-19");
Cap.say(G, " status=" + part.statusCode() + " body=" + part.body());
Cap.say(G, " content-range: " + part.headers().firstValue("content-range").orElse("(absent)"));
HttpResponse<String> past = Cap.HTTP.send(HttpRequest.newBuilder(URI.create(base() + "/files/" + id))
.header("Range", "bytes=500-").build(), HttpResponse.BodyHandlers.ofString());
Cap.say(G, "GET /files/{id} with Range: bytes=500-");
Cap.say(G, " status=" + past.statusCode() + " content-range: " + past.headers().firstValue("content-range").orElse("(absent)"));
assertThat(part.statusCode()).isEqualTo(206);
assertThat(part.body()).isEqualTo("KLMNOPQRST");
}
}