DRF additional action without suffix [duplicate] - django

I'm working with two dev servers on my local machine (node & django's).
I've added django-cors-headers to the project to allow all origins & methods (on dev) with the following settings :
CORS_ORIGIN_ALLOW_ALL = 'ALL'
CORS_ALLOW_METHODS = (
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS'
)
I'm getting 405 when attempting DELETE.
Looking at the response headers
HTTP/1.0 405 METHOD NOT ALLOWED
Date: Mon, 03 Nov 2014 10:04:43 GMT
Server: WSGIServer/0.1 Python/2.7.5
Vary: Cookie
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Access-Control-Allow-Origin: *
Allow: GET, POST, HEAD, OPTIONS
Notice that DELETE & PATCH / PUT are not present in the allowed methods list.
Is there something missing from my configuration ?

The response looks very similar to that of the list view (/api/resource/) for a ViewSet. List views only support GET, to list all of the objects, and POST to create a new object.
DELETE requests are only allowed on the detail view (/api/resource/1/). This is because Django REST Framework needs to know what object you are looking to delete, and this information cannot be retrieved from just the list view.

If you need to connect http method DELETE with URL without pk in DRF try this inside of your ModelViewSet:
#action(methods=['delete'], detail=False)
def delete(self, request):
# your code
UPD: Note that action attribute inside of ModelViewSet class will be None due request. If you check it somewhere, handle not only action name, but request method and request path.

Related

why is XMLHttpRequest.withCredentials necessary even for same site Ajax requests

I am trying to implement an authentication service deployed in a different HTTP server from the one serving my login page.
The following diagram depicts my setup:
On step #1 my browser makes an HTTP GET request to fetch the login page. This is provided as the HTTP response in #2. My browser renders the login page and when I click the login button I send an HTTP POST to a different server (on the same localhost). This is done in #3. The authentication server checks the login details and sends a response that sets the cookie in #4.
The ajax POST request in #3 is made using jQuery:
$.post('http://127.0.0.1:8080/auth-server/some/path/',
{username: 'foo', password: 'foo'},
someCallback);
The authentication service's response (assuming authentication was successful) has the following header:
HTTP/1.1 200
Set-Cookie: session-id=v3876gc9jf22krlun57j6ellaq;Version=1
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: origin, content-type, accept, authorization
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS, HEAD
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 21 Nov 2016 16:17:08 GMT
So the cookie (session-id) is present in the HTTP response in step #4.
Now if the user tries to login again I would like the authentication service to detect that. To test that scenario I press the login button again in order to repeat the post in #3. I would have expected the second request to contain the cookie. However the second time I press the login button, the request header in the post sent out in #3 does not contain the cookie.
What I have discovered is that for the post in #3 to contain the cookie, I have to do it like this:
$.ajax({
type: 'post',
url: 'http://127.0.0.1:8080/auth-server/some/path',
crossDomain: true,
dataType: 'text',
xhrFields: {
withCredentials: true
},
data: {
username : 'foo',
password : 'foo',
},
success: someCallback
});
Why would that be necessary? MDN states that this is only required for cross-site requests. This SO post also uses xhrFields but only in relation to a cross-domain scenario. I understand that my case is not cross-domain as both the page that serves the script is on localhost, and the page to where the ajax request is sent is on the same host. I also understand that cookie domains are not port specific. Moreover, since my cookie did not explicitly specify a domain, then the effective domain is that of the request meaning 127.0.0.1 which is identical the second time I send the POST request (#3). Finally, the HTTP reponse on #4 already includes Access-Control-Allow-Origin: * which means that the resource can be accessed by any domain in a cross-site manner.
So why did I have to use the xhrFields: {withCredentials: true} to make this work?
What I understand is that setting Access-Control-Allow-Origin: * is simply enabling cross-site requests but that in order for cookies to be sent then the xhrFields: {withCredentials: true} should be used regardless (as explained in MDN section on requests with credentials). Moreover, I understand that the request is indeed cross-site since the port number is important when deciding whether a request is cross-site or not. Whether the domain of a cookie includes ports is irrelevant. Is this understanding correct?
update
I think this is explained very clearly in this answer, so maybe this question should be deleted.
All parts of the origin must match the host(ajax target) for it to be considered same-origin. The 3 part of the origin https://sales.company.com:9443 for example includes:
protocol/scheme (https - won't match http)
hostname (sales.company.com - won't match subdomain.sales.company.com)
port (9443 - won't match 443)
see https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

CSRF exempt on REST View

I am serving an API which will be accessible with a small sensor sending a POST request with data. This sensor has a limited software, and I want to disable the CSRF protection on my API view.
So I've added the decorator:
url(
regex=r'^beacons/$',
view=csrf_exempt(ScanListCreateAPIView.as_view()),
name='beacons'
),
Unfortunately, when I perform a POST with my sensor, I still get a 403 error:
<h1>Forbidden <span>(403)</span></h1>
<p>CSRF verification failed. Request aborted.</p>
<p>You are seeing this message because this HTTPS site requires a 'Referer
header' to be sent by your Web browser, but none was sent
. This header is
required for security reasons, to ensure that your browser is not being
hijacked by third parties.</p>
<p>If you have configured your browser to disable 'Referer' headers, please
re-enable them, at least for this site, or for HTTPS connections, or for
'same-origin' requests.</p>
I've try to add a "Referer: " null header in my POST request, but I still have a 403 response, mentionning that CSRF failed.
My request is:
POST /api/beacons HTTP/1.1
Host: vincent.pythonanywhere.com
Content-Type: application/json
Accept: */*
User-Agent: Mozilla/4.0 (compatible; esp8266 Lua; Windows NT 5.1)
Content-Length: 597
{"beacon":"aaa"," ...
The same request passed throught curl is working ok, with a 201 response.
Here is the solution to diable CSRF:
1- As DRF does its own csrf with SessionAuth, you have to specify in the view:
authentication_classes = (BasicAuthentication,)
2- Then I don't know exacly why, but view=csrf_exempt(ScanListCreateAPIView.as_view()), in urls doesn't work. Instead, use the braces mixin:
from braces.views import LoginRequiredMixin, CsrfExemptMixin
class ScanListCreateAPIView(ListCreateAPIView, CsrfExemptMixin):
authentication_classes = (BasicAuthentication,)

Serve images with django-rest-framework

I am trying to serve images from an api based on django-rest-api.
So far I have a very basic api view that loads an image and its mimetype and returns it.
#api_view(['GET'])
def get_pic(request, pk=None, format=None):
//get image and mimetype here
return HttpResponse(image, content_type=mimetype)
This has been working fine, however I have tested it on some new image libraries on iOS and have found that the API is returning a 406 error.
I believe this is because the client is sending a Accept image/* which this api view doesn't except.
Doing a HEAD on the api returns the following;
GET, OPTIONS
Connection → keep-alive
Content-Type → application/json
Date → Wed, 17 Jun 2015 10:11:07 GMT
Server → gunicorn/19.2.1
Transfer-Encoding → chunked
Vary → Accept, Cookie
Via → 1.1 vegur
X-Frame-Options → SAMEORIGIN
How can I change the Content-Type for this API to accept image requests?
I Have tried adding a custom parser with the right media_type but his doesn't seem to help. Is this the right approach or is there an easier way to serve images from django-rest-framework?
You need to create a custom renderer like:
class JPEGRenderer(renderers.BaseRenderer):
media_type = 'image/jpeg'
format = 'jpg'
charset = None
render_style = 'binary'
def render(self, data, media_type=None, renderer_context=None):
return data

Django/DRF - 405 Method not allowed on DELETE operation

I'm working with two dev servers on my local machine (node & django's).
I've added django-cors-headers to the project to allow all origins & methods (on dev) with the following settings :
CORS_ORIGIN_ALLOW_ALL = 'ALL'
CORS_ALLOW_METHODS = (
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS'
)
I'm getting 405 when attempting DELETE.
Looking at the response headers
HTTP/1.0 405 METHOD NOT ALLOWED
Date: Mon, 03 Nov 2014 10:04:43 GMT
Server: WSGIServer/0.1 Python/2.7.5
Vary: Cookie
X-Frame-Options: SAMEORIGIN
Content-Type: application/json
Access-Control-Allow-Origin: *
Allow: GET, POST, HEAD, OPTIONS
Notice that DELETE & PATCH / PUT are not present in the allowed methods list.
Is there something missing from my configuration ?
The response looks very similar to that of the list view (/api/resource/) for a ViewSet. List views only support GET, to list all of the objects, and POST to create a new object.
DELETE requests are only allowed on the detail view (/api/resource/1/). This is because Django REST Framework needs to know what object you are looking to delete, and this information cannot be retrieved from just the list view.
If you need to connect http method DELETE with URL without pk in DRF try this inside of your ModelViewSet:
#action(methods=['delete'], detail=False)
def delete(self, request):
# your code
UPD: Note that action attribute inside of ModelViewSet class will be None due request. If you check it somewhere, handle not only action name, but request method and request path.

Any idea why I can't POST to this Django REST API?

I'm currently trying to get a POST request using multipart/form-data running to the Django REST framework. I've successfully run through some test requests via the interactive API screens, which work fine. I've then tried to convert these over to using a non-Session based auth strategy, and I've consistently got errors. The requests I've sent are of the form:
POST /api/logs/ HTTP/1.1
Host: host:8080
Connection: keep-alive
Content-Length: 258
Accept: application/json
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTOhRsMbL8ak9EMQB
Authorization: Token -token-
------WebKitFormBoundaryx6ThtBDZxZNUCkKl
Content-Disposition: form-data; name="SubmittedAt"
2014-01-23T10:39:00
------WebKitFormBoundaryx6ThtBDZxZNUCkKl
Content-Disposition: form-data; name="Device"
CheeseDevice
------WebKitFormBoundaryx6ThtBDZxZNUCkKl--
Sadly, the result has been (for all the requests I've run):
{"Device": ["This field is required."], "SubmittedAt": ["This field is required."], "LogFile": ["This field is required."]}
Interestingly, I've been able to send chunks of JSON through to the endpoint, and they're accepted as expected, eg:
POST /api/logs/ HTTP/1.1
Content-Type: application/json
Host: host:8080
Connection: keep-alive
Content-Length: 35
Accept: application/json
Authorization: Token -token-
{
"Device": "CheeseDevice"
}
Returns:
{"SubmittedAt": ["This field is required."], "LogFile": ["This field is required."]}
As expected - it actually accepts the Device argument and only raises errors on the missing items. I'd switch to using JSON, but sadly cannot upload files with it...
Thanks in advance for any help!
Edit:
Further investigation (ie: writing a view method that returns the request data shows that request.DATA isn't getting populated, for some reason. Method I'm using to debug follows:
def test_create(self, request, pk=None):
return Response(request.DATA)
Edit 2:
Even further investigation (and dropping code chunks into the framework for debugging) indicates that the requests are getting caught up in _perform_form_overloading and never hitting the MultiPartParser. Not sure why this is occurring but I'll try and trace it further.
After delving down every level I could find...
Looks like the problem stems from the line endings - ie: the libs and request senders I've been using send the content through with "\n" (LF) endings, while the HTTP spec requires "\r\n" endings (CR,LF)
This hinges on the following code in the Django core, within http/multipartparser.py - in parse_boundary_stream:
header_end = chunk.find(b'\r\n\r\n')
For dev purposes (and because it's going to be way easier to patch at the Django end than in the clients...) I've switched the above line to:
header_end = chunk.replace("\r\n","\n").find(b'\n\n')
This updated code follows the recommendations in Section 19.3 of the HTTP/1.1 spec regarding Tolerant Applications and accepting LF instead of just CRLF - I'll try and get around to seeing if this is suitable for inclusion in the Django core.
Edit:
For reference, the patch is up on GitHub: https://github.com/tr00st/django/commit/9cf6075c113dd27e3743626ab0e18c6616488bd9
This could be due to malformed multipart post data.
Also possible that you don't have MultiPartParser installed, but I don't think that'll be it as you'd normally expect to see a 415 Unsupported Media Type response in that case.