Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion modules/swagger-generator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@
<configuration>
<url>https://github.com/swagger-api/swagger-ui/archive/master.tar.gz</url>
<unpack>true</unpack>
<!--<skipCache>true</skipCache>-->
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
</execution>
Expand Down Expand Up @@ -327,6 +326,18 @@
<version>${junit-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<servlet-api-version>2.5</servlet-api-version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,43 @@
import io.swagger.codegen.CodegenConfig;
import io.swagger.codegen.CodegenType;
import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.generator.exception.ApiException;
import io.swagger.generator.exception.BadRequestException;
import io.swagger.generator.model.Generated;
import io.swagger.generator.model.GeneratorInput;
import io.swagger.generator.model.ResponseCode;
import io.swagger.generator.online.Generator;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.File;
import java.util.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@Path("/gen")
@Api(value = "/gen", description = "Resource for generating swagger components")
@Api(value = "/gen")
@SuppressWarnings("static-method")
public class SwaggerResource {
static List<String> clients = new ArrayList<String>();
static List<String> servers = new ArrayList<String>();
private static Map<String, Generated> fileMap = new HashMap<String, Generated>();
private static final Logger LOGGER = LoggerFactory.getLogger(SwaggerResource.class);

static List<String> clients = new ArrayList<>();
static List<String> servers = new ArrayList<>();
private static Map<String, Generated> fileMap = new HashMap<>();

static {
List<CodegenConfig> extensions = Codegen.getExtensions();
Expand All @@ -43,8 +57,8 @@ public class SwaggerResource {
}
}

Collections.sort(clients, String.CASE_INSENSITIVE_ORDER);
Collections.sort(servers, String.CASE_INSENSITIVE_ORDER);
clients.sort(String.CASE_INSENSITIVE_ORDER);
servers.sort(String.CASE_INSENSITIVE_ORDER);
}

@GET
Expand All @@ -55,20 +69,16 @@ public class SwaggerResource {
notes = "A valid `fileId` is generated by the `/clients/{language}` or `/servers/{language}` POST "
+ "operations. The fileId code can be used just once, after which a new `fileId` will need to "
+ "be requested.", response = String.class, tags = {"clients", "servers"})
public Response downloadFile(@PathParam("fileId") String fileId) throws Exception {
public Response downloadFile(@PathParam("fileId") String fileId) {
Generated g = fileMap.get(fileId);
System.out.println("looking for fileId " + fileId);
System.out.println("got filename " + g.getFilename());
if (g.getFilename() != null) {
LOGGER.info("Looking for fileId: {}", fileId);
if (g != null && g.getFilename() != null) {
LOGGER.info("Got filename: {}", g.getFilename());
SecureFileUtils.validatePath(g.getFilename());
File file = new java.io.File(g.getFilename());
byte[] bytes = org.apache.commons.io.FileUtils.readFileToByteArray(file);
final File file = new java.io.File(g.getFilename());

try {
FileUtils.deleteDirectory(file.getParentFile());
} catch (Exception e) {
System.out.println("failed to delete file " + file.getAbsolutePath());
}
byte[] bytes = getFileBytes(file);
removeFile(fileId, file);

return Response
.ok(bytes, "application/zip")
Expand All @@ -80,6 +90,23 @@ public Response downloadFile(@PathParam("fileId") String fileId) throws Exceptio
}
}

private static byte[] getFileBytes(File file) {
try {
return FileUtils.readFileToByteArray(file);
} catch (IOException e) {
throw new IllegalStateException("Cannot read the file: " + file.getAbsolutePath(), e);
}
}

private static void removeFile(String fileId, File file) {
try {
FileUtils.deleteDirectory(file.getParentFile());
fileMap.remove(fileId);
} catch (Exception e) {
LOGGER.error("Failed to delete file: {} ", file.getAbsolutePath());
}
}

@POST
@Path("/clients/{language}")
@ApiOperation(
Expand All @@ -90,7 +117,7 @@ public Response generateClient(
@Context HttpServletRequest request,
@ApiParam(value = "The target language for the client library", required = true) @PathParam("language") String language,
@ApiParam(value = "Configuration for building the client library", required = true) GeneratorInput opts)
throws Exception {
throws ApiException {

String filename = Generator.generateClient(language, opts);
String host = getHost(request);
Expand All @@ -101,7 +128,6 @@ public Response generateClient(
g.setFilename(filename);
g.setFriendlyName(language + "-client");
fileMap.put(code, g);
System.out.println(code + ", " + filename);
String link = host + "/api/gen/download/" + code;
return Response.ok().entity(new ResponseCode(code, link)).build();
} else {
Expand All @@ -117,7 +143,7 @@ public Response generateClient(
public Response getClientOptions(
@SuppressWarnings("unused") @Context HttpServletRequest request,
@ApiParam(value = "The target language for the client library", required = true) @PathParam("language") String language)
throws Exception {
throws ApiException {

Map<String, CliOption> opts = Generator.getOptions(language);

Expand All @@ -136,7 +162,7 @@ public Response getClientOptions(
public Response getServerOptions(
@SuppressWarnings("unused") @Context HttpServletRequest request,
@ApiParam(value = "The target language for the server framework", required = true) @PathParam("framework") String framework)
throws Exception {
throws ApiException {

Map<String, CliOption> opts = Generator.getOptions(framework);

Expand Down Expand Up @@ -173,14 +199,13 @@ public Response serverOptions() {
value = "Generates a server library",
notes = "Accepts a `GeneratorInput` options map for spec location and generation options.",
response = ResponseCode.class, tags = "servers")
public Response generateServerForLanguage(@Context HttpServletRequest request, @ApiParam(
value = "framework", required = true) @PathParam("framework") String framework,
@ApiParam(value = "parameters", required = true) GeneratorInput opts) throws Exception {
public Response generateServerForLanguage(@Context HttpServletRequest request, @ApiParam(value = "framework", required = true) @PathParam("framework") String framework,
@ApiParam(value = "parameters", required = true) GeneratorInput opts) throws ApiException {
if (framework == null) {
throw new BadRequestException("Framework is required");
}
String filename = Generator.generateServer(framework, opts);
System.out.println("generated name: " + filename);
LOGGER.info("Generated filename: {}", filename);

String host = getHost(request);

Expand All @@ -190,7 +215,6 @@ public Response generateServerForLanguage(@Context HttpServletRequest request, @
g.setFilename(filename);
g.setFriendlyName(framework + "-server");
fileMap.put(code, g);
System.out.println(code + ", " + filename);
String link = host + "/api/gen/download/" + code;
return Response.ok().entity(new ResponseCode(code, link)).build();
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,114 @@
package io.swagger.generator.resource;

import io.swagger.codegen.utils.SecureFileUtils;
import io.swagger.generator.model.Generated;
import org.apache.commons.io.FileUtils;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.ws.rs.core.Response;
import java.io.File;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import static org.testng.Assert.assertFalse;
import static org.testng.AssertJUnit.assertEquals;

public class SwaggerResourceTest {

private SwaggerResource resource;
private Map<String, Generated> fileMap;

@BeforeMethod
public void setUp() throws Exception {
resource = new SwaggerResource();

fileMap = new HashMap<>();
Field fm = SwaggerResource.class.getDeclaredField("fileMap");
fm.setAccessible(true);
fm.set(null, fileMap);
}

@AfterMethod
public void after() {
fileMap.clear();
}

@Test
public void shouldReturnSuccessWhenDownloadFileAndBadRequestAfterSecondTry() throws Exception {
File dir = new File("target/testng-gen1");
File zip = new File(dir, "client.zip");
FileUtils.write(zip, "TESTDATA", StandardCharsets.UTF_8);

Generated g = new Generated();
g.setFilename(zip.getAbsolutePath());
g.setFriendlyName("clientX");

fileMap.put("123", g);

Response response = resource.downloadFile("123");

assertEquals(200, response.getStatus());
assertEquals("TESTDATA", new String((byte[]) response.getEntity()));
assertFalse(zip.exists(), "File should be removed after download.");
assertFalse(dir.exists(), "Directory should be removed after download.");

Response response2 = resource.downloadFile("123");
assertEquals(404, response2.getStatus());
}

@Test
public void shouldReturnNotFoundWhenFileDoesntExist() {
Response response = resource.downloadFile("nope");
assertEquals(404, response.getStatus());
}

@Test(expectedExceptions = Exception.class)
public void testDownloadFile_missingPhysicalFile_causes500() {
Generated g = new Generated();
g.setFilename("target/no_such_dir/file.zip");
g.setFriendlyName("missing");

fileMap.put("777", g);

resource.downloadFile("777");
}

@Test(expectedExceptions = Exception.class)
public void shouldPathValidationFailsWhenDownloadFile() throws Exception {
try (MockedStatic<SecureFileUtils> mocked = Mockito.mockStatic(SecureFileUtils.class)) {

mocked.when(() -> SecureFileUtils.validatePath(Mockito.anyString()))
.thenThrow(new RuntimeException("Invalid path"));

File dir = new File("target/testng-gen2");
File zip = new File(dir, "client.zip");
FileUtils.write(zip, "XYZ", StandardCharsets.UTF_8);

Generated g = new Generated();
g.setFilename(zip.getAbsolutePath());
g.setFriendlyName("clientY");

fileMap.put("xyz", g);

resource.downloadFile("xyz");
}
}

@Test(expectedExceptions = SecurityException.class)
public void testDownloadFileWithPathTraversal() throws Exception {
SwaggerResource resource = new SwaggerResource();

io.swagger.generator.model.Generated generated = new io.swagger.generator.model.Generated();
generated.setFilename("../../../etc/passwd");

java.lang.reflect.Field fileMapField = SwaggerResource.class.getDeclaredField("fileMap");
fileMapField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.Map<String, io.swagger.generator.model.Generated> fileMap =
(java.util.Map<String, io.swagger.generator.model.Generated>) fileMapField.get(null);

fileMap.put("test-file-id", generated);

try {
Expand Down
Loading