I am trying to retrieve some info from the camera system DVR from HIK Vision with GAS' UrlFetchApp. I am able to perform a successful request with Postman but having trouble getting this to work with the Google App Script's UrlFetchApp.fetch. I confirmed that my Digest calculation is correct. I did that by substituting values for nonce, cnonce, qop etc in my GAS with the ones which were in Postman and got the same Response string as the Postman did. See images of the Postman below:
Postman gets a 200 response and all the data that I need.
The way my app works is it sends a first request and gets 401 response. Then it sends a second request with the auth header calculated from the data received from the first request. I always get 401 no matter what I do. I played with the double quotes in the headers properties etc.
Here's the code:
function getRecordedDaysCount(){
const url = 'my_server_url';
const userName = 'user';
const pass = "password";
const uri = "/ISAPI/ContentMgmt/record/tracks/101/dailyDistribution";
const method = "POST"
const updatedUrl = url + uri;
let year = '2021';
let month = '03';
//payload required by the server for this request
let payload = '<?xml version: "1.0" encoding="utf-8"?><trackDailyParam><year>' + year + '</year><monthOfYear>' + month + '</monthOfYear></trackDailyParam>';
let options = {
"method" : method,
"muteHttpExceptions": true,
"headers":{
"Accept": "application/xml, text/plain, */*",
}
}
let data = UrlFetchApp.fetch(updatedUrl, options);
if(data.getResponseCode() == 401){
let wwwAuthenticate = data.getAllHeaders()["WWW-Authenticate"];
// Example WWWAuthenticate
// "Digest realm="493b21e13dddb4ef7745edaa", domain="::", qop="auth",
// nonce="17fbf10682dc4a7ceb04206cbcd95d8d:1616709300571", opaque="", algorithm="MD5", stale="FALSE""
let authData = wwwAuthenticate.split(',');
let authType = authData[0].split(' ')[0]; // Digest
if(authType === "Digest"){
let realm = authData[0].split('"')[1]; // "493b21e13dddb4ef7745edaa"
let qop=authData[2].split('"')[1]; // "auth"
let nonce = authData[3].split('"')[1]; // "17fbf10682dc4a7ceb04206cbcd95d8d:1616709300571"
let algorithm = authData[5].split('"')[1]; // "MD5"
let nc="00000001";
let cnonce= new Date().getTime().toString(16);
let hash1 = signMD5(userName +':'+ realm +':'+ pass);
let hash2 = signMD5(method+':'+ uri);
let response = signMD5(hash1+':'+nonce+':'+nc+':'+cnonce+':'+qop+':'+hash2);
let digestAuth = "Digest username=\"" + userName + "\"" +
", realm=\"" + realm + "\"" +
", nonce=\"" + nonce + "\"" +
", uri=\"" + uri + "\"" +
", qop=auth" +
", nc=" + nc +
", algorithm=\"MD5\"" +
", cnonce=\"" + cnonce + "\"" +
", response=\"" + response + "\"";
let headers = {
"Content-Type": "text/plain", // copied from Postman
"Accept-Encoding": "gzip, deflate, br", // copied from Postman
"Authorization": digestAuth,
}
let options = {
"method" : method,
"muteHttpExceptions": true,
"headers": headers,
"payload": (payload)
}
logUrlFetch(updatedUrl, options);
}
}
}
function signMD5(message){
let signature = Utilities.computeDigest(
Utilities.DigestAlgorithm.MD5,
message,
Utilities.Charset.UTF_8);
let signatureStr = '';
for (i = 0; i < signature.length; i++) {
let byte = signature[i];
if (byte < 0)
byte += 256;
let byteStr = byte.toString(16);
// Ensure we have 2 chars in our byte, pad with 0
if (byteStr.length == 1) byteStr = '0'+byteStr;
signatureStr += byteStr;
}
Logger.log(signatureStr);
return signatureStr;
}
function logUrlFetch(url, opt_params) {
let params = opt_params || {};
params.muteHttpExceptions = true;
let request = UrlFetchApp.getRequest(url, params);
Logger.log('Request: >>> ' + JSON.stringify(request));
let response = UrlFetchApp.fetch(url, params);
Logger.log('Response Code: <<< ' + response.getResponseCode());
Logger.log('Response text: <<< ' + response.getContentText());
if (response.getResponseCode() >= 400) {
throw Error('Error in response: ' + response);
}
return response;
}
These are the logs I'm getting:
headers after the first request
log after the second request
I compared the headers in the Postman and my app multiple times and they are mostly the same with some differences. The UrlFetchApp.fetch adds a few headers to the request however I added them also in Postman and it still worked so I concluded these additional headers were not a problem. Also the UrlFetchApp.fetch sends a request method as 'post' (lower case) and the Postman uses all caps - 'POST'. The http method is a part of the Digest HA2 calculation, which is case sensitive. This was the only idea that I had for why my requests didn't work. I changed my app's method to lower case 'post' for the digest calculation and for the headers and this did not help.
Is there a way to send requests in the Google App Script other than UrlFetchApp.fetch?
Thanks a lot!
Related
I am trying to implement twitter media upload on google apps script via OAuth1.0a... as there has been no oauth2 for media uploads since 2 years. Following is the code. Still facing 401, 402, 403 , 400... all such return codes since last one week. Is this end point not working? anyone has any info? any ideas why its failing again and again.
using OAuth1 (https://github.com/googleworkspace/apps-script-oauth1/tree/3f3a6697d95a3ed9a91d09c65ffc34941136f587)
var url = 'https://upload.twitter.com/1.1/media/upload.json?media_category=tweet_image';
var baseUrl = 'https://upload.twitter.com/1.1/media/upload.json';
var params = {
'payload': {'media': imageBlob},
'method': 'POST',
'muteHttpExceptions' : true
};
var token = JSON.parse(PropertiesService.getUserProperties().getProperty("oauth1."+ account));
var oauth_token = token.oauth_token
var oauth_token_secret = token.oauth_token_secret
var oauth_consumer_key = PropertiesService.getUserProperties().getProperty("TWITTER_CONSUMER_KEY");
var oauth_consumer_secret = PropertiesService.getUserProperties().getProperty("TWITTER_CONSUMER_SECRET");
const method = params['method'] || 'post';
params['method'] = method;
const oauthParameters = {
oauth_version: "1.0",
oauth_token: oauth_token,
oauth_consumer_key: oauth_consumer_key,
oauth_signature_method: "HMAC-SHA1",
oauth_timestamp: (Math.floor((new Date()).getTime() / 1000)).toString(),
};
oauthParameters.oauth_nonce = oauthParameters.oauth_timestamp + Math.floor(Math.random() * 100000000);
const payload = params['payload'] || {};
const q = {"media_category": "tweet_image"} //parms from url
const queryKeys = Object.keys(oauthParameters).concat(Object.keys(payload)).concat(Object.keys(q)).sort();
const baseString = queryKeys.reduce(function(acc, key, idx) {
if (idx) acc += encodeURIComponent("&");
if (oauthParameters.hasOwnProperty(key))
acc += _encode(key + "=" + oauthParameters[key]);
else if (payload.hasOwnProperty(key))
acc += _encode(key + "=" + _encode(payload[key]));
return acc;
}, method.toUpperCase() + '&' + _encode(baseUrl) + '&');
oauthParameters.oauth_signature = Utilities.base64Encode(
Utilities.computeHmacSignature(
Utilities.MacAlgorithm.HMAC_SHA_1,
baseString, oauth_consumer_secret + '&' + oauth_token_secret
)
);
if (!params['headers']) params['headers'] = {};
params['headers']['authorization'] = "OAuth " + Object.keys(oauthParameters)
.sort().reduce(function(acc, key) {
acc.push(key + '="' + _encode(oauthParameters[key]) + '"');
return acc;
}, []).join(', ');
params['payload'] = Object.keys(payload).reduce(function(acc, key) {
acc.push(key + '=' + _encode(payload[key]));
return acc;
}, []).join('&');
console.log(params)
response = UrlFetchApp.fetch(url, params);
for info...other than this, I also tried this repo - https://github.com/airhadoken/twitter-lib
still facing similar issues.
EDIT: on postman it works.. somethings wrong with the code then.. :(
I am attempting to authenticate with a service account to work on behalf of a user account on the domain. I have delegated admin access and added to the GSuite console. I can get an access token with the below but the making batch requests to copy drive files returns "code: 404, message: 'File not found:". The below code is writted in Google Apps Script. Am I missing something form the process to creating and authenticating the service account?
var CREDENTIALS = {
private_key: "-----BEGIN PRIVATE KEY----- XXXXXXX \n-----END PRIVATE KEY-----\n",
client_email: "XXXXXX#fXXXXXX.iam.gserviceaccount.com",
client_id: "1XXXXXXXXXXXXXXXX",
user_email: "XXXXX#XXXX.XXX.XXX",
scopes: ["https://www.googleapis.com/auth/drive","https://www.googleapis.com/auth/spreadsheets","https://www.googleapis.com/auth/userinfo.email","https://www.googleapis.com/auth/script.external_request"]
};
function oAuthToken(){
var url = "https://www.googleapis.com/oauth2/v3/token";
var header = {
alg: "RS256",
typ: "JWT",
};
var now = Math.floor(Date.now() / 1000);
var claim = {
iss: CREDENTIALS.client_id,
sub: CREDENTIALS.user_email,
scope: CREDENTIALS.scopes.join(" "),
aud: url,
exp: (now + 3600).toString(),
iat: now.toString(),
};
var signature = Utilities.base64Encode(JSON.stringify(header)) + "." + Utilities.base64Encode(JSON.stringify(claim));
var jwt = signature + "." + Utilities.base64Encode(Utilities.computeRsaSha256Signature(signature, CREDENTIALS.private_key));
var params = {
method: "post",
payload: {
assertion: jwt,
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
},
};
var res = UrlFetchApp.fetch(url, params).getContentText();
return JSON.parse(res)
}
The batch process is a bit rough but this is the gist of it.
var request={
batchPath:
requests:[]
}
var backoff =0
function batch(request) {
var oAuth=oAuthToken().access_token
var url ='https://www.googleapis.com/'+request.batchPath
var body =request.requests
if(body.length<1){
return []
}
var boundary = 'xxxxxxxxxx';
var contentId = 0;
var data = '--' + boundary + '\r\n';
for (var i in body) {
if(typeof body[i]=='object'){
data += 'Content-Type: application/http\r\n';
data += 'Content-ID: ' + ++contentId + '\r\n\r\n';
data += body[i].method + ' ' + body[i].endpoint + '\r\n';
data += body[i].requestBody ? 'Content-Type: application/json; charset=utf-8\r\n\r\n' : '\r\n';
data += body[i].requestBody ? JSON.stringify(body[i].requestBody) + '\r\n' : '';
data += "--" + boundary + '\r\n';
}
}
var parseBatchRes = function(res) {
var splittedRes = res.split('--batch');
return splittedRes.slice(1, splittedRes.length - 1).map(function(e) {
return {
contentId: Number(e.match(/Content-ID: response-(\d+)/)[1]),
status: Number(e.match(/HTTP\/\d+.\d+ (\d+)/)[1]),
object: JSON.parse(e.match(/{[\S\s]+}/)[0]),
};
});
};
var payload = Utilities.newBlob(data).getBytes();
var head = {Authorization: 'Bearer ' + oAuth}
var options = {
method: 'POST',
contentType: 'multipart/mixed; boundary=' + boundary,
payload: payload,
headers: head,
muteHttpExceptions: false
};
var complete=false;
var finalResponse=[];
for (var n=0; n<=backoff; n++) {
if(complete){
break;
}
var complete = true
console.log('backoff',n);
var response =UrlFetchApp.fetch(url, options).getContentText();
for(var j=0;j<response.length;j++){
if(response[r].status!=200){
var complete = false
}
}
}
}
Add the supportsAllDrives = true query parameter to the request.
The parameters indicates whether the requesting application supports both My Drives and shared drives and the default value for this is false.
Reference
Drive API Parameters
I am accessing API using Google Apps Script. I am looking for a https://developer.xero.com/documentation/api/reports#TrialBalance
I have tried with the API code as but I get signature invaid as reposnse result [19-09-03 19:45:46:402 IST] oauth_problem=signature_invalid&oauth_problem_advice=Failed%20to%20validate%20signature
function doGet(e) {
getTrialBalances();
}
function getTrialBalances() {
var oauth_nonce = createGuid();
var oauth_timestamp = (new Date().valueOf() / 1000).toFixed(0);
var CONSUMER_KEY = 'B7D5YA8D1HWHUZIGXL1AZS44N'
var PEM_KEY = '-----BEGIN RSA PRIVATE KEY-----' +
'ANIICXAIBAAKBgQC2WiSrkljVAZIgNUe/nBZ+PGJzauBJ6szlzPow1XoySkVikswui1IX4wUzgLmvnCmnQkRPgA43oiZqmK1H68MvirYzQkMa3sETViQAOiRPOrDEUTkemKiDXpaIKedD8T6/P9qzgtgU5hlP/R45POanIuNFvYPdpkm2yybOmI+1TwIjAQABAoGADt/3kc9UU7vXEa2G9shixVVjqoqTVTREFpLL7ePcHfIVCt9yrHFM9wnbyMG9uRZRIyDmbpumClROJImuADxc6reamXdTMX0OwEPogAREnY2diadjVjicoMYYEcdbb6pgDSOWcYtamNmzD5tkPI0bPFU+fTdpzGCOCECQQDvZTha0SRcCZPZipCs7PtAOWtMP1FBe140+cvsWiq2eHMmYDtIi7Mx210i3wzz4+Izl4jXeICKprppaBlJxSFZAkEAwwALfSnpqWeop86nnUICOPmksbK2rTtNVd+WGiAK4reUDJArOOXdDm7fYqppQNA35hxcRmvxeKK7jSYLQYHO5wJAeLFubRL+IszNVqLud9Buh52rQ+C0RbA9+bVqozl+SUqGu3VOzi9oY5114kvUCu38MAiY/BELtVuDpfrOrQuO2QJAHrZZGOOLC8VpyNRBjgEhfHvFNr+hCfO3IHlQmNjHHiIvzTK/u/xoLqfDwzR30194DmQVHHpP0+I9i+OcDjs1rQJBAJMY6h4QdYSFpTPxUOPA/s1lKVvJUIzgzX6oMfvc4TDb0RCz4nCvjJ1NEqPjveB6ze5TzC8BzfRW/aUh49vmgRA=' +
'-----END RSA PRIVATE KEY-----';
var payload = '';
var URL = 'https://api.xero.com/api.xro/2.0/Reports/TrialBalance';
var signatureBase = "GET" + "&" +
encodeURIComponent(URL) + "&" +
encodeURIComponent('date=2019-02-01') + "&" +
encodeURIComponent("oauth_consumer_key=" + CONSUMER_KEY +
"&oauth_nonce=" + oauth_nonce + "&oauth_signature_method=RSA-SHA1&oauth_timestamp=" +
oauth_timestamp + "&oauth_token=" + CONSUMER_KEY + "&oauth_version=1.0");
var rsa = new RSAKey();
rsa.readPrivateKeyFromPEMString(PEM_KEY);
var hashAlg = "sha1";
var hSig = rsa.signString(signatureBase, hashAlg);
var oauth_signature = encodeURIComponent(hextob64(hSig));
var authHeader = "OAuth oauth_token=\"" + CONSUMER_KEY + "\",oauth_nonce=\"" + oauth_nonce +
"\",oauth_consumer_key=\"" + CONSUMER_KEY + "\",oauth_signature_method=\"RSA-SHA1\",oauth_timestamp=\"" +
oauth_timestamp + "\",oauth_version=\"1.0\",oauth_signature=\"" + oauth_signature + "\"";
var headers = {
"Authorization": authHeader,
"Accept": "application/json"
};
var options = {
"headers": headers,
'method': 'GET',
'payload': payload,
'muteHttpExceptions': true,
};
var requestURL = URL + '?date=2019-02-01';
var response = UrlFetchApp.fetch(requestURL, options);
var responseXml = response.getContentText();
Logger.log(responseXml);
}
function createGuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16)
});
}
For RSA signing I have used https://github.com/csi-lk/google-app-script-xero-api/blob/master/jsrsasign.gs
UPDATE 2:
I code this, but still not able to get the result
var signatureBase = encodeURIComponent("GET" + "&" + URL + "&" + 'date=2019-02-01' + "&" + "oauth_consumer_key=" + CONSUMER_KEY +
"&oauth_nonce=" + oauth_nonce + "&oauth_signature_method=RSA-SHA1&oauth_timestamp=" +
oauth_timestamp + "&oauth_token=" + CONSUMER_KEY + "&oauth_version=1.0");
Before reading the rest of my answer can you please urgently reset your applications Consumer Key/Secret, as well as create and upload a new public certificate to the developer portal as you've provided both in your question.
At least one issue you're running into that I can spot is how you're building up the signature base string.
Only the initial & should be left unencoded, however the rest of them in the signature base string should be encoded. It looks like the & after the encoded URL and encoded date query param are being left unencoded.
Edit:
The following two lines are leaving the &s out ouf encoding, but they need to be included in the uri encoding
encodeURIComponent(URL) + "&" +
encodeURIComponent('date=2019-02-01') + "&" +
i'm building a gmail add on application to read a current email and retrieve the gmail attachment associated with the email.
Once I get the blob of the attachment I try to upload it to my API via a multipart/form-data post request. Currently, my request is failing with a 500 while my postman request work just fine.
my back end is giving the following exception:
Exception has been thrown by the target of an invocation. Either BinaryRead, Form, Files, or InputStream was accessed before the internal storage was filled by the caller of HttpRequest.GetBufferedInputStream.
with postman I don't get any error.
Not sure if i'm creating the request in app script currently. Here is my code.
var blob =
Utilities.newBlob(parsedBlob,payload.contentType,payload.name);
Logger.log('encode url ' + encodedUrl)
var metadata = {
'account_ref.id': payload.metaData.account_ref.id,
'matter_ref.id': payload.metaData.matter_ref.id,
description: "uploaded from gmail add on.",
content_type: payload.contentType,
file_name: 'test.png',
size: JSON.parse(payload.size).toString(),
};
for (var i in metadata) {
data += "--" + boundary + "\r\n";
data += "Content-Disposition: form-data; name=\"" + i + "\"
\r\n\r\n" + metadata[i] + "\r\n";
}
data += "--" + boundary + "\r\n";
data += "Content-Disposition: form-data; name=\"\"; filename=\"" +
blob.getName() + "\"\r\n";
data += "Content-Type:" + blob.getContentType() + "\r\n\r\n";
var payload = Utilities.newBlob(data).getBytes()
.concat(blob.getBytes())
.concat(Utilities.newBlob("\r\n--" + boundary + "--\r\n").getBytes());
var headers = {
Authorization: Utilities.formatString("Bearer %s", this.oauthService.getAccessToken())
};
var options = {
method: "post",
contentLength: blob.getBytes().length,
payload: payload,
contentType : "multipart/form-data;charset=utf-8; boundary=--" +
boundary,
muteHttpExceptions: true,
headers: headers,
};
Anything i'm doing wrong in preparing the payload?
i want a telegram bot that set a specific photo as a group image. Here you can see the bot api about that method(https://core.telegram.org/bots/api#setchatphoto).
I'm using this function but it doesn't work.
function myFunction(){
var chatId = SUPERGOUP_CHAT_ID
var photo = {
file_id: FILE_ID,
file_size: 425707,
file_path: 'documents/file_2.png'
}
var data = {
method: "post",
payload: {
method: "setChatPhoto",
chat_id: String(zone),
photo: JSON.stringify(photo)
}
}
UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', data);
}
Telegram answer to the function up here:
{"ok":false,"error_code":400,"description":"Bad Request: photo should be uploaded as an InputFile"}
Searching online i found this solution but it also doesn't work.
function uploadFile() {
var group = SUPERGOUP_CHAT_ID
var boundary = "labnol";
var blob = DriveApp.getFileById('FILE_ID').getBlob();
var attributes = "{\"name\":\"asd.jpg\", \"parent\":{\"id\":\"FOLDER_ID\"}}";
var requestBody = Utilities.newBlob(
"--"+boundary+"\r\n"
+ "Content-Disposition: form-data; name=\"attributes\"\r\n\r\n"
+ attributes+"\r\n"+"--"+boundary+"\r\n"
+ "Content-Disposition: form-data; name=\"file\"; filename=\""+blob.getName()+"\"\r\n"
+ "Content-Type: " + blob.getContentType()+"\r\n\r\n").getBytes()
.concat(blob.getBytes())
.concat(Utilities.newBlob("\r\n--"+boundary+"--\r\n").getBytes());
var options = {
method: "post",
contentType: "multipart/form-data; boundary="+boundary,
payload: {
method: "setChatPhoto",
chat_id: String(group),
photo: requestBody
}
};
var request = UrlFetchApp.fetch('https://api.telegram.org/bot' + token , options);
Logger.log(request.getContentText());
}
Telegram answer to the function up here:
Timeout: https://api.telegram.org/bot_TOKEN
Can you maybe find a solution?
Thanks in advance.