Skip to content

fix: Refactor AnkiServer for future HTTPS support (Issue #15991)#20885

Draft
mixelas wants to merge 10 commits intoankidroid:mainfrom
mixelas:fix/issue-15991-https-support
Draft

fix: Refactor AnkiServer for future HTTPS support (Issue #15991)#20885
mixelas wants to merge 10 commits intoankidroid:mainfrom
mixelas:fix/issue-15991-https-support

Conversation

@mixelas
Copy link
Copy Markdown

@mixelas mixelas commented Apr 28, 2026

Purpose / Description

This PR prepares the codebase for HTTPS support by refactoring AnkiServer to support SSL/TLS configuration, while maintaining backward compatibility.

Fixes

Approach

Added Context parameter to AnkiServer constructor to enable SSL setup when needed

  • Created SslUtil as a placeholder for future certificate generation
  • Updated all AnkiServer instantiations to pass context where available
  • Added unit tests for AnkiServer and SslUtil to verify basic functionality
  • Maintained full backward compatibility

How Has This Been Tested?

  • Full gradle build was successful
  • Kotlin compilation was successful
  • Unit tests created for:
    AnkiServer baseUrl() returning HTTP when context not provided
    AnkiServer baseUrl() containing localhost address
    SslUtil placeholder initialization

Tested on:

  • Android Gradle Plugin 9.3.1
  • Kotlin 1.9.x
  • Java 21

Learning (optional, can help others)

While i was working with Android SDK, i noticed that it doesn't include sun.security internal APIs in modern versions. The proper approach for self-signed certificate generation on Android involves either:

  1. Using BouncyCastle library
  2. Using Android's KeyStore and KeyGenParameterSpec APIs

Checklist

Please, go through these checks before submitting the PR.

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

Copy link
Copy Markdown
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning on adding an implementation? Otherwise this PR doesn't feel mergeable on its own

Copy link
Copy Markdown
Member

@BrayanDSO BrayanDSO left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you used AI on this PR?

@@ -0,0 +1,78 @@
/*
* Copyright (c) 2024 AnkiDroid Contributors
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyright is wrong

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied this from another file in the codebase. I wanted it to look like the other files in there. I also didn't add my name because i didn't think that this would get merged.

Comment on lines +29 to +31
private const val KEYSTORE_FILENAME = "anki_keystore.bks"
private const val KEYSTORE_PASSWORD = "ankidroid"
private const val KEY_PASSWORD = "ankidroid"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that hardcoding these values isn't safe

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Their purpose was strictly as a placeholder. I didn't think that you would entrust me to do the whole thing so i made this instead.


// TODO: Generate self-signed X.509 certificate
// Currently creates empty keystore. Future implementations should use:
// - Android KeyStore API (preferred), or
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably should be doing that now

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I acknowledge this is incomplete. I didn't know which architecture you preferred.

val statesMutationEvalFlow = MutableSharedFlow<String>()

override val server: AnkiServer = AnkiServer(this, repository.getServerPort()).also { it.start() }
override val server: AnkiServer = AnkiServer(this, null, repository.getServerPort()).also { it.start() }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this means the issue still isn't fixed on the new study screen

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the concern, if I passed null as the context, HTTPS wouldn't be enabled on the new study screen, which defeats the purpose of this PR.

* Ignores any matching test defined with `@Flaky` in CI
* @see Flaky
*/
class IgnoreFlakyTestsInCIRule : TestRule {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk why are you copying this file

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files aren't in my commits.

* @param runnable a function that is expected to throw an exception when executed
* @return the exception thrown by [runnable]
*/
inline fun <reified T : Throwable> assertThrows(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk why you are copying this file

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These files aren't in my commits. they appear to be pre-existing in the repository or from the base branch

@BrayanDSO BrayanDSO added the Needs Author Reply Waiting for a reply from the original author label Apr 29, 2026
@mixelas
Copy link
Copy Markdown
Author

mixelas commented Apr 29, 2026

This was initially a placeholder as i didn't think that I had to make the complete HTTPS support. That is why it looks bad and AI generated, as I tried to do a simple fix. I will get on with pushing a PR with complete HTTPS support and give it my best.

@mixelas
Copy link
Copy Markdown
Author

mixelas commented May 2, 2026

@david-allison can you please review again.

@david-allison
Copy link
Copy Markdown
Member

david-allison commented May 3, 2026

@mixelas mixelas force-pushed the fix/issue-15991-https-support branch from ba113ab to ffb28ba Compare May 3, 2026 14:09
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2024 David Allison <davidallisongithub@gmail.com>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these files, they shouldn't have been committed. testutils and testlib have been replaced with test fixtures

@mixelas mixelas force-pushed the fix/issue-15991-https-support branch from 349aa3e to 7c75875 Compare May 4, 2026 10:12
import timber.log.Timber

abstract class CardViewerViewModel(
app: Application,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the code, the Application dependency is only necessary to pass it to AnkiServer -> SslUtil so it can get a File from the cache directory.

Instead of all that, get the file on the fragment and pass it down. That way we avoid the Application dependency

@mixelas mixelas force-pushed the fix/issue-15991-https-support branch from 7c75875 to 797d3c6 Compare May 4, 2026 18:07
@mixelas mixelas force-pushed the fix/issue-15991-https-support branch from 9ddccc0 to 5731752 Compare May 4, 2026 18:24
init {
// Enable HTTPS if context was provided (Issue #15991)
// This allows WebView to load cards without cleartext restrictions on modern Android
if (isHttps && sslContext != null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logically, it feels like isHttps doesn't need to exist. If it's true, then cacheDir is not null, which means sslContext is non-null

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, i see, I removed the separate isHttps flag and now derive the scheme from the SSL context itself, so the server is HTTPS only when SSL initialization succeeds

) : NanoHTTPD(LOCALHOST, port) {
fun baseUrl(): String = "http://$LOCALHOST:$listeningPort/"
private val isHttps = cacheDir != null
private val sslContext = cacheDir?.let { SslUtil.getSSLContext(it) }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this throws?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i fixed this one as well. I changed SSL initialization to fail safely: if keystore/context creation throws, the server logs the failure and falls back to HTTP instead of crashing on startup.

savedInstanceState: Bundle?,
) {
server = AnkiServer(this).also { it.start() }
server = AnkiServer(this, requireContext().cacheDir).also { it.start() }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why cacheDir?

  • It differs per-profile, is that intended
  • The OS can wipe is, is that intended?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used cacheDir because the keystore is a local, disposable artifact for loopback HTTPS. It is profile-scoped, and can be regenerated if the OS clears it. If you’d prefer a more persistent location, I can switch it, but for this use case cacheDir keeps the storage ephemeral.

@david-allison
Copy link
Copy Markdown
Member

Heyy... there's a lot of changes here which aren't relevant to the PR. Take a look at this from a reviewer's perspective:

The 'git' window can show you what's relevant, and you can revert the changes which aren't necessary

Screenshot 2026-05-04 at 23 53 50

This design seems complex. Moving reviewerViewModelFactory to a separate file seems unnecessary.

It might be worth explaining the design, I'm sure it can be simplified

@mixelas
Copy link
Copy Markdown
Author

mixelas commented May 5, 2026

I addressed the points you raised. I inlined the ViewModel creation into the fragments and kept the cache-dir threading as small as possible. The goal was just to pass the local directory through to the server setup without adding extra architectural layers. Let me know if there is anything else that i need to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Has Conflicts Needs Author Reply Waiting for a reply from the original author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Restricting cleartext network traffic breaks import and statistics pages

3 participants