Process Paypal IPN in Django view - django

According to the PayPal IPN documentation, you have to respond to an IPN by first sending an empty HTTP 200 response to https://www.sandbox.paypal.com/cgi-bin/webscr and then send an HTTP POST request. The only way I could figure out to send an HTTP 200 response was by returning HttpResponse('') in the view, which works since the IPN simulator says the "IPN was sent and the handshake was verified." But then how would I send the HTTP POST request when the view has already returned? My plan was to use urllib2 to generate the POST request.
I also would like to send an HTTP 200 response without using HttpResponse('') since I don't know where the original request is coming from, and I want to be sure that I'm sending the response to https://www.sandbox.paypal.com/cgi-bin/webscr. I've looked at urllib2, requests, and PyCurl, and I've been unable to find a way to create an empty response to a particular URL.

You want to use requests or urllib2 to make a POST request to verify the IPN before you return the HttpResponse(''); the docs imply you have to do it in the opposite order, but you don't actually have to, and this way is a lot simpler in most web frameworks. So your view should look vaguely like
def ipn(request):
verification = requests.POST(PAYPAL_URL, data=request.body)
if verification.text == 'VERIFIED':
# process IPN
return HttpResponse('')
else:
return HttpResponseForbidden()
Note that if this errors somewhere, you will return a 500 to PayPal, and they will retry. If you do this too much, they will get annoyed at you and eventually deactivate your account, so make sure you are getting alerted of the error in some fashion. (You could also wrap things in a try/except block, but then you risk returning a 200 without actually doing your processing if there's an error, which may be worse.)

just respond to the request made by paypal. for the other requests no response (403 http status)
from django.http import HttpResponseForbidden
def answer_to_pp_ipn():
# ...
if 'paypal' in request.META['HTTP_HOST']:
return HttpResponse('OK 200')
return HttpResponseForbidden()
and as you mentioned, requests is the perfect choice to make a post request.

Related

Django to catch all invalid logins and unauthorized access like 401s and 403s

I want to capture all invalid logins/unauthorized access such as 401s and 403s returned from the site so I can log them to a security logging service, investigating if there is an easy way to catch all of these without putting in much custom logic.
I have tried using middleware approach:
def simple_middleware(get_response):
# One-time configuration and initialization.
def middleware(request):
response = get_response(request)
if response.status_code in [403, 401]:
log.warning('invalid login')
return response
return middleware
Unfortunately an incorrect login to the /admin/ login, it returns status 200, however I think this would work for custom login that explicitly throws 401/403.
I have also tried using the signal approach using request_finished but all I get is just the handler class.
So... looking for ideas.
As you found out, a login attempt doesn't necessarily imply a specific response code, since you may decide to treat the attempt with a redirect or any other type of answer.
In case of Django, the default auth middleware (which I assume you are using) fires a user_login_failed signal which you can handle with your logging logic.
You can see in the documentation how to register a signal handler, so it should be something like
from django.contrib.auth.signals import user_login_failed
from django.dispatch import receiver
#receiver(request_finished)
def handle_login_failed(sender, **kwargs):
print(f"Oops, login failed using these credentials: {kwargs.get('credentials', None)}")
The signal triggering is in the source code for the auth package.

Telegram BOT webook sending empty POST

I'm trying to get the payload of the webhook in a BOT in Django.
#csrf_exempt
def webhook(request):
print(request.get_full_path())
print(request.POST)
print(request.GET)
return HttpResponse("OK")
The webhook calls are working fine
{"ok":true,"result":{"url":"...","has_custom_certificate":false,"pending_update_count":0,"last_error_date":1516490058,"last_error_message":"Wrong response from the webhook: 503 Service Unavailable","max_connections":40}}
(last_error_message was solved)
<QueryDict: {}>
<QueryDict: {}>
[20/Jan/2018 23:16:17] "POST /webhook/secure/ HTTP/1.1" 200 2
But above text is what I'm getting in the POST and GET method each time I get a message. Always empty. Maybe I'm missing something in the Telegram part, since I've made a POST request to the same URL and it's printing correct information.
You have to POST testing data to your own server in order to know if there have something wrong.
Here are some payloads you can execute by curl, or this Android application have webhook debugger for new developers.

django and angular.js with authentication

I have a web app that's already written in Django, and it is working quite well. I want to add a few views for angular as the plan is to move into that direction. One issue i'm facing is that some of my controllers require login, with django we normally use the #login_required decorator and everything is fine.
But with angular.js calling the controllers (for api's), the redirect is not happening. I'm assuming I'll have to somehow check if my django user is authenticated directly from angular side. Is there any explanation on how to confirm this on angular? I have been struggling with this and have read the following:
https://docs.angularjs.org/api/ng/service/$http
https://medium.com/#vince_prignano/angular-js-with-django-bde834dbd61e
$routeProvider not triggered after login redirect
http://www.django-rest-framework.org/api-guide/authentication/
Basically, I want to confirm, via Angular, that my user is logged in and if not, redirect them to the login page.
EDIT
I'm implementing a request interceptor as shown here:
Interceptor not working
However, in django #login_required it's returning the html of the redirecting page. Is there a way to get the URL and forward the user there?
Add resolve in your $routeProvider as:
$routeProvider
.when('/', {
templateUrl: '/views/main.html' })
.when('/admin', {
templateUrl: 'views/admin.html',
controller: 'AdminCtrl',
resolve: { loggedin: checkLoggedin } })
.when('/login', {
templateUrl: 'views/login.html',
controller: 'LoginCtrl' })
.otherwise({ redirectTo: '/' }); -
See more at: https://vickev.com/#!/article/authentication-in-single-page-applications-node-js-passportjs-angularjs
As mentioned in the previous answer, it would be best if your Django back-end can issue a 401 response when login is required. Right now it sounds like it's sending a 302, which you can still observe in the browser if you're making an XHR request. As you've found, using $http interceptors in Angular are a common way of looking for a 401 and sending the user to a login page.
I've taken a different approach: implement a service that abstracts this a bit, via a method called $user.get() - it makes a request to a known endpoint (in my case, /api/users/current) and rejects the returned promise if it sees a 401. In your case you could implement a rejection handler that uses window.location.href to send the user to your dedicated login page
Disclaimer: I work at Stormpath and we’ve spent a log of time thinking about this :) In my comments above I’m referring to our Stormpath Angular SDK - you can look at this library to see how I’ve solved this problem.
When defining my app i'm doing this:
$httpProvider.interceptors.push('errorInterceptor');
and The code for that is below:
app.factory('errorInterceptor', ['$q', '$rootScope', '$location',
function ($q, $rootScope, $location) {
return {
request: function (config) {
return config || $q.when(config);
},
requestError: function(request){
return $q.reject(request);
},
response: function (response) {
return response || $q.when(response);
},
responseError: function (response) {
if (response && response.status == 302 && response.data.url) {
window.location = response.data.url;
return;
}
return $q.reject(response);
}
};
}]);
Basically, we can't use login_required. We have to create a new decorator and provide a 302 status with a url.
If you're making APIs calls via AJAX back to the server, most likely you don't want the response to be redirected to the login page.
I have the same use case and made my own decorator to return a 403 when the user is not logged in. You may also use 401 instead if you like (I left it commented out).
I use 403 because 401 seems to imply WWW authentication.
from django.http import HttpResponse, HttpResponseForbidden
def logged_in_or_deny(func):
def check_request(request, *args, **kwargs):
if (request.user.is_authenticated()):
return func(request, *args, **kwargs)
else:
return HttpResponseForbidden('You must be logged in') # 403 Response
#return HttpResponse('You must be logged in', status=401) # 401 Response
return check_request
Then you would protect your view like this:
#logged_in_or_deny
def your_view(request):
# ... your code ...
return HttpResponse('Your normal response :)')
From Angular, it seems like you already know how to use an interceptor to check the response code and redirect the user accordingly. If you use the 403 or 401, you should check the against the response body just in case you respond with similar errors in the future.
While what you already have would work, 302 responses can be used for other reasons. It's better to have an explicit 4xx response rather than a 3xx redirection response since it'll be immediately obvious that it's a client side error (missing authentication).
You can use Http-Auth-Interceptor. Suppose A view requires login and a user makes a request to the view without login then #login_required decorator returns the response with response code 401. In auth interceptor intercept the status code if status code is 401 then redirect user to the login page.
example site: http://witoldsz.github.io/angular-http-auth/

HTTP post request in Django

I am trying to do the following:
1) A payment solution is supposed to send an HTTP Post to my site
2) I would like to read the contents of the request(xml) and update my records to reflect the payment
I am trying this for the first time. When I create a URL path, and send a post to that address I get the csrf error.
Is there a way using Django wherein I can accept a post and don't have to return a response.
Thanks
Tanmay
Your view should return an http response, otherwise you will get an error. However, Django does not mind if that response does not contain any content. Your view can be as simple as:
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
#csrf_exempt
def my_view(request):
# do something with request.POST
return HttpResponse("")
Since it is a third party that is submitting the post request, and not a user submitting a form on your site, you can mark the view as exempt from CSRF protection using the csrf_exempt decorator, as above.
Note that anyone could submit a post request to your url, so you should have some way of checking that the response is genuine. Your payment solution should be able to advise a suitable way to do this.

How to debug Django PayPal IPN?

I'm using this django app to implement PayPal IPN. I'm testing it using PayPal's IPN simulator, but it's telling me
IPN delivery failed. HTTP error code 500: Internal Server Error
So how can I debug this and see what's really going on? I've dug into code:
#require_POST
def ipn(request, item_check_callable=None):
"""
PayPal IPN endpoint (notify_url).
Used by both PayPal Payments Pro and Payments Standard to confirm transactions.
http://tinyurl.com/d9vu9d
PayPal IPN Simulator:
https://developer.paypal.com/cgi-bin/devscr?cmd=_ipn-link-session
"""
flag = None
ipn_obj = None
form = PayPalIPNForm(request.POST)
if form.is_valid():
try:
ipn_obj = form.save(commit=False)
except Exception, e:
flag = "Exception while processing. (%s)" % e
else:
flag = "Invalid form. (%s)" % form.errors
if ipn_obj is None:
ipn_obj = PayPalIPN()
ipn_obj.initialize(request)
if flag is not None:
ipn_obj.set_flag(flag)
else:
# Secrets should only be used over SSL.
if request.is_secure() and 'secret' in request.GET:
ipn_obj.verify_secret(form, request.GET['secret'])
else:
ipn_obj.verify(item_check_callable)
ipn_obj.save()
return HttpResponse("OKAY")
All looks fine and dandy there, but since it's not sending a response to my browser, it's kinda tricky to debug. What should I do? I'm trying to look at my apache logs, but it really isn't telling me much.
216.113.191.33 - - [06/Mar/2010:14:10:30 -0600] "POST /paypal/ipn HTTP/1.0" 500 16282 "-" "-"
I tried to send emails and log messages when this view was called, but neither wanted to work. It's possible that I entered the wrong URL into the IPN simulator :) I disabled the "post required" decorator and went to the page directly to see what was going on. My system started to logging "invalid transactions" as expected (since there was no post-data) and then I took a random stab in the dark and figured that Django's CSRF protection was kicking in and preventing PayPal from sending me the data. Adding the #csrf_exempt decorator seems to have fixed it. Yay for guessing errors.
In your django settings.py file, set DEBUG = False
Then for any HTTP 500s (incl. for those being returned to PayPal), you'll be sent a debugging email with all the python stack information.
You'll need to have Django email already set up for this to work, see http://docs.djangoproject.com/en/dev/howto/error-reporting/ for more info on that
You can install a Poster Add-on and make a POST to IPN notify_url from the browser. You will get a Response with all errors. Pretty helpful for debugging.
I just ran into the same problem and this was what I did wrong. Just in case anyone else is as silly as me...
Do not change the method signature from the wiki's
def show_me_the_money(sender, **kwargs):
to something like
def show_me_the_money(ipn, **kwargs):
Reason: in paypal.standard.ipn.models.PayPalIPN.send_signals the listeners are being called with a named argument: payment_was_successful.send(sender=self)
Therefore the method has to have an argument called sender.
I recall having hit (something like) this when using django-paypal too. I can't remember for sure what my cause was, but have you created/migrated the appropriate IPN tables in your database after including the ipn app in your setttings.py?