I'm getting a POST request inside a Flask app from Slack. The request is sent when a user presses on an interactive message button. According to Slack docs I must extract the body of the request to verify the signature.
My computed signature doesn't match the one sent by Slack, though.
In fact, the body of the request comes as some encoded string. The string is actually an encoded dictionary instead of a query str parameters, as expected.
Here's the beginning of my view:
#app.route('/register', methods=['POST'])
def register_visit():
data = request.get_data()
signature = request.headers.get('X-Slack-Signature', None)
timestamp = request.headers.get('X-Slack-Request-Timestamp', None)
signing_secret = b'aaaaaaaaaaaaaaaa'
# old message, ignore
if round(actual_time.time() - float(timestamp)) > 60 * 5:
return
concatenated = ("v0:%s:%s" % (timestamp, data)).encode('utf-8')
computed_signature = 'v0=' + hmac.new(signing_secret, msg=concatenated, digestmod=hashlib.sha256).hexdigest()
if hmac.compare_digest(computed_signature, signature):
...
I've tried to format the received data to make it look like:
token=fdjkgjl&user_id=1234... but I am not aware of all of the necessary parameters that have to be present in the data.
Any ideas are highly appreciated.
The body of the message is following - after being URL decoded (note I've modified possibly sensitive data):
b'payload={"type":"interactive_message","actions":
[{"name":"yes_button","type":"button","value":"236"}],"callback_id":"visit_button","team":{"id":"fffff","domain":"ffff"},"channel":{"id":"ffff","name":"directmessage"},"user":{"id":"ffffff","name":"fffft"},"action_ts":"1540403943.419120","message_ts":"1541403949.000100","attachment_id":"1","token":"8LpjBuv13J7xAjhl2lEajoBU","is_app_unfurl":false,"original_message":{"text":"Test","bot_id":"DDDDDDDDD","attachments":[{"callback_id":"visit_button","text":"Register","id":1,"color":"3AA3E3","actions":[{"id":"1","name":"yes_button","text":"Yes","type":"button","value":"236","style":""}],"fallback":"Register"}],"type":"message","subtype":"bot_message","ts":"1540413949.000100"},"response_url":"https://hooks.slack.com/actions/ffffff/ffffff/tXJjx1XInaUhrikj6oEzK08e","trigger_id":"464662548327.425084163429.dda35a299eedb940ab98dbb9386b56f0"}'
The reason you are getting the "garbled" data is that you are using request.get_data(). That method will return the raw data of a request, but not do any decoding for you.
Much more convenient is to use request.form.get('payload'), which will directly give you the JSON string of the request object. You can then convert that into a dict object with json.loads() to process it further in your app.
Note that the format you received is the correct format for interactive messages. You will not get a query string (e.g. "token=abc;user_id?def...") as you suggested (like for slash command requests). Interactive message request will always contain the request as JSON string in a payload form property. See here for reference.
Here is a simple working example, which will reply a greeting to the user that pressed the button. It will work directly with Slack, but I recommend using Postman to test it.
#app.py
from flask import Flask, request #import main Flask class and request object
import json
app = Flask(__name__) #create the Flask app
#app.route('/register', methods=['POST'])
def register_visit():
slack_req = json.loads(request.form.get('payload'))
response = '{"text": "Hi, <#' + slack_req["user"]["id"] + '>"}'
return response, 200, {'content-type': 'application/json'}
if __name__ == '__main__':
app.run(debug=True, port=5000) #run app in debug mode on port 5000
OK, the issue wasn't related to how Slack sends me the message. It was about misunderstanding which data comes as bytes and which data is unicode. The culprit was string formatting in my case - the line concatenated = ("v0:%s:%s" % (timestamp, data)).encode('utf-8') should have been concatenated = (b"v0:%b:%b" % (timestamp.encode("utf-8"), data)). Data is already bytes, timestamp meanwhile is unicode.
Cannot believe I've banged my head on this for hours -_-
#app.route('/register', methods=['POST'])
def register_visit():
data = request.get_data()
signature = request.headers.get('X-Slack-Signature', None)
timestamp = request.headers.get('X-Slack-Request-Timestamp', None)
signing_secret = b'aaaaaaaaaaaaaaaa'
# old message, ignore
if round(actual_time.time() - float(timestamp)) > 60 * 5:
return
concatenated = (b"v0:%b:%b" % (timestamp.encode("utf-8"), data))
computed_signature = 'v0=' + hmac.new(signing_secret, msg=concatenated,
digestmod=hashlib.sha256).hexdigest()
if hmac.compare_digest(computed_signature, signature):
...
This worked for me
from urllib import parse
parsed_text = parse.unquote('your bytes text here')
Related
I am a novice in Flask framework. I have made an app which is suppose to provide some NLP-NER results. The app works fine with when I CURL using GET, but while doing this I have to encode the URL which is taking some time. I want to use POST and send the data to the app. I have tried various ways mentioned but unable to find a solution. This is what I am sending using cURL.
curl -X POST http://localhost:5000/ner -d "text=I am avinash"
Error: No text field provided. Please specify text to process.(base) Avinashs-MBP:NER avinash.chourasiya$
This is my flask app:
import spacy, sys, json
from spacy import displacy
import flask
from flask import request
nlp = spacy.load('en_core_web_sm')
app = flask.Flask(__name__)
app.config["DEBUG"] = True
#app.route('/',methods=['POST'])
def home():
return "<h1>Spacy NLP Demo</h1><p>This site is a prototype API for Spacy NLP</p>"
#app.route('/ner', methods=['POST'])
def ner():
print(request.args,"The post is working, but it is not reading the requests")
if 'text' in request.args:
text = request.args['text']
else:
return "Error: No text field provided. Please specify text to process."
#Limit the text size to 10K characters for safety
print("text len: ", len(text))
text = request.args['text']
truncated_text = text[:10000]
doc = nlp(truncated_text)
ents = []
for sent in doc.sents:
for span in sent.ents:
ent = {"verbatim": span.text, "ent_type": span.label_, "hit_span_start": span.start_char, "hit_span_end": span.end_char }
ents.append(ent)
return json.dumps(ents)
app.run()
This app works fine with the GET. Please help me out in this.
You should know that the data will be sent from curl as a urlencoded form. So, to get the data from that POST request, you have to use request.form not request.args
So, the ner() method should be like so:
#app.route('/ner', methods=['POST'])
def ner():
print(request.form,"The post is working, but it is not reading the requests")
if 'text' in request.form:
text = request.form['text']
else:
return "Error: No text field provided. Please specify text to process."
#Limit the text size to 10K characters for safety
print("text len: ", len(text))
text = request.form['text']
...
...
You are doing a post and expecting data in get format.
You need to get data from form.
text = request.form.get("text")
if not text:
return "Error: No text field provided. Please specify text to process."
Similarly everywhere.
Hi I am new to writing web APIs in python. And my understanding of REST is limited
I have a simple Flask API that takes in a python dict {'pdf':pdf_as_bytes, 'filename':string}
The below is my server script:
#app.route("/predict", methods=["POST"])
def predict():
data = {"success": False}
if flask.request.method == "POST":
pdf = flask.request.files["pdf"].read()
filename = flask.request.files["filename"].read().decode("utf-8")
assert isinstance(filename, str), "Got {}".format(type(filename))
assert isinstance(pdf, bytes), "Got {}".format(type(pdf))
# further processing happens and returns a json
This works as intended when I write a python client as follows:
import requests
import os
ip = "localhost"
port = 8605
url = "http://{}:{}/predict".format(ip,port)
path_to_pdf = "./617339931.pdf"
with open(path_to_pdf, "rb") as f:
pdf = f.read() # pdf is a bytes
# the payload must have the following fields: "pdf": bytes, "filename": string object
payload = {"pdf":pdf,"filename":os.path.basename(path_to_pdf).split(".")[0]}
# post request
result = requests.post(url=url, files=payload).json()
# the resulting json always has a field called as success, which returns True or False accordingly
if result["success"] == True:
print(result["data"].keys())
But, When I send a request using Postman I get a 400 Error! Below is the screen shot of the error
I don't understand. How can I change my server code so that it works with Postman and also Python client programs
I just did the same thing, and I think it's because of the double quotes you are putting in key and value, try to take them out.
My API works fine and I see a 200 status when I test it using Postman. However I'm trying to access it using a Kivy application but I'm seeing a 400 response from the server after some waiting or quitting the app. By the way when testing with Postman I specify header as Content-Type: application/json and in body I see my parameters
{
"search_text": "Hello",
"num_results": 1
}
being sent as raw data.
My code
def search(self, search_text):
header = {'Content-Type':'application/json'}
req = UrlRequest('http://127.0.0.1:5000/search',req_body={"search_text": search_text,"num_results": 1},on_success=Test.got_json,req_headers=header)
print("Search method called")
#staticmethod
def got_json(req,result):
print(result)
Kivy docs say that you don't have to specify a method as this would send a POST request so I've not specified that here
Edit: The code for the server is kind of irrelevant for my issue here so I've removed it
UrlRequest should be passed a str object as request body. You can serialize the request dictionary as a string object by dumping it. Pass this dumped dictionary as the request body to UrlRequest.
import json
req_body=json.dumps({'search_text': search_text, 'num_results': 1})
req = UrlRequest(
'http://127.0.0.1:5000/search',
req_body=req_body,
on_success=Test.got_json,
req_headers=header)
req_body is a string parameter, might be a bit confusing as req_headers is a dict. You can use:
req_body=json.dumps({"search_text": search_text,"num_results": 1})
I am trying to send a JSON message from a computer to another one via a post request.
The script which sends the message is the following:
message = {'station':'turn on'}
res = rest.send( 'POST', server_addr + "/newstation", json.dumps(message), {'Content-Type': 'application/json'} )
The rest.send(...) method should be correct as I used it before and it worked fine.
The PC which sends the post request runs Linux, while the receiving one runs Win 8, if that means anything.
On the receiving machine I have the following:
#app.route('/newstation', methods = ['POST'])
def new_station ():
j_data = request.get_json()
d = decode_data(j_data)
where decode_data(j_data) is the following
def decode_data(j_data):
d = json.loads(j_data)
return d
My problem is: whenever I try to send the post request from the first machine the response is "Internal server error" and on the machine with the server the error returned is "TypeError: expected string or buffer".
Now I am thinking that it may be a matter of encoding of the string.
The post request is received and I can print the json content without problems, the issue arises when I try to decode.
I fixed the issue, it was a mistake on my part (of course). I misunderstood the documentation.
#app.route('/newstation', methods = ['POST'])
def new_station ():
j_data = request.get_json()
#d = decode_data(j_data)
request.get_json() already returns me a dictionary, so the decode_data function isn't actually needed. I already have the result without the need for json.loads().
I am having trouble sending a string of bytes (an image) to my backend.
In my code I have:
# sends a httplib2.Request
backend_resp, backend_content = self.mirror_service._http.request(
uri=backend_path,
body=urllib.urlencode({"img":content}))
This sends a request where content is a large string of bytes.
in my backend I have:
class Handler(webapp2.RequestHandler):
def get(self):
image_bytes = self.request.get("img")
logging.info(image_bytes) # output is empty string
Which logs an empty string.
I have also tried
image_bytes = self.request.body
and just setting body = content in the request, but these also return nothing
I know the backend is receiving the request because the backend logs have messages I have placed.
What is the correct way to send and retrieve my GET data?
EDIT:
Here's what content logs before trying to send it to my backend:
logging.info(str(type(content)))
# returns <type 'str'>
logging.info(content)
# logs a long string of bytes
On another note, I also get this warning in the logs when sending the request, but I'm not sure how to fix it:
new_request() takes at most 1 positional argument (2 given)
I'm guessing that this warning means that the 1 positional argument it takes is path=, and it's ignoring my body= argument. I think the warning changes to (3 given) if I add method="POST" or method="GET"
I tried using a POST method too, but logging.info won't display in my logs. I tried just writing self.request.body or self.request.get('img') back to the response, and it still just returns an empty string like the GET method.
To send a post from httplib2 :
import urllib
import httplib2
http = httplib2.Http()
url = '<your post url>'
body = {'img': 'all your image bytes...'}
headers = {'Content-type': 'application/x-www-form-urlencoded'}
response, content = http.request(url, 'POST', headers=headers, body=urllib.urlencode(body))
see httplib2 docs
To receive a post in Webapp2:
class Handler(webapp2.RequestHandler):
def post(self):
image_bytes = self.request.POST.get("img")
logging.info(image_bytes) # output is empty string
I haven't tested this code, but it should give you and idea how it should be done.