Last active
April 15, 2025 18:28
-
-
Save luca992/969af9735c71a284128ba91623741efa to your computer and use it in GitHub Desktop.
Kotlin Multiplatform S3 Request
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import io.ktor.client.* | |
import io.ktor.client.request.* | |
import io.ktor.client.statement.* | |
import io.ktor.http.* | |
import kotlinx.datetime.Clock | |
import kotlinx.datetime.LocalDateTime | |
import kotlinx.datetime.TimeZone | |
import kotlinx.datetime.toLocalDateTime | |
import okio.ByteString.Companion.encodeUtf8 | |
/** | |
* https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html | |
*/ | |
suspend fun s3Request( | |
httpReqMethod: String = "PUT", | |
fileName: String, | |
bodyBytes: ByteArray? = null, | |
contentType: ContentType? = null, | |
bucket: String, | |
region: String = "us-east-1", | |
domain: String = "amazonaws.com", | |
awsAccess: String, | |
awsSecret: String | |
): HttpResponse { | |
val authType = "AWS4-HMAC-SHA256" | |
val service = "s3" | |
val baseUrl = ".$service.$domain" | |
val now = Clock.System.now().toLocalDateTime(TimeZone.UTC) | |
val dateValueS = getDateValueS(now) | |
val dateValueL = getDateValueL(now) | |
val payloadHash = "UNSIGNED-PAYLOAD"//(bodyBytes ?: "".toByteArray()).toByteString().sha256().hex() | |
// should be sorted by header name | |
val canonicalHeaders = mapOf( | |
// "Content-Type" to contentType!!.toString(), | |
"host" to "$bucket$baseUrl", | |
// "x-amz-content-sha256" to payloadHash, | |
) | |
val headerKeys = canonicalHeaders.mapKeys { it.key.lowercase() }.keys.joinToString(";") | |
// should be sorted by param name | |
val canonicalQueryParams = mapOf( | |
"X-Amz-Algorithm" to authType, | |
"X-Amz-Credential" to "${awsAccess}/${dateValueS}/${region}/${service}/aws4_request", | |
"X-Amz-Date" to dateValueL, | |
"X-Amz-Expires" to "86400", | |
"X-Amz-SignedHeaders" to headerKeys | |
// "X-Amz-Signature" will be added after generating the signature | |
) | |
val canonicalRequest = buildString { | |
append("$httpReqMethod\n") | |
append("/$fileName\n") | |
append(canonicalQueryParams.entries.joinToString("&") { "${it.key.encodeURLParameter()}=${it.value.encodeURLParameter()}" }) | |
append("\n") | |
canonicalHeaders.forEach { (key, value) -> | |
append("${key.lowercase()}:${value.trim()}\n") | |
} | |
append("\n") | |
append(headerKeys + "\n") | |
append(payloadHash) | |
} | |
val canonicalRequestHash = canonicalRequest.encodeUtf8().sha256().hex() | |
val stringToSign = "$authType\n$dateValueL\n$dateValueS/$region/$service/aws4_request\n$canonicalRequestHash" | |
val kSecret = "AWS4$awsSecret".encodeUtf8() | |
val kDate = dateValueS.encodeUtf8().hmacSha256(kSecret) | |
val kRegion = region.encodeUtf8().hmacSha256(kDate) | |
val kService = service.encodeUtf8().hmacSha256(kRegion) | |
val kSigning = "aws4_request".encodeUtf8().hmacSha256(kService) | |
val signature = stringToSign.encodeUtf8().hmacSha256(kSigning) | |
val client = HttpClient {} | |
return client.request { | |
method = HttpMethod.parse(httpReqMethod) | |
url { | |
host = "${bucket}${baseUrl}" | |
protocol = URLProtocol.HTTPS | |
path(fileName) | |
bodyBytes?.run { setBody(this) } | |
canonicalQueryParams.forEach { (key, value) -> | |
parameters.append(key, value) | |
} | |
parameters.append("X-Amz-Signature", signature.hex()) | |
} | |
contentType?.let { contentType(it) } | |
} | |
} | |
private fun getDateValueS(dateTime: LocalDateTime): String { | |
return buildString { | |
append(dateTime.year.toString().padStart(4, '0')) | |
append(dateTime.monthNumber.toString().padStart(2, '0')) | |
append(dateTime.dayOfMonth.toString().padStart(2, '0')) | |
} | |
} | |
private fun getDateValueL(dateTime: LocalDateTime): String { | |
return buildString { | |
append(getDateValueS(dateTime)) | |
append('T') | |
append(dateTime.hour.toString().padStart(2, '0')) | |
append(dateTime.minute.toString().padStart(2, '0')) | |
append(dateTime.second.toString().padStart(2, '0')) | |
append('Z') | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
PUT
andGET
have been tested