working API level 1

This commit is contained in:
Sergey Chernov 2022-07-18 15:16:34 +03:00
parent b214ddb6c9
commit e3a0522e87
20 changed files with 2762 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/.gradle/
/.idea/
build

78
build.gradle.kts Normal file
View File

@ -0,0 +1,78 @@
val ktor_version: String by project
val kotlin_version: String by project
plugins {
kotlin("multiplatform") version "1.7.10"
kotlin("plugin.serialization") version "1.7.10"
`maven-publish`
}
group = "net.sergeych"
version = "0.0.1-SNAPSHOT"
repositories {
maven("https://maven.universablockchain.com")
mavenCentral()
mavenLocal()
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
withJava()
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(IR) {
browser {
commonWebpackConfig {
cssSupport.enabled = true
}
}
}
// val hostOs = System.getProperty("os.name")
// val isMingwX64 = hostOs.startsWith("Windows")
// val nativeTarget = when {
// hostOs == "Mac OS X" -> macosX64("native")
// hostOs == "Linux" -> linuxX64("native")
// isMingwX64 -> mingwX64("native")
// else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
// }
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
// implementation("org.jetbrains.kotlin:atomicfu:1.6.21")
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
api("net.sergeych:unikrypto:1.1.1-SNAPSHOT")
api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2")
api("net.sergeych:mp_stools:1.2.3-SNAPSHOT")
api("net.sergeych:boss-serialization-mp:0.1.2-SNAPSHOT")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-client-cio:$ktor_version")
}
}
val jvmTest by getting
val jsMain by getting
val jsTest by getting
// val nativeMain by getting
// val nativeTest by getting
}
}

6
gradle.properties Normal file
View File

@ -0,0 +1,6 @@
kotlin.code.style=official
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
kotlin.js.generate.executable.default=false
ktor_version=2.0.3
kotlin_version=1.7.10

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2232
kotlin-js-store/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

3
settings.gradle.kts Normal file
View File

@ -0,0 +1,3 @@
rootProject.name = "crypstie3api"

View File

@ -0,0 +1,18 @@
package crypstie3
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.days
@Serializable
data class ApiCrypstie(
val guid: String,
val encryptedData: ByteArray,
val deleteAt: Instant = Clock.System.now() + 90.days,
val burnOnShow: Boolean = false,
val showWide: Boolean = false,
val syntaxHighlighting: String? = null,
val editable: Boolean = false,
val ownerHandle: String? = null,
)

View File

@ -0,0 +1,167 @@
@file:OptIn(ExperimentalSerializationApi::class)
package crypstie3
import api.ApiError
import api.ErrorResult
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.boss_serialization_mp.decodeBoss
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.mptools.CachedExpression
import net.sergeych.unikrypto.SymmetricKeys
import tools.decodeBase64Url
import tools.encodeToBase64Url
import kotlin.time.Duration.Companion.days
class CrypstieClient(val rootUrl: String) {
private val hc = CompletableDeferred<HttpClient>()
init {
globalLaunch {
try {
hc.complete(HttpClient() {
install(ContentNegotiation) {
json()
}
})
} catch (x: Exception) {
hc.completeExceptionally(x)
}
}
}
private val cachedVersion = CachedExpression<String>()
suspend fun version(): String {
return cachedVersion.get {
hc.await()
.get("$rootUrl/api/version")
.body<VersionInfo>()
.version
}
}
/**
* Create encrypted crypstie on the server and return it as a path part of the
* of the secret url, e.g. `_guid#haskey`. You should prepend protocol and host to your
* web service to use it or whatever else but leave has part as a hash to not to compromise
* your data!
*/
suspend fun createPath(
text: String,
deleteAt: Instant = Clock.System.now() + 90.days,
burnOnShow: Boolean = false,
ownerHandle: String? = null,
) = createPath(text.toByteArray(), deleteAt, burnOnShow, ownerHandle = ownerHandle)
/**
* Create encrypted crypstie and return its path-patr of the url, e.g. `_guid#haskey`
* just add your interface host to it
*/
suspend fun createPath(
data: ByteArray,
deleteAt: Instant = Clock.System.now() + 90.days,
burnOnShow: Boolean = false,
profileIds: List<String>? = null,
useWide: Boolean = false,
syntaxHighlighting: String? = null,
editable: Boolean = false,
ownerHandle: String? = null,
): String {
// Generate random key and encrypt data
val k = SymmetricKeys.random()
val encrypted = k.etaEncrypt(data)
val keyAsString = k.keyBytes.encodeToBase64Url()
// post crypstie and return URL from the service
val response = hc.await().submitFormWithBinaryData(
url = "$rootUrl/api/crypstie",
formData = formData {
append(
"data",
BossEncoder.encode(
ApiCrypstie(
"",
encrypted, deleteAt, burnOnShow, useWide,
syntaxHighlighting, editable, ownerHandle
)
),
Headers.build {
append(HttpHeaders.ContentType, "application/octet-stream")
append(HttpHeaders.ContentDisposition, "filename=\"dummy.bin\"")
}
)
}
)
val guid = checkResponse(response).body<String>()
// now complete it with a key:
return "_$guid#$keyAsString"
}
private suspend fun checkResponse(response: HttpResponse): HttpResponse {
if (!response.status.isSuccess()) {
if (response.status.value == 404)
ApiError.INTERNAL_ERROR.raise("404: not found")
try {
response.body()
} catch (x: Exception) {
ErrorResult(ApiError.UNKNOWN_ERROR, x.toString())
}.raise()
}
return response
}
/**
* donwload and decrypt crypstie by its url path part with hashtah
* @param pathPart path part of the url e.g. `_<id>#<encoded_key>`.
* @throws ApiError if can't download/decrypt.
*/
suspend fun decryptPath(pathPart: String, profileIds: List<String>? = null): DecryptedCrypstie {
if (pathPart[0] != '_') ApiError.BAD_PARAMETER.raise("pathPart should start with #")
val parts = pathPart.split('#')
fun invalidFormat(text: String? = null): Nothing {
ApiError.BAD_PARAMETER.raise(
"invalid pathPart format${text?.let { ": $it" } ?: ""} "
)
}
if (parts.size != 2 || parts[0].isBlank() || parts[1].isBlank())
invalidFormat()
val key = try {
val keyBytes = parts[1].decodeBase64Url()
if (keyBytes.size != 32) invalidFormat("wrong hashkey size")
SymmetricKeys.create(keyBytes)
} catch (x: Exception) {
invalidFormat("failed to decode hashkey: $x")
}
val query = if (profileIds.isNullOrEmpty()) ""
else "?pids=${profileIds.joinToString(",")}"
val packed = checkResponse(hc.await().get("$rootUrl/api/crypstie/${parts[0].substring(1)}$query"))
.body<ByteArray>()
val ac = packed.decodeBoss<ApiCrypstie>()
try {
return DecryptedCrypstie(key, ac)
} catch (x: Exception) {
invalidFormat("failed to decrypt")
}
}
}

View File

@ -0,0 +1,17 @@
package crypstie3
import kotlinx.datetime.Instant
import net.sergeych.unikrypto.SymmetricKey
class DecryptedCrypstie(
val guid: String,
val data: ByteArray,
val deleteAt: Instant,
val burnOnShow: Boolean,
val ownerHandle: String?
) {
constructor(key: SymmetricKey, ac: ApiCrypstie)
: this(ac.guid, key.etaDecrypt(ac.encryptedData), ac.deleteAt, ac.burnOnShow, ac.ownerHandle)
val text by lazy { data.decodeToString() }
}

View File

@ -0,0 +1,27 @@
package api
enum class ApiError {
UNKNOWN_ERROR,
BAD_PARAMETER,
INTERNAL_ERROR,
INVALID_DELETE_DATE,
DATA_TOO_BIG,
NOT_FOUND;
fun raise(text: String? = null): Nothing {
throw ApiException(this, text ?: defaultText())
}
fun defaultText(): String = name.lowercase().replace('_', ' ')
}
class ApiException(val code: ApiError, _text: String? = null, cause: Throwable? = null) :
Exception(_text ?: code.defaultText(), cause) {
val text by lazy { _text ?: code.defaultText() }
}
@kotlinx.serialization.Serializable
data class ErrorResult(val code: ApiError,val text: String) {
fun raise(): Nothing { throw ApiException(code, text) }
}

View File

@ -0,0 +1,72 @@
package tools
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import kotlin.reflect.KProperty
interface BackingStorge {
fun getItem(key: String): String?
fun setItem(key: String, value: String)
fun clearItem(key: String)
}
class TempBackingStorage : BackingStorge {
private val m = mutableMapOf<String,String>()
override fun getItem(key: String): String? = m[key]
override fun setItem(key: String, value: String) {
m[key] = value
}
override fun clearItem(key: String) {
m.remove(key)
}
}
class PrefixedStorage(private val bs: BackingStorge, val prefix: String): BackingStorge {
override fun getItem(key: String): String? {
return bs.getItem(prefix + key)
}
override fun setItem(key: String, value: String) {
bs.setItem(prefix+key, value)
}
override fun clearItem(key: String) {
bs.clearItem(prefix+key)
}
}
private val format = Json { prettyPrint = false }
inline fun <reified T>storage(backingStorge: BackingStorge,overrideName: String?=null): SimpleStorageDelegate<T> {
return SimpleStorageDelegate(backingStorge, overrideName, serializer())
}
class SimpleStorageDelegate<T>(
val storage: BackingStorge,
val overrideName: String? = null,
val sr: KSerializer<T>,
) {
private var cacheIsSet = false
private var cachedValue: T? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? =
if (cacheIsSet)
cachedValue
else
storage.getItem(overrideName ?: property.name)?.let { format.decodeFromString(sr, it) }
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
val name = overrideName ?: property.name
if (value == null) storage.clearItem(name)
else storage.setItem(name, format.encodeToString(sr, value))
cachedValue = value
cacheIsSet = true
}
}

View File

@ -0,0 +1,43 @@
package tools
import api.ApiError
import api.ApiException
class CatchApiResult<T>(_result: T?, val exception: ApiException?) {
var errorIsProcessed = false
private set
var result = _result
private set
suspend fun on(vararg errors: ApiError, block: suspend () -> T?): CatchApiResult<T> {
if (exception != null && exception.code in errors) {
errorIsProcessed = true
result = block()
}
return this
}
fun ignore(vararg errors: ApiError): CatchApiResult<T> {
if (exception != null && exception.code in errors)
errorIsProcessed = true
return this
}
fun orThrowError(): CatchApiResult<T> {
if (exception != null && !errorIsProcessed) throw exception
return this
}
fun get(): T? = orThrowError().result
}
suspend fun <T> catchApi(block: suspend () -> T): CatchApiResult<T> =
try {
CatchApiResult(block(), null)
} catch (e: ApiException) {
CatchApiResult(null, e)
} catch (t: Throwable) {
CatchApiResult(null, ApiException(ApiError.UNKNOWN_ERROR, cause= t))
}

View File

@ -0,0 +1,21 @@
package tools
import kotlin.random.Random
private const val lowerLetters = "qwertyuiopasdfghjklzxcvbnm"
private val idFirstLetters: String = lowerLetters + lowerLetters.uppercase() + "_"
private val idLetters: String = idFirstLetters + "1234567890-"
val CharSequence.sampleChar: Char
get() = this[Random.nextInt(0,length)]
val <T> List<T>.sample: T
get() = this[Random.nextInt(0,size)]
fun randomId(length: Int): String {
if( length < 2 ) throw IllegalArgumentException("too short")
val result = StringBuilder(idFirstLetters.sampleChar.toString())
for( i in 1 until length ) result.append(idLetters.sampleChar)
return result.toString()
}

View File

@ -0,0 +1,30 @@
package tools
import kotlinx.coroutines.CancellationException
import net.sergeych.mp_tools.decodeBase64
import net.sergeych.mp_tools.decodeBase64Compact
import net.sergeych.mp_tools.encodeToBase64Compact
suspend fun ignoreErrors(block: suspend ()->Unit) {
try {
block()
}
catch(x: CancellationException) {
throw x
}
catch(t: Throwable) {}
}
fun String.trimOrNull(): String?{
val s = trim()
return if( s == "" ) null else s
}
fun String.decodeBase64Url(): ByteArray = replace('.', '+')
.replace('_', '/')
.decodeBase64Compact()
fun ByteArray.encodeToBase64Url(): String = encodeToBase64Compact()
.replace('+', '.')
.replace('/', '_')

View File

@ -0,0 +1,7 @@
package crypstie3
import kotlinx.serialization.Serializable
@Serializable
data class VersionInfo(val version: String,val service: String)

View File

@ -0,0 +1,14 @@
import crypstie3.CrypstieClient
import kotlin.test.*
internal class CrypstieClientTest {
@Test
fun create() = runTest {
val client = CrypstieClient("http://localhost:8296")
assertEquals(3, client.version().split('.').size)
val x = client.createPath("foobarbazz")
val dc = client.decryptPath(x)
assertEquals("foobarbazz", dc.text)
}
}

View File

@ -0,0 +1,4 @@
import net.sergeych.mp_tools.globalLaunch
expect fun runTest(block: suspend () -> Unit)

View File

@ -0,0 +1,4 @@
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.promise
actual fun runTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() }

View File

@ -0,0 +1,6 @@
import kotlinx.datetime.Instant
import kotlinx.datetime.toKotlinInstant
import java.time.ZonedDateTime
fun ZonedDateTime.toKotlinInstant(): Instant = toInstant().toKotlinInstant()

View File

@ -0,0 +1,5 @@
import kotlinx.coroutines.runBlocking
actual fun runTest(block: suspend () -> Unit) {
runBlocking { block() }
}