Skip to content

Commit e736be3

Browse files
fix: remove file from fileMap in online generator (#12651)
1 parent dc839b8 commit e736be3

File tree

4 files changed

+168
-34
lines changed

4 files changed

+168
-34
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
- [ ] Read the [contribution guidelines](https://github.com/swagger-api/swagger-codegen/blob/master/CONTRIBUTING.md).
44
- [ ] Ran the shell script under `./bin/` to update Petstore sample so that CIs can verify the change. (For instance, only need to run `./bin/{LANG}-petstore.sh` and `./bin/security/{LANG}-petstore.sh` if updating the {LANG} (e.g. php, ruby, python, etc) code generator or {LANG} client's mustache templates). Windows batch files can be found in `.\bin\windows\`.
55
- [ ] Filed the PR against the correct branch: `3.0.0` branch for changes related to OpenAPI spec 3.0. Default: `master`.
6-
- [ ] Copied the [technical committee](https://github.com/swagger-api/swagger-codegen/#swagger-codegen-technical-committee) to review the pull request if your PR is targeting a particular programming language.
76

87
### Description of the PR
98

modules/swagger-generator/pom.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@
146146
<configuration>
147147
<url>https://github.com/swagger-api/swagger-ui/archive/master.tar.gz</url>
148148
<unpack>true</unpack>
149-
<!--<skipCache>true</skipCache>-->
150149
<outputDirectory>${project.build.directory}</outputDirectory>
151150
</configuration>
152151
</execution>
@@ -327,6 +326,18 @@
327326
<version>${junit-version}</version>
328327
<scope>test</scope>
329328
</dependency>
329+
<dependency>
330+
<groupId>org.mockito</groupId>
331+
<artifactId>mockito-core</artifactId>
332+
<version>5.20.0</version>
333+
<scope>test</scope>
334+
</dependency>
335+
<dependency>
336+
<groupId>org.mockito</groupId>
337+
<artifactId>mockito-inline</artifactId>
338+
<version>5.2.0</version>
339+
<scope>test</scope>
340+
</dependency>
330341
</dependencies>
331342
<properties>
332343
<servlet-api-version>2.5</servlet-api-version>

modules/swagger-generator/src/main/java/io/swagger/generator/resource/SwaggerResource.java

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,50 @@
33
import io.swagger.annotations.Api;
44
import io.swagger.annotations.ApiOperation;
55
import io.swagger.annotations.ApiParam;
6+
import io.swagger.annotations.ApiResponse;
7+
import io.swagger.annotations.ApiResponses;
68
import io.swagger.codegen.CliOption;
79
import io.swagger.codegen.Codegen;
810
import io.swagger.codegen.CodegenConfig;
911
import io.swagger.codegen.CodegenType;
1012
import io.swagger.codegen.utils.SecureFileUtils;
13+
import io.swagger.generator.exception.ApiException;
1114
import io.swagger.generator.exception.BadRequestException;
1215
import io.swagger.generator.model.Generated;
1316
import io.swagger.generator.model.GeneratorInput;
1417
import io.swagger.generator.model.ResponseCode;
1518
import io.swagger.generator.online.Generator;
1619
import org.apache.commons.io.FileUtils;
1720
import org.apache.commons.lang3.StringUtils;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
1823

1924
import javax.servlet.http.HttpServletRequest;
20-
import javax.ws.rs.*;
25+
import javax.ws.rs.GET;
26+
import javax.ws.rs.POST;
27+
import javax.ws.rs.Path;
28+
import javax.ws.rs.PathParam;
29+
import javax.ws.rs.Produces;
2130
import javax.ws.rs.core.Context;
2231
import javax.ws.rs.core.MediaType;
2332
import javax.ws.rs.core.Response;
2433
import java.io.File;
25-
import java.util.*;
34+
import java.io.IOException;
35+
import java.util.ArrayList;
36+
import java.util.HashMap;
37+
import java.util.List;
38+
import java.util.Map;
39+
import java.util.UUID;
2640

2741
@Path("/gen")
28-
@Api(value = "/gen", description = "Resource for generating swagger components")
42+
@Api(value = "/gen")
2943
@SuppressWarnings("static-method")
3044
public class SwaggerResource {
31-
static List<String> clients = new ArrayList<String>();
32-
static List<String> servers = new ArrayList<String>();
33-
private static Map<String, Generated> fileMap = new HashMap<String, Generated>();
45+
private static final Logger LOGGER = LoggerFactory.getLogger(SwaggerResource.class);
46+
47+
static List<String> clients = new ArrayList<>();
48+
static List<String> servers = new ArrayList<>();
49+
private static Map<String, Generated> fileMap = new HashMap<>();
3450

3551
static {
3652
List<CodegenConfig> extensions = Codegen.getExtensions();
@@ -43,8 +59,8 @@ public class SwaggerResource {
4359
}
4460
}
4561

46-
Collections.sort(clients, String.CASE_INSENSITIVE_ORDER);
47-
Collections.sort(servers, String.CASE_INSENSITIVE_ORDER);
62+
clients.sort(String.CASE_INSENSITIVE_ORDER);
63+
servers.sort(String.CASE_INSENSITIVE_ORDER);
4864
}
4965

5066
@GET
@@ -55,20 +71,21 @@ public class SwaggerResource {
5571
notes = "A valid `fileId` is generated by the `/clients/{language}` or `/servers/{language}` POST "
5672
+ "operations. The fileId code can be used just once, after which a new `fileId` will need to "
5773
+ "be requested.", response = String.class, tags = {"clients", "servers"})
58-
public Response downloadFile(@PathParam("fileId") String fileId) throws Exception {
74+
@ApiResponses(value = {
75+
@ApiResponse(code = 200, message = "File successfully downloaded. Response contains ZIP file bytes."),
76+
@ApiResponse(code = 404, message = "File with the given fileId not found or already downloaded."),
77+
@ApiResponse(code = 500, message = "Server error while reading or returning the file.")
78+
})
79+
public Response downloadFile(@PathParam("fileId") String fileId) {
5980
Generated g = fileMap.get(fileId);
60-
System.out.println("looking for fileId " + fileId);
61-
System.out.println("got filename " + g.getFilename());
62-
if (g.getFilename() != null) {
81+
LOGGER.info("Looking for fileId: {}", fileId);
82+
if (g != null && g.getFilename() != null) {
83+
LOGGER.info("Got filename: {}", g.getFilename());
6384
SecureFileUtils.validatePath(g.getFilename());
64-
File file = new java.io.File(g.getFilename());
65-
byte[] bytes = org.apache.commons.io.FileUtils.readFileToByteArray(file);
85+
final File file = new java.io.File(g.getFilename());
6686

67-
try {
68-
FileUtils.deleteDirectory(file.getParentFile());
69-
} catch (Exception e) {
70-
System.out.println("failed to delete file " + file.getAbsolutePath());
71-
}
87+
byte[] bytes = getFileBytes(file);
88+
removeFile(fileId, file);
7289

7390
return Response
7491
.ok(bytes, "application/zip")
@@ -80,6 +97,23 @@ public Response downloadFile(@PathParam("fileId") String fileId) throws Exceptio
8097
}
8198
}
8299

100+
private static byte[] getFileBytes(File file) {
101+
try {
102+
return FileUtils.readFileToByteArray(file);
103+
} catch (IOException e) {
104+
throw new IllegalStateException("Cannot read the file: " + file.getAbsolutePath(), e);
105+
}
106+
}
107+
108+
private static void removeFile(String fileId, File file) {
109+
try {
110+
FileUtils.deleteDirectory(file.getParentFile());
111+
fileMap.remove(fileId);
112+
} catch (Exception e) {
113+
LOGGER.error("Failed to delete file: {} ", file.getAbsolutePath());
114+
}
115+
}
116+
83117
@POST
84118
@Path("/clients/{language}")
85119
@ApiOperation(
@@ -90,7 +124,7 @@ public Response generateClient(
90124
@Context HttpServletRequest request,
91125
@ApiParam(value = "The target language for the client library", required = true) @PathParam("language") String language,
92126
@ApiParam(value = "Configuration for building the client library", required = true) GeneratorInput opts)
93-
throws Exception {
127+
throws ApiException {
94128

95129
String filename = Generator.generateClient(language, opts);
96130
String host = getHost(request);
@@ -101,7 +135,6 @@ public Response generateClient(
101135
g.setFilename(filename);
102136
g.setFriendlyName(language + "-client");
103137
fileMap.put(code, g);
104-
System.out.println(code + ", " + filename);
105138
String link = host + "/api/gen/download/" + code;
106139
return Response.ok().entity(new ResponseCode(code, link)).build();
107140
} else {
@@ -117,7 +150,7 @@ public Response generateClient(
117150
public Response getClientOptions(
118151
@SuppressWarnings("unused") @Context HttpServletRequest request,
119152
@ApiParam(value = "The target language for the client library", required = true) @PathParam("language") String language)
120-
throws Exception {
153+
throws ApiException {
121154

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

@@ -136,7 +169,7 @@ public Response getClientOptions(
136169
public Response getServerOptions(
137170
@SuppressWarnings("unused") @Context HttpServletRequest request,
138171
@ApiParam(value = "The target language for the server framework", required = true) @PathParam("framework") String framework)
139-
throws Exception {
172+
throws ApiException {
140173

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

@@ -173,14 +206,13 @@ public Response serverOptions() {
173206
value = "Generates a server library",
174207
notes = "Accepts a `GeneratorInput` options map for spec location and generation options.",
175208
response = ResponseCode.class, tags = "servers")
176-
public Response generateServerForLanguage(@Context HttpServletRequest request, @ApiParam(
177-
value = "framework", required = true) @PathParam("framework") String framework,
178-
@ApiParam(value = "parameters", required = true) GeneratorInput opts) throws Exception {
209+
public Response generateServerForLanguage(@Context HttpServletRequest request, @ApiParam(value = "framework", required = true) @PathParam("framework") String framework,
210+
@ApiParam(value = "parameters", required = true) GeneratorInput opts) throws ApiException {
179211
if (framework == null) {
180212
throw new BadRequestException("Framework is required");
181213
}
182214
String filename = Generator.generateServer(framework, opts);
183-
System.out.println("generated name: " + filename);
215+
LOGGER.info("Generated filename: {}", filename);
184216

185217
String host = getHost(request);
186218

@@ -190,7 +222,6 @@ public Response generateServerForLanguage(@Context HttpServletRequest request, @
190222
g.setFilename(filename);
191223
g.setFriendlyName(framework + "-server");
192224
fileMap.put(code, g);
193-
System.out.println(code + ", " + filename);
194225
String link = host + "/api/gen/download/" + code;
195226
return Response.ok().entity(new ResponseCode(code, link)).build();
196227
} else {

modules/swagger-generator/src/test/java/io/swagger/generator/resource/SwaggerResourceTest.java

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,114 @@
11
package io.swagger.generator.resource;
22

3+
import io.swagger.codegen.utils.SecureFileUtils;
4+
import io.swagger.generator.model.Generated;
5+
import org.apache.commons.io.FileUtils;
6+
import org.mockito.MockedStatic;
7+
import org.mockito.Mockito;
8+
import org.testng.annotations.AfterMethod;
9+
import org.testng.annotations.BeforeMethod;
310
import org.testng.annotations.Test;
411

12+
import javax.ws.rs.core.Response;
13+
import java.io.File;
14+
import java.lang.reflect.Field;
15+
import java.nio.charset.StandardCharsets;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
19+
import static org.testng.Assert.assertFalse;
20+
import static org.testng.AssertJUnit.assertEquals;
21+
522
public class SwaggerResourceTest {
623

24+
private SwaggerResource resource;
25+
private Map<String, Generated> fileMap;
26+
27+
@BeforeMethod
28+
public void setUp() throws Exception {
29+
resource = new SwaggerResource();
30+
31+
fileMap = new HashMap<>();
32+
Field fm = SwaggerResource.class.getDeclaredField("fileMap");
33+
fm.setAccessible(true);
34+
fm.set(null, fileMap);
35+
}
36+
37+
@AfterMethod
38+
public void after() {
39+
fileMap.clear();
40+
}
41+
42+
@Test
43+
public void shouldReturnSuccessWhenDownloadFileAndBadRequestAfterSecondTry() throws Exception {
44+
File dir = new File("target/testng-gen1");
45+
File zip = new File(dir, "client.zip");
46+
FileUtils.write(zip, "TESTDATA", StandardCharsets.UTF_8);
47+
48+
Generated g = new Generated();
49+
g.setFilename(zip.getAbsolutePath());
50+
g.setFriendlyName("clientX");
51+
52+
fileMap.put("123", g);
53+
54+
Response response = resource.downloadFile("123");
55+
56+
assertEquals(200, response.getStatus());
57+
assertEquals("TESTDATA", new String((byte[]) response.getEntity()));
58+
assertFalse(zip.exists(), "File should be removed after download.");
59+
assertFalse(dir.exists(), "Directory should be removed after download.");
60+
61+
Response response2 = resource.downloadFile("123");
62+
assertEquals(404, response2.getStatus());
63+
}
64+
65+
@Test
66+
public void shouldReturnNotFoundWhenFileDoesntExist() {
67+
Response response = resource.downloadFile("nope");
68+
assertEquals(404, response.getStatus());
69+
}
70+
71+
@Test(expectedExceptions = Exception.class)
72+
public void testDownloadFile_missingPhysicalFile_causes500() {
73+
Generated g = new Generated();
74+
g.setFilename("target/no_such_dir/file.zip");
75+
g.setFriendlyName("missing");
76+
77+
fileMap.put("777", g);
78+
79+
resource.downloadFile("777");
80+
}
81+
82+
@Test(expectedExceptions = Exception.class)
83+
public void shouldPathValidationFailsWhenDownloadFile() throws Exception {
84+
try (MockedStatic<SecureFileUtils> mocked = Mockito.mockStatic(SecureFileUtils.class)) {
85+
86+
mocked.when(() -> SecureFileUtils.validatePath(Mockito.anyString()))
87+
.thenThrow(new RuntimeException("Invalid path"));
88+
89+
File dir = new File("target/testng-gen2");
90+
File zip = new File(dir, "client.zip");
91+
FileUtils.write(zip, "XYZ", StandardCharsets.UTF_8);
92+
93+
Generated g = new Generated();
94+
g.setFilename(zip.getAbsolutePath());
95+
g.setFriendlyName("clientY");
96+
97+
fileMap.put("xyz", g);
98+
99+
resource.downloadFile("xyz");
100+
}
101+
}
102+
7103
@Test(expectedExceptions = SecurityException.class)
8104
public void testDownloadFileWithPathTraversal() throws Exception {
9-
SwaggerResource resource = new SwaggerResource();
10105

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

14109
java.lang.reflect.Field fileMapField = SwaggerResource.class.getDeclaredField("fileMap");
15110
fileMapField.setAccessible(true);
16-
@SuppressWarnings("unchecked")
17-
java.util.Map<String, io.swagger.generator.model.Generated> fileMap =
18-
(java.util.Map<String, io.swagger.generator.model.Generated>) fileMapField.get(null);
111+
19112
fileMap.put("test-file-id", generated);
20113

21114
try {

0 commit comments

Comments
 (0)