Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add view resolver for JSON response by web controllers. #1074

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

emerout
Copy link

@emerout emerout commented Feb 26, 2023

This pull request permits to invoke the web app controllers and ask to respond with a JSON body instead of a JSP page.

You just need to add Accept: application/json header to the http request. Request POST form is like JSP : Content-Type: x-www-form-urlencoded.

Samples below are for occpTags, but should work in the same way with other controllers.

Steve is configured in profile = dev mode. Prod mode works the same:

  • sign in with a POST request
  • get the JSESSIONID into the response header
  • pass it to the subsequent request header
  • same thing for the CSRF token, which change for each request

Sample request inserting a new tag :

curl -v -s --request POST 'http://localhost:8080/steve/manager/ocppTags/add/single' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'idTag=5374657665' \
  --data-urlencode 'add=Add' | jq .
> POST /steve/manager/ocppTags/add/single HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Accept: application/json
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 24
} [24 bytes data]
< HTTP/1.1 302 Found
< Content-Language: fr-FR
< Location: http://localhost:8080/steve/manager/ocppTags
< Content-Length: 0

An HTTP 302 redirect is expected, as Controller sends a redirect after a successful post request creating an item.

In case of a duplicate tag, we have the following response:

[same request as above]
< HTTP/1.1 400 Bad Request
< Content-Language: fr-FR
< Content-Type: application/json;charset=utf-8
< Cache-Control: no-store
< Transfer-Encoding: chunked
{
  "exception": "de.rwth.idsg.steve.SteveException.AlreadyExists",
  "message": "A user with idTag '5374657665' already exists."
}

Sample request retrieving tags:

curl -s 'http://localhost:8080/steve/manager/ocppTags' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/x-www-form-urlencoded' | jq .
{
  "ocppTagList": [
    {
      "ocppTagPk": 46,
      "idTag": "5374657665",
      "parentOcppTagPk": null,
      "parentIdTag": null,
      "inTransaction": false,
      "blocked": false,
      "expiryDate": null,
      "maxActiveTransactionCount": 1,
      "activeTransactionCount": 0,
      "note": null
    }
  ],
  "params": {
    "ocppTagPk": null,
    "idTag": null,
    "parentIdTag": null,
    "expired": "FALSE",
    "inTransaction": "ALL",
    "blocked": "FALSE",
    "ocppTagPkSet": false,
    "parentIdTagSet": false,
    "idTagSet": false
  },
  "idTagList": [
    "5374657665"
  ],
  "parentIdTagList": [],
  "unknownList": []
}

In case of malformed request (redacted):

curl -v -s --request POST 'http://localhost:8080/steve/manager/ocppTags/add/single' \
  --header 'Accept: application/json' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'foo=bar' \
  --data-urlencode 'add=Add' | jq .
> POST /steve/manager/ocppTags/add/single HTTP/1.1
> Accept: application/json
> Content-Type: application/x-www-form-urlencoded
< HTTP/1.1 400 Bad Request
< Content-Type: application/json;charset=utf-8
{
  "errors": [
    {
      "codes": [
        "NotEmpty.ocppTagForm.idTag",
        "NotEmpty.idTag",
        "NotEmpty.java.lang.String",
        "NotEmpty"
      ],
      "arguments": [
        {
          "codes": [
            "ocppTagForm.idTag",
            "idTag"
          ],
          "arguments": null,
          "defaultMessage": "idTag",
          "code": "idTag"
        }
      ],
      "defaultMessage": "ID Tag is required",
      "objectName": "ocppTagForm",
      "field": "idTag",
      "rejectedValue": null,
      "bindingFailure": false,
      "code": "NotEmpty"
    }
  ]
}

@erbg
Copy link

erbg commented Mar 14, 2023

Hello @emerout,
thanks for you PR, was looking for something like this. I was trying the call but i'm blocked on the authorisation post:

curl --location 'http://localhost:8180/steve/manager/signin'
--header 'Accept: application/json'
--header 'Content-Type: application/x-www-form-urlencoded'
--header 'Cookie: JSESSIONID=node0eco0ul600th110m4i71v2crm95.node0'
--data-urlencode 'username=admin'
--data-urlencode 'password=**************'

I always get back 403 :(
Can you give me a hint?

@emerout
Copy link
Author

emerout commented Mar 15, 2023

Hello @erbg,
Happy it can help !

Authentication is a bit tricky because of the CSRF token and the JSESSIONID cookie.

Here is the steps:

Make a first GET to the signin URL, without any Accept header or --location option

curl -v http://localhost:8080/steve/manager/signin | grep _csrf
> GET /steve/manager/signin HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Language: fr-FR
< Content-Type: text/html;charset=utf-8
< Set-Cookie: JSESSIONID=node03ml7bz8mc3zqqs7rhdv3v3yt10.node0; Path=/steve
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: DENY
< Vary: Accept-Encoding
< Content-Length: 1223
<
{ [1223 bytes data]
* Connection #0 to host localhost left intact
            <input type="hidden" name="_csrf" value="1e69f25d-272e-440d-910b-62dd408952c9"/>

Then make the signin call, with the CSRF token and session cookie:

curl -v --request POST 'http://localhost:8080/steve/manager/signin' \  
--header 'Cookie: JSESSIONID=node03ml7bz8mc3zqqs7rhdv3v3yt10.node0' \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=admin' \
--data-urlencode 'password=**************' \
--data-urlencode '_csrf=1e69f25d-272e-440d-910b-62dd408952c9'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /steve/manager/signin HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Cookie: JSESSIONID=node03ml7bz8mc3zqqs7rhdv3v3yt10.node0
> Accept: application/json
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 71
>
< HTTP/1.1 302 Found
< Set-Cookie: JSESSIONID=node01ntih1gove1la13ifkyhzukenz12.node0; Path=/steve
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< X-Frame-Options: DENY
< Location: http://localhost:8080/steve/
< Content-Length: 0
<

Response is a 302 with Location: http://localhost:8080/steve/

You can then make some call with the JSESSIONID (the last one, which was in the response to the actual POST signin):

curl http://localhost:8080/steve/manager/ocppTags \
  --header 'Accept: application/json'  \
  --header 'Cookie: JSESSIONID=node01ntih1gove1la13ifkyhzukenz12.node0'
{"ocppTagList":[....  

I admit this auth process is not straightforward.

There is a way to disable csrf protection in SecurityConfiguration java class:

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        final String prefix = CONFIG.getSpringManagerMapping();

        return http
                .csrf().disable()  // disable CSRF protection
                .authorizeHttpRequests(
                req -> req.antMatchers(prefix + "/**").hasRole("ADMIN")
            )
       // ...

I would not advice to do so if steve is exposed to internet.

@goekay
Copy link
Member

goekay commented Apr 16, 2023

hey @emerout thanks. i appreciate your valuable work. this is interesting from a technical perspective!

however, the interaction with steve is weird since it was never designed for this use case. as an example, to send some data one needs to do things like this:

  --data-urlencode 'idTag=5374657665' \
  --data-urlencode 'add=Add' | jq .

i wonder whether this is manageable with more complex flows (i.e. input data). i think bending the implementation just to make it machine-interactable can create some brittle situations.

i guess having something instead of nothing is better, but i am hesitant about making this part of the master.

if you are okay with it, i want to leave the PR open for anyone interested to cherry pick your solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants