spring mvc Proxy server - maintaining url context (contextpath) - java

I have the below piece spring REST controller class.
#RestController
#RequestMapping("/global")
public class ProxyController extends BaseController{
#RequestMapping(value = "/**")
public ResponseEntity<String> proxy(HttpServletRequest request, HttpServletResponse response ) throws Exception {
try {
String restOfTheUrl = (String) request.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
URL uri = new URL("https://myrealserver" +
restOfTheUrl);
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> httpEntity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
return resp;
} catch (Exception e) {
logger.error("Error ", e);
return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
What I am trying to achieve here is to hide a server behind a proxy, which blindly forwards requests to the server.
This piece of code is invoked with url
https://myproxyserver/myapp1/end/point1
which in turn returns an html page with few clickable links. Now when the user clicks I am expecting the link to be invoked as
https://myproxyserver/myapp1/end/point2
Where as actually the endpoint invoked is
https://myproxyserver/end/point2
In the html page returned by the actual server, the path is end/point2 and has no mention of myapp1. So on click on those links my context changes to https://myproxyserver/end/point2 instead of https://myproxyserver/myapp1/end/point2
How do I ensure that the root context is always https://myproxyserver/myapp1 and not https://myproxyserver ?

You want to get your server context path. this is sample code.
like this :
public static String getServerNameAndContextPath(HttpServletRequest req) {
return "https://" + req.getServerName() + req.getContextPath();
}

Finally I resolved the problem by taking what D D suggested. I scanned through the whole response body, fortunately I had a pattern that I could use to scan and appended the context of the url to where ever required. That resolved the problem for me this problem.

Related

How to pick values from a redirected_url using Springboot

I want to be able to fetch a param from the redirect url whenever it is automated. I am having difficulties doing this as I am getting a bad request after I created another endpoint to effect this.
I have an endpoint that works fine. The endpoint is a get method. Loading the endpoint takes a user to a page where they need to provide some necessary details. Once these details have been verified, the user is redirected to my redirecr_uri. The redirect_uri now contains important information like session_id, code, etc. The most important thing I need is the code. I need to pass the code into yet another endpoint which will return an access token.
I have manually done this process and it works but I want it to be done automatically because I can't keep doing that when I push the code to staging or production.
Here is the endpoint that redirects as well as the method.
#GetMapping("/get-token")
public RedirectView getBvn() throws UnirestException {
return nibss.getAccessToken();
}
This is the method that the controller calls
public RedirectView getAccessToken() throws UnirestException {
String url = "https://idsandbox.nibss-plc.com.ng/oxauth/authorize.htm?scope=profile&acr_values=otp&response" +
"_type=code&redirect_uri=https://www.accionmfb.com/&client_id=0915cd00-67f2-4768-99ac-1b2ff9f1da2e";
RedirectView redirectView = new RedirectView();
redirectView.setUrl(url);
return redirectView;
}
When the user provides the right information they are redirected to something like this
https://www.accionmfb.com/?code=9ad91f13-4698-4030-8a8f-a857e6a9907e&acr_values=otp&scope=profile&session_state=fa525cabc5b62854c73315d0322fd830c12a5941b89fd8e6e518da369e386572.b78a3d21-e98e-4e9a-8d60-afca779d9fad&sid=fd60ab92-ef37-4a5b-99b9-f8f52321985d
It is important to state that this 3rd party API I am trying to consume uses oath2.0 client authentication.
I created this endpoint to get the code from the redirected_uri
#GetMapping("/redirect-url")
public void handleRedirect(#RequestParam("code") String code) throws UnirestException {
if(Objects.nonNull(code) || !code.isEmpty()){
nibss.getToken(code);
log.info("Code is not being passed {}", code);
} else {
log.info("Code is not being passed {}", code);
}
}
public String getToken(String code) throws UnirestException {
log.info("This is the code here oooooooooo {}", code);
String url = "https://idsandbox.nibss-plc.com.ng/oxauth/restv1/token";
String parameters = "client_id=0915cd00-67f2-4768-99ac-1b2ff9f1da2e&code="+code+"&redirect_uri=https://www.accionmfb.com/&grant_type=authorization_code";
HttpResponse<String> apiResponse = Unirest.post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Authorization", "Basic MDkxNWNkMDAtNjdmMi00NzY4LTk5YWMtMWIyZmY5ZjFkYTJlOlRVRnEwcGFBQXRzbzBZOEcxMkl2WFZHUmx6WG5zaERiaGt1dzI1YUM=")
.body(parameters)
.asString();
//JSONObject apiJson = apiResponse.getBody().getObject();
//return apiJson.getString("access_token");
JSONObject json = new JSONObject(apiResponse.getBody());
String accessToken = json.getString("access_token");
log.info(accessToken);
return accessToken;
}
But this is not working, I get 400 whenever I hit the second endpoint. What am I doing wrong?
The redirect_uri that you are passing to the OAuth server is https://www.accionmfb.com which does not include the path /redirect-url so the redirect never hits your method.
Either register and pass a callback uri like redirect_uri=https://www.accionmfb.com/redirect-url
Or change #GetMapping("/redirect-url") to #GetMapping("/")

SSRF Vulnerability while calling REST API

I am using a method where it calls another REST API to retrieve an ID from the DB. When I run the veracode scan for the class I am getting Security flaw "Server-side Request Forgery" at below line.
response = resttemplate.getForEntity(resturl, String.class);
Not sure How to fix this issue. Any help is appreciated. Below is my full code for that method.
public static String getIDFromDB(String resturl) {
String id = null;
RestTemplate resttemplate = new RestTemplate();
ResponseEntity<String> response = new ResponseEntity<>(HTTPStatus.OK)
try {
response = resttemplate.getForEntity(resturl, String.class);
if (response.getStatusCode == HTTPStatus.OK && response.getBody.trim() != null) {
id = response.getBody.trim() ;
}
} Catch(Exception e) {
log.error("failed to get msgID: {}", e);
}
}
This is because you are allowing in your code to pass the resturl completely in your code, so it enables the attacker to bypass and route the URL to their intended destination.
To avoid this, so should externalise and refer the URL having domain and the application contexts with operation name in config files or dB

Using RestTemplate in spring-boot returns with 404

I am trying to send a body in a post request in a springboot application using rest template. Here is the controller:(I removed #RequestBody because I used application/x-www-form-urlencoded header)
#RestController
#CrossOrigin
#RequestMapping("/api")
public class SentimentParserController {
#Autowired
private SentimentParserService sentimentParserService;
#RequestMapping(value = "/something", method = RequestMethod.POST, consumes="application/x-www-form-urlencoded")
public ResponseEntity<mcResponse>getTheSentiments( mcSentimentRequestDTO sentimentRequestDTO){
return sentimentParserService.getSentimentsMc(sentimentRequestDTO);
}
}
I want to send the sentimentRequestDTO object(lang, key, and text) as the body in a post request to get the mcResponse:
public mcResponse parseTheSentiments(String text, Languages lang, String key) throws Exception {
RestTemplate restTemplate = new RestTemplate();
String request = "http://localhost:8080";
mcSentimentRequestDTO mSentiments =new mcSentimentRequestDTO(key,"EN",text);
HttpHeaders headers = new HttpHeaders();
headers.add("content-type", "application/x-www-form-urlencoded");
MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
map.add("key", key);
map.add("txt", text);
map.add("lang", Languages.ENGLISH.toString());
HttpEntity<MultiValueMap<String, String>> request1 = new HttpEntity<MultiValueMap<String, String>>(map, headers);
mcResponse response = restTemplate.postForObject(request, request1 , mcResponse.class );
return response;
}
However, I am getting the following error: 404 null.
Can you please help me? Thanks in advance
and here is the service class:
public ResponseEntity<mcResponse> getSentimentsMc(mcSentimentRequestDTO sentimentRequestDTO){
ResponseEntity<mcResponse> dto = null;
try {
dto = sentimentConverter.getTheSentiments(mcsParser.parseTheSentiments(sentimentRequestDTO.getText(),
Languages.ENGLISH, sentimentRequestDTO.getKey()));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return dto;
}
Looks like variable request should be
String request = "http://localhost:8080/something";
Also if controller class has prefix, this prefix also should be in request.
I mean if your class looks like this
#RestController
#RequestMapping("/myApi")
public class CertificateController {
....
#RequestMapping(value = "/something", method = RequestMethod.POST)
public ResponseEntity<mcResponse>getTheSentiments( mcSentimentRequestDTO sentimentRequestDTO){
return sentimentParserService.getSentimentsMc(sentimentRequestDTO);
}
Then request should be
String request = "http://localhost:8080/myApi/something";
It sounds like the controller isn't getting included in the spring context. If you just have an app annotated with #SpringBootApplication, then make sure that your controller is in a package that is the same as or lower than your annotated application.
To check the controller is being picked up you can add the following logging options to your application.properties
logging.level.org.springframework.beans=debug
logging.level.org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping=trace
When your server starts up you should see something like the following in the log
1. To show the controller is in the spring-context
DefaultListableBeanFactory : Creating shared instance of singleton bean 'sentimentParserController'
2. To show the mapping for the /api/something url
RequestMappingHandlerMapping : Mapped 1 handler method(s) for class SentimentParserController: {public org.springframework.http.ResponseEntity SentimentParserController.getTheSentiments(mcSentimentRequestDTO)={[/api/something],methods=[POST]}}
If you see both of these, then what you say you're doing should work. Just make sure you are sending the request to /api/something and the server is running on port 8080.

Java spring file download proxy with rest calls

I need to create a rest service in java which will in turn connect to another rest service for file download. For now, I just need to transfer the file from the other backend to client but in future some processing/transformations would be done.
For all the web services in my project, we are using spring rest (for providing as well as consuming the services).
My question is what would be the appropriate way of doing it considering that the files would be large and I don't want to run into OutOfMemory errors.
People in some other posts have suggested to use streams on both the ends but is that really possible? For this, do I need to write the file on disk first?
My current code for file download (consumer) -
public BackendResponse<byte[]> callBackendForFile(BackendRequest request) {
String body = null;
ResponseEntity<byte[]> responseEntity = null;
URI uri = createURI(request);
MultiValueMap<String, String> requestHeaders = getHeadersInfo(request.getHttpRequest());
if (HttpMethod.GET.equals(request.getMethod())) {
responseEntity = restTemplate.exchange(uri, request.getMethod(),
new HttpEntity<String>(body, requestHeaders), byte[].class);
} else {
LOG.error("Method:{} not supported yet", request.getMethod());
}
BackendResponse<byte[]> response = new BackendResponse<>();
response.setResponse(responseEntity);
return response;
}
My client code (provider):
#RequestMapping(value = "/file", method = RequestMethod.GET, produces = "application/xml")
#ResponseBody
public void downloadFileWithoutSpring(HttpMethod method, HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
BackendRequest request = new BackendRequest(method,
httpRequest.getRequestURI(), httpRequest.getQueryString(), httpRequest);
BackendResponse<byte[]> backendResponse = dutyplanService.getFile(request);
ResponseEntity<byte[]> response = backendResponse.getResponse();
httpResponse.addHeader("Content-Disposition", "attachment; filename=\"" + "attachment.zip" + "\"");
httpResponse.getOutputStream().write(response.getBody());
httpResponse.flushBuffer();
}
Note: The code above doesn't work somehow as the attachment downloaded is a corrupt file
I don't think you will need to create that file on server as long as you are having the bytearray content of it received from another server.
You can try changing value of produces annotation to the value application/zip (or application/octet-stream, depending on the target browser) instead of 'application/xml'
you can pass HttpServletResponse#getOutputStream() directly in restTemplate and write it without save file in server.
public void getFile(HttpServletResponse response) throws IOException {
restTemplate.execute(
"http://ip:port/temp.csv",
HttpMethod.GET,
null,
clientHttpResponse -> {
StreamUtils.copy(clientHttpResponse.getBody(), response.getOutputStream());
return null;
}
);
}
note that after call getFile(), you should close outputStream like this
response.getOutputStream().close()

Using #RequestBody and forwarding to another endpoint throws exception Stream closed

My Java spring REST API controller looks like this:
public void signup(#RequestBody RequestBody requestBody) throws IOException, ServletException {
I get this exception:
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Stream closed; nested exception is java.io.IOException: Stream closed
This happens because I want to cast the request body to RequestBody class (which opens the request input stream and finishes it), and also forward/redirect it to another endpoint.
The actual controller is:
#RequestMapping(value = "/signup", method = RequestMethod.POST)
public void signup(#RequestBody CustomUserDetails user, HttpServletRequest request, HttpServletResponse response) {
String userName = user.getUsername();
logger.debug("User signup attempt with username: " + userName);
try{
if(customUserDetailsService.exists(userName))
{
logger.debug("Duplicate username " + userName);
userName + " already exists");
String newUrl = "login";
RequestDispatcher view = request.getRequestDispatcher(newUrl);
view.forward(request, response);
} else {
customUserDetailsService.save(user);
authenticateUserAndSetSession(user, response);
}
} catch(Exception ex) {
}
}
How should I handle this ?
You can forward to login page in a ExceptionHandler,like this:
#RequestMapping(value = "/signup", method = RequestMethod.POST)
public void signup(#RequestBody CustomUserDetails user, HttpServletResponse response) {
String userName = user.getUsername();
logger.debug("User signup attempt with username: " + userName);
//try{
if (customUserDetailsService.exists(userName)) {
logger.debug("Duplicate username " + userName);
throw new SignupException(userName + " already exists");
} else {
customUserDetailsService.save(user);
authenticateUserAndSetSession(user, response);
}
/*} catch(Exception ex) {
}*/
}
define a ExceptionHandler in the same Controller:
#ExceptionHandler(SignupException.class)
public String duplicateName() {
return "login";
}
and the SignupException could be like this:
public class SignupException extends RuntimeException {
public SignupException(String message) {
super(message);
}
public SignupException() {
}
}
The request body object is a stream which can be read only once. So forwarding it is not very trivial. One way around this is to create a filter which reads the input steam and replace the input stream to something which can be read multiple times. Example can be found at another answer:
How can I read request body multiple times in Spring 'HandlerMethodArgumentResolver'?
As for your method, there is also another problem:
public void signup(#RequestBody RequestBody requestBody)
As far as I know, RequestBody is an annotation and you can't map it like that. But to get the raw data, you can map it as String.
public void signup(#RequestBody String requestBody)
And then you can just manually make an REST call to the api you want to forward it to using the String request body. Just make sure you set the content-type as the original one, which I assume in this case would be JSON.
Root of your problem is using #RequestBody RequestBody requestBody together with HttpServletRequest request.
Opening input stream twice on the same request is not allowed. In your case a system should open a input stream to extract request body and then propagate in forward to reuse.
To handle it you should avoid multiple usage of the same request stream. Possible solutions are:
Wrap request
Copy request body
Use spring native forward
I think you are trying to forward to a url with the RequestBody, please have a check
Spring 3.2 forward request with new object for the answers.
Create the object and add it to the request as an attribute in the first controller,
request.setAttribute("user",user),
return "forward:/login";
Try putting in request mapping consumes= {" application/json"}, produces={"application/json"}
RequestBody is an annotation to process your request object as expected class deserialization. They help you avoid boilerplate code by extracting the logic of messageconversion and making it an aspect.
You can not get RequestBody directly as an object in any controller. It was not designed to use this way.
Though it is not possible to get RequestBody in a RestController so you can't judge whether it is good or bad practice.
If you have to use new controller/endpoint to process.Then i think the better approach is to get the CustomUserDetails as #RequestBody in the controller then process it. Then call nested method or service to further process instead of thinking forwarding to another controller. Then return response from the controller.

Categories