Using Okhttp AWS Signer for s3 upload (PUT) - android

Can I use this signer https://github.com/babbel/okhttp-aws-signer for upload file to s3?
I use this https://bytes.babbel.com/en/articles/2019-01-03-okhttp-aws-signer.html reference when build my sample app:
class AwsSigingInterceptor(private val signer: OkHttpAwsV4Signer) : Interceptor {
private val dateFormat: ThreadLocal<SimpleDateFormat>
init {
dateFormat = object : ThreadLocal<SimpleDateFormat>() {
override fun initialValue(): SimpleDateFormat {
val localFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US)
localFormat.timeZone = TimeZone.getTimeZone("UTC")
return localFormat
}
}
}
override fun intercept(chain: Chain): Response =
chain.run {
val request = request()
val newRequest = request.newBuilder()
.addHeader("x-amz-date", dateFormat.get().format(clock.now()))
.addHeader("host", request.url().host())
.build()
val signed = signer.sign(newRequest, "<accessKeyId>", "<secretAccessKey>")
proceed(signed)
}
}
but I didn't see any x-amz-content-sha256 there.. only additional Authorization header added. When I try the debug result header in Advanced Rest Client or Postman, it says "Missing required header for this request: x-amz-content-sha256"

Related

Android retrofit: 400 bad request response code

So I'm still in the process of learning android dev and I'm currently working on an app which is supposed to show students their grades. Right now I am stuck at getting login to a service from which grades are collected. For that process I am using https://eduo-ocjene-docs.vercel.app/ api (documentation is in Croatian).
This is what curl request for logging in looks like:
curl --location --request GET 'https://ocjene.eduo.help/api/login' \--header 'Content-Type: application/json' \--data-raw '{ "username":"ivan.horvat#skole.hr", "password":"ivanovPassword123"}'
Here are screenshots of what I have tried until now
Here is how I build retrofit
object ApiModule {
private const val BASE_URL = "https://ocjene.eduo.help/"
lateinit var retrofit: EdnevnikApiService
private val json = Json { ignoreUnknownKeys = true }
fun initRetrofit() {
val okhttp = OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build()
retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.client(okhttp).build().create(EdnevnikApiService::class.java)
}
}
The login method
interface EdnevnikApiService {
#HTTP(method = "get", path = "/api/login", hasBody = true)
fun login(#Body request: LoginRequest): Call<LoginResponse>
}
This is what happens when the login button is clicked
fun onLoginButtonClicked(email: String, password: String) {
val request = LoginRequest(email, password)
ApiModule.retrofit.login(request).enqueue(object : Callback<LoginResponse> {
override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
loginResultLiveData.value = response.isSuccessful
val body = response.body()
}
override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
loginResultLiveData.value = false
}
})
}
and this is what kotlin request and kotlin response data classes look like
#kotlinx.serialization.Serializable
data class LoginRequest(
#SerialName("username") val username: String,
#SerialName("password") val password: String,
)
#kotlinx.serialization.Serializable
data class LoginResponse(
#SerialName("LoginSuccessful") val isSuccessful: Boolean,
)
Oh and this is what I get from the interceptor when I send the request
My guess is server is responding with 400 Bad Request due to unsupported method type. When I replaced method = "get" with method = "GET" in your sample code, I received:
java.lang.IllegalArgumentException: method GET must not have a request body.
which makes sense. Luckily, the /login API you shared works with POST method type, so you can try using:
#HTTP(method = "POST", path = "/api/login", hasBody = true,)
I checked at my end and I received the following response:
<-- 200 https://ocjene.eduo.help/api/login (1390ms)
access-control-allow-origin: *
access-control-allow-credentials: true
set-cookie: epicCookie=f69fbd6d4f10b5cc38e038b5da0843b356776c58c4fb32aed24dbcc49026778724bc25e21448c05a29df9f4b5558b254011fb3f8a992710f9901f23c53be5eaadaa799f3f5ac9e18de191bed02ef3e96030b83042ee8392755b03dd785edca6a;
content-type: application/json; charset=utf-8
etag: "bkrbkvg0eo6c"
vary: Accept-Encoding
date: Thu, 10 Nov 2022 03:07:08 GMT
server: Fly/b1863e2e7 (2022-11-09)
via: 2 fly.io
fly-request-id: 01GHFR2T56X9K0GFN3DH1Z9JYV-sin
{"LoginSuccessful":false,"token":"f69fbd6d4f10b5cc38e038b5da0843b356776c58c4fb32aed24dbcc49026778724bc25e21448c05a29df9f4b5558b254011fb3f8a992710f9901f23c53be5eaadaa799f3f5ac9e18de191bed02ef3e96030b83042ee8392755b03dd785edca6a"}
<-- END HTTP (228-byte body)
object ApiModule {
private const val BASE_URL = "https://ocjene.eduo.help/"
lateinit var retrofit: EdnevnikApiService
private val json = Json { ignoreUnknownKeys = true }
fun initRetrofit() {
val okhttp = OkHttpClient.Builder().addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build()
retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.client(okhttp).build().create(EdnevnikApiService::class.java)
}
}

403 Forbidden. The request signature we calculated does not match signature you provided. Check your key and signing method from Android to Amazon S3

I am trying to call get api using an AWS signing method but not able to get the response.
Below is my code.
val secretkey = "E+t5/nDf6/NKNJBjbsdjv"
val accesskey = "DJKSBDKSBNKFGNBFG"
val credentials: AWSCredentials = BasicAWSCredentials(accesskey, secretkey)
val API_GATEWAY_SERVICE_NAME = "s3"
val requestAws: Request<*> = DefaultRequest<Any?>(API_GATEWAY_SERVICE_NAME)
val uri = URI.create("https://s3.us-west-2.amazonaws.com/..../../sample")
requestAws.endpoint = uri
requestAws.resourcePath = "https://s3.us-west-2.amazonaws.com/..../../sample"
requestAws.httpMethod = HttpMethodName.GET
val signer = AWS4Signer() signer . setServiceName (API_GATEWAY_SERVICE_NAME)
signer.setRegionName("us-west-2")
signer.sign(requestAws, credentials)
val headers = requestAws.headers
val key: MutableList<String> = ArrayList()
val value: MutableList<String> = ArrayList()
for ((key1, value1) in headers)
{
key.add(key1) value . add (value1)
}
val httpClient = OkHttpClient()
val request: okhttp3.Request = okhttp3.Request.Builder()
.url("https://s3.us-west-2.amazonaws.com/..../../sample")
.addHeader(key[0], value[0])
.addHeader(key[1], value[1])
.addHeader(key[2], value[2])
.addHeader("X-Amz-Content-Sha256",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
.build()
val response: okhttp3.Response = httpClient.newCall(request).execute()
Log.i("LOG", response.body.toString())
Not able to figure out, what I am doing mistake.
Please help me out with this issue.
If you want to create an Android app written in Kotlin and invokes AWS Services, use the AWS SDK for Kotlin.
This SDK has strongly typed Service Clients that you can use in an Android Studio project that lets you invoke a given service. (as opposed to using okhttp3.Request, etc)
For example, here is Kotlin code that invoke SNS using a Strongly typed service client named SnsClient.
// Get all subscriptions.
fun getSubs(view: View) = runBlocking {
val subList = mutableListOf<String>()
val snsClient: SnsClient = getClient()
try {
val request = ListSubscriptionsByTopicRequest {
topicArn = topicArnVal
}
val response = snsClient.listSubscriptionsByTopic(request)
response.subscriptions?.forEach { sub ->
subList.add(sub.endpoint.toString())
}
val listString = java.lang.String.join(", ", subList)
showToast(listString)
} catch (e: SnsException) {
println(e.message)
snsClient.close()
}
}
fun getClient() : SnsClient{
val staticCredentials = StaticCredentialsProvider {
accessKeyId = "<Enter key>"
secretAccessKey = "<Enter key>"
}
val snsClient = SnsClient{
region = "us-west-2"
credentialsProvider = staticCredentials
}
return snsClient
}
TO learn how to use the AWS SDK for Kotlin, see
AWS SDK for Kotlin Developer Guide

400 error after successfully refreshing Oauth 2 Token using Retrofit2 / Kotlin in Android

This is the code I have for the refreshing token in an android app using kotlin ad retrofit 2.
The gradle :
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-moshi:retrofit:2.9.0"
And Authenticator is :
class OAuthAuthenticator(
private val refreshTokenService: Repository,
private val sessionManager: SessionManager
) : Authenticator {
#Synchronized
override fun authenticate(route: Route?, response: Response): Request? {
try {
//synchronized call to refresh the token
val refreshTokenResponse =
refreshTokenService.refreshJWTToken(sessionManager.getAuthTokens())
val sessionDataResponseBody = refreshTokenResponse.body()
if (refreshTokenResponse.isSuccessful && sessionDataResponseBody != null && !sessionDataResponseBody.jwt.isNullOrEmpty()) {
sessionManager.jwtToken = sessionDataResponseBody.jwt
// retry request with the new tokens (I get 400 error)
return response.request()
.newBuilder()
.addHeader("Authorization", "Bearer ${sessionManager.jwtToken}")
.build()
} else {
throw HttpException(refreshTokenResponse)
}
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
onSessionExpiration()
return null
}
}
}
return null
}
private fun onSessionExpiration() {
sessionManager.clear()
}
}
This is the Repository class :
object Repository {
fun refreshJWTToken(authTokens : AuthTokens) = RetrofitBuilder.userApi.getAuthenticationToken(authTokens).execute()
}
This is the API :
interface UserAPI {
#Headers("Cache-Control: no-cache")
#POST(AUTH_TOKENS_URL)
fun getAuthenticationToken(
#Header("Accept") accept : String,
#Header("Content-Type") contentType : String,
#Body params: AuthTokens
): Call<AuthTokenResponse>
}
The retrofit builder:
init {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val sessionManager = SessionManager.getInstance(context)
val httpLoggingInterceptor =
HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
httpClient = OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(ConnectivityCheckInterceptor(connectivityManager))
.addInterceptor(AuthInterceptor(sessionManager))
.authenticator(OAuth2Authenticator(UserRepository, sessionManager))
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.build()
}
Question :
I can confirm that the code refreshes the Auth token and persists it successfully. However I get a 400 error after that. Any suggestions on what I am doing wrong?
I know this question is old, but for everyone who facing the same issue, it was just a simple mistake.
Please, use header(..., ...) instead of addHeader(..., ...) in the TokenAuthenticator class.
It worked for me.

Change DNS on all requests

I need to change DNS on all Android requests. How to get it done?
I found this:
class HTTPDNSInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originRequest = chain.request()
val httpUrl = originRequest.url()
val url = httpUrl.toString()
val host = httpUrl.host()
val hostIP = HttpDNS.getIpByHost(host)
val builder = originRequest.newBuilder()
if (hostIP != null) {
builder.url(HttpDNS.getIpUrl(url, host, hostIP))
builder.header("host", hostIP)
}
val newRequest = builder.build()
val newResponse = chain.proceed(newRequest)
return newResponse
}
}
But, I don't have all the helper classes and I have not found any library where I can explicitly set certain DNS. Even if there is a way to dynamically check valid DNS and setting any working, would be great too.

Upload to pre-signed S3 URL fails with OkHttp

I am getting an SSL exception when I try to upload a file to Amazon S3's pre-signed URL with OkHttp 3.9.1: SSLException: Write error: ssl=0xa0b73280: I/O error during system call, Connection reset by peer
It is the same problem as in another SO question but in my case it fails always. I upload just files over 1MiB in size, I have not tried small files.
As I mentioned in my answer in that question, switching to Java's HttpURLConnection fixed the problem and the upload works perfectly.
Here is my RequestBody implementation (in Kotlin) to upload a file from Android's Uri and I do use OkHttp's .put() method:
class UriRequestBody(private val file: Uri, private val contentResolver: ContentResolver, private val mediaType: MediaType = MediaType.parse("application/octet-stream")!!): RequestBody() {
override fun contentLength(): Long = -1L
override fun contentType(): MediaType? = mediaType
override fun writeTo(sink: BufferedSink) {
Okio.source((contentResolver.openInputStream(file))).use {
sink.writeAll(it)
}
}
}
and here is my HttpURLConnection implementation:
private fun uploadFileRaw(file: Uri, uploadUrl: String, contentResolver: ContentResolver) : Int {
val url = URL(uploadUrl)
val connection = url.openConnection() as HttpURLConnection
connection.doOutput = true
connection.requestMethod = "PUT"
val out = connection.outputStream
contentResolver.openInputStream(file).use {
it.copyTo(out)
}
out.close()
return connection.responseCode
}
What is OkHttp doing differently so it can lead to this SSL exception?
EDIT:
Here is the OkHttp code to upload the file (using the default application/octet-stream mime type):
val s3UploadClient = OkHttpClient().newBuilder()
.connectTimeout(30_000L, TimeUnit.MILLISECONDS)
.readTimeout(30_000L, TimeUnit.MILLISECONDS)
.writeTimeout(60_000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.build()
val body: RequestBody = UriRequestBody(file, contentResolver)
val request = Request.Builder()
.url(uploadUrl)
.put(body)
.build()
s3UploadClient.newCall(request).execute()
And this is the JavaScript server code that generates the pre-signed upload URL:
const s3 = new aws.S3({
region: 'us-west-2',
signatureVersion: 'v4'
});
const signedUrlExpireSeconds = 60 * 5;
const signedUrl = s3.getSignedUrl('putObject', {
Bucket: config.bucket.name,
Key: `${fileName}`,
Expires: signedUrlExpireSeconds
});
This seems to work with retrofit library:
fun uploadImage(imagePath: String, directUrl: String): Boolean {
Log.d(TAG, "Image: ${imagePath}, Url: $directUrl")
val request = Request.Builder()
.url(directUrl)
.put(RequestBody.create(null, File(imagePath)))
.build()
val response = WebClient.getOkHttpClient().newCall(request).execute()
return response.isSuccessful
}

Categories

Resources