I am struggling to get HttpServletResponse response into Spring Boot’s REST controller. The purpose behind is that I would like to return a file stream from a REST controller.
Here is the code.
#Component
#Api(value = "/api/1/download", description = "act upon selected file.")
#Path("/api/1/download")
#Consumes(MediaType.APPLICATION_JSON)
#RestController
public class DownloadResource {
private final Logger log = LoggerFactory.getLogger(DownloadResource.class);
#Autowired
HttpServletResponse response;
#ApiOperation(value = "Download a selected file", notes = "allows to download a selected file")
#Path("/downloadFile")
#POST
#Autowired
public void download(Object fileObject) {
String name = (String) ((LinkedHashMap) fileObject).get("path");
response.setContentType("text/plain");
response.addHeader("Content-Disposition", "attachment; filename=abcd.txt");
try
{
Files.copy(Paths.get(name), response.getOutputStream());
response.getOutputStream().flush();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
Neither File is download nor it throwing an error. Please help suggest. Thank you.
Give this a try. Did I understand what you were trying to do?
#SpringBootApplication
#RestController
public class FiledownloadApplication {
public static void main(String[] args) {
SpringApplication.run(FiledownloadApplication.class, args);
}
#PostMapping("/downloadFile")
public ResponseEntity<FileSystemResource> download(#RequestBody FileDownload fileDownload) throws IOException {
String path = fileDownload.getPath();
FileSystemResource fileSystemResource = new FileSystemResource(path);
return new ResponseEntity<>(fileSystemResource, HttpStatus.OK);
}
class FileDownload {
String path;
public FileDownload() {
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
}
I believe that it should be like this.
public ResponseEntity<FileSystemResource> download(#RequestBody FileDownload fileDownload, HttpServletResponse response) throws IOException { ... }
And Spring will set the response object for you. What you were trying to do is to inject the bean "HttpServletResponse" which does not make much sense as it is not a bean.
According to this documentation https://docs.spring.io/spring-framework/docs/3.0.0.RC2/reference/html/ch15s03.html#mvc-ann-requestmapping-arguments
you can simply declare HttpServletResponse as an argument of your download method in arbitrary order, i.e.
public void download(Object fileObject, HttpServletResponse response) {
or for your specific needs you can directly reference OutputStream i.e.
public void download(Object fileObject, OutputStream out) {
When using either of these you should use void as return type of your method
Or another alternative worth a look would be to use
StreamingResponseBody
Related
I had developed a Rest API application and have handled Authentication and Authorization using custom JWT.
I want to further make the application secure from XSS attacks or validation for untrusted data which could be handled for each and every field of JSON request.
Can I get some help in this regard so that efficient data processing will happen at the entry-level of the request without touching internal business validation?
You don't filter or escape data in a restful API. API's should be client agnostic. It is the clients responsibility to provide XSS protection. If the clients are doing their job appropriately you will end up with doubly escaped data. Remember potential clients can be:
Mobile Apps
Backend Web Servers
Web Browsers
Desktop Applications
Embedded systems/ IoT
In the above only a limited number of clients and configurations are vulnerable to XSS.
Need to override the HttpServletRequest in a Servlet Filter(if you are using Servlet).
Extends HttpServletRequestWrapper that stores JSON body(intention is to sanitize JSON body).
Strip/ escape the eligible JSON value
Extented "HttpServletRequestWrapper" :
public class SanitizationRequestWrapper extends HttpServletRequestWrapper {
public byte[] getBody() {
return body;
}
public void setBody(byte[] body) {
this.body = body;
}
private byte[] body;
public SanitizationRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
try {
body = IOUtils.toByteArray(super.getInputStream());
}catch (NullPointerException e){
}
}
#Override
public ServletInputStream getInputStream() throws IOException {
return new ServletInputStreamImpl(new ByteArrayInputStream(body));
}
#Override
public BufferedReader getReader() throws IOException {
String enc = getCharacterEncoding();
if (enc == null) enc = "UTF-8";
return new BufferedReader(new InputStreamReader(getInputStream(), enc));
}
private class ServletInputStreamImpl extends ServletInputStream {
private InputStream is;
public ServletInputStreamImpl(InputStream is) {
this.is = is;
}
public int read() throws IOException {
return is.read();
}
public boolean markSupported() {
return false;
}
public synchronized void mark(int i) {
throw new RuntimeException(new IOException("mark/reset not supported"));
}
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
}
}
Servlet filter which sanitize request body:
public class XSSSanitizeFilters implements Filter {
#Override
public void destroy() {
}
#Override
public void doFilter(ServletRequest arg0, ServletResponse arg1, FilterChain arg2) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) arg0;
HttpServletResponse response = (HttpServletResponse) arg1;
SanitizationRequestWrapper sanitizeRequest = new SanitizationRequestWrapper(request);
if (null != sanitizeRequest.getBody()) {
try {
sanitizeJson(sanitizeRequest);
} catch (ParseException e) {
LOG.error("Unable to Sanitize the provided JSON .");
}
arg2.doFilter(sanitizeRequest, arg1);
} else {
arg2.doFilter(arg0, arg1);
}
}
public void init(FilterConfig filterConfig) throws ServletException {
}
private void sanitizeJson(SanitizationRequestWrapper sanitizeRequest ) throws IOException, ParseException {
JSONParser parser= new JSONParser();
Object obj = parser.parse(sanitizeRequest.getReader());
ObjectMapper oMapper = new ObjectMapper();
Map <String, Object> map = oMapper.convertValue(obj, Map.class);
sanitizeRequest.setBody((new JSONObject(map)).toString().getBytes());
}
For this you need XSS filter using HTMLUtils which will filter any injected script and prevent your site. Please refer my answer https://stackoverflow.com/a/55741351/10232467 for its complete code and implementation.
If your API doesn't accecpt any HTML Characters then you can follow the below logic.
You can Sanitize the Input Payload with EncodeHtml and Compare it with Provided Payload.
If both Sanitized Payload and Provided payload doesn't match then there exists some Html Content and straight way throw an Excpetion.
String unsanitizedPayload = IOUtils.toString(multiReadRequest.getReader());
String sanitizedPayload = Encode.forHtmlContent(unsanitizedPayload);
if(!unsanitizedPayload.equals(sanitizedPayload)) {
throw new Exception("Improper Payload");
}
If you're using Spring, Spring security guarantees basic level of protection against XSS attack. You can also use
#SafeHtml
private String value;
You will also need to add org.jsoup dependency.
In my project, I have a set of api calls which should filtered through certain set of common validation. In that case, I have to intercept the request before it hits the REST controller, read the request body, do the validations and pass it to the controller if the request passes the validations.
Since the HttpServletRequest cannot be deserialized more than once, I used a HttpServletRequestWrapper to make a copy of the actual request. Using the copy it makes, I do the validations.
Following is the configuration class for intercepting the requests.
public class InterceptorConfig extends WebMvcConfigurerAdapter {
#Autowired
CustomInterceptor customInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customInterceptor).addPathPatterns("/signup/**");
}
}
Here is my preHandle method inside CustomInterceptor class which extends HandlerInterceptorAdaptor
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ServletRequest copiedRequest = new HttpRequestWrapper(request);
Map<String, Object> jsonMap = mapper.readValue(copiedRequest.getInputStream(), Map.class);
if(jsonMap.containsKey("userId")){
long userId = jsonMap.get("userId");
MyClass myObject= myAutowiredService.getMyObject(userId);
if(myObject == null){
response.setStatus(HttpStatus.SC_NOT_ACCEPTABLE);
return false;
}
// some more validations which end up returning false if they are met
}
return true;
}
This is my HttpRequestWrapper
public class HttpRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody;
public HttpRequestWrapper(HttpServletRequest request) throws IOException{
super(request);
try {
requestBody = IOUtils.toByteArray(request.getInputStream());
} catch (IOException ex) {
requestBody = new byte[0];
}
}
#Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
#Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
public int read () throws IOException {
return byteArrayInputStream.read();
}
};
}
}
All set now. Now, when I send a request to any url with the pattern of /signup/**, all the validations are happening fine. However, once the request hits the controller method, error pops out saying the request body is not available.
Required request body is missing: public
com.mypackage.myResponseObject
com.mypackage.myController.myControllerMethod(com.mypackage.myDTO)
I am struggling to find the reason for this and also a way to overcome the issue. Is there anything I have done wrong in RequestWrapper class? or anything missing?
Help me to sort this thing out.
Thanks!
The Problem seems to be that you are using an Interceptor to read the HttpServletRequest's InputStream and just wrap it in HttpRequestWrapper but the wrapper is never returned.
I think you should use a Filter
public class CustomFilter extends OncePerRequestFilter {
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ServletRequest copiedRequest = new HttpRequestWrapper(request);
Map<String, Object> jsonMap = mapper.readValue(copiedRequest.getInputStream(), Map.class);
if(jsonMap.containsKey("userId")){
long userId = jsonMap.get("userId");
MyClass myObject= myAutowiredService.getMyObject(userId);
if(myObject == null){
response.setStatus(HttpStatus.SC_NOT_ACCEPTABLE);
//return false;
}
// some more validations which end up returning false if they are met
}
filterChain.doFilter(copiedRequest, (ServletResponse) response);
}
}
And you need to use this Filter in either web.xml or WebApplicationInitializer
I have a REST api that responds with some additional non JSON data in the body content. This breaks the use of RestTemplate and jackson. Can I intercept the http response body prior to the parsing?
I am using RestTemplate.getForObject.
I've taken a look at the RestTemplate and couldn't see an appropriate method.
You can try to implement ClientHttpRequestInterceptor and assign it to restTemplate. Implement intercept method:
#Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
ClientHttpResponse response=clientHttpRequestExecution.execute(httpRequest, bytes);
//...do magic with response body from getBody method
return response;
}
You might have to extend AbstractClientHttpResponse with your own implementation to do that.
Another option could be to treat the response from the REST API as String, then format the string as needed and explicitly map it to object using ObjectMapper.
Then in your restTemplate you would have:
String result = restTemplate.getForObject(url, String.class, host);
//..trim the extra stuff
MyClass object=objectMapper.readValue(result, MyClass.class);
Yet another option would be to implement your own HttpMessageConverter which extends AbstractJackson2HttpMessageConverter and register it with restTemplate. In my opinion that would be the cleaneast from the Spring point of view
Another way would be to unwrap the response by implementing a ClientHttpRequestInterceptor along with a ClientHttpResponse.
#Component
public class MyInterceptor implements ClientHttpRequestInterceptor {
#Autowired
Function<ClientHttpResponse, MyResponseWrapper> responseWrapperBeanFactory;
#Autowired
MyRequestAdvice requestAdvice;
#Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
byte[] wrappedBody = requestAdvice.wrapRequest(bytes);
ClientHttpResponse res = clientHttpRequestExecution.execute(httpRequest, wrappedBody);
return responseWrapperBeanFactory.apply(res);
}
}
Here's the bean config for the MyResponseWrapper:
#Bean
Function<ClientHttpResponse, MyResponseWrapper> responseWrapperBeanFactory() {
return this::getMyResponseWrapper;
}
#Bean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public MyResponseWrapper getMyResponseWrapper(ClientHttpResponse originalResponse) {
return new MyResponseWrapper(originalResponse);
}
#Bean
public RestTemplate restTemplate(#Autowired MyInterceptor interceptor) {
RestTemplate t = new RestTemplate();
t.setInterceptors(Arrays.asList(interceptor));
// other setup code
return t;
}
And here's the ClientHttpResponse implementation:
public class MyResponseWrapper implements ClientHttpResponse {
private byte[] filteredContent;
private ByteArrayInputStream responseInputStream;
private ClientHttpResponse originalResponse;
public MyResponseWrapper(ClientHttpResponse originalResponse) {
this.originalResponse = originalResponse;
try {
filteredContent = MyContentUnwrapper.unwrapResponse(originalResponse.getBody().readAllBytes());
} catch (Exception e) {
throw new RuntimeException("There was a problem reading/decoding the response coming from the service ", e);
}
}
#Override
public HttpStatus getStatusCode() throws IOException {
return originalResponse.getStatusCode();
}
#Override
public int getRawStatusCode() throws IOException {
return originalResponse.getRawStatusCode();
}
#Override
public String getStatusText() throws IOException {
return originalResponse.getStatusText();
}
#Override
public void close() {
if (responseInputStream != null) {
try {
responseInputStream.close();
} catch (IOException e) { /* so long */}
}
}
#Override
public InputStream getBody() throws IOException {
if (responseInputStream == null) {
responseInputStream = new ByteArrayInputStream(filteredContent);
}
return responseInputStream;
}
#Override
public HttpHeaders getHeaders() {
return originalResponse.getHeaders();
}
}
From your Controller you can try to return a ResponseEntity and manipulate the entity object manually
If you don't need these extra properties you may add:
#JsonIgnoreProperties(ignoreUnknown = true)
to your mapping class.
From docs:
Property that defines whether it is ok to just ignore any unrecognized
properties during deserialization. If true, all properties that are
unrecognized -- that is, there are no setters or creators that accept them
-- are ignored without warnings (although handlers for unknown properties,
if any, will still be called) without exception.
Does not have any effect on serialization.
I wanted to get the HTML Content of processed JSP from controller. I am using Tiles Implementation for Views.
In my scenario, I want to generate the HTML from jsp and send it as JSONP.
So i need to get hold of Generated html from controller.
Here you go!
This worked perfect for me:
#Service
public class CustomViewProcessor
{
private static Logger m_log = LoggerFactory.getLogger(CustomViewProcessor.class);
#Autowired
#Qualifier("tilesViewResolver")
private ViewResolver viewResolver;
#Autowired
#Qualifier("commonInterceptor")
CommonInterceptor commonInterceptor;
public String renderView(HttpServletRequest request, Model model, String viewName)
{
ModelAndView mav = new ModelAndView();
mav.addAllObjects(model.asMap());
mav.setViewName(viewName);
commonInterceptor.updateRequest(mav, request);
try
{
View view = viewResolver.resolveViewName(mav.getViewName(), request.getLocale());
HttpServletResponse localResponse = new MyHttpServletResponseWrapper(new MockHttpServletResponse());
view.render(mav.getModel(), request, localResponse);
return localResponse.toString();
}
catch (Exception e)
{
return "";
}
}
public boolean doesViewExist(HttpServletRequest request, String viewName)
{
try
{
if (viewResolver.resolveViewName(viewName, request.getLocale()) != null)
{
return true;
}
}
catch (Exception e)
{
m_log.error(e.getMessage(), e);
}
return false;
}
static class MyHttpServletResponseWrapper extends HttpServletResponseWrapper
{
private StringWriter sw = new StringWriter();
public MyHttpServletResponseWrapper(HttpServletResponse response)
{
super(response);
}
public PrintWriter getWriter() throws IOException
{
return new PrintWriter(sw);
}
public ServletOutputStream getOutputStream() throws IOException
{
throw new UnsupportedOperationException();
}
public String toString()
{
return sw.toString();
}
}
}
You need to annotate your controller method with #ResponseBody. Please look at the documentation for more details
Control flow leaves the DispatcherServlet and moves to the Servlet Container (e.g. Tomcat) before the JSP page template is populated. So Spring MVC will never have visibility into the final HTML generated from the JSP template.
If you want to pipe your final HTML output into a JSONP request, you're going to do have to implement that as a filter in your Servlet Container, which may or may not support that behavior.
All of my controllers extend the following abstract class:
public abstract class AbstractController {
public HttpServletRequest request;
public HttpServletResponse response;
public ModelMap model;
}
Moreover, I implemented the following interceptor:
public class HttpRequestInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException {
if (handler instanceof AbstractController) {
AbstractController controller = (AbstractController) handler;
controller.request = request;
controller.response = response;
controller.model = new ModelMap();
}
return true;
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
if (handler instanceof AbstractController && modelAndView != null) {
AbstractController controller = (AbstractController) handler;
modelAndView.addAllObjects(controller.model);
}
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
This is a solution I found to improve the factorization of my code, since you won't need to pass the request, the response and the model as method parameters within your controllers. The solution works fine, until I found this issue:
public class HomeController extends AbstractController {
#RequestMapping
public void download1() {
// use the parent attribute response
File file = new File(MY_FILE_PATH);
InputStream in = new BufferedInputStream(new FileInputStream(file));
ServletOutputStream out = response.getOutputStream();
IOUtils.copy(in, out);
response.flushBuffer();
}
#RequestMapping
public void download2(HttpServletResponse response) {
// use the response passed as parameter
File file = new File(MY_FILE_PATH);
InputStream in = new BufferedInputStream(new FileInputStream(file));
ServletOutputStream out = response.getOutputStream();
IOUtils.copy(in, out);
response.flushBuffer();
}
}
Both of the two methods above make the browser downloading a file, but the download1 one generated an empty file while the download2 generates the original file as it should. Any idea why?
Thanks to the debugger, I noticed that in the postHandle method of the interceptor, the download2 method generates a modelAndView which equals null, while the download1 one generated an instanciated one. This should mean something for the issue, but I can't find what.
How get a response instanciated when passed as a parameter of a controller's method?
Don't do this :
public abstract class AbstractController {
public HttpServletRequest request;
public HttpServletResponse response;
public ModelMap model;
}
Instance variables in controllers (which have default scope of singleton btw) is a bad idea.
Just make something like this (to a txt file):
#RequestMapping(value="/download", method=RequestMethod.GET, produces=MediaType.APPLICATION_OCTET_STREAM_VALUE)
#ResponseBody
public String download(HttpServletResponse response) throws IOException {
response.setContentType("application/force-download");
FileReader fr = new FileReader("/folder/file.extension");
return IOUtils.toString(fr); // IOUtils come from Apache Commons IO
}