Is it possible to store pathparams as a list? - java

I have a Rest Service that I want to respond to requests with the following paths
1) /v1/config/type/service
2) /v1/config/type/service, service2
What I'd like is to be able to store the path param serviceName as a List where each element is delimited by a comma. For example, if someone types v1/config/foo/bar1,bar2,bar3 I'd like serviceName to be a List with 3 elements (bar1, bar2, bar3). Right now it just returns a list with 1 element that contains all three service strings. Is that even possible? Or is that something I'll simply have to parse. The code I have is shown below, it's pretty rough as I'm in the beginning stages of the project:
#ApplicationPath("/")
#Path("/v1/config")
public class ServiceRetriever extends Application {
#GET
#Produces(MediaType.TEXT_PLAIN)
public String getHelloWorld() {
return "Hello World";
}
#GET
#Path("{type}/{serviceName}")
#Produces("application/zip")
public Response getServices(#PathParam("type") String type, #PathParam("serviceName")List<String> serviceNames,
#QueryParam("with_config") boolean withConfig, #QueryParam("with_drive") boolean withDriver) throws IOException
{
//some random file i made to test that we can return a zip
File file = new File(System.getProperty("user.home")+"/dummy.zip");
System.out.println(serviceNames.size()); //returns 1
//we can change the zip file name to be whatever
return Response.ok(file).header("Content-Type","application/zip").
header("Content-Disposition", "attachment; filename="+file.getName()).build();
}

The problems is that you have to alter the deserialization process of that variable. Typically only query parameters are lists so this might not be compatible with some libraries.
You could:
Capture the parameter as a string and parse it internally via helper method (obvious)
Create your own annotation like #PathParamMutli and return Arrays.asList(parameter.split(","));. Ideally you should have access to the framework source code and branching privileges.
Use a query parameter instead

Related

Passing json object to an endpoint developed with spring

I have an endpoint I created using spring.io. My GetMapping declaration can be seen below
#ApiOperation(
value = "Returns a pageable list of CustomerInvoiceProducts for an array of CustomerInvoices.",
notes = "Must be authenticated.")
#EmptyNotFound
#GetMapping({
"customers/{customerId}/getProductsForInvoices/{invoiceIds}"
})
public Page<CustomerInvoiceProduct> getProductsForInvoices(
#PathVariable(required = false) Long customerId,
#PathVariable String[] invoiceIds,
Pageable pageInfo) {
//Do something fun here
for (string i: invoiceIds){
//invoiceIds is always empty
}
}
Here is how I am calling the url from postman and passing the data.
http://localhost:8030/api/v1/customers/4499/getProductsForInvoices/invoiceIds/
{
"invoiceIds": [
"123456",
"234566",
"343939"
]
}
My string array for invoiceIds is always empty in the for loop Nothing gets passed to the array. What am I doing wrong?
The mapping you are using is this:
customers/{customerId}/getProductsForInvoices/{invoiceIds}
Both customerId and invoiceIds are Path variables here.
http://localhost:8030/api/v1/customers/4499/getProductsForInvoices/invoiceIds/
The call you are making contains customerId but no invoiceIds. Either you can pass the list in place of invoiceIds as String and read it as a String and then create a List by breaking up the List - which will be a bad practice.
Other way is to change your path variable - invoiceId to RequestBody.
Generally, Path Variables are used for single id or say navigating through some structured data. When you want to deal in a group of ids, the recommended practice would be to pass them as RequestBody in a Post method call rather than a Get method call.
Sample code snippet for REST API (post calls):
Here, say you are trying to pass Employee object to the POST call, the REST API will look like something below
#PostMapping("/employees")
Employee newEmployee(#RequestBody Employee newEmployee) {
//.. perform some operation on newEmployee
}
This link will give you a better understanding of using RequestBody and PathVariables -
https://javarevisited.blogspot.com/2017/10/differences-between-requestparam-and-pathvariable-annotations-spring-mvc.html
https://spring.io/guides/tutorials/rest/

REST - Download list of users

Context:
I want to write an endpoint that will return a Collection of users based on their usernames. How should those username be passed to the REST endpoint - note that I can (potentially) have a lot of usernames (say > 5000)?
Solution #1:
Use a GET endpoint, concatenate the usernames on client side and pass them as a single request parameter. Split the request parameter on server side to get the list of usernames.
#RestController
public class UserController {
#GetMapping
// able to deserialize `filename1,filename2` to List out of the box
public Collection<User> getUser(#RequestParam List<String> usernames) {
return userService.getUsersByUsername(usernames);
}
}
Solution #2:
Use a POST endpoint and pass the list of usernames as request body. Although cleaner form a coding perspective, I end up using a POST to fetch data.
#RestController
public class UserController {
#PostMapping
public Collection<User> getUser(#RequestBody List<String> usernames) {
return userService.getUsersByUsername(usernames);
}
}
Questions:
Which of the two solutions would be the better approach?
Do you have a better approach to pass the list of usernames into the endpoint?
Edits:
I've updated the signature of the first solution based on suggestions from answers. Spring is able to deserialize filename1,filename2 to List out of the box for #RequestParam.
POST looks like a cleaner approach in this case because -
Sending a huge string in a URL is not a good idea and there is scope for error
You need to write additional code (logic) to create the string on frontend and split it on backend.
Sending a huge string in a URL is not scalable as there are limits on the length of URL.
Get approach might result into an issue since URL length is limited and then you have to limit your query parameters.
Though its not a post request but in your case i think post is the only way out.
I would agree with all the answers given above. I would like to specify one more point , if you are going with post request you might have to increase the payload capacity a server can receive , the default post capacity(The maximum size in bytes) of spring boot is 2mb (based on your server). While testing your code might work fine with 1000-2000 usernames but make sure to change the property to accept more bytes in request.
GET is not limited, yet the browser is. Your server client does not seem to be the browser, so I would say GET is the way to go.
P.S GET can receive a body (not so great, but POST is not also the best match).
You don need to concatenated the string and add extra computation on server server, GET can receive a list of separate strings.
UPDATE with example:
#RestController
public class MyController {
#GetMapping(value = "/test")
public List<String> getTestParams(#RequestParam List<String> params) {
return params;
}
}
The test with 3000 params
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestMyController {
#Autowired
private TestRestTemplate restTemplate;
#Test
public void testRequestWithParamsList() {
List<String> params = new ArrayList<>();
for (int i = 0; i < 3000; i++) {
params.add(String.valueOf(i));
}
List<String> result = restTemplate.getForObject(buildUrl(params),
List.class);
assertEquals(params, result);
}
private String buildUrl(List<?> params) {
return "/test?params=" + getUrlParameter(params);
}
private String getUrlParameter(List<?> params) {
return params.stream()
.map(Object::toString)
.collect(Collectors.joining(","));
}
}
If you are using tomcat you must specify also the max http header property in application.properties
server.max-http-header-size=30000

Java Jersey REST Request Parameter Sanitation

I'm trying to make sure my Jersey request parameters are sanitized.
When processing a Jersey GET request, do I need to filter non String types?
For example, if the parameter submitted is an integer are both option 1 (getIntData) and option 2 (getStringData) hacker safe? What about a JSON PUT request, is my ESAPI implementation enough, or do I need to validate each data parameter after it is mapped? Could it be validated before it is mapped?
Jersey Rest Example Class:
public class RestExample {
//Option 1 Submit data as an Integer
//Jersey throws an internal server error if the type is not Integer
//Is that a valid way to validate the data?
//Integer Data, not filtered
#Path("/data/int/{data}/")
#GET
#Produces(MediaType.TEXT_HTML)
public Response getIntData(#PathParam("data") Integer data){
return Response.ok("You entered:" + data).build();
}
//Option 2 Submit data as a String, then validate it and cast it to an Integer
//String Data, filtered
#Path("/data/string/{data}/")
#GET
#Produces(MediaType.TEXT_HTML)
public Response getStringData(#PathParam("data") String data) {
data = ESAPI.encoder().canonicalize(data);
if (ESAPI.validator().isValidInteger("data", data, 0, 999999, false))
{
int intData = Integer.parseInt(data);
return Response.ok("You entered:" + intData).build();
}
return Response.status(404).entity("404 Not Found").build();
}
//JSON data, HTML encoded
#Path("/post/{requestid}")
#POST
#Consumes({MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON})
#Produces(MediaType.TEXT_HTML)
public Response postData(String json) {
json = ESAPI.encoder().canonicalize(json);
json = ESAPI.encoder().encodeForHTML(json);
//Is there a way to iterate through each JSON KeyValue and filter here?
ObjectMapper mapper = new ObjectMapper();
DataMap dm = new DataMap();
try {
dm = mapper.readValue(json, DataMap.class);
} catch (Exception e) {
e.printStackTrace();
}
//Do we need to validate each DataMap object value and is there a dynamic way to do it?
if (ESAPI.validator().isValidInput("strData", dm.strData, "HTTPParameterValue", 25, false, true))
{
//Is Integer validation needed or will the thrown exception be good enough?
return Response.ok("You entered:" + dm.strData + " and " + dm.intData).build();
}
return Response.status(404).entity("404 Not Found").build();
}
}
Data Map Class:
public class DataMap {
public DataMap(){}
String strData;
Integer intData;
}
The short answer is yes, though by "filter" I interpret it as "validate," because no amount of "filtering" will EVER provide you with SAFE data. You can still run into integer overflows in Java, and while those may not have immediate security concerns, they could still put parts of your application in an unplanned for state, and hacking is all about perturbing the system in ways you can control.
You packed waaaaay too many questions into one "question," but here we go:
First off, the lines
json = ESAPI.encoder().canonicalize(json);
json = ESAPI.encoder().encodeForHTML(json);
Aren't doing what you think they're doing. If your JSON is coming in as a raw String right here, these two calls are going to be applying mass rules across the entire string, when you really need to handle these with more surgical precision, which you seem to at least be subconsciously aware of in the next question.
//Is there a way to iterate through each JSON KeyValue and filter
here?
Partial duplicate of this question.
While you're in the loop discussed here, you can perform any data transformations you want, but what you should really be considering is using the JSONObject class referenced in that first link. Then you'll have JSON parsed into an object where you'll have better access to JSON key/value pairs.
//Do we need to validate each DataMap object value and is there a
dynamic way to do it?
Yes, we validate everything that comes from a user. All users are assumed to be trained hackers, and smarter than you. However if you handled filtering before you do your data mapping transformation, you don't need to do it a second time. Doing it dynamically?
Something like:
JSONObject json = new JSONObject(s);
Iterator iterator = json.keys();
while( iterator.hasNext() ){
String data = iterator.next();
//filter and or business logic
}
^^That syntax is skipping typechecks but it should get you where you need to go.
/Is Integer validation needed or will the thrown exception be good
enough?
I don't see where you're throwing an exception with these lines of code:
if (ESAPI.validator().isValidInput("strData", dm.strData, "HTTPParameterValue", 25, false, true))
{
//Is Integer validation needed or will the thrown exception be good enough?
return Response.ok("You entered:" + dm.strData + " and " + dm.intData).build();
}
Firstly, in java we have autoboxing which means this:
int foo = 555555;
String bar = "";
//the code
foo + bar;
Will be cast to a string in any instance. The compiler will promote the int to an Integer and then silently call the Integer.toString() method. Also, in your Response.ok( String ); call, THIS is where you're going to want to encodeForHTML or whatever the output context may be. Encoding methods are ALWAYS For outputting data to user, whereas canonicalize you want to call when receiving data. Finally, in this segment of code we also have an error where you're assuming that you're dealing with an HTTPParameter. NOT at this point in the code. You'll validate http Parameters in instances where you're calling request.getParameter("id"): where id isn't a large blob of data like an entire JSON response or an entire XML response. At this point you should be validating for things like "SafeString"
Usually there are parsing libraries in Java that can at least get you to the level of Java objects, but on the validation side you're always going to be running through every item and punting whatever might be malicious.
As a final note, while coding, keep these principles in mind your code will be cleaner and your thought process much more focused:
user input is NEVER safe. (Yes, even if you've run it through an XSS filter.)
Use validate and canonicalize methods whenever RECEIVING data, and encode methods whenever transferring data to a different context, where context is defined as "Html field. Http attribute. Javascript input, etc...)
Instead of using the method isValidInput() I'd suggest using getValidInput() because it will call canonicalize for you, making you have to provide one less call.
Encode ANY time your data is going to be passed to another dynamic language, like SQL, groovy, Perl, or javascript.

Partial matching of request with RESTITO

Can I match a REST-request content without a exact match of content with test framework RESTITO? Lets say I have a timestamp from now in my request but I don't want to match with this specific value (I probably don't know it anyway)?
If your URL looks like
http://example.com/api/endpoint?weight=100&timestamp=1413108487
then you can to the following:
match(get("/api/endpoint"), parameter("weight", "100"))
It will just ignore all the timestamps. If timestamp is part of URI:
http://example.com/api/endpoint/1413108487/bla
then you can use matchesUri() e.g.:
match(method(Method.GET), matchesUri(new Regexp("/api/endpoint/[0-9]+/bla")))
And of course you always can write a custom condition, where you can do any checks on the request you want and return a boolean e.g.:
Predicate<Call> uriEndsWithA = new Predicate<Call>() {
#Override
public boolean apply(final Call input) {
return input.getUri().endsWith("a");
}
};
whenHttp(server).match(custom(uriEndsWithA)).then(ok());

Spring REST Controller understanding arrays of strings when having special characters like blank spaces or commas

I am trying to write a Spring REST Controller getting an array of strings as input parameter of a HTTP GET request.
The problem arises when in the GET request, in some of the strings of the array, I use special characters like commas ,, blank spaces or forward slash /, no matter if I URL encode the query part of the URL HTTP GET request.
That means that the string "1/4 cup ricotta, yogurt" (edit which needs to be considered as a unique ingredient contained as a string element of the input array) in either this format:
http://127.0.0.1:8080/[...]/parseThis?[...]&ingredients=1/4 cup ricotta, yogurt
This format (please note the blank spaces encoded as + plus, rather than the hex code):
http://127.0.0.1:8080/[...]/parseThis?[...]&ingredients=1%2F4+cup+ricotta%2C+yogurt
Or this format (please note the blank space encoded as hex code %20):
http://127.0.0.1:8080/[...]/parseThis?[...]&ingredients=1%2F4%20cup%20ricotta%2C%20yogurt
is not rendered properly.
The system does not recognize the input string as one single element of the array.
In the 2nd and 3rd case the system splits the input string on the comma and returns an array of 2 elements rather than 1 element. I am expecting 1 element here.
The relevant code for the controller is:
#RequestMapping(
value = "/parseThis",
params = {
"language",
"ingredients"
}, method = RequestMethod.GET, headers = HttpHeaders.ACCEPT + "=" + MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public HttpEntity<CustomOutputObject> parseThis(
#RequestParam String language,
#RequestParam String[] ingredients){
try {
CustomOutputObject responseFullData = parsingService.parseThis(ingredients, language);
return new ResponseEntity<>(responseFullData, HttpStatus.OK);
} catch (Exception e) {
// TODO
}
}
I need to perform HTTP GET request against this Spring controller, that's a requirement (so no HTTP POST can be used here).
Edit 1:
If I add HttpServletRequest request to the signature of the method in the controller, then I add a log statement like log.debug("The query string is: '" + request.getQueryString() + "'"); then I am seeing in the log a line like The query string is: '&language=en&ingredients=1%2F4+cup+ricotta%2C+yogurt' (So still URL encoded).
Edit 2:
On the other hand if I add WebRequest request to the signature of the method, the the log as log.debug("The query string is: '" + request.getParameter("ingredients") + "'"); then I am getting a string in the log as The query string is: '1/4 cup ricotta, yogurt' (So URL decoded).
I am using Apache Tomcat as a server.
Is there any filter or something I need to add/review to the Spring/webapp configuration files?
Edit 3:
The main problem is in the interpretation of a comma:
#ResponseBody
#RequestMapping(value="test", method=RequestMethod.GET)
public String renderTest(#RequestParam("test") String[] test) {
return test.length + ": " + Arrays.toString(test);
// /app/test?test=foo,bar => 2: [foo, bar]
// /app/test?test=foo,bar&test=baz => 2: [foo,bar, baz]
}
Can this behavior be prevented?
The path of a request parameter to your method argument goes through parameter value extraction and then parameter value conversion. Now what happens is:
Extraction:
The parameter is extracted as a single String value. This is probably to allow simple attributes to be passed as simple string values for later value conversion.
Conversion:
Spring uses ConversionService for the value conversion. In its default setup StringToArrayConverter is used, which unfortunately handles the string as comma delimited list.
What to do:
You are pretty much screwed with the way Spring handles single valued request parameters. So I would do the binding manually:
// Method annotations
public HttpEntity<CustomOutputObject> handlerMethod(WebRequest request) {
String[] ingredients = request.getParameterValues("ingredients");
// Do other stuff
}
You can also check what Spring guys have to say about this.. and the related SO question.
Well, you could register a custom conversion service (from this SO answer), but that seems like a lot of work. :) If it were me, I would ignore the declaration the #RequestParam in the method signature and parse the value using the incoming request object.
May I suggest you try the following format:
ingredients=egg&ingredients=milk&ingredients=butter
Appending &ingredients to the end will handle the case where the array only has a single value.
ingredients=egg&ingredients=milk&ingredients=butter&ingredients
ingredients=milk,skimmed&ingredients
The extra entry would need to be removed from the array, using a List<String> would make this easier.
Alternatively if you are trying to implement a REST controller to pipe straight into a database with spring-data-jpa, you should take a look at spring-data-rest. Here is an example.
You basically annotate your repository with #RepositoryRestResource and spring does the rest :)
A solution from here
public String get(WebRequest req) {
String[] ingredients = req.getParameterValues("ingredients");
for(String ingredient:ingredients ) {
System.out.println(ingredient);
}
...
}
This works for the case when you have a single ingredient containing commas

Categories