From d6f4b34eae2e977cdd0339fb17302976fdae0574 Mon Sep 17 00:00:00 2001 From: Jakob Heher Date: Tue, 4 Oct 2022 15:02:43 +0200 Subject: QR code handling --- .../asit/pdfover/gui/bku/MobileBKUConnector.java | 76 ++++++++++++++++++++-- .../pdfover/gui/bku/OLDMobileBKUConnector.java | 12 +++- .../asit/pdfover/gui/bku/mobile/ATrustParser.java | 59 ++++++++++++++--- 3 files changed, 129 insertions(+), 18 deletions(-) (limited to 'pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku') diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java index 6f6e5301..14971ce1 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/MobileBKUConnector.java @@ -14,6 +14,7 @@ import javax.annotation.Nonnull; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -25,9 +26,12 @@ import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.NoHttpResponseException; +import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.json.JSONObject; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -172,13 +176,19 @@ public class MobileBKUConnector implements BkuSlConnector { return post; } - private static @Nonnull ClassicHttpRequest buildFormSubmit(@Nonnull ATrustParser.HTMLResult html) { + private static @Nonnull ClassicHttpRequest buildFormSubmit(@Nonnull ATrustParser.HTMLResult html, @Nonnull String submitButtonId) { HttpPost post = new HttpPost(html.formTarget); var builder = MultipartEntityBuilder.create(); for (var pair : html.iterateFormOptions()) builder.addTextBody(pair.getKey(), pair.getValue()); - + if (html.submitButtons.containsKey(submitButtonId)) { + var submitInfo = html.submitButtons.get(submitButtonId); + builder.addTextBody(submitInfo.name, submitInfo.value); + } else { + log.warn("Attempted to use submit button #{} which does not exist. Omitting.", submitButtonId); + } + post.setEntity(builder.build()); return post; } @@ -189,11 +199,65 @@ public class MobileBKUConnector implements BkuSlConnector { */ private @Nonnull ClassicHttpRequest presentResponseToUserAndReturnNextRequest(@Nonnull ATrustParser.HTMLResult html) throws UserCancelledException { if (html.usernamePasswordBlock != null) { - while ((this.credentials.username == null) || (this.credentials.password == null)) { - this.state.getCredentialsFromUserTo(this.credentials, null); + try { + while ((this.credentials.username == null) || (this.credentials.password == null)) { + this.state.getCredentialsFromUserTo(this.credentials, null); // TODO error message + } + html.usernamePasswordBlock.setUsernamePassword(this.credentials.username, this.credentials.password); + return buildFormSubmit(html, "Button_Identification"); + } catch (UserCancelledException e) { + return buildFormSubmit(html, "Button_Cancel"); + } + } + if (html.qrCodeBlock != null) { + try (final CloseableHttpClient httpClient = HttpClients.custom().disableRedirectHandling().build()) { + final HttpGet request = new HttpGet(html.qrCodeBlock.qrCodeURI); + boolean[] done = new boolean[1]; + done[0] = false; + Thread longPollThread = new Thread(() -> { + log.debug("longPollThread hello"); + while (!done[0]) { + try (final CloseableHttpResponse response = httpClient.execute(new HttpGet(html.qrCodeBlock.pollingURI))) { + JSONObject jsonResponse = new JSONObject(EntityUtils.toString(response.getEntity())); + if (jsonResponse.getBoolean("Fin")) + state.signalQRScanned(); + else if (jsonResponse.getBoolean("Wait")) + { + log.debug("longPollThread continue..."); + continue; + } + else if (jsonResponse.getBoolean("Error")) + state.signalQRScanned(); /* will trigger reload and find error; this is the same thing a-trust does */ + else { + log.warn("Unknown long poll response:\n{}", jsonResponse.toString(2)); + break; + } + } catch (NoHttpResponseException e) { + continue; + } catch (IOException | ParseException | IllegalStateException e) { + log.warn("QR code long polling exception", e); + /* sleep so we don't hammer a-trust too hard in case this goes wrong */ + try { Thread.sleep(5000); } catch (InterruptedException e2) {} + } + } + log.debug("longPollThread goodbye"); + }); + try { + longPollThread.start(); + MobileBKUState.QRResult result = this.state.showQRCode(html.qrCodeBlock.referenceValue, html.qrCodeBlock.qrCodeURI, null); + switch (result) { + case UPDATE: return new HttpGet(html.htmlDocument.baseUri()); + case TO_FIDO2: throw new IllegalStateException(); + case TO_SMS: throw new IllegalStateException(); + } + } finally { + done[0] = true; + request.abort(); + try { longPollThread.join(1000); } catch (InterruptedException e) {} + } + } catch (IOException e) { + log.warn("closing CloseableHttpClient threw exception", e); } - html.usernamePasswordBlock.setUsernamePassword(this.credentials.username, this.credentials.password); - return buildFormSubmit(html); } throw new IllegalStateException("No top-level block is set? Something has gone terribly wrong."); } diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDMobileBKUConnector.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDMobileBKUConnector.java index a71f37df..9703e602 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDMobileBKUConnector.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/OLDMobileBKUConnector.java @@ -15,6 +15,9 @@ */ package at.asit.pdfover.gui.bku; +import java.io.IOException; +import java.net.URISyntaxException; + // Imports import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +28,7 @@ import at.asit.pdfover.gui.bku.OLDmobile.ATrustStatus; import at.asit.pdfover.gui.workflow.states.MobileBKUState; import at.asit.pdfover.signer.BkuSlConnector; import at.asit.pdfover.signer.SignatureException; +import at.asit.pdfover.signer.UserCancelledException; import at.asit.pdfover.signer.pdfas.PdfAs4SLRequest; import at.asit.pdfover.signer.pdfas.PdfAs4SigningState; @@ -51,7 +55,7 @@ public class OLDMobileBKUConnector implements BkuSlConnector { * @see at.asit.pdfover.signer.BkuSlConnector#handleSLRequest(java.lang.String) */ @Override - public String handleSLRequest(PdfAs4SLRequest request) throws SignatureException { + public String handleSLRequest(PdfAs4SLRequest request) throws SignatureException, UserCancelledException { PdfAs4SigningState signingState = this.state.getSigningState(); signingState.signatureRequest = request; @@ -120,7 +124,11 @@ public class OLDMobileBKUConnector implements BkuSlConnector { boolean enterTAN = true; String responseData = null; if (status.qrCodeURL != null) { - this.state.showQR(); + try { + this.state.OLDshowQR(); + } catch (IOException | URISyntaxException e) { + throw new SignatureException(e); + } if ("cancel".equals(this.state.status.errorMessage)) throw new SignatureException(new IllegalStateException()); if (status.qrCodeURL == null) { diff --git a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java index f452202d..890ffad1 100644 --- a/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java +++ b/pdf-over-gui/src/main/java/at/asit/pdfover/gui/bku/mobile/ATrustParser.java @@ -33,9 +33,13 @@ public class ATrustParser { log.debug("Tested for element {} -- not found.", selector); throw new ComponentParseFailed(); } - protected @Nonnull String getAttributeEnsureNotNull(@Nonnull String selector, @Nonnull String attribute) throws ComponentParseFailed { + protected @Nonnull org.jsoup.nodes.Element getElementEnsureNotNull(@Nonnull String selector) throws ComponentParseFailed { var elm = this.htmlDocument.selectFirst(selector); if (elm == null) { log.warn("Expected element not found in response: {}", selector); throw new ComponentParseFailed(); } + return elm; + } + protected @Nonnull String getAttributeEnsureNotNull(@Nonnull String selector, @Nonnull String attribute) throws ComponentParseFailed { + var elm = getElementEnsureNotNull(selector); if (!elm.hasAttr(attribute)) { log.warn("Element {} is missing expected attribute '{}'.", selector, attribute); throw new ComponentParseFailed(); } return ISNOTNULL(elm.attr(attribute)); } @@ -55,6 +59,7 @@ public class ATrustParser { public static class UsernamePasswordBlock extends TopLevelFormBlock { private @Nonnull String usernameKey; private @Nonnull String passwordKey; + public @CheckForNull String errorMessage; public void setUsernamePassword(String username, String password) { formOptions.put(usernameKey, username); formOptions.put(passwordKey, password); @@ -65,22 +70,38 @@ public class ATrustParser { abortIfElementMissing("#handynummer"); this.usernameKey = getAttributeEnsureNotNull("#handynummer", "name"); this.passwordKey = getAttributeEnsureNotNull("#signaturpasswort", "name"); - - /* remove unused submit buttons */ - // TODO: we should generalize this somehow for input type="submit" - // TODO: do we maybe want to use the actual cancel button? - formOptions.remove(getAttributeEnsureNotNull("#Button_Cancel", "name")); - formOptions.remove(getAttributeEnsureNotNull("#Button_localBku", "name")); } } public static class QRCodeBlock extends TopLevelFormBlock { + public @Nonnull String referenceValue; public @Nonnull URI qrCodeURI; + public @Nonnull URI pollingURI; + public @Nullable String errorMessage; private QRCodeBlock(@Nonnull org.jsoup.nodes.Document htmlDocument, @Nonnull Map formOptions) throws ComponentParseFailed { super(htmlDocument, formOptions); abortIfElementMissing("#qrimage"); + + this.referenceValue = ISNOTNULL(getElementEnsureNotNull("#vergleichswert").ownText()); this.qrCodeURI = getURIAttributeEnsureNotNull("#qrimage", "abs:src"); + + var pollingScriptElm = getElementEnsureNotNull("#jsLongPoll script"); + String pollingScript = pollingScriptElm.data(); + int startIdx = pollingScript.indexOf("qrpoll(\""); + if (startIdx < 0) { log.warn("Failed to find 'qrpoll(\"' in jsLongPoll script:\n{}", pollingScript); throw new ComponentParseFailed(); } + startIdx += 8; + + int endIdx = pollingScript.indexOf("\");", startIdx); + if (endIdx < 0) { log.warn("Failed to find qrpoll terminator '\");' in jsLongPoll script:\n{}", pollingScript); throw new ComponentParseFailed(); } + + String pollingUriString = pollingScript.substring(startIdx, endIdx); + try { + this.pollingURI = ISNOTNULL(new URI(pollingScriptElm.baseUri()).resolve(pollingUriString)); + } catch (URISyntaxException e) { + log.warn("URI '{}' could not be parsed", pollingUriString); + throw new ComponentParseFailed(); + } } } @@ -104,6 +125,16 @@ public class ATrustParser { public final @Nonnull URI formTarget; public final @Nonnull Map formOptions = new HashMap<>(); + public static class NameValuePair { + public final @Nonnull String name; + public final @Nonnull String value; + public NameValuePair(@Nonnull String n, @Nonnull String v) { name = n; value = v; } + } + /** + * map: id -> (name, value) + */ + public final @Nonnull Map submitButtons = new HashMap<>(); + public @Nonnull Iterable> iterateFormOptions() { return ISNOTNULL(formOptions.entrySet()); } /* optional mode switch links (any number may or may not be null) */ @@ -182,9 +213,17 @@ public class ATrustParser { for (var input : mainForm.select("input")) { String name = input.attr("name"); - if (name.isEmpty()) - continue; - this.formOptions.put(name, input.attr("value")); + + /* special handling for submit inputs, they only get sent if they are "clicked" */ + if ("submit".equals(input.attr("type"))) { + if (name.isEmpty()) + this.submitButtons.put(input.attr("id"), null); + else + this.submitButtons.put(input.attr("id"), new NameValuePair(name, ISNOTNULL(input.attr("value")))); + } else { + if (!name.isEmpty()) + this.formOptions.put(name, input.attr("value")); + } } this.fido2Link = getHrefIfExists("#FidoButton"); -- cgit v1.2.3