Multipart upload with presigned urls - Scaleway S3-compatible object storage - amazon-web-services

I’m trying to have multipart upload working on Scaleway Object Storage (S3 compatible) with presigned urls and I’m getting errors (403) on preflight request generated by the browser but my CORS settings seems correctly set. (Basically wildcard on allowed headers and origins).
The error comes with a 403 status code and is as follow:
<?xml version='1.0' encoding='UTF-8'?>
<Error><Code>AccessDenied</Code><Message>Access Denied.</Message><RequestId>...</RequestId></Error>
I’m stuck on this one for a while now, I tried to copy the pre-flight request from my browser to reproduce it elsewhere and tried to tweak it a little bit.
Removing the query params from the url of the pre-flight request make the request successful (returns a 200 with Access-Control-Allow-* response headers correctly set) but this is obviously not the browser behavior...
This Doesn’t work (secrets, keys and names have been changed)
curl 'https://bucket-name.s3.fr-par.scw.cloud/tmp-screenshot-2021-01-20-at-16-21-33.png?AWSAccessKeyId=XXXXXXXXXXXXXXXXXXXX&Expires=1638217988&Signature=NnP1XLlcvPzZnsUgDAzm1Uhxri0%3D&partNumber=1&uploadId=OWI1NWY5ZGrtYzE3MS00MjcyLWI2NDAtNjFkYTM1MTRiZTcx' -X OPTIONS -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Referer: http://domain.tech/' -H 'Access-Control-Request-Method: PUT' -H 'Origin: http://domain.tech' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: no-cors' -H 'Sec-Fetch-Site: cross-site' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache'
This Works (secrets, keys and names have been changed)
curl 'https://bucket-name.s3.fr-par.scw.cloud/tmp-screenshot-2021-01-20-at-16-21-33.png' -X OPTIONS -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:94.0) Gecko/20100101 Firefox/94.0' -H 'Accept: */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Referer: http://domain.tech/' -H 'Access-Control-Request-Method: PUT' -H 'Origin: http://domain.tech' -H 'DNT: 1' -H 'Connection: keep-alive' -H 'Sec-Fetch-Dest: empty' -H 'Sec-Fetch-Mode: no-cors' -H 'Sec-Fetch-Site: cross-site' -H 'Pragma: no-cache' -H 'Cache-Control: no-cache'
The url comes from the aws-sdk and is generated this way :
const S3Client = new S3({
credentials: {
accessKeyId: env.SCW_ACCESS_KEY,
secretAccessKey: env.SCW_SECRET_KEY,
},
endpoint: `https://s3.${env.SCW_REGION}.scw.cloud`,
})
S3Client.getSignedUrlPromise('uploadPart', {
Bucket: bucket,
Key: key,
UploadId: multipartUpload.UploadId,
PartNumber: idx + 1,
})
and used this way in frontend:
// url being the url generated in backend as demonstrated above
const response = await fetch(url, {
method: 'PUT',
body: filePart,
signal: abortController.signal,
})
If anyone can give me a hand at this or that would be great!

As it turns out, Scaleway Object Storage is not fully S3-compatible on this case.
Here is a workaround:
Install aws4 library to sign request easily (or follow this scaleway doc to manually sign your request)
Form your request exactly as per stated in this other scaleway doc (this is where aws-sdk behavior differs, it generates an url with AWSAccessKeyId, Expires and Signature query params that cause the scaleway API to fail. Scaleway API only wants partNumber and uploadId).
Return the generated url and headers to the frontend
// Backend code
const signedRequest = aws4.sign(
{
method: 'PUT',
path: `/${key}?partNumber=${idx + 1}&uploadId=${
multipartUpload.UploadId
}`,
service: 's3',
region: env.SCW_REGION,
host: `${bucket}.s3.${env.SCW_REGION}.scw.cloud`,
},
{
accessKeyId: env.SCW_ACCESS_KEY,
secretAccessKey: env.SCW_SECRET_KEY,
},
)
return {
url: `https://${signedRequest.host}${signedRequest.path}`,
headers: Object.keys(signedRequest.headers).map((key) => ({
key,
value: signedRequest.headers[key] as string,
})),
}
And then in frontend:
// Frontend code
const headers = signedRequest.headers.reduce<Record<string, string>>(
(acc, h) => ({ ...acc, [h.key]: h.value }),
{},
)
const response = await fetch(signedRequest.url, {
method: 'PUT',
body: filePart,
headers,
signal: abortController.signal,
})
Scaleway knows this issue as I directly discussed with their support team and they are putting some effort in order to be as compliant as possible with S3. This issue might be fixed by the time you read this.
Thanks to them for the really quick response time and for taking this seriously.

Related

AWS: Why isn't my lambda function URL returning CORS headers for my preflight requests?

I am playing around with lambda function URLs, which seem like a perfect fit for my use case.
I have a lambda configured with function URL turned on, Auth type of AWS_IAM, and CORS turned on with default settings (Allow Origin *, nothing else set). From Javascript running in Chrome I am sending a signed request to the lambda with code that looks like:
const url = new URL(rawUrl)
const signer = new SignatureV4({
credentials: *******,
sha256: Sha256,
service: "lambda",
region: region,
})
const request = new HttpRequest({
hostname: url.hostname,
path: url.pathname,
body: JSON.stringify({}),
method: 'POST',
headers: {
'Content-Type': 'application/json',
host: url.hostname,
}
})
const send = async () => {
const {headers, body, method} = await signer.sign(request)
console.log('send.headers', headers)
const result = await fetch(rawUrl, { headers, body, method })
.then((res) => res.json())
return result
}
send()
.then((data) => {
console.log('success', data)
})
.catch((reason) => {
console.log('error', reason)
})
When this code runs it generates a request that fails CORS because the preflight is not validated. The curl equivalents of the requests Chrome is sending are
curl 'https://**********.lambda-url.us-west-2.on.aws/' \
-H 'sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'authorization: AWS4-HMAC-SHA256 Credential=*******/*******/us-west-2/lambda/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token, Signature=***********' \
-H 'Content-Type: application/json' \
-H 'x-amz-content-sha256: ************' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36' \
-H 'x-amz-security-token: ********' \
-H 'Referer: http://localhost:3000/' \
-H 'x-amz-date: 20220916T035331Z' \
-H 'sec-ch-ua-platform: "macOS"' \
--data-raw '{}' \
--compressed
for the request and
curl 'https://*******.lambda-url.us-west-2.on.aws/' \
-X 'OPTIONS' \
-H 'Accept: */*' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Access-Control-Request-Headers: authorization,content-type,x-amz-content-sha256,x-amz-date,x-amz-security-token' \
-H 'Access-Control-Request-Method: POST' \
-H 'Connection: keep-alive' \
-H 'Origin: http://localhost:3000' \
-H 'Referer: http://localhost:3000/' \
-H 'Sec-Fetch-Dest: empty' \
-H 'Sec-Fetch-Mode: cors' \
-H 'Sec-Fetch-Site: cross-site' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36' \
-H 'dnt: 1' \
-H 'sec-gpc: 1' \
--compressed
for the preflight.
If I run that preflight in a terminal I get the following response:
HTTP/1.1 200 OK
Date: Xxx, xx Xxx 2022 xx:xx:xx GMT
Content-Type: application/json
Content-Length: 0
Connection: keep-alive
x-amzn-RequestId: xxxxxxxxx
Note that there aren't valid CORS headers being sent back, which results in Chrome not making the actual request. If I run the request itself in a terminal and bypass the preflight, then it runs successfully.
Is there an error in the way that I am making a request or how I've configured the lambda (I've tried various combinations)?
Thanks for your help!
Thanks to the comment from #jub0bs above I figured out the answer.
All the CORS configuration values default to being locked down. I had tried * for all the values, but did not set the Max age configuration value. With all setting properly filled out to the origins, methods, and headers that I am using AND Max age set to 0 everything started behaving itself.

How to call the API of AppSync in curl from my local machine?

I created an API with AppSync. Now I want to call it with curl, and I get the following error: You are not authorized to make this call.
I guessed the following:
curl -g -X POST -H "Content-Type: application/json" -H "Authorization: Bearer da2-XXXXXXXXXXXXXXXXXXXXXXXXXX" -d '{"query":"listMyModelTypes{listMyModelTypes {items {id title}}}"}' https://wuw4mcnvautpl4v5ox33fdzoq.appsync-api.us-east-1.amazonaws.com/graphql
Or should I also include the API ID somewhere in the query?
Making an Appsync query via CURL or Postman depends on getting the request body and headers right. The required headers depend on auth type.
# common variables
API_URL='https://<APPSYNC-ID>.appsync-api.eu-west-1.amazonaws.com/graphql'
QUERY='query GetImages($t: String!) { images(topic:$t) { edges { cursor } } }'
VARIABLES='{"t":"cats"}' # no spaces!
API Key Auth: x-api-key header
API_KEY='da2-XXXXXXXXXXXXXXXXXXXXXXXXXX'
curl -s -XPOST -H "Content-Type:application/graphql" -H "x-api-key:$API_KEY" -d '{"query": "'"$QUERY"'", "variables": '$VARIABLES'}' $API_URL
Token-based Auth (e.g. Cognito): Authorization and host headers
TOKEN='<YOUR JWT AUTH TOKEN HERE>'
HOST='<APPSYNC-ID>.appsync-api.eu-west-1.amazonaws.com'
curl -s -XPOST -H "Content-Type:application/graphql" -H "Authorization:$TOKEN" -H "host:$HOST" -d '{"query": "'"$QUERY"'", "variables": '$VARIABLES'}' $API_URL

Can you help me with Google Workflow?

I'm getting the error:
"in step "readGcpadmin": {"message":"HTTP body unsupported with: 'GET'","tags":["ValueError"]}"
and I don't know how to solve it, here is the code with the hidden data below:
- readGcpadmin:
call: http.get
args:
url: https://admin.googleapis.com/admin/directory/v1/users
#method: get
headers:
Authorization: "Bearer [My token]"
Content-type: "application/json"
#body:
#domain: [my domain.page]
#query:
auth:
type: OAuth2
#scope: https://www.googleapis.com/auth/cloud-platform
#timeout: 20
result: teste
- returnResult:
return: ${teste.body}
When I try the terminal it works:
curl \
'https://admin.googleapis.com/admin/directory/v1/users?domain=MyDomain&key=MyKey' \
--header 'Authorization: Bearer MyToken' \
--header 'Accept: application/json' \
--compressed
this is a similar working example, notice the query section which you miss:
readItem:
call: http.get
args:
url: ${"https://storage.googleapis.com/storage/v1/b/"+bucket+"/o"}
auth:
type: OAuth2
query:
prefix: ${prefix}
fields: items/name,items/bucket
result: documentValue
next: documentFound
The problem was really with the position of the "auth" field, it was enough to put it in the "Header" directly "auth: OAuth2". In addition, I removed the "Content-type" field. Thank you guys!

GCP IoTCore won't parse payload using Gateways and HTTP bridge

Steps taken so far
Create a new key pair and use it for the gateway that is about to be created
Create a gateway, let's call it 'my_first_gateway'
Create a new device, let's call it 'gw_device_1'
Associate gw_device_1 with my_first_gateway
Works fine so far.
Now I want to use the HTTP bridge to send gw_device_1's state data to IoTCore via my gateway using my_first_gateway's private key, following this tutorial: https://cloud.google.com/iot/docs/how-tos/gateways/http-bridge#setting_device_state_through_the_gateway
Observation1: the URL in this tutorial seems malformatted, there is a missing double quote at the end of 'delegated_device_id':
curl -X POST -H 'authorization: Bearer GATEWAY_JWT' -H 'content-type: application/json' --data '{"binary_data": "DATA", "gateway_info": {"delegated_device_id: "device-id"}}' -H 'cache-control: no-cache' 'https://cloudiotdevice.googleapis.com/v1/projects/{project-id}/locations/{cloud-region}/registries/{registry-id}/devices/{gateway-id}:setState'
When I am now replacing all placeholders and replace "DATA" with say "ewogICJhUHJvcCI6ICJhVmFsdWUiCn0" I execute the following curl (the token is obviously not real):
curl -X POST -H 'authorization: Bearer GW_JWT_TOKEN' -H 'content-type: application/json' --data '{"binary_data": "ewogICJhUHJvcCI6ICJhVmFsdWUiCn0=", "gateway_info": {"delegated_device_id": "gw_device_1"}}' -H 'cache-control: no-cache' 'https://cloudiotdevice.googleapis.com/v1/projects/my_project_id/locations/europe-west1/registries/my_registry/devices/my_first_gateway:setState'
I receive this error:
{
"error": {
"code": 400,
"message": "Invalid JSON payload received. Unknown name \"binary_data\": Cannot find field.",
"status": "INVALID_ARGUMENT",
"details": [
{
"#type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"description": "Invalid JSON payload received. Unknown name \"binary_data\": Cannot find field."
}
]
}
]
}
}
The fun thing is: There is another 'endpoint' that is used to publish events to IoTCore. It has the same signature but instead of 'setState' it ends with 'publishEvent' (see: https://cloud.google.com/iot/docs/how-tos/gateways/http-bridge#publishing_the_devices_telemetry_events_through_the_gateway).
Executing the exact same request with this method works just fine:
curl -X POST -H 'authorization: Bearer GW_JWT_TOKEN' -H 'content-type: application/json' --data '{"binary_data": "ewogICJhUHJvcCI6ICJhVmFsdWUiCn0=", "gateway_info": {"delegated_device_id": "gw_device_1"}}' -H 'cache-control: no-cache' 'https://cloudiotdevice.googleapis.com/v1/projects/my_project_id/locations/europe-west1/registries/my_registry/devices/my_first_gateway:publishEvent'
Am I missing something?
Any help appreciated.
Actually, the curl that google provides is not correct.
The payload needs to be adapted a bit, the binary_data string needs to be wrapped in an object called 'state'
{ "state": { "binary_data": "ewogICJhUHJvcCI6ICJhVmFsdWUiCn0=" }, "gateway_info": {"delegated_device_id": "gw_device_1"}}
The curl then works as expected.

send image using http post to django restfull api

fileChange(event) {
debugger;
let fileList: FileList = event.target.files;
if (fileList.length > 0) {
let file: File = fileList[0];
let formData: FormData = new FormData();
formData.append('uploadFile', file, file.name);
let headers = new Headers()
headers.append('Authorization', this.token);
headers.append('Content-Type', 'application/json');
headers.append('Content-Disposition', 'form-data; filename="'+file.name+'"');
// Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
headers.append('Content-Type', 'multipart/form-data');
let options = new RequestOptions({ headers: headers });
// let apiUrl1 = "/api/UploadFileApi";
this.http.post('http://192.168.1.160:8000/profilepic/', {FormData:formData}, options)
.map(res => res.json())
.catch(error => Observable.throw(error))
.subscribe(
data => alert('success'),
error => alert(error)
)
}
// window.location.reload();
}
<input class="uploadfile-style" [(ngModel)]="FilePath" (change)="fileChange($event)" name="CContractorFPath"
size="10" type="file" enctype="multipart/form-data">
Hii
I have write this code for post image in angular 2...but it shows error Unsupported media type \"application/json,multipart/form-data\" in request
same file accepted when i post it from Postman
following are two different curl commands
1. This is accepted by Json
curl -X POST \
http://192.168.1.223:8010/profilepic/ \
-H 'authorization: Basic YWRtaW46YWRtaW4=' \
-H 'cache-control: no-cache' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-H 'postman-token: 794107c3-791d-e198-fe36-48f407a3ec8c' \
-F datafile=#/home/ashwini/Pictures/b.jpeg
2. This is not accepted by API
curl 'http://192.168.1.223:8010/profilepic/' -H 'Authorization: 6df62b2d808c15acbdd8598d0153c8ca7e9ea28a' -H 'Origin: http://localhost:4201' -H 'Accept-Encoding: gzip, deflate' -H 'Accept-Language: en-GB,en-US;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36' -H 'Content-Type: application/json,multipart/form-data' -H 'Accept: application/json, text/plain, */*' -H 'Referer: http://localhost:4201/' -H 'Connection: keep-alive' --data-binary $'{\n "FormData": {}\n}' --compressed