I tried controlling my GoSund smart socket using Tuya IoT Development Platform, but I'm stuck on this error response when trying to switch its state:
{"code":1004,"msg":"sign invalid","success":false,"t":1658384161392,"tid":"97e938e608bc11eda4f0322e56e3d437"}
The following code is basically slightly modified copy of develop code sample from official Tuya API site with my keys and deviceId pasted(https://developer.tuya.com/en/docs/iot/singnature?id=Ka43a5mtx1gsc)
When I tried to do the exact same thing using Tuya's site debug device option it just works. When I try to do it using their code sample in a web app, it fails with 1004. Except for the token that is new every time I call this, basically all the request headers are the same as when calling them from Tuya's site. Payload is the same too, but the response is very different.
same request on Tuya website device debugging & in a web app
Adding sign_version: '2.0' to request headers or using different url (const url = /v1.0/iot-03/devices/${deviceId}/commands;) doesn't seem to help.
const config = {
/* openapi host */
//host: 'https://openapi.tuyacn.com',
host: 'https://openapi.tuyaeu.com',
/* fetch from openapi platform */
accessKey: 'I pasted here my Access ID/Client ID from iot.tuya.com',
/* fetch from openapi platform */
secretKey: 'I pasted here my Access Secret/Client Secret from iot.tuya.com',
/* Interface example device_ID */
deviceId: 'I pasted here Device ID of my GoSund smart plug',
};
const httpClient = axios.create({
baseURL: config.host,
timeout: 5 * 1e3,
});
async main(switchValue: boolean) {
try{
await this.getToken();
const data = await this.getDeviceInfo(config.deviceId, switchValue);
console.log('fetch success: ', JSON.stringify(data));
}catch(error){
console.log(error);
}
}
/**
* fetch highway login token
*/
async getToken() {
const method = 'GET';
const timestamp = Date.now().toString();
const signUrl = '/v1.0/token?grant_type=1';
const contentHash = crypto.createHash('sha256').update('').digest('hex');
const stringToSign = [method, contentHash, '', signUrl].join('\n');
const signStr = config.accessKey + timestamp + stringToSign;
const headers = {
t: timestamp,
sign_method: 'HMAC-SHA256',
client_id: config.accessKey,
sign: await this.encryptStr(signStr, config.secretKey),
};
const { data: login } = await httpClient.get('/v1.0/token?grant_type=1', { headers });
if (!login || !login.success) {
throw Error(`fetch failed: ${login.msg}`);
}
this.setState({ token: login.result.access_token })
}
/**
* fetch highway business data
*/
async getDeviceInfo(deviceId: string, switchValue: boolean) {
const query = {};
const method = 'POST';
const url = `/v1.0/devices/${deviceId}/commands`;
const reqHeaders: { [k: string]: string } = await this.getRequestSign(url, method, {}, query);
const { data } = await httpClient.request({
method,
data: {commands: [{code: "countdown_1", value: 0}, {code: "switch", value: switchValue}]},
params: {},
headers: reqHeaders,
url: reqHeaders.path,
});
if (!data || !data.success) {
throw Error(`request api failed: ${data.msg}`);
}
}
/**
* HMAC-SHA256 crypto function
*/
async encryptStr(str: string, secret: string): Promise<string> {
return crypto.createHmac('sha256', secret).update(str, 'utf8').digest('hex').toUpperCase();
}
/**
* request sign, save headers
* #param path
* #param method
* #param headers
* #param query
* #param body
*/
async getRequestSign(
path: string,
method: string,
headers: { [k: string]: string } = {},
query: { [k: string]: any } = {},
body: { [k: string]: any } = {},
) {
const t = Date.now().toString();
const [uri, pathQuery] = path.split('?');
const queryMerged = Object.assign(query, qs.parse(pathQuery));
const sortedQuery: { [k: string]: string } = {};
Object.keys(queryMerged)
.sort()
.forEach((i) => (sortedQuery[i] = query[i]));
const querystring = decodeURIComponent(qs.stringify(sortedQuery));
const url = querystring ? `${uri}?${querystring}` : uri;
const contentHash = crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
const client_id = config.accessKey
const access_token = this.state.token
const stringToSign = [method, contentHash, '', url].join('\n');
const signStr = client_id + access_token + t + stringToSign;
return {
t,
path: url,
client_id: config.accessKey,
sign: await this.encryptStr(signStr, config.secretKey),
sign_method: 'HMAC-SHA256',
sign_version: '2.0',
access_token: access_token
};
}
Looks like you're not passing the body to the signature method.
The whole request needs to be signed including any body. You can't change the request details after signing it, except to add the sign header.
It's probably worth structuring your call into three steps - one to build up the request object. One to add the signing header based on the whole request object (so it's responsible for signing the right fields). Then finally send it to httpClient.request to make the call.
I presume there's a bit of left over "trying things out to get it working" in your code, e.g. setting the url to the requestHeaders.path. And I think you need a timestamp header in there too. All should be in the docu, or look at Tuya's postman collection's pre-request script.
Their example script has a few errors:
In getDeviceInfo():
set method to GET instead of POST
set url to "/v1.0/iot-03/devices/${deviceId}/functions" or "/v1.0/iot-03/devices/${deviceId}/specification"
return data; at the end, so it gets output
This made it work for me.
Ah, and this answer relates to today's version of their example:
import * as qs from 'qs';
import * as crypto from 'crypto';
import { default as axios } from 'axios';
let token = '';
const config = {
/* openapi host */
host: 'https://openapi.tuyacn.com',
/* fetch from openapi platform */
accessKey: '',
/* fetch from openapi platform */
secretKey: '',
/* Interface example device_ID */
deviceId: '',
};
const httpClient = axios.create({
baseURL: config.host,
timeout: 5 * 1e3,
});
async function main() {
await getToken();
const data = await getDeviceInfo(config.deviceId);
console.log('fetch success: ', JSON.stringify(data));
}
/**
* fetch highway login token
*/
async function getToken() {
const method = 'GET';
const timestamp = Date.now().toString();
const signUrl = '/v1.0/token?grant_type=1';
const contentHash = crypto.createHash('sha256').update('').digest('hex');
const stringToSign = [method, contentHash, '', signUrl].join('\n');
const signStr = config.accessKey + timestamp + stringToSign;
const headers = {
t: timestamp,
sign_method: 'HMAC-SHA256',
client_id: config.accessKey,
sign: await encryptStr(signStr, config.secretKey),
};
const { data: login } = await httpClient.get('/v1.0/token?grant_type=1', { headers });
if (!login || !login.success) {
throw Error(`fetch failed: ${login.msg}`);
}
token = login.result.access_token;
}
/**
* fetch highway business data
*/
async function getDeviceInfo(deviceId: string) {
const query = {};
const method = 'POST';
const url = `/v1.0/devices/${deviceId}/commands`;
const reqHeaders: { [k: string]: string } = await getRequestSign(url, method, {}, query);
const { data } = await httpClient.request({
method,
data: {},
params: {},
headers: reqHeaders,
url: reqHeaders.path,
});
if (!data || !data.success) {
throw Error(`request api failed: ${data.msg}`);
}
}
/**
* HMAC-SHA256 crypto function
*/
async function encryptStr(str: string, secret: string): Promise<string> {
return crypto.createHmac('sha256', secret).update(str, 'utf8').digest('hex').toUpperCase();
}
/**
* request sign, save headers
* #param path
* #param method
* #param headers
* #param query
* #param body
*/
async function getRequestSign(
path: string,
method: string,
headers: { [k: string]: string } = {},
query: { [k: string]: any } = {},
body: { [k: string]: any } = {},
) {
const t = Date.now().toString();
const [uri, pathQuery] = path.split('?');
const queryMerged = Object.assign(query, qs.parse(pathQuery));
const sortedQuery: { [k: string]: string } = {};
Object.keys(queryMerged)
.sort()
.forEach((i) => (sortedQuery[i] = query[i]));
const querystring = decodeURIComponent(qs.stringify(sortedQuery));
const url = querystring ? `${uri}?${querystring}` : uri;
const contentHash = crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
const stringToSign = [method, contentHash, '', url].join('\n');
const signStr = config.accessKey + token + t + stringToSign;
return {
t,
path: url,
client_id: config.accessKey,
sign: await encryptStr(signStr, config.secretKey),
sign_method: 'HMAC-SHA256',
access_token: token,
};
}
main().catch(err => {
throw Error(`error: ${err}`);
});
Related
So basically here is the scenario. The user tries to access a protected API link. During the request stage, The Next.js middleware checks if the token is correct, decodes the token and then creates a current user property with response.user and if possible, checks if the exists on the MongoDB database
This is what the middleware.js currently looks like
interface CustomNextResponse extends NextResponse {
request: {
userId: string;
};
}
export async function sign(payload: JwtPayload, secret: string): Promise<string> {
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 60 * 60; // one hour
return new SignJWT({ ...payload })
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setExpirationTime(exp)
.setIssuedAt(iat)
.setNotBefore(iat)
.sign(new TextEncoder().encode(secret));
}
export async function verify(token: string, secret: string): Promise<JwtPayload> {
const { payload } = await jwtVerify(token, new TextEncoder().encode(secret));
return payload;
}
export async function middleware(request: NextRequest) {
try {
const refreshToken = request.cookies.get('X-Refresh-Token')?.value;
const requestHeaders = new Headers(request.headers);
const bearerToken = requestHeaders.get('Authorization');
if (!refreshToken) {
throw new Error('Unauthorized');
}
if (!bearerToken) {
throw new Error('Unauthorized');
}
const accessToken = bearerToken.split(' ')[1];
if (!accessToken) {
throw new Error('Unauthorized');
}
const decode: JwtPayload = (await verify(accessToken, secretKey)) as JwtPayload;
const response = NextResponse.next() as CustomNextResponse;
response.request = {
userId: decode.id
}
return response;
} catch (error) {
const errorObject = error as Error;
return new NextResponse(JSON.stringify({ success: false, message: errorObject.message }), {
status: 401,
headers: { 'content-type': 'application/json' },
});
}
}
But in the Nextapi at pages/api/me I can neither access the property with req nor res
async function handler(req: CustomNextApiRequest, res: NextApiResponse) {
//req.userId and res.userId give me undefined
Is it case the case then that middleware cant does not store custom properties to be accessed when making a request the same that Express.js does it? plus can you access MongoDB within the middleware?
I want to get an image generated in OpenAI/Dall E and save it to an S3 Bucket.
So far I can get the image URL and create a buffer, with the following:
const configuration = new Configuration({
apiKey: procEnvVars.OPENAI_API_KEY,
});
export const openai = new OpenAIApi(configuration);
const defaultImageParams: CreateImageRequest = {
n: 1,
prompt: "a bad request message",
};
interface InputParams extends CreateImageRequest {
prompt: string; // make this mandatory for the function params
}
// Once we get a URL from the OpenAI API, we want to convert it to a buffer
export async function getBufferFromUrl(openAiUrl: string) {
const axiosResponse = await axios({
url: openAiUrl, //your url
method: "GET",
responseType: "arraybuffer",
});
const data = axiosResponse.data;
if (!(data instanceof Buffer))
throw new Error("Axios response should be of type Buffer");
return data;
}
export async function getUrlFromOpenAi(inputParams: InputParams) {
const ImageResponse = await openai.createImage({
...defaultImageParams,
...inputParams,
});
const dataArray = ImageResponse.data.data;
if (!dataArray || dataArray.length === 0) {
console.error({
error: "We did not return choices from createOpenAiImage()",
data: ImageResponse.data,
datadata: ImageResponse.data.data,
});
}
return dataArray;
}
Next we need to take the buffer and save to S3:
// Create service client module using ES6 syntax.
import { S3Client } from "#aws-sdk/client-s3";
// Set the AWS Region.
const REGION = "eu-west-2";
// Create an Amazon S3 service client object.
const s3Client = new S3Client({ region: REGION });
export { s3Client };
// Import required AWS SDK clients and commands for Node.js.
import { PutObjectCommand } from "#aws-sdk/client-s3";
// Set the parameters.
export const bucketParams = {
Bucket: "<my s3 bucket name. Can be found in S3 console>",
};
// Create and upload an object to the S3 bucket.
export async function putS3Object(inputParams: { Body: Buffer; Key: string }) {
try {
const data = await s3Client.send(
new PutObjectCommand({
...bucketParams,
Body: inputParams.Body,
Key: `public/myFolder/${inputParams.Key}`,
})
);
console.log(
"Successfully uploaded object: " +
bucketParams.Bucket +
"/" +
`public/myFolder/${inputParams.Key}`
);
return data; // For unit tests.
} catch (err) {
console.log("Error", err);
}
}
I'm trying to send a subscription over a Websocket connection to AppSync. But when I send the request, I get the error {errorType: "UnsupportedOperation", message: "unknown not supported through the realtime channel"}
Here's my test code (yes, a little messy :) ):
let ws = undefined;
const id = 'XXX';
const region = 'YYY';
const apikey = 'ZZZ';
const host = id + '.appsync-api.' + region + '.amazonaws.com';
const url = 'wss://' + id + '.appsync-realtime-api.' + region + '.amazonaws.com';
const httpUrl = 'https://' + host + '/graphql';
function openWebsocket(){
const api_header = {
host: host,
'x-api-key': apikey,
};
// payload should be an empty JSON object
const payload = {};
const base64_api_header = btoa(JSON.stringify(api_header));
const base64_payload = btoa(JSON.stringify(payload));
const appsync_url = url + '?header=' + base64_api_header + '&payload=' + base64_payload;
return new WebSocket(appsync_url, ['graphql-ws']);
}
function runWebsocket() {
ws = openWebsocket();
/**
* Send request over websocket
* (Convenience function)
*/
const _send = (obj) => {
ws.send(JSON.stringify(obj));
};
let initializingMode = true;
ws.onopen = (e) => {
// initialization phase start:
_send({ type: 'connection_init' });
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (initializingMode) {
if (data.type == 'connection_ack') {
// Acknowledge came, so we can start subscribing
// try to subscribe
{
const query = {
query: `subscription MySubscription {
onCreateNotifications {
creationTime
userid
}
}`,
};
const queryStr = JSON.stringify(query);
_send({
id: localStorage.getItem(HeaderItems.idToken),
type: 'start',
payload: {
data: queryStr,
},
authorization: {
host: host,
'x-api-key': apikey,
},
});
}
initializingMode = false;
return;
}
}
};
};
I think you need to put the "authorization" object inside an "extensions" property:
_send({
id: localStorage.getItem(HeaderItems.idToken),
type: 'start',
payload: {
data: queryStr,
extensions: { /* The authorization needs to be wrapped here */
authorization: {
host: host,
'x-api-key': apikey,
},
}
},
});
See: https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html - search for "Example using a custom domain name".
So I have recently stared to work with react, I am authenticating a user in my App component like this:
App
signIn(userData) {
console.log(userData)
//do a fetch call to get/users
axios.get('http://localhost:5000/api/users', {
auth: { //set auth headers so that userData will hold the email address and password for the authenticated user
username: userData. emailAddress,
password: userData.password
}
}).then(results => { console.log(results.data)
this.setState({
//set the authenticated user info into state
emailAddress: results.data,
password: results.data.user
});
})
}
and I also have another component called CreateCourse that allows a post request only if I provided the auth header from App,
CreateCourse
handleSubmit = event => {
event.preventDefault();
console.log(this.props)
const newCourse = {
title: this.state.title,
description: this.state.description,
estimatedTime: this.state.estimatedTime,
materialsNeeded: this.state.materialsNeeded
};
axios({
method: 'post',
url: 'http://localhost:5000/api/courses',
auth: {
username: this.props.emailAddress,
password: this.props.password
},
data: newCourse
}).then(
alert('The course has been successfully created!')
).then( () => {
const { history } = this.props;
history.push(`/`)
})
};
I was wondering if I could pass the auth header from App to the children components without using props or context api so that I don't have to manually put the auth headers on every axios request, for reference this is my repo : https://github.com/SpaceXar20/full_stack_app_with_react_and_a_rest_api_p10
I always create a singleton axios instance and set header for it after user signin successful.
let instance = null
class API {
constructor() {
if (!instance) {
instance = this
}
this.request = Axios.create({
baseURL: 'http://localhost:5000',
})
return instance
}
setToken = (accessToken) => {
this.request.defaults.headers.common.authorization = `Bearer ${accessToken}`
}
createCourses = () => this.request.post(...your post request...)
}
export default new API()
After your login successfull, you need call API.setToken(token). Then, when you call Api.createCourse(), the request will have token in headers.
singleton axios instance is the right approach . In the same pattern, use the below method .Import the file wherever required and use axiosapi.get .
const axiosConfig = {auth: {username: XXXX, password: YYYY}};
const axiosservice = axios.create(axiosConfig);
export const axiosapi = {
/**
* Describes the required parameters for the axiosapi.get request
* #param {string} url
* #param {Object} config - The configfor the get request (https://github.com/axios/axios#request-config)
*
* #returns {Promise}
*/
get: (url, config = {}, params) => {
return axiosservice.get(url, {
params,
responseType: 'json',
transformResponse: [
data => {
const parsedData = typeof data === 'string' ? JSON.parse(data) : data;
return get(parsedData);
},
],
...config,
})
.then()
.catch(error => {
return Promise.reject(error);
});
},
}
please help I don't know what is wrong with my code.
Endpoints that doesn't need signature work fine, so I guess is a problem with how I am getting the signature. I am getting this error:
data: { code: -2014, msg: 'API-key format invalid.' } } }
API Doc: https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md
SIGNED endpoints require an additional parameter, signature, to be
sent in the query string or request body. Endpoints use HMAC SHA256
signatures. The HMAC SHA256 signature is a keyed HMAC SHA256
operation. Use your secretKey as the key and totalParams as the value
for the HMAC operation. The signature is not case sensitive.
totalParams is defined as the query string concatenated with the
request body.
My code:
const axios = require('axios');
const crypto = require('crypto');
const qs = require('qs');
const binanceConfig = {
API_KEY: 'XXXXXXX',
API_SECRET: 'XXXXXX',
HOST_URL: 'https://api.binance.com',
};
const buildSign = (data, config) => {
return crypto.createHmac('sha256', config.API_SECRET).update(data).digest('hex');
};
const privateRequest = async (data, endPoint, type) => {
const dataQueryString = qs.stringify(data);
const signature = buildSign(dataQueryString, binanceConfig);
const requestConfig = {
method: type,
url: binanceConfig.HOST_URL + endPoint + '?' + dataQueryString + '&signature=' + signature,
headers: {
'Authorization': `X-MBX-APIKEY: ${binanceConfig.API_KEY}`,
},
};
try {
console.log('URL: ', requestConfig.url);
const response = await axios(requestConfig);
console.log(response);
return response;
}
catch (err) {
console.log(err);
return err;
}
};
const data = {
symbol: 'ARKBTC',
recvWindow: 20000,
timestamp: Date.now(),
};
privateRequest(data, '/api/v3/openOrders', 'GET');
Try setting the headers object to have a key of X-MBX-APIKEY directly:
headers: {
'X-MBX-APIKEY': binanceConfig.API_KEY,
},