Jersey PreMatching Filter : Adding parameters to modified URI - jersey

I have a rest api. Eg :
http://localhost:8080/api/user/view?name=user&lastname=demo
I want to modify my URI to maintain version.
I want to add '/v1/' between '/user/view'
So that my URI will look like as follows :
http://localhost:8080/api/user/v1/view?name=user&lastname=demo
I am able to modify my URI and create a new one , but I am not understanding how to pass parameters to the modified URI.
The following is my code :
#Provider
#PreMatching
public class RewriteUrl implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
UriInfo uriInfo = requestContext.getUriInfo();
String path = uriInfo.getPath();
MultivaluedMap<String, String> parameters = uriInfo.getQueryParameters();
path=path.replaceFirst("/","/v1/");
URI baseUri = uriInfo.getBaseUriBuilder().path(path).build();
URI requestUri = uriInfo.getRequestUri();
requestContext.setRequestUri(URI.create(baseUri.toString()));
}
}
Using the above code I am getting the correct URI :
http://localhost:8080/api/user/v1/view
But I am not understanding how to pass the parameters to the new URI.
I also want to know that is this the right and secure way to do this ?Or I am doing wrong.
Please let me know if their is a better way to do this.(adding 'v1' in the URI).
The following I found while debugging :
PRE - MATCHING FILTER
uriInfo.getRequestUri().toString() http://localhost:8080/api/user/view?name=user&lastname=demo
uriInfo.getAbsolutePath().toString() http://localhost:8080/api/user/view
uriInfo.getBaseUri().toString() http://localhost:8080/api/
uriInfo.getPath().toString() user/view
parameters.toString() {lastname=[demo], name=[user]}
POST FILTER
uriInfo.getRequestUri().toString() http://localhost:8080/api/user/v1/view
uriInfo.getAbsolutePath().toString() http://localhost:8080/api/user/v1/view
uriInfo.getBaseUri().toString() http://localhost:8080/api/
uriInfo.getPath().toString() user/v1/view
parameters.toString() {}

Not sure if still relevant, I also had to implement this recently.
Here is my solution:
#Provider
#PreMatching
public class RewriteUrl implements ContainerRequestFilter {
#Override
public void filter(ContainerRequestContext requestContext) throws
IOException {
UriInfo uriInfo = requestContext.getUriInfo();
String path = uriInfo.getPath();
path = path.replaceFirst("/","/v1/");
UriBuilder uriBuilder = UriBuilder
.fromUri(uriInfo.getBaseUri())
.path(path)
.replaceQuery(uriInfo.getRequestUri().getQuery());
requestContext.setRequestUri(uriBuilder.build());
}
}

Related

Not able to call SOAP API in WebServiceGatewaySupport by Spring WebServiceTemplate - Need help to fix this issue

I am trying to call SOAP API in Java Spring Boot using WebServiceGatewaySupport by Spring WebServiceTemplate
Config java class
public WebServiceTemplate createWebServiceTemplate(Jaxb2Marshaller marshaller, ClientInterceptor clientInterceptor) {
WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
//SOAP URL
webServiceTemplate.setDefaultUri("http://host/Services.asmx");
//Auth ---It seems issue is here only????? need to check
webServiceTemplate.setMessageSender(new Authentication());
webServiceTemplate.setMarshaller(marshaller);
webServiceTemplate.setUnmarshaller(marshaller);
webServiceTemplate.afterPropertiesSet();
webServiceTemplate.setCheckConnectionForFault(true);
webServiceTemplate.setInterceptors((ClientInterceptor[]) Arrays.asList(createLoggingInterceptor()).toArray());
return webServiceTemplate;
}
SOAP Client Call
public class TicketClient extends WebServiceGatewaySupport {
public String getTicket(Ticket req) {
System.out.println("test inside webservice support1");
response = (AcquireTicketResponse) getWebServiceTemplate().marshalSendAndReceive(req);
Authentication Class
public class Authentication extends HttpUrlConnectionMessageSender {
#Override protected void prepareConnection(HttpURLConnection connection) {
String userpassword = username+":"+password+":"+domain;
String encoded =
Base64.getEncoder().withoutPadding().encodeToString(userpassword.getBytes(StandardCharsets.UTF_8));
connection.setRequestProperty("Authorization", "Basic "+encoded); connection.setRequestProperty("Content-Type", "application/xml"); super.prepareConnection(connection);
}
Not using Authetication class and add the above into
ClientInterceptor
public class SoapLoggingInterceptor implements ClientInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
String username="test";
String password="test";
String domain = "#test";
String userpassword = username+":"+password+domain;
String encoded = Base64.getEncoder().withoutPadding().encodeToString(userpassword.getBytes(StandardCharsets.UTF_8));
messageContext.setProperty("Authorization", "Basic "+encoded);
messageContext.setProperty("Content-type", "XML");
Case -1 --->When I passed (user, pwd, domain and content-type) through messagesender, content type is taking but throwed "BAD REQUEST ERROR 400"....When i comment contenttype property, then it throwed "INTERNAL SERVER ERROR 500".
Case-2...when I passed (user, pwd, domain and content-type) through ClientInterceptor , always it throwed "INTERNAL SERVER ERROR 500"......It seems Authentication properties for the service are not going to API call.............................Please suggest some options
Both the cases, Authentication is not passing to service, if i comment,Authentication code (userid/pwd/domain) in both cases also...no efforts in output
After setting the user ID/pwd
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
String username="test";
String password="test";
String domain = "#test";
String userpassword = username+":"+password+domain;
byte[] userpassword = (username+":"+password).getBytes(StandardCharsets.UTF_8);
String encoded = Base64.getEncoder().encodeToString(userpassword);
ByteArrayTransportOutputStream os = new
ByteArrayTransportOutputStream();
try {
TransportContext context = TransportContextHolder.getTransportContext();
WebServiceConnection conn = context.getConnection();
((HeadersAwareSenderWebServiceConnection) conn).addRequestHeader("Authorization", "Basic " + encoded);
} catch (IOException e) {
throw new WebServiceIOException(e.getMessage(), e);
}
First of all don't set the content type Spring WebServices will do that for you, messing around with that will only make things worse.
You should get the WebServiceConnection and cast that to a HeadersAwareSenderWebServiceConnection to add a header.
public class BasicAuthenticationInterceptor implements ClientInterceptor {
#Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
String username="test#test";
String password="test";
byte[] userpassword = (username+":"+password).getBytes(UTF_8);
String encoded = Base64.getEncoder().encodeToString(userpassword);
WebServiceConnection conn = TransportContext.getConnection();
((HeadersAwareSenderWebServiceConnection) conn).addHeader("Authorization", "Basic " + encoded);
}
}
You also need to configure it. Assuming it is a bean don't call afterPropertiesSet (and ofcourse you are now using the ClientInterceptor remove the new Authentication() for your customized message sender.
The List<ClientInterceptor> will automatically create a list with all the interceptors so you can easily inject them.
#Bean
public WebServiceTemplate createWebServiceTemplate(Jaxb2Marshaller marshaller, List<ClientInterceptor> clientInterceptors) {
WebServiceTemplate webServiceTemplate = new WebServiceTemplate(marshaller);
//SOAP URL
webServiceTemplate.setDefaultUri("http://host/Services.asmx");
webServiceTemplate.setCheckConnectionForFault(true);
webServiceTemplate.setInterceptors(clientInterceptors);
return webServiceTemplate;
}
If this doesn't work there is something else you are doing wrong and you will need to get in touch with the server developers and get more information on the error.
Update:
Apparently you also need to provide a SOAP Action in your request, which you currently don't. For this you can specify the SoapActionCallback in the marshalSendAndReceive method. Which action to specify you can find in the WSDL you are using.
SoapActionCallback soapAction = new SoapActionCallback("SoapActionToUse");
response = (AcquireTicketResponse) getWebServiceTemplate().marshalSendAndReceive(req, soapAction);

Spring mvc uri path matching regular expression

I was hoping to match the mapping for /some-slug-1234 in following request mapping but it says no mapping found. What I am doing wrong ?
#Controller
public class MyRequestHandlerController {
#RequestMapping(value = "/[^/]+\\-[0-9]+", method = { RequestMethod.HEAD, RequestMethod.GET } )
public void handleDocument(HttpServletRequest request, HttpServletResponse response){
// render article
}
}

Spring MVC #RestController and redirect

I have a REST endpoint implemented with Spring MVC #RestController. Sometime, depends on input parameters in my controller I need to send http redirect on client.
Is it possible with Spring MVC #RestController and if so, could you please show an example ?
Add an HttpServletResponse parameter to your Handler Method then call response.sendRedirect("some-url");
Something like:
#RestController
public class FooController {
#RequestMapping("/foo")
void handleFoo(HttpServletResponse response) throws IOException {
response.sendRedirect("some-url");
}
}
To avoid any direct dependency on HttpServletRequest or HttpServletResponse I suggest a "pure Spring" implementation returning a ResponseEntity like this:
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(newUrl));
return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);
If your method always returns a redirect, use ResponseEntity<Void>, otherwise whatever is returned normally as generic type.
Came across this question and was surprised that no-one mentioned RedirectView. I have just tested it, and you can solve this in a clean 100% spring way with:
#RestController
public class FooController {
#RequestMapping("/foo")
public RedirectView handleFoo() {
return new RedirectView("some-url");
}
}
redirect means http code 302, which means Found in springMVC.
Here is an util method, which could be placed in some kind of BaseController:
protected ResponseEntity found(HttpServletResponse response, String url) throws IOException { // 302, found, redirect,
response.sendRedirect(url);
return null;
}
But sometimes might want to return http code 301 instead, which means moved permanently.
In that case, here is the util method:
protected ResponseEntity movedPermanently(HttpServletResponse response, String url) { // 301, moved permanently,
return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, url).build();
}
As the redirections are usually needed in a not-straightforward path, I think throwing an exception and handling it later is my favourite solution.
Using a ControllerAdvice
#ControllerAdvice
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {
NotLoggedInException.class
})
protected ResponseEntity<Object> handleNotLoggedIn(
final NotLoggedInException ex, final WebRequest request
) {
final String bodyOfResponse = ex.getMessage();
final HttpHeaders headers = new HttpHeaders();
headers.add("Location", ex.getRedirectUri());
return handleExceptionInternal(
ex, bodyOfResponse,
headers, HttpStatus.FOUND, request
);
}
}
The exception class in my case:
#Getter
public class NotLoggedInException extends RuntimeException {
private static final long serialVersionUID = -4900004519786666447L;
String redirectUri;
public NotLoggedInException(final String message, final String uri) {
super(message);
redirectUri = uri;
}
}
And I trigger it like this:
if (null == remoteUser)
throw new NotLoggedInException("please log in", LOGIN_URL);
if you #RestController returns an String you can use something like this
return "redirect:/other/controller/";
and this kind of redirect is only for GET request, if you want to use other type of request use HttpServletResponse

Implementing a JSONP interceptor in Jersey

I would like to implement a JsonP interceptor, and I'm using Jersey.
(I am using AsyncResponses with Long-polling, and my REST method returns 'void' therefore I can't annotate it with #JSONP)
My problem is I don't know how to get the query params. I need to know the 'callback' method name.
I also tried a regular Servlet filter. It worked, but strangely I got methodname() {my json} instead of
medhodname({my json}).
So I tried the Jersey way. Seems like I need a WriterInterceptor, but how do I get the query param?
Here's my code:
#Provider
public class JsonpResponseFilter implements WriterInterceptor {
#Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
final String callback = (String)context.getProperty("callback");
if (null != callback) {
context.getOutputStream().write((callback+"(").getBytes());
}
context.proceed();
if (null != callback) {
context.getOutputStream().write(')');
}
}
}
Edit:
I found a way to get the query params, but it seems like hacking to me (see below). There's got to be something simpler or more elegant. Any ideas?
#Provider
public class JsonpResponseFilter implements WriterInterceptor {
#Override
public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
final ServiceLocator locator = ServiceLocatorClientProvider.getServiceLocator(context);
ContainerRequestContext tmp = locator.getService(ContainerRequestContext.class);
List<String> callbacks = tmp.getUriInfo().getQueryParameters().get("callback");
String callback = (null == callbacks)? null:callbacks.get(0);
...
A less hacky solution is to inject a Provider as a field in your class:
#Inject
private Provider<ContainerRequest> containerRequestProvider;
From here, you can access the query params like this:
final ContainerRequest containerRequest = containerRequestProvider.get();
final UriInfo uriInfo = containerRequest.getUriInfo();
final MultivaluedMap<String, String> queryParameters = uriInfo.getQueryParameters();
final List<String> queryParameter = queryParameters.get("q");
...

Encoded slash (%2F) with Spring RequestMapping path param gives HTTP 400

This is not a duplicate referenced question, because it is Spring specific. Whoever added that (3 years after the fact!) didn't bother to read the question or comment thread to see what the real answer was. The accepted answer isn't quite the answer, but the author of the answer never came back and edited it like I asked.
Given the restful method below, Spring 3.1 gives a 400 error with "The request sent by the client was syntactically incorrect ()." when the token parameter contains a URL encoded slash (%2F), for example "https://somewhere.com/ws/stuff/lookup/resourceId/287559/token/R4o6lI%2FbBx43/userName/jim" Without the %2F everything works fine. A 3rd party is already calling this service (of course!) so I can't change what they send, in the short term at least. Any ideas on how to work around this on the server side?
This problem is described very well here https://jira.springsource.org/browse/SPR-8662 though that issue is related to UriTemplate which I am not using that I can tell.
#RequestMapping("/ws/stuff/**")
#Controller
public class StuffController {
#RequestMapping(value = "/ws/stuff/lookup/resourceId/{resourceId}/token/{token}/userName/{userName}", method = RequestMethod.GET)
public #ResponseBody
String provisionResource(#PathVariable("resourceId") String resourceId, #PathVariable("token") String token, #PathVariable("userName") String userName, ModelMap modelMap,
HttpServletRequest request, HttpServletResponse response) {
return handle(resourceId, userName, request, token, modelMap);
}
}
Note: This is on Glassfish 3.1.2, and at first it was Grizzly/Glassfish not accepting the slash, but
-Dcom.sun.grizzly.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
fixed that.
asadmin set configs.config.server-config.network-config.protocols.protocol.http-listener-2.http.encoded-slash-enabled=true
didn't seem to help.
for spring-boot, the following did the trick
#SpringBootApplication
public class Application extends WebMvcConfigurerAdapter {
public static void main(String[] args) throws Exception {
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
SpringApplication.run(Application.class, args);
}
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setUrlDecode(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
This could be your answer: urlencoded Forward slash is breaking URL
I would suggest not putting that in the path, move it to a request param instead.
Work around:
You could change the RequestMapping to
#RequestMapping(value = "/ws/stuff/lookup/resourceId/**", method = RequestMethod.GET)
and then parse the path variables manually from the request object.
2019 Update for Spring Boot 2+ / Spring (Security) 5+ / Java 8+:
As my edit to iamiddy's answer was rejected I want to also provide the complete solution for Spring Boot 2 + as an separate answer.
The WebMvcConfigurerAdapter is deprecated with Spring5 / Java8 and can be replaced directly with the Interface WebMvcConfigurer ending up with:
#SpringBootApplication
public class Application extends WebMvcConfigurer {
public static void main(String[] args) throws Exception {
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
SpringApplication.run(Application.class, args);
}
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setUrlDecode(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
Plus you also need to configure Spring's (Strict)HttpFirewall to avoid the blocking of encoded slashes with the error message The request was rejected because the URL contained a potentially malicious String "%2F"
#Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
Spring Boot will use the above HttpFirewall Bean when available - otherwise it might be necessary to configure the WebSecurity as mentioned here:
For spring boot application this worked for me..
Version 1
Add
org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
to your application.properties file
Version 2
run your spring boot application like this.
static void main(String[] args) {
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
SpringApplication.run this, args
}
Version 3 or run your java application with
-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true
This fixed %2F encoded slash path variable for me.
Here is a fix for Spring 3.2.4 (should work for other versions as well). One must overwrite the default UrlPathHelper
public class UrlPathHelperFixed extends UrlPathHelper {
public UrlPathHelperFixed() {
super.setUrlDecode(false);
}
#Override
public void setUrlDecode(boolean urlDecode) {
if (urlDecode) {
throw new IllegalArgumentException("Handler [" + UrlPathHelperFixed.class.getName() + "] does not support URL decoding.");
}
}
#Override
public String getServletPath(HttpServletRequest request) {
return getOriginatingServletPath(request);
}
#Override
public String getOriginatingServletPath(HttpServletRequest request) {
return request.getRequestURI().substring(request.getContextPath().length());
}
}
And inject it to the Mapping Handler:
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="order" value="-1"></property>
<property name="urlPathHelper">
<bean class="com.yoochoose.frontend.spring.UrlPathHelperFixed"/>
</property>
</bean>
After a day of hard works it works now for me :-)
It was suggested to Spring team as https://jira.springsource.org/browse/SPR-11101
I have found this solution which is working for me;
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
just before
springApplication.run(args);
and add below code in Application class
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setUrlDecode(false);
configurer.setUrlPathHelper(urlPathHelper);
}
We just ran into this issue at my office, we did what was suggestion above from what Solubris said where you put it in a query param. The only additional requirement is that the data could have an '&' as well, which would mess up the query param. All we had to do is encode the text before it is sent in the URL and even '&' were filtered out.
Another answer would be to encode "/" twice, which would produce "%252F". In your mapped endpoint, Spring will decode it back to "%2F". All you need more is to decode it one more time using something like this:
URLDecoder.decode(encoded_URL, "UTF-8");
The following resolved the BACK_SLASH issue:
public static void main(String[] args) throws Exception {
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
SpringApplication.run(Application.class, args);
}
But, same functionality could be done via application.yml.
org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH: true
This setting doesn't work. I did not find a way for that, and still looking at it.
In order to avoid parsing the variables manually I did the following:
Add the following before executing any other code:
System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
And in the controller, add 2 variables instead one, for example:
#RequestMapping(value = "/api/devices-by-name/device={deviceId}/restconf/data/ietf-interfaces:interfaces-state/interface={dpuIdPrefix}/{dpuIdSuffix}",
method = RequestMethod.GET,
produces = "application/json")
public ResponseEntity<String> getInterfaceState(#PathVariable(value = "deviceId") String deviceId,
#PathVariable(value = "dpuIdPrefix") String dpuIdPrefix,
#PathVariable(value = "dpuIdSuffix") String dpuIdSuffix) {
String dpuId = dpuIdPrefix + "/" + dpuIdSuffix;
And with that I can retrieve the following:
curl -s -X GET http://localhost:9090/api/devices-by-name/device=ZNSDX16DPU03/restconf/data/ietf-interfaces:interfaces-state/interface=gfast%200%2F14
If the slash is optional, then you might need to configure two different request mappings.

Resources