URL parse vs constructor: path missing - javascript

I am total beginner in Node.js but I am trying to fix what I thought was a simple issue. I am using the following code example for an AWS Lambda function using Node.js 12 runtime:
function respond(event, context, responseStatus, responseData, physicalResourceId, noEcho) {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: noEcho || false,
Data: responseData
});
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length
}
};
var request = https.request(options, function(response) {
context.done();
});
request.on("error", function(error) {
console.log("send(..) failed executing https.request(..): " + error);
context.done();
});
request.write(responseBody);
request.end();
}
Full source code can be found here: https://github.com/aws-samples/amazon-cloudfront-secure-static-site/blob/7f96cdbcfbd7f94c3ab5a4c028b6bafd10744c83/source/witch/witch.js#L70
My IDE gives me a warning that the URL.parse() method is deprecated and so that I should use the URL constructor instead. So the only change I made is replacing:
var parsedUrl = url.parse(event.ResponseURL);
with
var parsedUrl = new url.URL(event.ResponseURL);
But when I do that, the options.path field ends up missing. What is even more confusing to me is that if I log the parsedUrl variable (passing it through JSON.stringify()), I can see that when I use url.parse(), I get a simple string in parsedUrl:
"https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
But when using the constructor, I can see in the log an object structure with all the expected fields (protocol, hostname, port, even path):
{
"protocol": "https:",
"slashes": true,
"auth": null,
"host": "cloudformation-custom-resource-response-useast1.s3.amazonaws.com",
"port": null,
"hostname": "cloudformation-custom-resource-response-useast1.s3.amazonaws.com",
"hash": null,
"search": "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"query": "XX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"pathname": "/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff",
"path": "/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"href": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
}
So if anything the constructor seems to provide a better break down of the URL. I don't why, when I try to copy the parsedUrl.path field to options.path it works when parsedUrl comes from the parse() method but not when it comes from the constructor. The hostname field on the other hand works in both cases.
Any idea what's the issue here?

As indicated by the OP, the use of the url.parse method is discouraged in favor of the WATWG URL API. The legacy .path attribute returns the combined pathname and search components. Although the preferred WATWG URL API does not have the path attribute, the value required by options.path can be constructed by combining the .pathname and .search attributes.

Related

Twilio Autopilot's custom channel Memory payload

I'm creating a web chat and I need to integrate an assistant using Twilio Autopilot, I use Autopilot's custom channels as follows:
I send a POST request to https://channels.autopilot.twilio.com/v2/{AccountSid}/{AssistantSid}/custom/webchat
I fill in these fields in the request data (Language=en-US, UserId=user123, Text=Something)
I add a Memory parameter in the Assistant URL
My backend code:
// Assistant URL Memory parameter
const context = JSON.stringify({
accountHolder: "foo bar",
balance: 123
}, null, "");
// Request params
const urlencoded = new URLSearchParams();
urlencoded.append("Language", "en-US");
urlencoded.append("UserId", message.Author);
urlencoded.append("Text", message.Body);
const requestConfig: AxiosRequestConfig = {
headers: {
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json",
"authorization" : "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
},
method: "POST",
url: `https://channels.autopilot.twilio.com/v2/${accountSid}/${assistantSid}/custom/webchat?Memory=${context}`,
data: urlencoded
};
// Trigger the bot
const twilioResponse = this.httpService.request(requestConfig);
const result = await lastValueFrom(twilioResponse.pipe(map((response) => response.data)));
Twilio function:
exports.handler = async function(context, event, callback) {
console.log(event.Memory);
const userData = JSON.parse(event.Memory); // Memory does not contains my payload !
return callback(null, {
"actions": [
{
"say": `Hello ${userData.accountHolder}, your balance is ${userData.balance}`
},
{
"listen": true
}
]
});
};
My problem is, The Memory parameter is not sent to the function
I had a similar problem trying to pass the payload in and reached out to a Twilio support.
The answer I received was:
"As per the present update from our engineering team, they are not looking at implementing the Memory parameter on V2 endpoint as there are very less users consuming it. You will have to use V1 endpoint for Autopilot if you are looking to use URL Parameter "Memory" for your use case. "
Therefore the URL will need to look like so:
https://channels.autopilot.twilio.com/v1/ACxxx/UAxxx/custom/chat?Memory={"CarModel":"Diablo","CarMake":"Lamborghini","CarYear":"2019"}'
Twilio developer evangelist here.
I would have thought you might want to URL encode the JSON memory when you add it to the URL. Try this:
const context = encodeURIComponent(
JSON.stringify({
accountHolder: "foo bar",
balance: 123
}, null, "")
);

When using PUT method body is not passed using Koa

I am trying to make update function, where a user can put the new data and the data in the server would get updated, a simple task, however, when I try to PUT new data, the body is always undefined.
The data which gets sent:
request: {
method: 'PUT',
url: '/api/v1.0/articles/1',
header: {
'user-agent': 'PostmanRuntime/7.17.1',
accept: '*/*',
'cache-control': 'no-cache',
host: 'localhost:3000',
'accept-encoding': 'gzip, deflate',
'content-length': '98',
connection: 'keep-alive'
}
},
response: {
status: 404,
message: 'Not Found',
header: [Object: null prototype] {}
},
Now I tried passing it as keys using other methods and not RAW method, this is what is inside of the body I am trying to pass:
{
"title": "another article",
"fullText": "again here is some text hereto fill the body"
}
This is the function which should update the data, but it gets undefined from the put request.
router.put("/:id", updateArticle);
function updateArticle(cnx, next) {
let id = parseInt(cnx.params.id);
console.log(cnx);
if (articles[id - 1] != null) {
//articles[id - 1].title = cnx.request.body.title;
cnx.body = {
message:
"Updated Successfully: \n:" + JSON.stringify(updateArticle, null, 4)
};
} else {
cnx.body = {
message:
"Article does not exist: \n:" + JSON.stringify(updateArticle, null, 4)
};
}
}
I am using postman, Body -> Raw | JSON, I do have to mention all other methods work perfectly - delete, create, getAll, getById
With a PUT or POST, the data is in the body of the request. You have to have some code (in your request handler or some middleware) that actually reads the body from the stream and populates the body property for you. If you don't have that, then the data is still sitting in the request stream waiting to be read.
You can see an example of reading it yourself here: https://github.com/koajs/koa/issues/719 or there is pre-built middleware that will do that for you.
Here's are a couple modules that will do that middleware for you:
https://github.com/dlau/koa-body
https://www.npmjs.com/package/koa-body-parser

How to modify request headers using Puppeteer & Chrome DevTools Protocol? (Possibly a JS syntax issue)

I have the following Typescript function that assumes a Chrome browser has already been launched using Puppeteer. The documentation for the Fetch functions used below can be found here.
async function modify(client: CDPSession) {
client.on('Fetch.requestPaused', async ({ requestId, request, frameId, resourceType, responseErrorReason, responseStatusCode, responseHeaders, networkId }) => {
// Correctly prints out the User-Agent header's value
console.log(request.headers["User-Agent"]);
// After this line is run, I can inspect the request.headers object and see that User-Agent was successfully edited
request.headers['User-Agent'] = 'trying to edit this header';
// Continuing the request gives an error
await client.send('Fetch.continueRequest', {
requestId: requestId,
headers: request.headers,
});
});
}
Here is the specific error I'm seeing:
Error: Protocol error (Fetch.continueRequest): Invalid parameters headers: array expected
How can I resolve this error and successfully modify the request.headers? Is this a silly Javascript/Typescript syntax issue that I just can't figure out?
Fetch.requestPaused returns the headers as an object. e.g.:
{
"Upgrade-Insecure-Requests":"1",
"Accept": "text/html,application/xhtml+xml"}
}
Fetch.continueRequest expects an Array<{name: string, value: string}>. e.g.
[
{"name": "Accept", value: "text/html,application/xhtml+xml"}
]
You can use the code that Puppeteer is using:
function headersArray(headers) {
const result = [];
for (const name in headers) {
if (!Object.is(headers[name], undefined))
result.push({name, value: headers[name] + ''});
}
return result;
}

Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body in react-admin

I am using react-adminframework, and I have written my own DataProvider. I am trying to accomplish that when an User is created, an instance of UserPossession is created as well. My code bellow accomplishes that, but react-admin Front-end just displays the warning message:
Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body
I checked the Network tab in Developer Tools and every request to server is correct, there is no error. Which leaves me confused and stuck with this, because I have no idea what that warning means or why is it even occuring.
My code is a part of convertDataRequestToHTTP constant and looks like this:
if (resource === 'User') {
url = `${apiUrl}/${resource}`;
options.body = params.data;
httpClient(url, {
method: 'POST',
body: JSON.stringify(options.body),
})
.then(response => (
url = `${apiUrl}/Location`,
options.method = 'POST',
options.body = JSON.stringify({
"odata.type": "HardwareDatabase.UserPossession",
"Name": response.json.Login,
"UserId": response.json.Id
}),
httpClient(url, {
method: options.method,
body: options.body
})
));
}
If you have any questions regarding the code I can clarify.
Thank you for any ideas in advance.
Since you are stating that this code snippet is a part of convertDataRequestToHTTP I might see the issue. httpClient cannot be used in this constant since it creates duplicit calls to API or in your case, this Warning. Correct way would be to only state the options constant.
url = `${apiUrl}/${resource}`;
options.body = JSON.stringifiy(params.data);
options.method = 'POST';
Later in the constant that converts response from OData to mandatory React Admin format, state the httpClient.
params.data = {
"odata.type": "HardwareDatabase.UserPossession",
"Name": response.json.Login,
"UserId": response.json.Id
};
httpClient(`${apiUrl}/Location`, {
method: 'POST',
body: JSON.stringify(params.data),
})
Unfortunately, the GET method for XMLHttpRequest and fetch don't support request bodies.
A temporary work around I found was to use the axios library, which does let you send GET with request bodies.
const res = await axios.get("/api/devices", {
data: { deviceName: 'name' }
})

Twitter REST API: Bad Authentication data

I'm trying to post a tweet but for any reason it doesn't work as expected.
I suspect that the issue could be related to the signature string, but following what twitter says according to signing requests looks ok.
Here is my code:
function postTweet(user_id, AccessToken, AccessTokenSecret) {
var base_url = 'https://api.twitter.com/1.1/statuses/update.json',
oauth_nonce = randomString(),
oauth_signature,
oauth_timestamp = Math.floor(new Date().getTime() / 1000),
reqArray,
req,
signature_base_string,
signing_key;
reqArray = [
"include_entities=true",
'oauth_consumer_key="' + CONFIG.TWITTER_CONSUMER_KEY + '"',
'oauth_nonce="' + oauth_nonce + '"',
'oauth_signature_method="HMAC-SHA1"',
'oauth_timestamp="' + oauth_timestamp + '"',
'oauth_token="' + AccessToken + '"',
'oauth_version="1.0"',
'status=' + encodeURIComponent("hello world")
];
req = reqArray.sort().join('&');
signature_base_string = "POST&" + encodeURIComponent(base_url) + "&" + encodeURIComponent(req);
signing_key = CONFIG.TWITTER_CONSUMER_KEY_SECRET + '&' + AccessTokenSecret;
oauth_signature = encodeURIComponent(CryptoJS.HmacSHA1(signature_base_string, signing_key).toString(CryptoJS.enc.Base64));
return $http.post('https://api.twitter.com/1.1/statuses/update.json', {
status: 'hello world'
}).then(function (response) {
return response;
}).catch(function (error) {
console.log(error);
});
}
As a response, I get that:
UPDATE
considering that in my project I already have $cordovaOauthUtility I started using it this way:
function postTweet(accessToken, accessTokenSecret) {
var params, signature;
params = {
include_entities: true,
oauth_consumer_key: CONFIG.TWITTER_CONSUMER_KEY,
oauth_nonce: $cordovaOauthUtility.createNonce(10),
oauth_signature_method: "HMAC-SHA1",
oauth_token: accessToken,
oauth_timestamp: Math.round((new Date()).getTime() / 1000.0),
oauth_version: "1.0"
};
signature = $cordovaOauthUtility.createSignature('POST', 'https://api.twitter.com/1.1/statuses/update.json', params, { status: "hello" }, CONFIG.TWITTER_CONSUMER_KEY_SECRET, accessTokenSecret);
return $http.post('https://api.twitter.com/1.1/statuses/update.json', {
status: "hello"
}, {
headers: {
Authorization: signature.authorization_header
}
})
.then(function (response) {
return response;
}).catch(function (error) {
console.log(error);
});
}
UPDATE 2
After trying all the posibilities, the problem persist. Here I paste a plnkr where I have my code.
You are using crypto's HmacSHA256 but sending HMAC-SHA1 as the oauth_signature_method parameter which is the twitter one.
You should probably change your code to
oauth_signature = CryptoJS.HmacSHA1(signature_base_string, signing_key).toString(CryptoJS.enc.Base64);
If you look at your authorization header, you can also notice that something is wrong with it. Indeed, you can see that the oauth_nonce and the oauth_version are prefixed by a & sign, which shouldn't be the case and most likely mean to the api you are not specifying them. It probably comes from the fact you are using the same reqArray to construct both the signature and the header, or your code is not updated.
You also probably don't want to change the global headers sent from your app, in case another request is sent to another api at the same time. Rather, you should send this authorization header only for this specific xhr.
return $http.post('https://api.twitter.com/1.1/statuses/update.json', {
status: 'hello world',
}, {
headers: {
Authorization: auth,
},
})
Well, you're clearly adding oauth_token in your request array but it didn't show up in the screenshot? Is the AccessToken in the params undefined?
EDIT
According to the documentation, we must append double quotes to the headers. Try this?
reqArray = [
"include_entities=true",
'oauth_consumer_key="'+CONFIG.TWITTER_CONSUMER_KEY+'"',
'oauth_nonce="'+oauth_nonce+'"',
'oauth_signature_method="HMAC-SHA1"',
'oauth_timestamp="'+oauth_timestamp+'"',
'oauth_token="'+AccessToken+'"',
'oauth_version="1.0"',
'status='+encodeURIComponent("hello world")
];
Yikes.
I've downloaded your plnkr bundle and added a read only application key set. I only had to set up and make one change to get a {"request":"\/1.1\/statuses\/update.json","error":"Read-only application cannot POST."} response. Initially I was receiving {"errors":[{"code":32,"message":"Could not authenticate you."}]}.
Remove status: "hello" from between the curly brackets { } where you create your signature.
signature = $cordovaOauthUtility.createSignature('POST', 'https://api.twitter.com/1.1/statuses/update.json', params, { }, twitter.consumer_secret, twitter.access_token_secret);
My request headers become the following:
:authority:api.twitter.com
:method:POST
:path:/1.1/statuses/update.json
:scheme:https
accept:application/json, text/plain, */*
accept-encoding:gzip, deflate, br
accept-language:en-US,en;q=0.8
authorization:OAuth oauth_consumer_key="x",oauth_nonce="QFMmqiasFs",oauth_signature_method="HMAC-SHA1",oauth_token="y",oauth_timestamp="1496340853",oauth_version="1.0",oauth_signature="7Ts91LKcP%2FrYsLcF5WtryCvZQFU%3D"
content-length:18
content-type:application/json;charset=UTF-8
origin:http://localhost
referer:http://localhost/twits/
user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36
Googling eventually led me to a tutorial:
Displaying the Twitter Feed within Your Ionic App. I noted his general createTwitterSignature function does not take parameters and tweaked your code similarly.
function createTwitterSignature(method, url) {
var token = angular.fromJson(getStoredToken());
var oauthObject = {
oauth_consumer_key: clientId,
oauth_nonce: $cordovaOauthUtility.createNonce(10),
oauth_signature_method: "HMAC-SHA1",
oauth_token: token.oauth_token,
oauth_timestamp: Math.round((new Date()).getTime() / 1000.0),
oauth_version: "1.0"
};
var signatureObj = $cordovaOauthUtility.createSignature(method, url, oauthObject, {}, clientSecret, token.oauth_token_secret);
$http.defaults.headers.common.Authorization = signatureObj.authorization_header;
}
I've read conflicting things about why there should/shouldn't be other parameters there, but I believe the signature is just supposed to be the basis of access and doesn't hash in every operation you want to perform - see Understanding Request Signing For Oauth 1.0a Providers.

Categories

Resources