├── src └── doc │ ├── docs │ ├── .gitignore │ ├── stylesheets │ │ └── extra.css │ ├── okhttp.md │ ├── about │ │ └── license.md │ ├── getting-started.md │ ├── scrubbing.md │ ├── logging-requests.md │ ├── usage.md │ └── index.md │ ├── dokka │ └── 0.1.0 │ │ ├── version.json │ │ ├── scripts │ │ ├── sourceset_dependencies.js │ │ ├── clipboard.js │ │ ├── symbol-parameters-wrapper_deferred.js │ │ └── navigation-loader.js │ │ ├── ui-kit │ │ ├── assets │ │ │ ├── arrow-down.svg │ │ │ ├── homepage.svg │ │ │ ├── checkbox-off.svg │ │ │ ├── placeholder.svg │ │ │ ├── checkbox-on.svg │ │ │ ├── burger.svg │ │ │ ├── cross.svg │ │ │ ├── exception-class.svg │ │ │ ├── enum.svg │ │ │ ├── field-value.svg │ │ │ ├── interface.svg │ │ │ ├── field-variable.svg │ │ │ ├── filter.svg │ │ │ ├── interface-kotlin.svg │ │ │ ├── enum-kotlin.svg │ │ │ ├── typealias-kotlin.svg │ │ │ ├── theme-toggle.svg │ │ │ ├── class.svg │ │ │ ├── object.svg │ │ │ ├── class-kotlin.svg │ │ │ ├── function.svg │ │ │ ├── abstract-class.svg │ │ │ ├── annotation.svg │ │ │ ├── abstract-class-kotlin.svg │ │ │ └── annotation-kotlin.svg │ │ └── ui-kit.min.js │ │ ├── images │ │ ├── copy-icon.svg │ │ ├── footer-go-to-link.svg │ │ ├── copy-successful-icon.svg │ │ ├── go-to-top-icon.svg │ │ ├── logo-icon.svg │ │ └── anchor-copy-button.svg │ │ ├── package-list │ │ ├── styles │ │ ├── logo-styles.css │ │ ├── multimodule.css │ │ ├── font-jb-sans-auto.css │ │ └── prism.css │ │ ├── dokka-configuration.json │ │ ├── index.html │ │ ├── not-found-version.html │ │ └── harbringer │ │ └── se.ansman.harbringer │ │ └── -harbringer │ │ ├── -entry │ │ ├── id.html │ │ ├── request.html │ │ ├── timings.html │ │ └── response.html │ │ ├── -device │ │ └── ip.html │ │ └── -request │ │ ├── headers.html │ │ └── url.html │ └── mkdocs.yml ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── dependabot.yml ├── workflows │ ├── update-gradle-wrapper.yml │ ├── pages.yml │ └── gradle.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .idea ├── .gitignore ├── kotlinc.xml ├── vcs.xml ├── AndroidProjectSystem.xml └── artifacts │ ├── har_exporter_jvm.xml │ ├── harbringer_jvm_0_1_0.xml │ ├── har_exporter_jvm_0_1_0.xml │ ├── harbringer_jvm_0_1_0_SNAPSHOT.xml │ └── request_logger_jvm_0_1_0.xml ├── HAR 1.2 Spec | Software is hard.pdf ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── harbringer ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── se │ │ │ └── ansman │ │ │ └── harbringer │ │ │ ├── internal │ │ │ ├── Uuid.kt │ │ │ ├── ByteStrings.kt │ │ │ ├── Url.kt │ │ │ ├── InternalRequestLoggerApi.kt │ │ │ ├── datetime.kt │ │ │ ├── atomic │ │ │ │ └── Lock.kt │ │ │ ├── SortedMap.kt │ │ │ ├── json │ │ │ │ ├── IterableSerializer.kt │ │ │ │ └── LazyString.kt │ │ │ ├── MimeTypes.kt │ │ │ └── Lists.kt │ │ │ ├── scrubber │ │ │ ├── RequestScrubber.kt │ │ │ ├── ResponseScrubber.kt │ │ │ ├── RealResponseScrubber.kt │ │ │ ├── BodyScrubber.kt │ │ │ ├── RealScrubber.kt │ │ │ ├── RealRequestScrubber.kt │ │ │ └── JsonScrubber.kt │ │ │ └── storage │ │ │ ├── HarbringerStorage.kt │ │ │ └── InMemoryHarbringerStorage.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── se │ │ │ └── ansman │ │ │ └── harbringer │ │ │ └── internal │ │ │ ├── Uuid.jvm.kt │ │ │ ├── datetime.jvm.kt │ │ │ ├── atomic │ │ │ └── Lock.jvm.kt │ │ │ ├── ByteStrings.jvm.kt │ │ │ └── Url.jvm.kt │ └── commonTest │ │ └── kotlin │ │ └── se │ │ └── ansman │ │ └── harbringer │ │ ├── internal │ │ └── TestClock.kt │ │ ├── DiscardBodyScrubberTest.kt │ │ ├── ReplaceBodyScrubberTest.kt │ │ ├── storage │ │ └── SortedMapTest.kt │ │ └── scrubber │ │ └── JsonScrubberTest.kt ├── okhttp3 │ ├── api │ │ └── okhttp3.api │ ├── src │ │ └── main │ │ │ ├── api │ │ │ └── okhttp3.api │ │ │ └── kotlin │ │ │ └── se │ │ │ └── ansman │ │ │ └── harbringer │ │ │ └── okhttp3 │ │ │ └── Strings.kt │ └── build.gradle.kts └── build.gradle.kts ├── SECURITY.md ├── CONTRIBUTING.md ├── publish.sh ├── settings.gradle.kts ├── gradle.properties ├── RELEASING.md ├── .gitignore ├── README.md ├── gradlew.bat └── CODE_OF_CONDUCT.md /src/doc/docs/.gitignore: -------------------------------------------------------------------------------- 1 | _data -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ansman 2 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/version.json: -------------------------------------------------------------------------------- 1 | {"version":"0.1.0"} -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /HAR 1.2 Spec | Software is hard.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansman/harbringer/HEAD/HAR 1.2 Spec | Software is hard.pdf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansman/harbringer/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/Uuid.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | internal expect fun randomUuid(): String -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/scripts/sourceset_dependencies.js: -------------------------------------------------------------------------------- 1 | sourceset_dependencies = '{":harbringer:okhttp3/main":[],":harbringer/commonMain":[],":harbringer/jvmMain":[":harbringer/commonMain"]}' -------------------------------------------------------------------------------- /src/doc/docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | [data-md-color-scheme="slate"] { 2 | --md-default-bg-color: #1f1f1f; 3 | --md-typeset-a-color: color-mix(in srgb, var(--md-primary-fg-color) 80%, white); 4 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Describe the reason for the change** 2 | 3 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /harbringer/src/jvmMain/kotlin/se/ansman/harbringer/internal/Uuid.jvm.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | import java.util.* 4 | 5 | internal actual fun randomUuid(): String = UUID.randomUUID().toString() -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/ByteStrings.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | import okio.ByteString 4 | 5 | internal expect fun ByteString.readString(charset: String?): String -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/homepage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/checkbox-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /harbringer/okhttp3/api/okhttp3.api: -------------------------------------------------------------------------------- 1 | public final class se/ansman/harbringer/okhttp3/HarbringerOkHttp3 { 2 | public static final fun addHarbringer (Lokhttp3/OkHttpClient$Builder;Lse/ansman/harbringer/Harbringer;)Lokhttp3/OkHttpClient$Builder; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /harbringer/okhttp3/src/main/api/okhttp3.api: -------------------------------------------------------------------------------- 1 | public final class se/ansman/harbringer/okhttp3/HarbringerOkHttp3 { 2 | public static final fun addHarbringer (Lokhttp3/OkHttpClient$Builder;Lse/ansman/harbringer/Harbringer;)Lokhttp3/OkHttpClient$Builder; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/images/copy-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/images/footer-go-to-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /harbringer/okhttp3/src/main/kotlin/se/ansman/harbringer/okhttp3/Strings.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.okhttp3 2 | 3 | import java.net.URLDecoder 4 | 5 | internal fun String.decodeContentTypeParameter(): String = URLDecoder.decode(removeSurrounding("\""), Charsets.UTF_8) -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | All versions of harbringer will be supported with security patches. 6 | 7 | ## Reporting a Vulnerability 8 | To report an vulnerability simple [open an issue](https://github.com/ansman/harbringer/security/advisories/new). 9 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/package-list: -------------------------------------------------------------------------------- 1 | $dokka.format:html-v1 2 | $dokka.linkExtension:html 3 | 4 | module:harbringer 5 | se.ansman.harbringer 6 | se.ansman.harbringer.internal 7 | se.ansman.harbringer.scrubber 8 | se.ansman.harbringer.storage 9 | module:okhttp3 10 | se.ansman.harbringer.okhttp3 11 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/Url.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | internal expect class Url(url: String) { 4 | val queryParameters: Sequence> 5 | fun replaceQueryParameters(parameters: Iterable>): Url 6 | } 7 | -------------------------------------------------------------------------------- /harbringer/src/jvmMain/kotlin/se/ansman/harbringer/internal/datetime.jvm.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | import java.time.Instant 4 | 5 | internal actual fun currentTime(): Long = System.currentTimeMillis() 6 | internal actual fun formatIso8601(time: Long): String = Instant.ofEpochMilli(time).toString() -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/images/copy-successful-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/styles/logo-styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | :root { 6 | --dokka-logo-image-url: url('../images/logo-icon.svg'); 7 | --dokka-logo-height: 28px; 8 | --dokka-logo-width: 28px; 9 | } 10 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/images/go-to-top-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/InternalRequestLoggerApi.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | @RequiresOptIn( 4 | level = RequiresOptIn.Level.ERROR, 5 | message = "This API is internal to the RequestLogger library and should not be used directly.") 6 | annotation class InternalRequestLoggerApi() 7 | -------------------------------------------------------------------------------- /harbringer/okhttp3/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("published-library") 4 | } 5 | 6 | dependencies { 7 | api(project(":harbringer")) 8 | implementation(platform(libs.okhttp3.bom)) 9 | implementation(libs.okhttp3) 10 | testImplementation(libs.okhttp3.mockwebserver) 11 | testImplementation(libs.okio.fakeFileSystem) 12 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/checkbox-on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Harbringer welcomes pull requests and issue filing, here are some tips to help speed things up: 2 | * Make sure there isn't already an open issues: https://github.com/ansman/harbringer/issues 3 | * Include what you are fixing and why 4 | * Make sure the code is well tested and include a unit test if applicable 5 | * Follow the general code style in the project 6 | -------------------------------------------------------------------------------- /.idea/artifacts/har_exporter_jvm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/har-exporter/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/harbringer_jvm_0_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/harbringer/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/har_exporter_jvm_0_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/har-exporter/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=16f2b95838c1ddcf7242b1c39e7bbbb43c842f1f1a1a0dc4959b6d4d68abcac3 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-all.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ./gradlew clean 4 | ./gradlew publishAllPublicationsToMavenCentralRepository \ 5 | -PsignArtifacts=true \ 6 | -Psigning.gnupg.executable=/opt/homebrew/bin/gpg \ 7 | -Psigning.gnupg.keyName=$(op --account my.1password.com read op://private/GnuPG/keyID | xargs) \ 8 | -Psigning.gnupg.passphrase=$(op --account my.1password.com read op://private/GnuPG/password | xargs) -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/artifacts/harbringer_jvm_0_1_0_SNAPSHOT.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/harbringer/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/request_logger_jvm_0_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/request-logger/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/datetime.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | internal interface Clock { 4 | fun currentTime(): Long 5 | 6 | object System : Clock { 7 | override fun currentTime(): Long = se.ansman.harbringer.internal.currentTime() 8 | } 9 | } 10 | 11 | internal expect fun currentTime(): Long 12 | internal expect fun formatIso8601(time: Long): String 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("UnstableApiUsage") 2 | dependencyResolutionManagement { 3 | repositories { 4 | mavenCentral() 5 | } 6 | 7 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 8 | } 9 | 10 | plugins { 11 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 12 | } 13 | 14 | include(":harbringer") 15 | include(":harbringer:okhttp3") 16 | 17 | rootProject.name = "harbringer" -------------------------------------------------------------------------------- /harbringer/src/jvmMain/kotlin/se/ansman/harbringer/internal/atomic/Lock.jvm.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal.atomic 2 | 3 | import java.util.concurrent.locks.ReentrantLock 4 | 5 | internal actual typealias Lock = ReentrantLock 6 | 7 | internal actual fun newLock(): Lock = ReentrantLock() 8 | 9 | internal actual fun Lock.lock() = lock() 10 | internal actual fun Lock.unlock() = unlock() 11 | internal actual fun Lock.tryLock(): Boolean = tryLock() -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.jvmargs=-Xmx10g -Xms4g -Dfile.encoding\=UTF-8 -XX\:+UseG1GC 3 | org.gradle.vfs.watch=true 4 | org.gradle.caching=true 5 | org.gradle.configuration.cache=true 6 | org.gradle.configuration-cache.parallel=true 7 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 8 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 9 | 10 | kotlin.code.style=official 11 | 12 | version=0.2.0-SNAPSHOT 13 | latestRelease=0.1.0 14 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/RequestScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import se.ansman.harbringer.Harbringer 4 | 5 | /** 6 | * A [RequestScrubber] is used to modify the request before it is persisted. 7 | */ 8 | fun interface RequestScrubber { 9 | /** 10 | * Scrubs the given [request], and returns a scrubbed version, or `null` if the request should be discarded. 11 | */ 12 | fun scrub(request: Harbringer.Request): Harbringer.Request? 13 | } -------------------------------------------------------------------------------- /src/doc/docs/okhttp.md: -------------------------------------------------------------------------------- 1 | OkHttp 2 | === 3 | To use Harbringer with OkHttp, you need to add the `harbringer-okhttp3` dependency to your project. Then you can 4 | add it to your OkHttp builder: 5 | 6 | ```kotlin 7 | val harbringer = Harbringer( 8 | storage = FileSystemHarbringerStorage(storageDirectory.toPath()), 9 | maxRequests = 1000, // 1000 requests 10 | maxDiskSize = 100 * 1024 * 1024, // 100MB 11 | maxAge = 2.days, 12 | ) 13 | 14 | val okHttpClient = OkHttpClient.Builder() 15 | .addHarbringer(harbringer) 16 | .build() 17 | ``` 18 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/exception-class.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/styles/multimodule.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | .versions-dropdown { 6 | white-space: nowrap; 7 | } 8 | 9 | @media (width < 900px) { 10 | .versions-dropdown { 11 | height: 52px; 12 | margin-left: -8px; 13 | } 14 | } 15 | 16 | @media (width >= 900px) { 17 | .versions-dropdown .dropdown--list { 18 | min-width: 60px; 19 | top: 44px; 20 | } 21 | } 22 | 23 | .no-js .versions-dropdown:hover .dropdown--list { 24 | display: block; 25 | } 26 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/enum.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /harbringer/src/jvmMain/kotlin/se/ansman/harbringer/internal/ByteStrings.jvm.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | import okio.ByteString 4 | import java.nio.charset.Charset 5 | import java.nio.charset.UnsupportedCharsetException 6 | 7 | internal actual fun ByteString.readString(charset: String?): String { 8 | Charsets.UTF_8 9 | val cs = try { 10 | Charset.forName(charset ?: "UTF-8") 11 | } catch (_: UnsupportedCharsetException) { 12 | Charsets.UTF_8 13 | } 14 | return if (cs == Charsets.UTF_8) { 15 | utf8() 16 | } else { 17 | string(cs) 18 | } 19 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/field-value.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/interface.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /harbringer/src/commonTest/kotlin/se/ansman/harbringer/internal/TestClock.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | import kotlin.time.Duration 4 | import kotlin.time.ExperimentalTime 5 | import kotlin.time.Instant 6 | 7 | @OptIn(ExperimentalTime::class) 8 | class TestClock( 9 | var now: Long = 1745787552123L 10 | ) : Clock { 11 | val kotlinClock = object : kotlin.time.Clock { 12 | override fun now(): Instant = Instant.fromEpochMilliseconds(now) 13 | } 14 | 15 | override fun currentTime(): Long = now 16 | 17 | operator fun plusAssign(delta: Duration) { 18 | now += delta.inWholeMilliseconds 19 | } 20 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | google: 4 | type: maven-repository 5 | url: "https://dl.google.com/dl/android/maven2/" 6 | gradle-plugin-portal: 7 | type: maven-repository 8 | url: "https://plugins.gradle.org/m2" 9 | maven-central: 10 | type: maven-repository 11 | url: "https://repo.maven.apache.org/maven2/" 12 | updates: 13 | - package-ecosystem: "gradle" 14 | directory: "/" 15 | open-pull-requests-limit: 25 16 | registries: 17 | - google 18 | - gradle-plugin-portal 19 | - maven-central 20 | schedule: 21 | interval: "daily" 22 | time: "08:00" 23 | timezone: "EST" 24 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/field-variable.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/atomic/Lock.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal.atomic 2 | 3 | internal expect class Lock 4 | 5 | internal expect fun newLock(): Lock 6 | internal expect fun Lock.lock() 7 | internal expect fun Lock.unlock() 8 | internal expect fun Lock.tryLock(): Boolean 9 | internal inline fun Lock.withLock(block: () -> R): R { 10 | lock() 11 | return try { 12 | block() 13 | } finally { 14 | unlock() 15 | } 16 | } 17 | internal inline fun Lock.tryWithLock(block: () -> Unit) { 18 | if (tryLock()) { 19 | try { 20 | block() 21 | } finally { 22 | unlock() 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: actions/setup-java@v3 16 | with: 17 | distribution: 'zulu' 18 | java-version: '22' 19 | 20 | - uses: actions/setup-python@v5 21 | with: 22 | python-version: '3.13' 23 | 24 | - name: Update Gradle Wrapper 25 | uses: gradle-update/update-gradle-wrapper-action@v1 26 | with: 27 | repo-token: ${{ secrets.GRADLE_WRAPPER_UPDATE_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for harbringer 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/images/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in harbringer 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Actual behavior** 20 | 21 | 22 | **Environment** 23 | - Kotlin Version [2.1.20 for example]: 24 | - Networking library: [OkHttp for example]: 25 | 26 | **Additional context** 27 | 28 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/interface-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/ResponseScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import se.ansman.harbringer.Harbringer 4 | 5 | /** 6 | * A [ResponseScrubber] is used to modify a response before it is persisted. 7 | */ 8 | fun interface ResponseScrubber { 9 | /** 10 | * Scrubs the given [response], and returns a scrubbed version, or `null` if the request should be 11 | * discarded. 12 | * 13 | * @param request The request the response belongs to. 14 | * @param response The response to scrub. 15 | * @return A scrubbed version of the response, or `null` if the request should be discarded. 16 | */ 17 | fun scrub(request: Harbringer.Request, response: Harbringer.Response): Harbringer.Response? 18 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/enum-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/doc/docs/about/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | ```plain 3 | 4 | Copyright 2025 Nicklas Ansman 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ``` 18 | 19 | You can find the full license at [ansman/harbringer](https://github.com/ansman/harbringer/blob/{{gradle.version}}/LICENSE). -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/images/anchor-copy-button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/typealias-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/RealResponseScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import se.ansman.harbringer.Harbringer 4 | 5 | internal class RealResponseScrubber( 6 | private val scrubHeader: (Harbringer.Header) -> Harbringer.Header?, 7 | private val onlyIf: (Harbringer.Request) -> Boolean, 8 | ) : ResponseScrubber { 9 | 10 | override fun scrub(request: Harbringer.Request, response: Harbringer.Response): Harbringer.Response? { 11 | if (!onlyIf(request) || scrubHeader == defaultScrubHeader) { 12 | return response 13 | } 14 | return response.copy( 15 | headers = response.headers.values 16 | .mapNotNull(scrubHeader) 17 | .let(Harbringer::Headers) 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/BodyScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import okio.Sink 4 | import se.ansman.harbringer.Harbringer 5 | 6 | /** 7 | * A [BodyScrubber] is used to modify the request body before it is persisted. 8 | */ 9 | fun interface BodyScrubber { 10 | /** 11 | * Returns a new [Sink], that will receive the unscrubbed body. The scrubbed data should be written to the given 12 | * [sink]. 13 | * 14 | * The given [sink] must be closed when the returned sink is closed. 15 | * 16 | * @param request The request the body belongs to. 17 | * @param sink The sink to write the scrubbed data to. 18 | * @return A new [Sink] that will receive the unscrubbed body. 19 | */ 20 | fun scrub(request: Harbringer.Request, sink: Sink): Sink 21 | } -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | concurrency: 12 | group: "pages" 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | 22 | - uses: actions/setup-java@v3 23 | with: 24 | distribution: 'zulu' 25 | java-version: '22' 26 | 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.13' 30 | 31 | - name: Publish Docs 32 | uses: gradle/gradle-build-action@v2 33 | env: 34 | GRGIT_USER: x-access-token 35 | GRGIT_PASS: ${{ secrets.GITHUB_TOKEN }} 36 | with: 37 | arguments: mkdocsPublish 38 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | 1. `git checkout main` 2 | 2. `git pull origin main` 3 | 3. Change the version in `gradle.properties` to a non-snapshot version. 4 | 4. Update the `README.md` with the new version. 5 | 5. `./gradlew versionCurrentDocs` 6 | 6. `git add .` 7 | 7. `git commit -am "Prepare for release X.Y.Z"` 8 | 8. `./publish.sh`. 9 | 9. Close and release on [Maven Central](https://central.sonatype.com/publishing). 10 | 10. `git push origin main` 11 | 11. Release on GitHub: 12 | 1. Create a new release [here](https://github.com/ansman/harbringer/releases/new). 13 | 2. Use the automatic changelog. Update if needed. 14 | 3. Ensure you pick the "Prepare for release X.Y.Z" as the target commit. 15 | 12. `git pull origin main --tags` 16 | 13. Update the `gradle.properties` to the next SNAPSHOT version. 17 | 14. `git commit -am "Prepare next development version"` 18 | 15. `git push origin main` -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/theme-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/class.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | /local.properties 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea/modules.xml 10 | .idea/misc.xml 11 | .idea/gradle.xml 12 | .idea/kotlinc.xml 13 | .idea/caches 14 | .idea/runConfigurations.xml 15 | .idea/jarRepositories.xml 16 | .idea/compiler.xml 17 | .idea/libraries/ 18 | .idea/artifacts/ 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | !**/src/main/**/out/ 24 | !**/src/test/**/out/ 25 | 26 | ### Kotlin ### 27 | .kotlin 28 | 29 | ### Eclipse ### 30 | .apt_generated 31 | .classpath 32 | .factorypath 33 | .project 34 | .settings 35 | .springBeans 36 | .sts4-cache 37 | bin/ 38 | !**/src/main/**/bin/ 39 | !**/src/test/**/bin/ 40 | 41 | ### NetBeans ### 42 | /nbproject/private/ 43 | /nbbuild/ 44 | /dist/ 45 | /nbdist/ 46 | /.nb-gradle/ 47 | 48 | ### VS Code ### 49 | .vscode/ 50 | 51 | ### Mac OS ### 52 | .DS_Store 53 | 54 | local.properties 55 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/object.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/RealScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import okio.Sink 4 | import se.ansman.harbringer.Harbringer 5 | 6 | internal class RealScrubber( 7 | private val requestScrubber: RequestScrubber, 8 | private val requestBodyScrubber: BodyScrubber, 9 | private val responseScrubber: ResponseScrubber, 10 | private val responseBodyScrubber: BodyScrubber, 11 | ) : Scrubber { 12 | override fun scrubRequest(request: Harbringer.Request): Harbringer.Request? = requestScrubber.scrub(request) 13 | 14 | override fun scrubRequestBody(request: Harbringer.Request, sink: Sink): Sink = 15 | requestBodyScrubber.scrub(request, sink) 16 | 17 | override fun scrubResponse(request: Harbringer.Request, response: Harbringer.Response): Harbringer.Response? = 18 | responseScrubber.scrub(request, response) 19 | 20 | override fun scrubResponseBody(request: Harbringer.Request, sink: Sink): Sink = 21 | responseBodyScrubber.scrub(request, sink) 22 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/class-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/function.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/doc/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | [](https://central.sonatype.com/search?smo=true&q=se.ansman.harbringer) 5 | 6 | 7 | ### Gradle 8 | To use Harbringer in your project, you'll need to add a dependency on the library. 9 | ```kotlin 10 | dependencies { 11 | implementation("se.ansman.harbringer:harbringer:{{gradle.version}}") 12 | // If you're using a supported HTTP client, then depend on the appropriate module 13 | implementation("se.ansman.harbringer:harbringer-okhttp3:{{gradle.version}}") 14 | } 15 | ``` 16 | 17 | ### Snapshots 18 | Snapshots are published on every commit to [Sonatype's snapshot repository](https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/se/ansman/harbringer). 19 | To use a snapshot, add the snapshot repository: 20 | ```kotlin 21 | buildscripts { 22 | repositories { 23 | ... 24 | maven("https://central.sonatype.com/repository/maven-snapshots/") 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation("se.ansman.harbringer:harbringer:{{gradle.snapshotVersion}}") 30 | } 31 | ``` -------------------------------------------------------------------------------- /src/doc/docs/scrubbing.md: -------------------------------------------------------------------------------- 1 | # Scrubbing 2 | You often want to ensure that sensitive data is not included in the logs. Harbringer supports scrubbing of sensitive 3 | data from requests and responses. You can pass a `Scrubber` to the `Harbringer` constructor: 4 | 5 | ```kotlin 6 | val harbringer = Harbringer( 7 | scrubber = Scrubber( 8 | request = Scrubber.request( 9 | // Replaces the value of the "apiKey" query parameter with "******" 10 | queryParameter = Scrubber.queryParameter("apiKey"), 11 | // Replaces the value of the "Authorization" query parameter with "******" 12 | header = Scrubber.header("Authorization"), 13 | ), 14 | // Removes the "password" and "username" fields from the request body, and if the request is against the login endpoint. 15 | requestBody = Scrubber.json("$.username", "$.password", onlyIf = { it.url.endsWith("/login") }), 16 | response = Scrubber.request( 17 | header = Scrubber.header("Sensitive-Header"), 18 | ), 19 | // Removes the "token" field from the response body, and if the request is against the login endpoint. 20 | responseBody = Scrubber.json("$.token", onlyIf = { it.url.endsWith("/login") }), 21 | ) 22 | ) 23 | ``` -------------------------------------------------------------------------------- /harbringer/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("published-library") 4 | alias(libs.plugins.kotlin.plugin.serialization) 5 | } 6 | 7 | val generateRequestLoggerMetadata by tasks.registering { 8 | val metadataOutput = layout.buildDirectory.dir("generated/$name/kotlin") 9 | val version = version 10 | inputs.property("version", version) 11 | outputs.dir(metadataOutput) 12 | doFirst { 13 | with(metadataOutput.get()) { 14 | asFile.deleteRecursively() 15 | asFile.mkdirs() 16 | file("HarbringerVersion.kt").asFile.writeText( 17 | """ 18 | package se.ansman.harbringer.internal 19 | 20 | internal const val HARBRINGER_VERSION = "$version" 21 | """.trimIndent() 22 | ) 23 | } 24 | } 25 | } 26 | 27 | kotlin { 28 | sourceSets { 29 | commonMain { 30 | kotlin.srcDir(generateRequestLoggerMetadata) 31 | } 32 | } 33 | 34 | jvm() 35 | } 36 | 37 | dependencies { 38 | commonMainApi(libs.okio) 39 | commonMainImplementation(libs.kotlinx.serialization.json) 40 | commonMainImplementation(libs.kotlinx.serialization.json.okio) 41 | 42 | commonTestImplementation(kotlin("test")) 43 | commonTestImplementation(libs.okio.fakeFileSystem) 44 | } -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/SortedMap.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | internal class SortedMap> { 4 | private val delegate = HashMap() 5 | private val _values = ArrayList() 6 | 7 | val size: Int 8 | get() = delegate.size 9 | 10 | val entries: Set> 11 | get() = delegate.entries 12 | 13 | val keys: Set 14 | get() = delegate.keys 15 | 16 | val values: List 17 | get() = _values 18 | 19 | fun isEmpty() = size == 0 20 | 21 | fun clear() { 22 | delegate.clear() 23 | _values.clear() 24 | } 25 | 26 | operator fun get(key: K): V? = delegate[key] 27 | 28 | operator fun set(key: K, value: V) { 29 | val existing = delegate.put(key, value) 30 | if (existing != null) { 31 | _values.removeAt(_values.binarySearch(existing)) 32 | } 33 | 34 | val index = _values.binarySearch(value) 35 | val insertionIndex = if (index >= 0) index else -(index + 1) 36 | _values.add(insertionIndex, value) 37 | } 38 | 39 | fun remove(key: K): V? { 40 | val existing = delegate.remove(key) 41 | if (existing != null) { 42 | _values.removeAt(_values.binarySearch(existing)) 43 | } 44 | return existing 45 | } 46 | } -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Build Gradle 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 13 | 14 | jobs: 15 | checks: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: actions/setup-java@v3 21 | with: 22 | distribution: 'zulu' 23 | java-version: '22' 24 | 25 | - uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.13' 28 | 29 | - name: Run Checks 30 | uses: gradle/gradle-build-action@v2 31 | with: 32 | arguments: check --continue --stacktrace --scan 33 | 34 | deploy-snapshot: 35 | runs-on: ubuntu-latest 36 | if: github.ref == 'refs/heads/main' 37 | needs: checks 38 | steps: 39 | - uses: actions/checkout@v3 40 | 41 | - uses: actions/setup-java@v3 42 | with: 43 | distribution: 'zulu' 44 | java-version: '22' 45 | 46 | - name: Deploy Snapshot 47 | uses: gradle/gradle-build-action@v2 48 | with: 49 | arguments: publishSnapshot --no-configuration-cache --stacktrace 50 | env: 51 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 52 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/json/IterableSerializer.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal.json 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.builtins.ListSerializer 5 | import kotlinx.serialization.descriptors.SerialDescriptor 6 | import kotlinx.serialization.encoding.Decoder 7 | import kotlinx.serialization.encoding.Encoder 8 | import kotlinx.serialization.encoding.encodeCollection 9 | import kotlinx.serialization.json.JsonEncoder 10 | 11 | internal class IterableSerializer( 12 | private val itemSerializer: KSerializer, 13 | ) : KSerializer> { 14 | private val delegate = ListSerializer(itemSerializer) 15 | override val descriptor: SerialDescriptor get() = delegate.descriptor 16 | 17 | override fun serialize(encoder: Encoder, value: Iterable) { 18 | require(encoder is JsonEncoder) 19 | // The JSON encoder doesn't care about the collection size so we can pass 0 here 20 | encoder.encodeCollection(descriptor, 0) { 21 | value.forEachIndexed { index, item -> 22 | encodeSerializableElement( 23 | descriptor = descriptor, 24 | index = index, 25 | serializer = itemSerializer, 26 | value = item 27 | ) 28 | } 29 | } 30 | } 31 | 32 | override fun deserialize(decoder: Decoder): Iterable = delegate.deserialize(decoder) 33 | } -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/json/LazyString.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal.json 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.builtins.nullable 7 | import kotlinx.serialization.builtins.serializer 8 | import kotlinx.serialization.encoding.Decoder 9 | import kotlinx.serialization.encoding.Encoder 10 | 11 | 12 | @Serializable(with = LazyString.Serializer::class) 13 | @OptIn(ExperimentalSerializationApi::class) 14 | internal class LazyString(private val stringProducer: () -> String?) { 15 | fun produceString(): String? = stringProducer() 16 | 17 | object Serializer : KSerializer { 18 | override val descriptor get() = String.serializer().nullable.descriptor 19 | 20 | override fun deserialize(decoder: Decoder): LazyString { 21 | val string = if (decoder.decodeNotNullMark()) { 22 | decoder.decodeString() 23 | } else { 24 | decoder.decodeNull() 25 | } 26 | return LazyString { string } 27 | } 28 | 29 | override fun serialize(encoder: Encoder, value: LazyString) { 30 | val string = value.produceString() 31 | if (string == null) { 32 | encoder.encodeNull() 33 | } else { 34 | encoder.encodeString(string) 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/MimeTypes.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | @InternalRequestLoggerApi 4 | object MimeTypes { 5 | fun isTextMimeType(mimeType: String?): Boolean { 6 | return when (mimeType?.substringBefore(';')) { 7 | "application/json", 8 | "application/javascript", 9 | "application/html", 10 | "application/xml", 11 | "application/css", 12 | "application/csv", 13 | "multipart/form-data", 14 | -> true 15 | 16 | else -> mimeType?.startsWith("text/") == true || mimeType?.contains("charset=") == true 17 | } 18 | } 19 | 20 | fun getCharset(mimeType: String?): String? { 21 | if (mimeType == null) { 22 | return null 23 | } 24 | return PARAMETER.findAll(mimeType) 25 | .find { it.groups[1]?.value == "charset" } 26 | ?.run { 27 | val token = groups[2]?.value 28 | when { 29 | token == null -> { 30 | // Value is "double-quoted". That's valid and our regex group already strips the quotes. 31 | groups[3]?.value 32 | } 33 | 34 | else -> token.removeSurrounding("'") 35 | } 36 | } 37 | } 38 | 39 | 40 | private const val TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)" 41 | private const val QUOTED = "\"([^\"]*)\"" 42 | private val PARAMETER = Regex(";\\s*(?:$TOKEN=(?:$TOKEN|$QUOTED))?") 43 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/abstract-class.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/doc/docs/logging-requests.md: -------------------------------------------------------------------------------- 1 | # Logging Requests 2 | If you want to log requests, but your HTTP client isn't supported out of the box then you can implement 3 | the logging yourself. 4 | 5 | To log a request, you need to call `Harbringer.record`: 6 | ```kotlin 7 | // Start the request 8 | val pendingRequest = harbringer.record( 9 | request = Harbringer.Request( 10 | method = "POST", 11 | url = "https://example.com", 12 | protocol = "HTTP/1.1", 13 | headers = Harbringer.Headers("Content-Type" to "application/json"), 14 | ) 15 | ) 16 | // Write the request body 17 | pendingRequest.requestBody.buffer().use { it.writeUtf8("""{"example":"request"}""") } 18 | // Write the response body 19 | pendingRequest.responseBody.buffer().use { it.writeUtf8("""{"example":"response"}""") } 20 | // Log the response 21 | pendingRequest.onComplete( 22 | request = Harbringer.Request( 23 | code = 200, 24 | message = "OK", 25 | protocol = "HTTP/1.1", 26 | headers = Harbringer.Headers("Content-Type" to "application/json"), 27 | body = Harbringer.Body( 28 | contentType = "application/json", 29 | byteCount = 22, 30 | ) 31 | ), 32 | // If known, you can pass the timings here too 33 | timings = Harbringer.Timings( 34 | total = 123.milliseconds, 35 | blocked = 10.milliseconds, 36 | dns = 8.milliseconds, 37 | connect = 10.milliseconds, 38 | send = 40.milliseconds, 39 | wait = 4.milliseconds, 40 | receive = 48.milliseconds, 41 | ssl = 3.milliseconds, 42 | ) 43 | ) 44 | // Or log the failure 45 | pendingRequest.onFailure(IOException("Failed to connect")) 46 | ``` -------------------------------------------------------------------------------- /src/doc/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | To use Harbringer, you first need to create a `Harbringer` instance: 3 | ```kotlin 4 | val harbringer = Harbringer( 5 | // Store requests on disk 6 | storage = FileSystemHarbringerStorage(storageDirectory.toPath()), 7 | // Store the last 1000 requests 8 | maxRequests = 1000, 9 | // Store up to 100MB of requests 10 | maxDiskSize = 100 * 1024 * 1024, 11 | // Store up to 2 days worth of logs 12 | maxAge = 2.days, 13 | ) 14 | ``` 15 | 16 | ## Storage 17 | Harbringer ships with two storage implementations; `FileSystemHarbringerStorage` and `InMemoryHarbringerStorage`. 18 | The former stores requests on disk, while the latter stores them in memory. You can also implement your own storage by 19 | implementing the `HarbringerStorage` interface. 20 | 21 | Please bear in mind that the storage must be thread safe so implementing it can be challenging. 22 | 23 | ## Exporting 24 | You can export the requests to a file using the `exportTo` method: 25 | ```kotlin 26 | FileSystem.SYSTEM.sink("/path/to/requests.har".toPath()).use { sink -> 27 | harbringer.exportTo(sink) 28 | } 29 | ``` 30 | 31 | This will write the requests to the file in the HAR format. You can then open the file in a HAR viewer, such as 32 | [Google Chrome's HAR viewer](https://toolbox.googleapps.com/apps/har_analyzer/), or import it into a tool like 33 | [Postman](https://www.postman.com/), [Charles Proxy](https://www.charlesproxy.com) or [Proxyman](https://proxyman.io). 34 | 35 | You can also implement your own exporter by reading the entries from the `Harbringer` instance. 36 | 37 | ## Scrubbing 38 | You often want to ensure that sensitive data is not included in the logs. Harbringer supports scrubbing of sensitive. 39 | For more information on scrubbing, see the [scrubbing guide](scrubbing.md). 40 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/internal/Lists.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | internal fun List.lazyMap(mapper: (T) -> R): List = object : List { 4 | override val size: Int get() = this@lazyMap.size 5 | override fun isEmpty(): Boolean = this@lazyMap.isEmpty() 6 | 7 | override fun contains(element: R): Boolean = throw UnsupportedOperationException("contains() is not supported") 8 | 9 | override fun iterator(): Iterator = listIterator() 10 | 11 | override fun containsAll(elements: Collection): Boolean = 12 | throw UnsupportedOperationException("containsAll() is not supported") 13 | 14 | override fun get(index: Int): R = mapper(this@lazyMap[index]) 15 | 16 | override fun indexOf(element: R): Int { 17 | throw UnsupportedOperationException("indexOf() is not supported") 18 | } 19 | 20 | override fun lastIndexOf(element: R): Int { 21 | throw UnsupportedOperationException("lastIndexOf() is not supported") 22 | } 23 | 24 | override fun listIterator(): ListIterator = listIterator(0) 25 | 26 | override fun listIterator(index: Int): ListIterator = object : ListIterator { 27 | private val delegate = this@lazyMap.listIterator(index) 28 | override fun next(): R = mapper(delegate.next()) 29 | override fun hasNext(): Boolean = delegate.hasNext() 30 | override fun hasPrevious(): Boolean = delegate.hasPrevious() 31 | override fun previous(): R = mapper(delegate.previous()) 32 | override fun nextIndex(): Int = delegate.nextIndex() 33 | override fun previousIndex(): Int = delegate.previousIndex() 34 | } 35 | 36 | override fun subList(fromIndex: Int, toIndex: Int): List = 37 | this@lazyMap.subList(fromIndex, toIndex).lazyMap(mapper) 38 | } -------------------------------------------------------------------------------- /src/doc/docs/index.md: -------------------------------------------------------------------------------- 1 | Harbringer 2 | === 3 | Harbringer tracks and logs networks requests in your application, letting you inspect and export them. 4 | 5 | This is particularly useful for mobile application, where you often want to inspect network requests and responses, 6 | but don't want to set up a proxy to capture the requests. 7 | 8 | This library can help you export the requests or display them in the app itself. 9 | 10 | It can support any HTTP client, but for now it supports: 11 | 12 | - [OkHttp](okhttp.md) 13 | 14 | You can find the project on GitHub in the [ansman/harbringer](https://github.com/ansman/harbringer) repo. 15 | 16 | If you're looking javadoc/dokka you can find it [here]({{ gradle.dokkaLink }}). 17 | 18 | ## Getting started 19 | You can add Harbringer to your project using Gradle: 20 | ```kotlin 21 | dependencies { 22 | implementation("se.ansman.harbringer:harbringer:{{gradle.version}}") 23 | implementation("se.ansman.harbringer:harbringer-okhttp3:{{gradle.version}}") 24 | } 25 | ``` 26 | 27 | See the [installation guide](getting-started.md) for more details. 28 | 29 | ## Example 30 | ```kotlin 31 | val harbringer = Harbringer( 32 | storage = FileSystemHarbringerStorage(storageDirectory.toPath()), 33 | maxRequests = 1000, // 1000 requests 34 | maxDiskSize = 100 * 1024 * 1024, // 100MB 35 | maxAge = 2.days, 36 | ) 37 | 38 | outputFile.sink().use { sink -> 39 | harbringer.exportTo(sink) 40 | } 41 | ``` 42 | 43 | For more details on how to use Harbringer, see the [usage guide](usage.md). 44 | 45 | ## Scrubbing 46 | You often want to ensure that sensitive data is not included in the logs. Harbringer supports scrubbing of sensitive 47 | data from requests and responses. You can pass a `Scrubber` to the `Harbringer` constructor. 48 | 49 | See the [scrubbing guide](scrubbing.md) for more details. -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/RealRequestScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import se.ansman.harbringer.Harbringer 4 | import se.ansman.harbringer.internal.Url 5 | 6 | internal class RealRequestScrubber( 7 | private val scrubUrl: (String) -> String?, 8 | private val scrubQueryParameter: (name: String, value: String?) -> Pair?, 9 | private val scrubHeader: (Harbringer.Header) -> Harbringer.Header?, 10 | private val scrubBodyParameter: (Harbringer.Request.Body.Param) -> Harbringer.Request.Body.Param?, 11 | ) : RequestScrubber { 12 | 13 | override fun scrub(request: Harbringer.Request): Harbringer.Request? { 14 | var url = scrubUrl(request.url) 15 | ?: return null 16 | 17 | if (scrubQueryParameter != defaultScrubQueryParameter) { 18 | url = scrubQueryParameters(Url(url)) 19 | } 20 | return request.copy( 21 | url = url, 22 | headers = if (scrubHeader == defaultScrubHeader) { 23 | request.headers 24 | } else { 25 | request.headers.values 26 | .mapNotNull(scrubHeader) 27 | .let(Harbringer::Headers) 28 | }, 29 | body = request.body?.run { 30 | if (scrubBodyParameter == defaultBodyParameterScrubber) { 31 | this 32 | } else { 33 | copy(params = params.mapNotNull(scrubBodyParameter)) 34 | } 35 | } 36 | ) 37 | } 38 | 39 | private fun scrubQueryParameters(url: Url): String = 40 | url.replaceQueryParameters( 41 | url.queryParameters 42 | .mapNotNull { scrubQueryParameter(it.first, it.second) } 43 | .asIterable()) 44 | .toString() 45 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/annotation.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/abstract-class-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/assets/annotation-kotlin.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/styles/font-jb-sans-auto.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | /* Light weight */ 6 | @font-face { 7 | font-family: 'JetBrains Sans'; 8 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Light.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Light.woff') format('woff'); 9 | font-weight: 300; 10 | font-style: normal; 11 | font-display: swap; 12 | } 13 | /* Regular weight */ 14 | @font-face { 15 | font-family: 'JetBrains Sans'; 16 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Regular.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-Regular.woff') format('woff'); 17 | font-weight: 400; 18 | font-style: normal; 19 | font-display: swap; 20 | } 21 | /* SemiBold weight */ 22 | @font-face { 23 | font-family: 'JetBrains Sans'; 24 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-SemiBold.woff2') format('woff2'), url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans-SemiBold.woff') format('woff'); 25 | font-weight: 600; 26 | font-style: normal; 27 | font-display: swap; 28 | } 29 | 30 | @supports (font-variation-settings: normal) { 31 | @font-face { 32 | font-family: 'JetBrains Sans'; 33 | src: url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff2') format('woff2 supports variations'), 34 | url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff2') format('woff2-variations'), 35 | url('https://resources.jetbrains.com/storage/jetbrains-sans/JetBrainsSans.woff') format('woff-variations'); 36 | font-weight: 100 900; 37 | font-style: normal; 38 | font-display: swap; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /harbringer/src/jvmMain/kotlin/se/ansman/harbringer/internal/Url.jvm.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.internal 2 | 3 | import java.net.URI 4 | import java.net.URLDecoder 5 | import java.net.URLEncoder 6 | 7 | internal actual class Url(private val uri: URI) { 8 | actual val queryParameters: Sequence> = uri.query 9 | ?.splitToSequence('&') 10 | ?.map { param -> 11 | val sep = param.indexOf('=') 12 | val name: String 13 | val value: String? 14 | if (sep < 0) { 15 | name = param 16 | value = null 17 | } else { 18 | name = param.substring(0, sep) 19 | value = param.substring(sep + 1, param.length) 20 | } 21 | URLDecoder.decode(name, Charsets.UTF_8) to value?.let { URLDecoder.decode(it, Charsets.UTF_8) } 22 | } 23 | ?: emptySequence() 24 | 25 | actual constructor(url: String) : this(URI(url)) 26 | 27 | actual fun replaceQueryParameters(parameters: Iterable>): Url = 28 | Url( 29 | URI( 30 | uri.scheme, 31 | uri.userInfo, 32 | uri.host, 33 | uri.port, 34 | uri.path, 35 | parameters.joinToString("&") { (name, value) -> 36 | if (value == null) { 37 | URLEncoder.encode(name, Charsets.UTF_8) 38 | } else { 39 | "${URLEncoder.encode(name, Charsets.UTF_8)}=${URLEncoder.encode(value, Charsets.UTF_8)}" 40 | } 41 | }, 42 | uri.fragment 43 | ) 44 | ) 45 | 46 | override fun toString(): String = uri.toString() 47 | override fun hashCode(): Int = uri.hashCode() 48 | override fun equals(other: Any?): Boolean = other === this || other is Url && uri == other.uri 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harbringer 2 | Harbringer is an HTTP request logger. It can store your network requests and let you inspect or export them. 3 | 4 | It supports OkHttp out of the box, but you can use it with any HTTP client. 5 | 6 | To read more, please refer to the [documentation](https://harbringer.ansman.se/). 7 | 8 | For the changelog, see the [releases page](https://github.com/ansman/harbringer/releases). 9 | 10 | ## Basic usage 11 | ```kotlin 12 | val harbringer = Harbringer( 13 | storage = FileSystemHarbringerStorage(storageDirectory.toPath()), 14 | maxRequests = 1000, // 1000 requests 15 | maxDiskSize = 100 * 1024 * 1024, // 100MB 16 | maxAge = 2.days, 17 | ) 18 | 19 | val okHttpClient = OkHttpClient.Builder() 20 | .addHarbringer(harbringer) 21 | .build() 22 | 23 | outputFile.sink().use { sink -> 24 | harbringer.exportTo(sink) 25 | } 26 | ``` 27 | 28 | For the full documentation see https://harbringer.ansman.se/ 29 | 30 | Setup 31 | --- 32 | For detailed instructions, see the [getting-started](https://harbringer.ansman.se/latest/getting-started/) page. 33 | ```kotlin 34 | dependencies { 35 | implementation("se.ansman.harbringer:harbringer:0.1.0") 36 | // If using okhttp 37 | implementation("se.ansman.harbringer:harbringer-okhttp3:0.1.0") 38 | } 39 | ``` 40 | 41 | ## License 42 | 43 | This project is licensed under the Apache-2.0 license. See [LICENSE](LICENSE) for the full license. 44 | 45 | ``` 46 | Copyright 2025 Nicklas Ansman 47 | 48 | Licensed under the Apache License, Version 2.0 (the "License"); 49 | you may not use this file except in compliance with the License. 50 | You may obtain a copy of the License at 51 | 52 | http://www.apache.org/licenses/LICENSE-2.0 53 | 54 | Unless required by applicable law or agreed to in writing, software 55 | distributed under the License is distributed on an "AS IS" BASIS, 56 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 57 | See the License for the specific language governing permissions and 58 | limitations under the License. 59 | ``` -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/scripts/clipboard.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | window.addEventListener('load', () => { 6 | document.querySelectorAll('span.copy-icon').forEach(element => { 7 | element.addEventListener('click', (el) => copyElementsContentToClipboard(element)); 8 | }) 9 | 10 | document.querySelectorAll('span.anchor-icon').forEach(element => { 11 | element.addEventListener('click', (el) => { 12 | if(element.hasAttribute('pointing-to')){ 13 | const location = hrefWithoutCurrentlyUsedAnchor() + '#' + element.getAttribute('pointing-to') 14 | copyTextToClipboard(element, location) 15 | } 16 | }); 17 | }) 18 | }) 19 | 20 | const copyElementsContentToClipboard = (element) => { 21 | const selection = window.getSelection(); 22 | const range = document.createRange(); 23 | range.selectNodeContents(element.parentNode.parentNode); 24 | selection.removeAllRanges(); 25 | selection.addRange(range); 26 | 27 | copyAndShowPopup(element, () => selection.removeAllRanges()) 28 | } 29 | 30 | const copyTextToClipboard = (element, text) => { 31 | var textarea = document.createElement("textarea"); 32 | textarea.textContent = text; 33 | textarea.style.position = "fixed"; 34 | document.body.appendChild(textarea); 35 | textarea.select(); 36 | 37 | copyAndShowPopup(element, () => document.body.removeChild(textarea)) 38 | } 39 | 40 | const copyAndShowPopup = (element, after) => { 41 | try { 42 | document.execCommand('copy'); 43 | element.nextElementSibling.classList.add('active-popup'); 44 | setTimeout(() => { 45 | element.nextElementSibling.classList.remove('active-popup'); 46 | }, 1200); 47 | } catch (e) { 48 | console.error('Failed to write to clipboard:', e) 49 | } 50 | finally { 51 | if(after) after() 52 | } 53 | } 54 | 55 | const hrefWithoutCurrentlyUsedAnchor = () => window.location.href.split('#')[0] 56 | 57 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.2.21" 3 | okhttp3 = "5.3.1" 4 | okio = "3.16.3" 5 | java = "22" 6 | junit = "5.13.4" 7 | kotlinx-serialization = "1.9.0" 8 | assertk = "0.28.1" 9 | gradleMavenPublish = "0.35.0" 10 | dokka = "2.1.0" 11 | 12 | [plugins] 13 | kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 14 | binaryCompatibilityValidator = "org.jetbrains.kotlinx.binary-compatibility-validator:0.18.1" 15 | mkdocs = "ru.vyarus.mkdocs:4.0.1" 16 | 17 | [libraries] 18 | junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" } 19 | junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api" } 20 | junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } 21 | junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } 22 | 23 | kotlinGradlePlugin = { module = "org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin", version.ref = "kotlin" } 24 | gradleMavenPublish = {module = "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin", version.ref = "gradleMavenPublish" } 25 | 26 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization"} 27 | kotlinx-serialization-json-okio = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-okio", version.ref = "kotlinx-serialization"} 28 | 29 | okhttp3-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp3" } 30 | okhttp3 = { module = "com.squareup.okhttp3:okhttp" } 31 | okhttp3-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver" } 32 | 33 | okio = { module = "com.squareup.okio:okio", version.ref = "okio" } 34 | okio-fakeFileSystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } 35 | 36 | assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } 37 | 38 | dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 39 | dokka-versioningPlugin = { module = "org.jetbrains.dokka:versioning-plugin", version.ref = "dokka" } 40 | dokka-allModulesPagePlugin = { module = "org.jetbrains.dokka:all-modules-page-plugin", version.ref = "dokka" } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/dokka-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleName": "harbringer", 3 | "moduleVersion": "0.1.0", 4 | "outputDir": "/Users/nicklas/Repositories/personal/harbringer/build/dokka/html", 5 | "cacheRoot": null, 6 | "offlineMode": false, 7 | "sourceSets": [ 8 | ], 9 | "pluginsClasspath": [ 10 | "/Users/nicklas/.gradle/caches/modules-2/files-2.1/org.jetbrains.dokka/templating-plugin/2.0.0/2c817817579cd8c92ded6f33fd9e6b746ecf01c7/templating-plugin-2.0.0.jar", 11 | "/Users/nicklas/.gradle/caches/modules-2/files-2.1/org.jetbrains.dokka/dokka-base/2.0.0/dcce26b10e9af3b3f2eba0403d6a9561e6da9098/dokka-base-2.0.0.jar", 12 | "/Users/nicklas/.gradle/caches/modules-2/files-2.1/org.jetbrains.dokka/all-modules-page-plugin/2.0.0/f00f3af81550dbbb769c159c70ab4cadab8fb2c/all-modules-page-plugin-2.0.0.jar", 13 | "/Users/nicklas/.gradle/caches/modules-2/files-2.1/org.jetbrains.dokka/versioning-plugin/2.0.0/7f5bb6094406f4cb83693b9511e05e6c2457ed58/versioning-plugin-2.0.0.jar" 14 | ], 15 | "pluginsConfiguration": [ 16 | { 17 | "fqPluginName": "org.jetbrains.dokka.base.DokkaBase", 18 | "serializationFormat": "JSON", 19 | "values": "{\"customAssets\":[],\"customStyleSheets\":[],\"separateInheritedMembers\":false,\"mergeImplicitExpectActualDeclarations\":false}" 20 | }, 21 | { 22 | "fqPluginName": "org.jetbrains.dokka.versioning.VersioningPlugin", 23 | "serializationFormat": "JSON", 24 | "values": "{\"version\":\"0.1.0\",\"olderVersionsDir\":\"/Users/nicklas/Repositories/personal/harbringer/src/doc/dokka\",\"olderVersions\":[],\"renderVersionsNavigationOnAllPages\":true}" 25 | } 26 | ], 27 | "modules": [ 28 | { 29 | "name": "harbringer", 30 | "relativePathToOutputDirectory": "harbringer", 31 | "includes": [ 32 | ], 33 | "sourceOutputDirectory": "/Users/nicklas/Repositories/personal/harbringer/harbringer/build/dokka-module/html/module" 34 | }, 35 | { 36 | "name": "okhttp3", 37 | "relativePathToOutputDirectory": "harbringer/okhttp3", 38 | "includes": [ 39 | ], 40 | "sourceOutputDirectory": "/Users/nicklas/Repositories/personal/harbringer/harbringer/okhttp3/build/dokka-module/html/module" 41 | } 42 | ], 43 | "failOnWarning": false, 44 | "delayTemplateSubstitution": false, 45 | "suppressObviousFunctions": true, 46 | "includes": [ 47 | ], 48 | "suppressInheritedMembers": false, 49 | "finalizeCoroutines": false 50 | } -------------------------------------------------------------------------------- /harbringer/src/commonTest/kotlin/se/ansman/harbringer/DiscardBodyScrubberTest.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isSameInstanceAs 6 | import assertk.assertions.isZero 7 | import assertk.assertions.prop 8 | import okio.Buffer 9 | import okio.Sink 10 | import okio.Timeout 11 | import se.ansman.harbringer.scrubber.Scrubber 12 | import kotlin.test.Test 13 | import kotlin.test.fail 14 | 15 | class DiscardBodyScrubberTest { 16 | private val request = Harbringer.Request( 17 | method = "GET", 18 | url = "https://example.com", 19 | headers = Harbringer.Headers(), 20 | protocol = "HTTP/1.1", 21 | ) 22 | private val scrubber = Scrubber.discardBody() 23 | private val sink = TestSink() 24 | 25 | @Test 26 | fun `discardBody discards the body`() { 27 | val input = scrubber.scrub(request, sink) 28 | input.write(Buffer().apply { writeUtf8("Hello") }) 29 | assertThat(sink).prop(TestSink::closeCount).isZero() 30 | assertThat(sink).prop(TestSink::flushCount).isZero() 31 | } 32 | 33 | @Test 34 | fun `flush flushes the delegate`() { 35 | val input = scrubber.scrub(request, sink) 36 | input.flush() 37 | assertThat(sink).prop(TestSink::closeCount).isZero() 38 | assertThat(sink).prop(TestSink::flushCount).isEqualTo(1) 39 | } 40 | 41 | @Test 42 | fun `close closes the delegate`() { 43 | val input = scrubber.scrub(request, sink) 44 | input.close() 45 | assertThat(sink).prop(TestSink::closeCount).isEqualTo(1) 46 | assertThat(sink).prop(TestSink::flushCount).isZero() 47 | } 48 | 49 | @Test 50 | fun `the delegates timeout is returned`() { 51 | val input = scrubber.scrub(request, sink) 52 | assertThat(input).prop(Sink::timeout).isSameInstanceAs(sink.timeout()) 53 | } 54 | 55 | private fun Sink.write(buffer: Buffer) { 56 | write(buffer, buffer.size) 57 | assertThat(buffer.size).isZero() 58 | } 59 | 60 | private class TestSink : Sink { 61 | val timeout = Timeout() 62 | var flushCount = 0 63 | var closeCount = 0 64 | override fun write(source: Buffer, byteCount: Long) { 65 | fail("Should not be called") 66 | } 67 | 68 | override fun flush() { 69 | ++flushCount 70 | } 71 | 72 | override fun timeout(): Timeout = timeout 73 | 74 | override fun close() { 75 | ++closeCount 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/scripts/symbol-parameters-wrapper_deferred.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | // helps with some corner cases where starts working already, 6 | // but the signature is not yet long enough to be wrapped 7 | (function() { 8 | const leftPaddingPx = 60; 9 | 10 | function createNbspIndent() { 11 | let indent = document.createElement("span"); 12 | indent.append(document.createTextNode("\u00A0\u00A0\u00A0\u00A0")); 13 | indent.classList.add("nbsp-indent"); 14 | return indent; 15 | } 16 | 17 | function wrapSymbolParameters(entry) { 18 | const symbol = entry.target; 19 | const symbolBlockWidth = entry.borderBoxSize && entry.borderBoxSize[0] && entry.borderBoxSize[0].inlineSize; 20 | 21 | // Even though the script is marked as `defer` and we wait for `DOMContentLoaded` event, 22 | // or if this block is a part of hidden tab, it can happen that `symbolBlockWidth` is 0, 23 | // indicating that something hasn't been loaded. 24 | // In this case, observer will be triggered onсe again when it will be ready. 25 | if (symbolBlockWidth > 0) { 26 | const node = symbol.querySelector(".parameters"); 27 | 28 | if (node) { 29 | // if window resize happened and observer was triggered, reset previously wrapped 30 | // parameters as they might not need wrapping anymore, and check again 31 | node.classList.remove("wrapped"); 32 | node.querySelectorAll(".parameter .nbsp-indent") 33 | .forEach(indent => indent.remove()); 34 | 35 | const innerTextWidth = Array.from(symbol.children) 36 | .filter(it => !it.classList.contains("block")) // blocks are usually on their own (like annotations), so ignore it 37 | .map(it => it.getBoundingClientRect().width) 38 | .reduce((a, b) => a + b, 0); 39 | 40 | // if signature text takes up more than a single line, wrap params for readability 41 | if (innerTextWidth > (symbolBlockWidth - leftPaddingPx)) { 42 | node.classList.add("wrapped"); 43 | node.querySelectorAll(".parameter").forEach(param => { 44 | // has to be a physical indent so that it can be copied. styles like 45 | // paddings and `::before { content: " " }` do not work for that 46 | param.prepend(createNbspIndent()); 47 | }); 48 | } 49 | } 50 | } 51 | } 52 | 53 | const symbolsObserver = new ResizeObserver(entries => entries.forEach(wrapSymbolParameters)); 54 | 55 | function initHandlers() { 56 | document.querySelectorAll("div.symbol").forEach(symbol => symbolsObserver.observe(symbol)); 57 | } 58 | 59 | if (document.readyState === 'loading') window.addEventListener('DOMContentLoaded', initHandlers); 60 | else initHandlers(); 61 | 62 | // ToDo: Add `unobserve` if dokka will be SPA-like: 63 | // https://github.com/w3c/csswg-drafts/issues/5155 64 | })(); 65 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/scrubber/JsonScrubber.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.json.* 5 | import kotlinx.serialization.json.okio.decodeFromBufferedSource 6 | import kotlinx.serialization.json.okio.encodeToBufferedSink 7 | import okio.Buffer 8 | import okio.Sink 9 | import okio.Timeout 10 | import se.ansman.harbringer.Harbringer 11 | 12 | @ExperimentalSerializationApi 13 | internal class JsonScrubber( 14 | private val json: Json, 15 | private val replace: (path: String, element: JsonElement) -> JsonElement?, 16 | private val onlyIf: (Harbringer.Request) -> Boolean, 17 | ) : BodyScrubber { 18 | override fun scrub(request: Harbringer.Request, sink: Sink): Sink = 19 | if (onlyIf(request)) { 20 | object : Sink { 21 | private var isClosed = false 22 | private val buffer = Buffer() 23 | 24 | override fun write(source: Buffer, byteCount: Long) { 25 | buffer.write(source, byteCount) 26 | } 27 | 28 | override fun flush() {} 29 | 30 | override fun timeout(): Timeout = Timeout.Companion.NONE 31 | 32 | override fun close() { 33 | if (isClosed) { 34 | return 35 | } 36 | isClosed = true 37 | val status = ReplacementStatus() 38 | val element = json.decodeFromBufferedSource(JsonElement.serializer(), buffer.peek()).scrub(status) 39 | if (status.wasReplaced) { 40 | buffer.clear() 41 | if (element == null) { 42 | buffer.writeUtf8("null") 43 | } else { 44 | json.encodeToBufferedSink(JsonElement.serializer(), element, buffer) 45 | } 46 | } 47 | buffer.readAll(sink) 48 | } 49 | } 50 | } else { 51 | sink 52 | } 53 | 54 | private fun JsonElement.scrub( 55 | status: ReplacementStatus, 56 | path: String = "$" 57 | ): JsonElement? { 58 | val replacement = replace(path, this) 59 | if (replacement != this) { 60 | status.wasReplaced = true 61 | return replacement 62 | } 63 | return when (this) { 64 | is JsonArray -> { 65 | val next = "$path[]" 66 | JsonArray(mapNotNull { it.scrub(status, next) }) 67 | } 68 | 69 | is JsonObject -> { 70 | JsonObject(mapNotNull { (key, value) -> 71 | val scrubbed = value.scrub(status, "$path.$key") 72 | if (scrubbed == null) { 73 | null 74 | } else { 75 | key to scrubbed 76 | } 77 | }.toMap()) 78 | } 79 | 80 | is JsonPrimitive, 81 | JsonNull -> this 82 | } 83 | } 84 | 85 | private class ReplacementStatus(var wasReplaced: Boolean = false) 86 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /harbringer/src/commonTest/kotlin/se/ansman/harbringer/ReplaceBodyScrubberTest.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isSameInstanceAs 6 | import assertk.assertions.isZero 7 | import assertk.assertions.prop 8 | import okio.Buffer 9 | import okio.Sink 10 | import okio.Timeout 11 | import se.ansman.harbringer.scrubber.Scrubber 12 | import kotlin.test.Test 13 | 14 | class ReplaceBodyScrubberTest { 15 | private val request = Harbringer.Request( 16 | method = "GET", 17 | url = "https://example.com", 18 | headers = Harbringer.Headers(), 19 | protocol = "HTTP/1.1", 20 | ) 21 | private val scrubber = Scrubber.replaceBody("******") 22 | private val sink = TestSink() 23 | 24 | @Test 25 | fun `writing is discarded`() { 26 | val input = scrubber.scrub(request, sink) 27 | input.write("Secret") 28 | assertThat(sink).prop(TestSink::buffer).prop(Buffer::size).isZero() 29 | assertThat(sink).prop(TestSink::closeCount).isZero() 30 | assertThat(sink).prop(TestSink::flushCount).isZero() 31 | } 32 | 33 | @Test 34 | fun `flush flushes the delegate`() { 35 | val input = scrubber.scrub(request, sink) 36 | input.flush() 37 | assertThat(sink).prop(TestSink::buffer).prop(Buffer::size).isZero() 38 | assertThat(sink).prop(TestSink::closeCount).isZero() 39 | assertThat(sink).prop(TestSink::flushCount).isEqualTo(1) 40 | } 41 | 42 | @Test 43 | fun `close writes the data and closes the delegate`() { 44 | val input = scrubber.scrub(request, sink) 45 | input.close() 46 | assertThat(sink.buffer.readUtf8()).isEqualTo("******") 47 | assertThat(sink).prop(TestSink::closeCount).isEqualTo(1) 48 | assertThat(sink).prop(TestSink::flushCount).isZero() 49 | } 50 | 51 | @Test 52 | fun `close twice only writes the data once`() { 53 | val input = scrubber.scrub(request, sink) 54 | input.close() 55 | input.close() 56 | assertThat(sink.buffer.readUtf8()).isEqualTo("******") 57 | assertThat(sink).prop(TestSink::closeCount).isEqualTo(2) 58 | assertThat(sink).prop(TestSink::flushCount).isZero() 59 | } 60 | 61 | @Test 62 | fun `the delegates timeout is returned`() { 63 | val input = scrubber.scrub(request, sink) 64 | assertThat(input).prop(Sink::timeout).isSameInstanceAs(sink.timeout()) 65 | } 66 | 67 | private fun Sink.write(string: String) { 68 | write(Buffer().writeUtf8(string)) 69 | } 70 | 71 | private fun Sink.write(buffer: Buffer) { 72 | write(buffer, buffer.size) 73 | assertThat(buffer.size).isZero() 74 | } 75 | 76 | private class TestSink : Sink { 77 | val timeout = Timeout() 78 | var flushCount = 0 79 | var closeCount = 0 80 | val buffer = Buffer() 81 | 82 | override fun write(source: Buffer, byteCount: Long) { 83 | source.copyTo(buffer, 0, byteCount) 84 | } 85 | 86 | override fun flush() { 87 | ++flushCount 88 | } 89 | 90 | override fun timeout(): Timeout = timeout 91 | 92 | override fun close() { 93 | ++closeCount 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/doc/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Harbringer 2 | 3 | site_description: HTTP Request logger and exporter 4 | site_author: Nicklas Ansman 5 | site_url: https://github.com/ansman/harbringer 6 | 7 | repo_name: harbringer 8 | repo_url: https://github.com/ansman/harbringer 9 | edit_uri: edit/main/src/doc/docs/ 10 | 11 | copyright: 'Copyright © 2025 Nicklas Ansman' 12 | 13 | plugins: 14 | - markdownextradata 15 | - search: 16 | separator: '[\s\u200b\-_,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 17 | - minify: 18 | minify_html: true 19 | 20 | theme: 21 | name: 'material' 22 | palette: 23 | - media: "(prefers-color-scheme: light)" 24 | scheme: default 25 | primary: indigo 26 | accent: indigo 27 | toggle: 28 | icon: material/toggle-switch-off-outline 29 | name: Switch to dark mode 30 | - media: "(prefers-color-scheme: dark)" 31 | scheme: slate 32 | primary: black 33 | accent: indigo 34 | toggle: 35 | icon: material/toggle-switch 36 | name: Switch to light mode 37 | social: 38 | - type: 'github' 39 | link: 'https://github.com/ansman/harbringer' 40 | features: 41 | - navigation.tracking 42 | - navigation.top 43 | - navigation.instant 44 | - navigation.instant.prefetch 45 | - navigation.expand 46 | - toc.follow 47 | - content.action.edit 48 | - content.action.view 49 | - content.code.annotate 50 | - content.code.copy 51 | extra_css: 52 | - stylesheets/extra.css 53 | 54 | extra: 55 | version: 56 | provider: mike 57 | 58 | analytics: 59 | provider: google 60 | property: G-GRS0F8KL20 61 | 62 | markdown_extensions: 63 | - abbr 64 | - admonition 65 | - attr_list 66 | - def_list 67 | - footnotes 68 | - md_in_html 69 | - toc: 70 | permalink: true 71 | - pymdownx.arithmatex: 72 | generic: true 73 | - pymdownx.betterem: 74 | smart_enable: all 75 | - pymdownx.caret 76 | - pymdownx.details 77 | - pymdownx.highlight: 78 | anchor_linenums: true 79 | line_spans: __span 80 | pygments_lang_class: true 81 | - pymdownx.inlinehilite 82 | - pymdownx.keys 83 | - pymdownx.magiclink: 84 | normalize_issue_symbols: true 85 | repo_url_shorthand: true 86 | user: squidfunk 87 | repo: mkdocs-material 88 | - pymdownx.mark 89 | - pymdownx.smartsymbols 90 | - pymdownx.snippets: 91 | auto_append: 92 | - includes/mkdocs.md 93 | - pymdownx.superfences: 94 | custom_fences: 95 | - name: mermaid 96 | class: mermaid 97 | format: !!python/name:pymdownx.superfences.fence_code_format 98 | - pymdownx.tabbed: 99 | alternate_style: true 100 | combine_header_slug: true 101 | slugify: !!python/object/apply:pymdownx.slugs.slugify 102 | kwds: 103 | case: lower 104 | - pymdownx.tasklist: 105 | custom_checkbox: true 106 | - pymdownx.tilde 107 | 108 | nav: 109 | - Home: index.md 110 | - Getting Started: getting-started.md 111 | - Usage: usage.md 112 | - Scrubbing: scrubbing.md 113 | - OkHttp: okhttp.md 114 | - Logging Requests: logging-requests.md 115 | - API Docs: https://harbringer.ansman.se/dokka/ 116 | - About: 117 | - Release Notes: https://github.com/ansman/harbringer/releases 118 | - License: about/license.md 119 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/scripts/navigation-loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | navigationPageText = fetch(pathToRoot + "navigation.html").then(response => response.text()) 6 | 7 | displayNavigationFromPage = () => { 8 | navigationPageText.then(data => { 9 | document.getElementById("sideMenu").innerHTML = data; 10 | }).then(() => { 11 | document.querySelectorAll(".toc--row > a").forEach(link => { 12 | link.setAttribute("href", pathToRoot + link.getAttribute("href")); 13 | }) 14 | }).then(() => { 15 | document.querySelectorAll(".toc--part").forEach(nav => { 16 | if (!nav.classList.contains("toc--part_hidden")) 17 | nav.classList.add("toc--part_hidden") 18 | }) 19 | }).then(() => { 20 | revealNavigationForCurrentPage() 21 | }).then(() => { 22 | scrollNavigationToSelectedElement() 23 | }) 24 | document.querySelectorAll('.footer a[href^="#"]').forEach(anchor => { 25 | anchor.addEventListener('click', function (e) { 26 | e.preventDefault(); 27 | document.querySelector(this.getAttribute('href')).scrollIntoView({ 28 | behavior: 'smooth' 29 | }); 30 | }); 31 | }); 32 | } 33 | 34 | revealNavigationForCurrentPage = () => { 35 | let pageId = document.getElementById("content").attributes["pageIds"].value.toString(); 36 | let parts = document.querySelectorAll(".toc--part"); 37 | let found = 0; 38 | do { 39 | parts.forEach(part => { 40 | if (part.attributes['pageId'].value.indexOf(pageId) !== -1 && found === 0) { 41 | found = 1; 42 | if (part.classList.contains("toc--part_hidden")) { 43 | part.classList.remove("toc--part_hidden"); 44 | part.setAttribute('data-active', ""); 45 | } 46 | revealParents(part) 47 | } 48 | }); 49 | pageId = pageId.substring(0, pageId.lastIndexOf("/")) 50 | } while (pageId.indexOf("/") !== -1 && found === 0) 51 | }; 52 | revealParents = (part) => { 53 | if (part.classList.contains("toc--part")) { 54 | if (part.classList.contains("toc--part_hidden")) 55 | part.classList.remove("toc--part_hidden"); 56 | revealParents(part.parentNode) 57 | } 58 | }; 59 | 60 | scrollNavigationToSelectedElement = () => { 61 | let selectedElement = document.querySelector('div.toc--part[data-active]') 62 | if (selectedElement == null) { // nothing selected, probably just the main page opened 63 | return 64 | } 65 | 66 | let hasIcon = selectedElement.querySelectorAll(":scope > div.toc--row span.toc--icon").length > 0 67 | 68 | // for an instance enums also have children and are expandable but are not package/module elements 69 | let isPackageElement = selectedElement.children.length > 1 && !hasIcon 70 | if (isPackageElement) { 71 | // if a package is selected or linked, it makes sense to align it to top 72 | // so that you can see all the members it contains 73 | selectedElement.scrollIntoView(true) 74 | } else { 75 | // if a member within a package is linked, it makes sense to center it since it, 76 | // this should make it easier to look at surrounding members 77 | selectedElement.scrollIntoView({ 78 | behavior: 'auto', 79 | block: 'center', 80 | inline: 'center' 81 | }) 82 | } 83 | } 84 | 85 | /* 86 | This is a work-around for safari being IE of our times. 87 | It doesn't fire a DOMContentLoaded, presumably because eventListener is added after it wants to do it 88 | */ 89 | if (document.readyState === 'loading') { 90 | window.addEventListener('DOMContentLoaded', () => { 91 | displayNavigationFromPage() 92 | }) 93 | } else { 94 | displayNavigationFromPage() 95 | } 96 | -------------------------------------------------------------------------------- /harbringer/src/commonTest/kotlin/se/ansman/harbringer/storage/SortedMapTest.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.storage 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.* 5 | import se.ansman.harbringer.internal.SortedMap 6 | import kotlin.test.Test 7 | 8 | class SortedMapTest { 9 | private val map = SortedMap() 10 | 11 | @Test 12 | fun `size returns the number of entries`() { 13 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 14 | map["one"] = 1 15 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(1) 16 | map["two"] = 2 17 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(2) 18 | map["three"] = 3 19 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(3) 20 | map.remove("one") 21 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(2) 22 | map.remove("two") 23 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(1) 24 | map.remove("three") 25 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 26 | } 27 | 28 | @Test 29 | fun `clearing removes all entries`() { 30 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 31 | 32 | map.clear() 33 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 34 | 35 | map["one"] = 1 36 | map["two"] = 2 37 | map["three"] = 3 38 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(3) 39 | 40 | map.clear() 41 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 42 | } 43 | 44 | @Test 45 | fun `adding an entry lets you read the entry`() { 46 | assertThat(map["one"]).isNull() 47 | 48 | map["one"] = 1 49 | assertThat(map["one"]).isEqualTo(1) 50 | 51 | map["one"] = 2 52 | assertThat(map["one"]).isEqualTo(2) 53 | 54 | map["two"] = 3 55 | assertThat(map["two"]).isEqualTo(3) 56 | } 57 | 58 | @Test 59 | fun `removing returns the removed entry or null`() { 60 | assertThat(map.remove("one")).isNull() 61 | 62 | map["one"] = 1 63 | assertThat(map.remove("one")).isEqualTo(1) 64 | } 65 | 66 | @Test 67 | fun `the values are ordered`() { 68 | assertThat(map).prop(SortedMap<*, *>::values).isEmpty() 69 | 70 | map["two"] = 2 71 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(2) 72 | 73 | map["three"] = 3 74 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(2, 3) 75 | 76 | map["one"] = 1 77 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(1, 2, 3) 78 | 79 | map["four"] = 4 80 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(1, 2, 3, 4) 81 | 82 | map["one"] = 5 83 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(2, 3, 4, 5) 84 | 85 | map["five"] = -1 86 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(-1, 2, 3, 4, 5) 87 | } 88 | 89 | @Test 90 | fun `duplicate values are supported`() { 91 | assertThat(map).prop(SortedMap<*, *>::keys).isEmpty() 92 | assertThat(map).prop(SortedMap<*, *>::values).isEmpty() 93 | assertThat(map).prop(SortedMap<*, *>::entries).isEmpty() 94 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 95 | 96 | map["one"] = 1 97 | map["two"] = 1 98 | map["three"] = 1 99 | assertThat(map).prop(SortedMap<*, *>::keys).containsExactlyInAnyOrder("one", "two", "three") 100 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(1, 1, 1) 101 | assertThat(map).prop(SortedMap<*, *>::entries).isEqualTo(mapOf("one" to 1, "two" to 1, "three" to 1).entries) 102 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(3) 103 | 104 | map.remove("two") 105 | assertThat(map).prop(SortedMap<*, *>::keys).containsExactlyInAnyOrder("one", "three") 106 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(1, 1) 107 | assertThat(map).prop(SortedMap<*, *>::entries).isEqualTo(mapOf("one" to 1, "three" to 1).entries) 108 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(2) 109 | 110 | map.remove("one") 111 | assertThat(map).prop(SortedMap<*, *>::keys).containsExactlyInAnyOrder("three") 112 | assertThat(map).prop(SortedMap<*, *>::values).containsExactly(1) 113 | assertThat(map).prop(SortedMap<*, *>::entries).isEqualTo(mapOf("three" to 1).entries) 114 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(1) 115 | 116 | map.remove("three") 117 | assertThat(map).prop(SortedMap<*, *>::keys).isEmpty() 118 | assertThat(map).prop(SortedMap<*, *>::values).isEmpty() 119 | assertThat(map).prop(SortedMap<*, *>::entries).isEmpty() 120 | assertThat(map).prop(SortedMap<*, *>::size).isEqualTo(0) 121 | } 122 | } -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/storage/HarbringerStorage.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.storage 2 | 3 | import okio.Sink 4 | import okio.Source 5 | import se.ansman.harbringer.Harbringer 6 | 7 | /** 8 | * A storage for storing request and response entries. 9 | * 10 | * This class must be thread safe and will be accessed from multiple threads. 11 | * 12 | * Please note that *all* functions and properties can perform I/O so access should not be done on a UI thread. 13 | * 14 | * @see FileSystemHarbringerStorage 15 | * @see InMemoryHarbringerStorage 16 | */ 17 | interface HarbringerStorage { 18 | /** 19 | * The number of bytes stored in this storage. This can return -1 if this storage does not track the disk usage. 20 | * This value can be an approximation. 21 | * 22 | * Calling this property should be fast and cached as it will be called frequently. 23 | * 24 | * Pending entries should not be included in this value. 25 | */ 26 | val bytesStored: Long 27 | 28 | /** 29 | * The number of entries stored in this storage. 30 | * 31 | * This does not include pending entries. 32 | * 33 | * Calling this property should be fast and cached as it will be called frequently. 34 | */ 35 | val entriesStored: Int 36 | 37 | /** Returns a set of all the IDs of the entries in the storage. */ 38 | fun getIds(): Set 39 | 40 | /** Returns the metadata for the entry with the given [id], or `null` if there is no entry with that ID. */ 41 | fun getEntryMetadata(id: String): StoredEntry? 42 | 43 | /** Returns the metadata for the entry with the oldest sent time, or `null` if the storage is empty. **/ 44 | fun getOldestEntryMetadata(): StoredEntry? 45 | 46 | /** Returns entry with the given [id], or `null` if there is no entry with that ID. */ 47 | fun getEntry(id: String): Harbringer.Entry? 48 | 49 | /** 50 | * Creates a [PendingEntry] for an entry with the given [id]. 51 | * 52 | * @throws IllegalStateException If there is already a stored or pending entry with the given ID. 53 | */ 54 | fun store(id: String): PendingEntry 55 | 56 | /** 57 | * Returns a [Source] that reads the response body for the entry with the given [id], or `null` if there is no 58 | * entry with that ID or if there is no body for the entry. 59 | * 60 | * The returned source must be closed after reading from it. 61 | */ 62 | fun readResponseBody(id: String): Source? 63 | 64 | /** 65 | * Returns a [Source] that reads the request body for the entry with the given [id], or `null` if there is no 66 | * entry with that ID or if there is no body for the entry. 67 | * 68 | * The returned source must be closed after reading from it. 69 | */ 70 | fun readRequestBody(id: String): Source? 71 | 72 | /** 73 | * Deletes the entry with the given [id]. This will also delete the request and response bodies for the entry. 74 | * 75 | * Returns the metadata for the deleted entry, or `null` if there is no entry with that ID. 76 | */ 77 | fun deleteEntry(id: String): StoredEntry? 78 | 79 | /** 80 | * Deletes the oldest entry. 81 | * 82 | * Returns the metadata for the deleted entry, or `null` if the storage was empty 83 | */ 84 | fun deleteOldestEntry(): StoredEntry? 85 | 86 | /** Removes all stored entries. */ 87 | fun clear() { 88 | for (id in getIds()) { 89 | deleteEntry(id) 90 | } 91 | } 92 | 93 | /** Some metadata for a stored entry. */ 94 | data class StoredEntry( 95 | /** 96 | * The entry's ID. 97 | */ 98 | val id: String, 99 | 100 | /** 101 | * The approximate size of the entry, in bytes. 102 | */ 103 | val size: Long, 104 | 105 | /** 106 | * The time the request was started, in milliseconds since the epoch. 107 | * 108 | * The precision of this value is not guaranteed. 109 | */ 110 | val startedAt: Long, 111 | ) : Comparable { 112 | override fun compareTo(other: StoredEntry): Int = 113 | when { 114 | startedAt != other.startedAt -> startedAt.compareTo(other.startedAt) 115 | id != other.id -> id.compareTo(other.id) 116 | else -> 0 117 | } 118 | } 119 | 120 | /** 121 | * An entry that's pending storage. 122 | * 123 | * You can write the request and response bodies to the [requestBody] and [responseBody] respectively. 124 | * 125 | * You *must* call [write] or [discard] when the entry is done. 126 | */ 127 | interface PendingEntry { 128 | val id: String 129 | val requestBody: Sink 130 | val responseBody: Sink 131 | 132 | fun write(entry: Harbringer.Entry) 133 | fun discard() 134 | } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /harbringer/src/commonTest/kotlin/se/ansman/harbringer/scrubber/JsonScrubberTest.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.scrubber 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.serialization.json.JsonPrimitive 8 | import okio.Buffer 9 | import okio.buffer 10 | import org.intellij.lang.annotations.Language 11 | import se.ansman.harbringer.Harbringer 12 | import kotlin.test.Test 13 | 14 | @OptIn(ExperimentalSerializationApi::class) 15 | class JsonScrubberTest { 16 | private val scrubber = Scrubber.json("$.password", "$.secret", "$.username") 17 | 18 | @Test 19 | fun `writes original json if nothing was replaced`() { 20 | Scrubber.json() 21 | assertThat(scrubber.scrub("{\"weird\":\n\"formatting\"\n}")) 22 | .isEqualTo("{\"weird\":\n\"formatting\"\n}") 23 | } 24 | 25 | @Test 26 | fun `can replace strings`() { 27 | assertThat(scrubber.scrub("""{"username":"example","password":"hunter2","rememberMe":true}""")) 28 | .isEqualTo("""{"username":"******","password":"******","rememberMe":true}""") 29 | } 30 | 31 | @Test 32 | fun `can replace arrays`() { 33 | assertThat(scrubber.scrub("""{"secret":[1,2,3]}""")) 34 | .isEqualTo("""{"secret":[]}""") 35 | } 36 | 37 | @Test 38 | fun `can replace objects`() { 39 | assertThat(scrubber.scrub("""{"secret":{"username":"example","password":"hunter2","rememberMe":true}}""")) 40 | .isEqualTo("""{"secret":{}}""") 41 | } 42 | 43 | @Test 44 | fun `can replace booleans`() { 45 | assertThat(scrubber.scrub("""{"secret":false}""")) 46 | .isEqualTo("""{"secret":"******"}""") 47 | } 48 | 49 | @Test 50 | fun `can replace number`() { 51 | assertThat(scrubber.scrub("""{"secret":4711}""")) 52 | .isEqualTo("""{"secret":"******"}""") 53 | } 54 | 55 | @Test 56 | fun `doesn't replace non affected things`() { 57 | assertThat(scrubber.scrub("""{"public":"stuff"}""")) 58 | .isEqualTo("""{"public":"stuff"}""") 59 | } 60 | 61 | @Test 62 | fun `supports arbitrary replacing`() { 63 | val scrubber = Scrubber.json( 64 | json = Json { 65 | prettyPrint = true 66 | prettyPrintIndent = " " 67 | } 68 | ) { path, element -> 69 | if (path.startsWith("$.secret.") || ".key2" in path || "$.nested.key1" == path) { 70 | JsonPrimitive("******") 71 | } else { 72 | element 73 | } 74 | 75 | } 76 | assertThat( 77 | scrubber.scrub( 78 | """ 79 | { 80 | "secret": { 81 | "username": "example", 82 | "password": "hunter2", 83 | "rememberMe": true 84 | }, 85 | "key1": "value1", 86 | "key2": "value2", 87 | "nested": { 88 | "key1": "value3", 89 | "key2": "value4" 90 | }, 91 | "array": [ 92 | { 93 | "key1": "value5", 94 | "key2": "value6" 95 | } 96 | ] 97 | } 98 | """.trimIndent() 99 | ) 100 | ).isEqualTo( 101 | """ 102 | { 103 | "secret": { 104 | "username": "******", 105 | "password": "******", 106 | "rememberMe": "******" 107 | }, 108 | "key1": "value1", 109 | "key2": "******", 110 | "nested": { 111 | "key1": "******", 112 | "key2": "******" 113 | }, 114 | "array": [ 115 | { 116 | "key1": "value5", 117 | "key2": "******" 118 | } 119 | ] 120 | } 121 | """.trimIndent() 122 | ) 123 | } 124 | 125 | @Test 126 | fun `can scrub arrays`() { 127 | val scrubber = Scrubber.json("$.secret[]", "$.secret2[].foo") 128 | assertThat(scrubber.scrub("""{"secret":[1,2,3],"secret2":[{"foo":"bar"},{"bar":"baz"}]}""")) 129 | .isEqualTo("""{"secret":["******","******","******"],"secret2":[{"foo":"******"},{"bar":"baz"}]}""") 130 | } 131 | 132 | private fun BodyScrubber.scrub(@Language("json") value: String): String? { 133 | val output = Buffer() 134 | val request = Harbringer.Request( 135 | method = "GET", 136 | url = "https://example.com", 137 | headers = Harbringer.Headers(), 138 | protocol = "HTTP/1.1", 139 | ) 140 | scrub(request, output).buffer().writeUtf8(value).close() 141 | return output.readUtf8() 142 | } 143 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/styles/prism.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom Dokka styles 3 | */ 4 | code .token { 5 | white-space: pre; 6 | } 7 | 8 | /** 9 | * Styles based on webhelp's prism.js styles 10 | * Changes: 11 | * - Since webhelp's styles are in .pcss, they use nesting which is not achievable in native CSS 12 | * so nested css blocks have been unrolled (like dark theme). 13 | * - Webhelp uses "Custom Class" prism.js plugin, so all of their prism classes are prefixed with "--prism". 14 | * Dokka doesn't seem to need this plugin at the moment, so all "--prism" prefixes have been removed. 15 | * - Removed all styles related to `pre` and `code` tags. Kotlinlang's resulting styles are so spread out and complicated 16 | * that it's difficult to gather in one place. Instead use code styles defined in the main Dokka styles, 17 | * which at the moment looks fairly similar. 18 | * 19 | * Based on prism.js default theme 20 | * Based on dabblet (http://dabblet.com) 21 | * @author Lea Verou 22 | */ 23 | 24 | .token.comment, 25 | .token.prolog, 26 | .token.doctype, 27 | .token.cdata { 28 | color: #8c8c8c; 29 | } 30 | 31 | .token.punctuation { 32 | color: #999; 33 | } 34 | 35 | .token.namespace { 36 | opacity: 0.7; 37 | } 38 | 39 | .token.property, 40 | .token.tag, 41 | .token.boolean, 42 | .token.number, 43 | .token.constant, 44 | .token.symbol, 45 | .token.deleted { 46 | color: #871094; 47 | } 48 | 49 | .token.selector, 50 | .token.attr-name, 51 | .token.string, 52 | .token.char, 53 | .token.builtin, 54 | .token.inserted { 55 | color: #067d17; 56 | } 57 | 58 | .token.operator, 59 | .token.entity, 60 | .token.url, 61 | .language-css .token.string, 62 | .style .token.string { 63 | color: #9a6e3a; 64 | /* This background color was intended by the author of this theme. */ 65 | background: hsla(0, 0%, 100%, 0.5); 66 | } 67 | 68 | .token.atrule, 69 | .token.attr-value, 70 | .token.keyword { 71 | font-size: inherit; /* to override .keyword */ 72 | color: #0033b3; 73 | } 74 | 75 | .token.function { 76 | color: #00627a; 77 | } 78 | 79 | .token.class-name { 80 | color: #000000; 81 | } 82 | 83 | .token.regex, 84 | .token.important, 85 | .token.variable { 86 | color: #871094; 87 | } 88 | 89 | .token.important, 90 | .token.bold { 91 | font-weight: bold; 92 | } 93 | .token.italic { 94 | font-style: italic; 95 | } 96 | 97 | .token.entity { 98 | cursor: help; 99 | } 100 | 101 | .token.operator { 102 | background: none; 103 | } 104 | 105 | /* 106 | * DARK THEME 107 | */ 108 | :root.theme-dark .token.comment, 109 | :root.theme-dark .token.prolog, 110 | :root.theme-dark .token.cdata { 111 | color: #808080; 112 | } 113 | 114 | :root.theme-dark .token.delimiter, 115 | :root.theme-dark .token.boolean, 116 | :root.theme-dark .token.keyword, 117 | :root.theme-dark .token.selector, 118 | :root.theme-dark .token.important, 119 | :root.theme-dark .token.atrule { 120 | color: #cc7832; 121 | } 122 | 123 | :root.theme-dark .token.operator, 124 | :root.theme-dark .token.punctuation, 125 | :root.theme-dark .token.attr-name { 126 | color: #a9b7c6; 127 | } 128 | 129 | :root.theme-dark .token.tag, 130 | :root.theme-dark .token.tag .punctuation, 131 | :root.theme-dark .token.doctype, 132 | :root.theme-dark .token.builtin { 133 | color: #e8bf6a; 134 | } 135 | 136 | :root.theme-dark .token.entity, 137 | :root.theme-dark .token.number, 138 | :root.theme-dark .token.symbol { 139 | color: #6897bb; 140 | } 141 | 142 | :root.theme-dark .token.property, 143 | :root.theme-dark .token.constant, 144 | :root.theme-dark .token.variable { 145 | color: #9876aa; 146 | } 147 | 148 | :root.theme-dark .token.string, 149 | :root.theme-dark .token.char { 150 | color: #6a8759; 151 | } 152 | 153 | :root.theme-dark .token.attr-value, 154 | :root.theme-dark .token.attr-value .punctuation { 155 | color: #a5c261; 156 | } 157 | 158 | :root.theme-dark .token.attr-value .punctuation:first-child { 159 | color: #a9b7c6; 160 | } 161 | 162 | :root.theme-dark .token.url { 163 | text-decoration: underline; 164 | 165 | color: #287bde; 166 | background: transparent; 167 | } 168 | 169 | :root.theme-dark .token.function { 170 | color: #ffc66d; 171 | } 172 | 173 | :root.theme-dark .token.regex { 174 | background: #364135; 175 | } 176 | 177 | :root.theme-dark .token.deleted { 178 | background: #484a4a; 179 | } 180 | 181 | :root.theme-dark .token.inserted { 182 | background: #294436; 183 | } 184 | 185 | :root.theme-dark .token.class-name { 186 | color: #a9b7c6; 187 | } 188 | 189 | :root.theme-dark .token.function { 190 | color: #ffc66d; 191 | } 192 | 193 | :root.theme-darkcode .language-css .token.property, 194 | :root.theme-darkcode .language-css, 195 | :root.theme-dark .token.property + .token.punctuation { 196 | color: #a9b7c6; 197 | } 198 | 199 | code.language-css .token.id { 200 | color: #ffc66d; 201 | } 202 | 203 | :root.theme-dark code.language-css .token.selector > .token.class, 204 | :root.theme-dark code.language-css .token.selector > .token.attribute, 205 | :root.theme-dark code.language-css .token.selector > .token.pseudo-class, 206 | :root.theme-dark code.language-css .token.selector > .token.pseudo-element { 207 | color: #ffc66d; 208 | } 209 | 210 | :root.theme-dark .language-plaintext .token { 211 | /* plaintext code should be colored as article text */ 212 | color: inherit !important; 213 | } 214 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /harbringer/src/commonMain/kotlin/se/ansman/harbringer/storage/InMemoryHarbringerStorage.kt: -------------------------------------------------------------------------------- 1 | package se.ansman.harbringer.storage 2 | 3 | import okio.* 4 | import se.ansman.harbringer.Harbringer 5 | import se.ansman.harbringer.internal.SortedMap 6 | import se.ansman.harbringer.internal.atomic.newLock 7 | import se.ansman.harbringer.internal.atomic.withLock 8 | 9 | /** 10 | * An in-memory implementation of [Harbringer] that stores entries in a map. 11 | * 12 | * This is useful for testing, but should be used with care since it does not persist data between sessions. 13 | * 14 | * The entries will be compressed but still stored in memory, so it is not suitable for large entries. 15 | */ 16 | class InMemoryHarbringerStorage : HarbringerStorage { 17 | private val storedEntries = SortedMap() 18 | private val pendingEntries = mutableMapOf() 19 | private val lock = newLock() 20 | 21 | override var bytesStored: Long = 0 22 | private set 23 | 24 | override var entriesStored: Int = 0 25 | private set 26 | 27 | override fun getIds(): Set = lock.withLock { 28 | storedEntries.values.mapTo(LinkedHashSet(storedEntries.size)) { it.id } 29 | } 30 | 31 | override fun getEntryMetadata(id: String): HarbringerStorage.StoredEntry? = lock.withLock { 32 | storedEntries[id]?.storedEntry 33 | } 34 | 35 | override fun getOldestEntryMetadata(): HarbringerStorage.StoredEntry? = lock.withLock { 36 | storedEntries.values.firstOrNull()?.storedEntry 37 | } 38 | 39 | override fun getEntry(id: String): Harbringer.Entry? = lock.withLock { 40 | storedEntries[id]?.entry 41 | } 42 | 43 | override fun store(id: String): HarbringerStorage.PendingEntry = lock.withLock { 44 | check(id !in pendingEntries.keys) { 45 | "There is already a pending entry with the ID $id" 46 | } 47 | check(id !in storedEntries.keys) { 48 | "There is already a stored entry with the ID $id" 49 | } 50 | 51 | return PendingEntry(id).also { pendingEntries[id] = it } 52 | } 53 | 54 | override fun readRequestBody(id: String): Source? = 55 | (lock.withLock { storedEntries[id]?.requestBody }?.let(Buffer()::write) as Source?)?.gzip() 56 | 57 | override fun readResponseBody(id: String): Source? = 58 | (lock.withLock { storedEntries[id]?.responseBody }?.let(Buffer()::write) as Source?)?.gzip() 59 | 60 | 61 | override fun deleteEntry(id: String): HarbringerStorage.StoredEntry? = lock.withLock { 62 | doDeleteEntry(id) 63 | } 64 | 65 | override fun deleteOldestEntry(): HarbringerStorage.StoredEntry? = lock.withLock { 66 | doDeleteEntry(storedEntries.values.firstOrNull()?.storedEntry?.id ?: return null) 67 | } 68 | 69 | private fun doDeleteEntry(id: String): HarbringerStorage.StoredEntry? { 70 | pendingEntries.remove(id)?.discard() 71 | return storedEntries.remove(id) 72 | ?.storedEntry 73 | ?.also { 74 | bytesStored -= it.size 75 | --entriesStored 76 | } 77 | } 78 | 79 | private data class StoredEntry( 80 | val entry: Harbringer.Entry, 81 | val requestBody: ByteString, 82 | val responseBody: ByteString 83 | ) : Comparable { 84 | val id get() = entry.id 85 | val storedEntry = HarbringerStorage.StoredEntry( 86 | id = entry.id, 87 | size = requestBody.size + responseBody.size.toLong(), 88 | startedAt = entry.startedAt, 89 | ) 90 | 91 | override fun compareTo(other: StoredEntry): Int = storedEntry.compareTo(other.storedEntry) 92 | } 93 | 94 | private inner class PendingEntry(override val id: String) : HarbringerStorage.PendingEntry { 95 | private var isClosed = false 96 | private val requestBuffer: Buffer = Buffer() 97 | private val responseBuffer: Buffer = Buffer() 98 | 99 | override val requestBody: Sink = CloseableSink(requestBuffer).gzip() 100 | override val responseBody: Sink = CloseableSink(responseBuffer).gzip() 101 | 102 | override fun write(entry: Harbringer.Entry) { 103 | check(id == entry.id) { 104 | "The ID of the entry does not match the ID of the pending entry" 105 | } 106 | check(close()) { 107 | "This entry is already closed, you may not call write twice or after calling discard" 108 | } 109 | val requestBody = requestBuffer.readByteString() 110 | val responseBody = responseBuffer.readByteString() 111 | lock.withLock { 112 | storedEntries[id] = StoredEntry( 113 | entry = entry, 114 | requestBody = requestBody, 115 | responseBody = responseBody 116 | ) 117 | pendingEntries.remove(id) 118 | } 119 | } 120 | 121 | override fun discard() { 122 | deleteEntry(id) 123 | } 124 | 125 | private fun close(): Boolean { 126 | if (isClosed) { 127 | return false 128 | } 129 | isClosed = true 130 | requestBody.close() 131 | responseBody.close() 132 | return true 133 | } 134 | } 135 | } 136 | 137 | private class CloseableSink( 138 | private val sink: Sink, 139 | ) : Sink { 140 | private var isClosed = false 141 | override fun write(source: Buffer, byteCount: Long) { 142 | check(!isClosed) { "closed" } 143 | sink.write(source, byteCount) 144 | } 145 | 146 | override fun flush() { 147 | check(!isClosed) { "closed" } 148 | sink.flush() 149 | } 150 | 151 | override fun timeout(): Timeout = sink.timeout() 152 | 153 | override fun close() { 154 | isClosed = true 155 | sink.close() 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | All modules 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | harbringer 43 | 44 | Toggle table of contents 46 | 47 | 48 | 49 | 0.1.0 50 | 51 | Switch theme 53 | 54 | Search in 55 | API 56 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | harbringer 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | All modules: 82 | 83 | 84 | 85 | 86 | harbringer 87 | 88 | Link copied to clipboard 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | okhttp3 98 | 99 | Link copied to clipboard 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/ui-kit/ui-kit.min.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";function t(e,n){return!(!e||!e.classList.contains(n))||!!e.parentElement&&t(e.parentElement,n)}document.addEventListener("DOMContentLoaded",(function(){document.querySelectorAll("div.button").forEach((function(t){t.addEventListener("keydown",(function(e){var n=e.key;"Enter"!==n&&" "!==n||t.dispatchEvent(new MouseEvent("click"))}))}))}));function e(){return window.innerWidth<440?"mobile":window.innerWidth>=440&&window.innerWidth<900?"tablet":"desktop"}var n=function(){function t(t){this.trapElement=t,this.handleKeyDown=this.handleKeyDown.bind(this),this.trapElement.addEventListener("keydown",this.handleKeyDown)}return t.prototype.handleKeyDown=function(t){var e=Array.from(this.trapElement.querySelectorAll('[role="option"]')).filter((function(t){return"none"!==t.style.display&&-1!==t.tabIndex}));if(["Tab","ArrowDown","ArrowUp"].includes(t.key)&&0!==e.length){var n=e[0],o=e[e.length-1];if("ArrowUp"===t.key)if(document.activeElement===n)o.focus();else{var r=e.indexOf(document.activeElement);e[r-1].focus()}"ArrowDown"===t.key&&(document.activeElement===o?n.focus():(r=e.indexOf(document.activeElement),e[r+1].focus())),"Tab"===t.key&&(t.shiftKey?document.activeElement===n&&(o.focus(),t.preventDefault()):document.activeElement===o&&(n.focus(),t.preventDefault()))}},t.prototype.destroy=function(){this.trapElement.removeEventListener("keydown",this.handleKeyDown)},t}(),o='[data-role="dropdown"]',r='[data-role="dropdown-toggle"]',i='[data-role="dropdown-listbox"]';function l(t){var e,n,o=t.querySelectorAll(r);null==o||o.forEach(a),e=t.querySelector(i),n=o[0].offsetWidth,e&&(e.classList.toggle("dropdown--list_expanded"),e.classList.contains("dropdown--list_expanded")?c(e,n):c(e,void 0))}function a(t){t.classList.contains("button_dropdown")&&t.classList.toggle("button_dropdown_active")}function c(t,e){if(e){var n=parseInt(getComputedStyle(t).minWidth,10),o=isNaN(n)?e:Math.max(n,e);t.style.minWidth="".concat(o,"px")}else t.style.minWidth=""}function d(e){var n=e.target;t(n,"dropdown")&&"dropdown--overlay"!==n.className||document.querySelectorAll(o).forEach((function(t){var e,n;null===(e=t.querySelectorAll(r))||void 0===e||e.forEach((function(t){t.classList.remove("button_dropdown_active")})),null===(n=t.querySelectorAll(i))||void 0===n||n.forEach((function(t){t.classList.remove("dropdown--list_expanded"),t.style.minWidth=""}))}))}function u(t){t.tag.removeAttribute("style"),t.option.setAttribute("style","display: none")}function s(t){t.tag.setAttribute("style","display: none"),t.option.removeAttribute("style")}function f(t){var e,n=null===(e=t.querySelector(".checkbox--input"))||void 0===e?void 0:e.getAttribute("data-filter");n&&(-1===filteringContext.activeFilters.findIndex((function(t){return t===n}))?unfilterSourceset(n):filterSourceset(n)),refreshFiltering()}document.addEventListener("DOMContentLoaded",(function(){document.querySelectorAll(o).forEach((function(t){var e;null===(e=t.querySelectorAll(r))||void 0===e||e.forEach((function(e){e.addEventListener("click",(function(){return l(t)}))})),function(t){new n(t),t.addEventListener("keydown",(function(e){var n;"Escape"===e.key&&(l(t),null===(n=t.querySelector(r))||void 0===n||n.focus())}))}(t)})),document.addEventListener("click",d)})),document.addEventListener("DOMContentLoaded",(function(){var t=document.getElementById("navigation-wrapper"),n=document.getElementById("library-version"),o=document.getElementById("filter-section"),r=document.querySelector("#filter-section + .navigation-controls--btn"),i=document.getElementById("filter-section-dropdown");if(t&&n&&o&&r&&i){var l=null==o?void 0:o.querySelectorAll(".dropdown--option"),a=null==o?void 0:o.querySelectorAll(".platform-selector");if(a&&l)if(a.length===l.length){var c=Array.from({length:a.length}).map((function(t,e){return{tag:a[e],option:l[e]}})),d=c.map((function(t){return t.tag.getBoundingClientRect().width})),v=e(),m=new ResizeObserver((function(){var n=e();v!==n&&(c.forEach(u),d=function(t){return t.map((function(t){return t.tag.getBoundingClientRect().width}))}(c)),v=n,y(),m.unobserve(t)})),g=function(){m.observe(t)};y(),g(),l.forEach((function(t){t.addEventListener("click",(function(t){f(t.target)})),t.addEventListener("keydown",(function(t){var e=t.key;"Enter"!==e&&" "!==e||f(t.target)}))})),window.addEventListener("resize",g)}else console.warn("Dokka: filter section items are not equal");else console.warn("Dokka: filter section items are not found")}else console.warn("Dokka: filter section is not found");function y(){var e,l;if(t&&i){if(t.getBoundingClientRect().width<900)return c.forEach(s),void i.removeAttribute("style");var a=(n&&r?r.getBoundingClientRect().left-n.getBoundingClientRect().right:0)-44-10,f=0;i.removeAttribute("style");var v=!1;c.forEach((function(t,e){(f+=d[e]+4)=t.length&&(t=void 0),{value:t&&t[o++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")},m=function(){var t=!1;try{var e="__testLocalStorageKey__";localStorage.setItem(e,e),localStorage.removeItem(e),t=!0}catch(t){console.error("Local storage is not available",t)}return{getItem:function(e){return t?localStorage.getItem(e):null},setItem:function(e,n){t&&localStorage.setItem(e,n)}}}();function g(t){var e,n=null===(e=t.getAttribute("data-togglable"))||void 0===e?void 0:e.split(",");!function(){var e,n,o,r;try{for(var i=v(document.getElementsByClassName("tabs-section")),l=i.next();!l.done;l=i.next()){var a=l.value;try{for(var c=(o=void 0,v(a.children)),d=c.next();!d.done;d=c.next()){var u=d.value;u.getAttribute("data-togglable")===t.getAttribute("data-togglable")?u.setAttribute("data-active",""):u.removeAttribute("data-active")}}catch(t){o={error:t}}finally{try{d&&!d.done&&(r=c.return)&&r.call(c)}finally{if(o)throw o.error}}}}catch(t){e={error:t}}finally{try{l&&!l.done&&(n=i.return)&&n.call(i)}finally{if(e)throw e.error}}}(),document.querySelectorAll(".tabs-section-body *[data-togglable]").forEach((function(t){var e=t.getAttribute("data-togglable");n&&e&&n.includes(e)?t.setAttribute("data-active",""):t.classList.contains("sourceset-dependent-content")||t.removeAttribute("data-active")}))}window.initTabs=function(){var t=document.querySelector(".main-content"),e="active-tab-"+(t?t.getAttribute("data-page-type"):null);document.querySelectorAll("div[tabs-section]").forEach((function(t){!function(t){var e=t.querySelector("button[data-active]");e&&g(e)}(t),t.addEventListener("click",(function(t){var n=t.target,o=n?n.getAttribute("data-togglable"):null;o&&(m.setItem(e,JSON.stringify(o)),g(n))}))}));var n=m.getItem(e);if(n){var o=document.querySelector('div[tabs-section] > button[data-togglable="'+JSON.parse(n)+'"]');o&&g(o)}},window.toggleSections=g,document.addEventListener("DOMContentLoaded",(function(){document.querySelectorAll('[data-remove-style="true"]').forEach((function(t){t.removeAttribute("style")}))}))})(); -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/not-found-version.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Unavailable page 6 | 100 | 101 | 102 | 133 | 134 | 136 | 137 | 138 | 139 | 140 | NOT 141 | FOUND 142 | 143 | 144 | 146 | 147 | 149 | 151 | 153 | 155 | 156 | 158 | 160 | 162 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | uh-oh! 182 | You are requesting a page that not 183 | available in documentation version 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-entry/id.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | id 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Entry/id 95 | 96 | id 97 | 98 | val id: String 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-entry/request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | request 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Entry/request 95 | 96 | request 97 | 98 | val request: Harbringer.Request 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-entry/timings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | timings 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Entry/timings 95 | 96 | timings 97 | 98 | val timings: Harbringer.Timings 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-device/ip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ip 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Device/ip 95 | 96 | ip 97 | 98 | val ip: String 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-entry/response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | response 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Entry/response 95 | 96 | response 97 | 98 | val response: Harbringer.Response 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-request/headers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | headers 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Request/headers 95 | 96 | headers 97 | 98 | val headers: Harbringer.Headers 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/doc/dokka/0.1.0/harbringer/se.ansman.harbringer/-harbringer/-request/url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | url 6 | 7 | 8 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | harbringer 41 | Toggle table of contents 42 | 43 | 44 | 45 | 0.1.0 46 | 47 | 48 | common 49 | 50 | 51 | 52 | Platform filter 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | common 62 | 63 | 64 | 65 | 66 | 67 | 68 | Switch theme 69 | 70 | Search in 71 | API 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | harbringer 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | harbringer/se.ansman.harbringer/Harbringer/Request/url 95 | 96 | url 97 | 98 | val url: String 99 | 100 | 110 | 111 | 112 | 113 | 114 | 115 | --------------------------------------------------------------------------------