How do I do a Regex path match for a POST request with Nock.js? - nock

I am trying to chain some requests in Nock with Regex matching on the path. All of the examples I see of Regex path matching are get requests. How do I do this properly? Is the below right?
nock('https://base-url.com')
.post('/v1/secrets/*$', {
username: 'pgte',
email: 'pedro.teixeira#gmail.com',
})
.reply(200)

The following code demonstrates a successful Nock on a POST request using a regex on the path.
nock('https://base-url.com')
.post(/^\/v1\/secrets\/.*$/, {
username: 'pgte',
email: 'pedro.teixeira#gmail.com',
})
.reply(200, 'OK')
const resp = await axios.post('https://base-url.com/v1/secrets/foobar', {
username: 'pgte',
email: 'pedro.teixeira#gmail.com',
})
expect(resp.status).toBe(200)
expect(resp.data).toBe('OK')
Things to note about the value passed to the post method; If you want to use regex matching, you need to pass an instance of a RegExp as the first argument. Here I'm using the short-hand notation, but you could also create an instance using the new RegExp("...") notation, if desired.
Also, the example in your question had the asterisk character directly after the last slash. Which translates to the path needs to end with zero or more slashes. I assume that's not what you want, but my point is, use something like https://regex101.com to validate your regexs.
Nock will test the pathname of the URL (as defined here) against the regex, including the leading slash.

Related

How to authenticate Shopware 6 <base-app-url> correctly

With the Admin SDK it's possible to further enrich the administration in Shopware 6. As in the installation guide for apps stated, an entry point (base-app-url) needs to be provided in the manifest file of an app.
Since every request needs to be authenticated properly, this GET request also needs authentication. However, I am not able to authenticate this one in the same way as I am successfully doing it with the GET request from modules.
The base-app-url request looks the following (in my case with some [custom] entity privileges):
http://localhost:3000/sdk?location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE&shopware-shop-signature=e7b20a46487046a515638f76c6fadab6b1c749ea4a8ac6e7653527e73ba18380
The shop has the following data
Shop {
_id: 'sbzqJiPRrbHAlC2K',
_url: 'http://localhost:8888',
_secret: '3c5a2f031006791f2aca40ffa22e8febbc8a53d8',
_apiKey: 'SWIAB2PVODCWSLZNDMC5ZM1XWA',
_secretKey: 'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg'
}
I am currently authenticating my modules like the following (Node.js):
const SHOPWARE_SHOP_SIGNATURE = 'shopware-shop-signature';
export function authenticateGetRequest(req: Request, shop: Shop): void {
// e7b20a46487046a515638f76c6fadab6b1c749ea4a8ac6e7653527e73ba18380
const signature = getSignatureFromQuery(req);
verifySignature(shop.secret, removeParamsFromQuery(req), signature);
}
function getSignatureFromQuery(req: Request): string {
if (!req.query[SHOPWARE_SHOP_SIGNATURE]) {
throw new Error('Signature is not present in request!');
}
return req.query[SHOPWARE_SHOP_SIGNATURE] as string;
}
function removeParamsFromQuery(req: Request): string {
// Some code
// Returns following string - Does neither work for base-app-url nor for module GET requests:
// 'shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964'
// If the string follows this pattern, it works only for modules:
// shop-id={id}&shop-url={url}&timestamp={ts}&sw-version={v}&sw-context-language={cl}&sw-user-language={ul}
}
function verifySignature(secret: string, message: string, signature: string): void {
const hmac = crypto.createHmac('sha256', secret).update(message).digest('hex');
if (hmac !== signature) {
throw new Error('Signature could not be verified!');
}
}
However the base-app-url cannot be verified correctly and the "Signature could not be verified!" error is thrown.
What am I doing wrong here?
More info:
Additionally I added a GET request for a module where everything is working:
http://localhost:3000/faq?shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE&shopware-shop-signature=0f0889c9e8086c6c3553dc946a01f2ef27b34cd1c55b0c03901b6d8a6a9b6f53
The resulting string can be verified:
shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE
Try out following code in some php sandbox environment:
<?php
$message = 'shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045963&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE';
$secret = '3c5a2f031006791f2aca40ffa22e8febbc8a53d8';
$signature = '0f0889c9e8086c6c3553dc946a01f2ef27b34cd1c55b0c03901b6d8a6a9b6f53';
$hmac = hash_hmac('sha256', $message, $secret);
if (!hash_equals($hmac, $signature)) {
echo 'Signature not valid';
} else {
echo 'Signature valid';
}
SOLUTION:
Express decodes the query strings automatically with req.query depending on your express configuration. Keep in mind to validate the hmac with encoded query params as they are passed from shopware.
In my case the only difference where the decoded privileges and they looked like this:
&privileges={"read":["language","ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"create":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"update":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"],"delete":["ce_atl_faq_group_faqs","ce_atl_faq_group","ce_atl_faq"]}
But they need to look like this:
&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D
Looking at the QuerySigner, this is how the signature is generated on the side of Shopware with the actual arguments:
hash_hmac(
'sha256',
'location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE',
'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg'
);
// 8034a13561b75623420b06fb7be01f20d97556441268939e9a5222ffec12215a
Given on your side you remove the shopware-shop-signature query param AND that the secrets are equal on both sides, you should be able to regenerate the matching signature.
const crypto = require('crypto');
const message = 'location-id=sw-main-hidden&privileges=%7B%22read%22%3A%5B%22language%22%2C%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22create%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22update%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%2C%22delete%22%3A%5B%22ce_atl_faq_group_faqs%22%2C%22ce_atl_faq_group%22%2C%22ce_atl_faq%22%5D%7D&shop-id=sbzqJiPRrbHAlC2K&shop-url=http://localhost:8888&timestamp=1674045964&sw-version=6.4.18.0&sw-context-language=2fbb5fe2e29a4d70aa5854ce7ce3e20b&sw-user-language=de-DE';
const hmac = crypto.createHmac('sha256', 'VnNwM0ZOMnN1Y05YdUlKazlPdlduWTdzOHhIdFpacjVCYkgzNEg').update(message).digest('hex');
// 8034a13561b75623420b06fb7be01f20d97556441268939e9a5222ffec12215a
So in theory your code looks fine. Verify that the query string matches exactly. Things to check:
Maybe your node server decodes the url entities unwantedly?
Does your node serve escape special characters in the query string?
Do the secrets match on both sides?
To consider additionally:
Consider to just point the base-app-url to a static page outside of the scope of your app server instead. As that page will be loaded inside an iframe, you can use client side javascript to read the query parameters and, only if necessary, make requests to your app server using the credentials from inside the iframe. Keep in mind you really only need the authentication if you need to handle personalized data, otherwise you might as well serve static assets without the need for authentication.

How can I parse HTTP headers using ktor

I'm using ktor 1.5.3 HTTP client and wondering how can I deserialize HTTP response headers into a list of LinkHeader values. I have the following value in my code:
response.headers.getAll("Link")
which is a list of strings, and I want to get a value of type
List<LinkHeader>
UPDATED:
The details of my use-case:
I have a backend that uses the following response headers to manage pagination:
Link: <https://hostname/v2/issues?orderBy=updated&orderAsc=false&perPage=15>; rel="first"
Link: <https://hostname/v2/issues?orderBy=updated&orderAsc=false&page=2&perPage=15>; rel="prev"
Link: <https://hostname/v2/issues?orderBy=updated&orderAsc=false&page=4&perPage=15>; rel="next"
Link: <https://hostname/v2/issues?orderBy=updated&orderAsc=false&page=116922&perPage=15>; rel="last"
I just have to parse them to understand where is the last page
Since there is not such functionality in Ktor right now, I've created this feature request to address your problem. As a workaround, you can use regular expressions for your particular case to parse headers' values:
data class Link(val url: Url, val rel: String)
fun parse(value: String): Link {
val matches = Regex("""<(.+?)>;\s*rel="(.+?)"""").matchEntire(value) ?: throw Exception("Cannot parse Link header value $value")
val (_, urlString, rel) = (matches.groupValues)
return Link(URLBuilder(urlString).build(), rel)
}
As there is no accurate solution from Ktor, I've implemented a workaround from this article. The same do-while loop worked in my case as well. It makes a redundant API call for an empty last page but works.

Nock fixtures: Ignore request body when matching

In the Readme for nock it explains how one can ignore the request body.
I am using the fixtures feature of nock and I need to ignore the request body (at least partially). How can I do this? Can I write a regex in the body field of the json entries in the fixtures files?
I solved it myself after some painful digging. Here is the solution:
I am trying to nock requests to the Kraken API. The request for the endpoint /0/private/TradesHistory is of method POST and sends a body containing a "query string like" string. For example the body will look like ofs=50&nonce=xxx. The nonce value changes with each request, so I want to ignore it to when looking for a matching nock. (The nonce can only be used once, it would make no sense for the client library to send the same value again).
So I have to add a "preprocessing" function as a config object to the call to nockBack like so:
import * as queryString from 'query-string'
import { back } from 'nock'
import * as path from 'path'
const before = (scope: any) => {
scope.filteringRequestBody = (body: string, aRecordedBody: string) => {
const { of: currentOffset, } = queryString.parse(`?${body}`) //Prepend a `?` so it is actually a query string.
const { of: recordedOffset, } = queryString.parse(`?${body}`)
if (!(currentOffset || recordedOffset)) {//This is the case where no offset is set. There is only possible recorded body in my case that matches this: The other body which has no offset. I replace the body with the recorded body to produce a match.
return aRecordedBody
}
if (currentOffset === recordedOffset) {//This is the case where the sent body has the same offset as the recorded body. I replace the body with the recorded body in order to produce a match.
return aRecordedBody
}
return body
}
}
back.fixtures = `${__dirname}/nockFixtures/`
const { nockDone } = back('nocks.json',{before})
...//Run my queris
nockDone()
Now it works like a charm.

Getting results from api

I am trying to do a domain availability search using an API from free domain API.
After i create an account, it shows:
**Make a REST request using this URL:**
http://freedomainapi.com/?key=11223344&domain=freedomainapi.com
And looking in the documentation page, it has only:
Request http://freedomainapi.com?key=YOUR_API_KEY&domain=DOMAIN_NAME
Result:
{
"status": "success",
"domain": "freedomainapi.com",
"available": false
}
I am very new to APIs...
What I need is to show a domain search box, and when the user enters, it should return with result.
It claims to show domain suggestions as well. I hope it will also work.
Using jquery and a jsonp proxy
http://jsfiddle.net/mp8pukbm/1/
$.ajax({
type: 'GET',
url: "https://jsonp.nodejitsu.com/?callback=?",
data: {url: 'http://freedomainapi.com?key=14ejhzc5h9&domain=freedomainapi.com'},
dataType: "jsonp",
success: myfn
});
function myfn(data) {
console.log(data);
}
you have to use the proxy because cross domain json is not permitted
EDIT:
i made an update to show the result in a div (stringified)
http://jsfiddle.net/mp8pukbm/2/
EDIT #2: i created a test key on that site, you have to use your own
EDIT #3: and there's your combo: http://jsfiddle.net/mp8pukbm/4/
Assuming that you will use java script for showing the search box, you can use AJAX feature of java script (or jQuery or Dojo) ... All you need to do is a "GET" request that like you can pasted and you will get the result back on the response object. To try out the API you can use "Postman" application in Chrome. https://chrome.google.com/webstore/detail/postman-rest-client/fdmmgilgnpjigdojojpjoooidkmcomcm?hl=en
In the response object of the AJAX call you will get a JSON object which you can parse and display the result.
Normally when we use REST we need to differentiate one REST call from another.
Assuming this url
http://freedomainapi.com/checkAvailability?key=YOUR_API_KEY&domain=DOMAIN_NAME
In Application layer we need to write an interface
#GET
#Path("/checkAvailability")
#Produces({MediaType.APPLICATION_JSON})
public ReturnObject getDomainAvailability(#QueryParam("key") String key,
#QueryParam("domain") String doaminName );
Once interface is done you need to write your implementation class.
This class will intract with business layer and perform search task and based on
result collected will create ReturnObject.
ReturnObject => will contain status, domain and availability
On screen
$.ajax({
type: "GET",
url: 'root/checkAvailability',
success: function(jsonData)
{
// read json and perform operation
}
,
error: function (error)
{
// handle error
}
});
If you are using JAVA as backend then you can use gson to parse the result, which is a json. After parsing you can read the values from result and display accordingly :)
Any API is a way to extend a given software. (Might be a website or an application)
In both ways there is a certain way to communicate with the software. In your example freedomainapi.com allows you to fetch if given domain is avaiable. There is no such thing as a suggestion tho, atleast i cannot find any suggestions at all.
Given output is a message format know as JSON. It can be easily interpreted by many major Languages such as Java, Javascript and PHP.
Given String might be easily interpreted as a Map consisting of a status (String), a domain (string) and avaiable (boolean)
A domain availability search could not be easier, assuming K is your key, D is your search input (Domain):
Download http://freedomainapi.com/checkAvailability?key=K&domain=D as input
Parse JSON from input as json
return json["status"] == "success" and json["avaiable"]
Depending on your language you might need to use methods to access properties of json, but that does not influence the basic usage of this api.
on user enters, it calls click_button function and I am assuming your result displaying div id is "main_container" you can give domain suggestions by passing related DOMAIN_NAME s as arguments to click_button function
function click_button(DOMAIN_NAME){
$.ajax({
url : 'http://freedomainapi.com?key=YOUR_API_KEY&domain=DOMAIN_NAME',
type: 'GET',
crossDomain: true,
contentType: "application/json; charset=utf-8",
success: function(data) {
data=JSON.parse(data);
if(data['available']){
$('#main_container').html($('#main_container').html()+'<br>'+DOMAIN_NAME+': Available');
else{
$('#main_container').html($('#main_container').html($('#main_container').html()+'<br>'+DOMAIN_NAME+': Not Available');
}//success
});//ajax
}
hope it helpful !

how to upload a file from node.js

I found many posts when I queried for this problem, but they all refer to how to upload a file from your browser to a node.js server. I want to upload a file from node.js code to another server. I tried to write it based on my limited knowledge of node.js, but it doesn't work.
function (data) {
var reqdata = 'file='+data;
var request = http.request({
host : HOST_NAME,
port : HOST_PORT,
path : PATH,
method : 'POST',
headers : {
'Content-Type' : 'multipart/form-data',
'Content-Length' : reqdata.length
}
}, function (response) {
var data = '';
response.on('data', function(chunk) {
data += chunk.toString();
});
response.on('end', function() {
console.log(data);
});
});
request.write(reqdata+'\r\n\r\n');
request.end();
})
The above function is called by other code that generates data.
I tried to upload same data file using curl -F "file=#<filepath>" and the upload is successful. But my code fails. The server returns an application specific error which hints that the uploaded file was invalid/corrupt.
I collected tcpdump data and analysed it in wireshark. The packet sent from my node.js code lacks the boundary required for the multipart data. I see this message in wireshark packet
The multipart dissector could not find the required boundary parameter.
Any idea how to accomplish this in node.js code?
jhcc's answer is almost there.
Having to come up with support for this in our tests, I tweaked it slightly.
Here's the modified version that works for us:
var boundaryKey = Math.random().toString(16); // random string
request.setHeader('Content-Type', 'multipart/form-data; boundary="'+boundaryKey+'"');
// the header for the one and only part (need to use CRLF here)
request.write(
'--' + boundaryKey + '\r\n'
// use your file's mime type here, if known
+ 'Content-Type: application/octet-stream\r\n'
// "name" is the name of the form field
// "filename" is the name of the original file
+ 'Content-Disposition: form-data; name="my_file"; filename="my_file.bin"\r\n'
+ 'Content-Transfer-Encoding: binary\r\n\r\n'
);
fs.createReadStream('./my_file.bin', { bufferSize: 4 * 1024 })
.on('end', function() {
// mark the end of the one and only part
request.end('\r\n--' + boundaryKey + '--');
})
// set "end" to false in the options so .end() isn't called on the request
.pipe(request, { end: false }) // maybe write directly to the socket here?
Changes are:
ReadableStream.pipe returns the piped-to stream, so end never gets called on that. Instead, wait for end on the file read stream.
request.end puts the boundary on a new line.
Multipart is pretty complex, if you want to make it look like how a client usually handles "multipart/form-data", you have to do a few things. You first have to select a boundary key, this is usually a random string to mark the beginning and end of the parts, (in this case it would be only one part since you want to send a single file). Each part (or the one part) will need a header (initialized by the boundary key), setting the content-type, the name of the form field and the transfer encoding. Once the part(s) are completed, you need to mark the end of each part with the boundary key.
I've never worked with multipart, but I think this is how it could be done. Someone please correct me if I'm wrong:
var boundaryKey = Math.random().toString(16); // random string
request.setHeader('Content-Type', 'multipart/form-data; boundary="'+boundaryKey+'"');
// the header for the one and only part (need to use CRLF here)
request.write(
'--' + boundaryKey + '\r\n'
// use your file's mime type here, if known
+ 'Content-Type: application/octet-stream\r\n'
// "name" is the name of the form field
// "filename" is the name of the original file
+ 'Content-Disposition: form-data; name="my_file"; filename="my_file.bin"\r\n'
+ 'Content-Transfer-Encoding: binary\r\n\r\n'
);
fs.createReadStream('./my_file.bin', { bufferSize: 4 * 1024 })
// set "end" to false in the options so .end() isnt called on the request
.pipe(request, { end: false }) // maybe write directly to the socket here?
.on('end', function() {
// mark the end of the one and only part
request.end('--' + boundaryKey + '--');
});
Again, I've never done this before, but I think that is how it could be accomplished. Maybe someone more knowledgable could provide some more insight.
If you wanted to send it as base64 or an encoding other than raw binary, you would have to do all the piping yourself. It will end up being more complicated, because you're going to have to be pausing the read stream and waiting for drain events on the request to make sure you don't use up all your memory (if it's not a big file you generally wouldn't have to worry about this though). EDIT: Actually, nevermind that, you could just set the encoding in the read stream options.
I'll be surprised if there isn't a Node module that does this already. Maybe someone more informed on the subject can help with the low-level details, but I think there should be a module around somewhere that does this.
As the error message states you are missing the boundary parameter. You need to add a random string to separate each file from the rest of the files/form-data.
Here is how a request could look like:
The content type:
Content-Type:multipart/form-data; boundary=----randomstring1337
The body:
------randomstring1337
Content-Disposition: form-data; name="file"; filename="thefile.txt"
Content-Type: application/octet-stream
[data goes here]
------randomstring1337--
Note that the -- in the beginning and end of of the random string in the body is significant. Those are part of the protocol.
More info here http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
The fastest way I was able to do this, that worked, was using the request package. The code was well documented and it just worked.
(For my testing I wanted a JSON result and non-strict SSL - there are many other options...)
var url = "http://"; //you get the idea
var filePath = "/Users/me/Documents/file.csv"; //absolute path created elsewhere
var r = request.post( {
url: url,
json: true,
strictSSL: false
}, function( err, res, data ) {
//console.log( "Finished uploading a file" );
expect( err ).to.not.be.ok();
expect( data ).to.be.ok();
//callback(); //mine was an async test
} );
var form = r.form();
form.append( 'csv', fs.createReadStream( filePath ) );