mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
implement auth47 authentication through platform uri registration
This commit is contained in:
parent
192657fa69
commit
08934d3c3c
7 changed files with 304 additions and 7 deletions
|
@ -218,7 +218,7 @@ jlink {
|
||||||
appVersion = "${sparrowVersion}"
|
appVersion = "${sparrowVersion}"
|
||||||
skipInstaller = os.macOsX || properties.skipInstallers
|
skipInstaller = os.macOsX || properties.skipInstallers
|
||||||
imageOptions = []
|
imageOptions = []
|
||||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--license-file', 'LICENSE']
|
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--license-file', 'LICENSE']
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||||
|
|
2
src/main/deploy/auth47.properties
Normal file
2
src/main/deploy/auth47.properties
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mime-type=x-scheme-handler/auth47
|
||||||
|
description=Auth47 Authentication URI
|
|
@ -6,4 +6,4 @@ Icon=/opt/sparrow/lib/Sparrow.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Unknown
|
Categories=Unknown
|
||||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin
|
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47
|
|
@ -45,6 +45,14 @@
|
||||||
<string>bitcoin</string>
|
<string>bitcoin</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.sparrowwallet.sparrow.auth47</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>auth47</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>UTImportedTypeDeclarations</key>
|
<key>UTImportedTypeDeclarations</key>
|
||||||
<array>
|
<array>
|
||||||
|
|
|
@ -77,6 +77,16 @@
|
||||||
|
|
||||||
<DirectoryRef Id="TARGETDIR">
|
<DirectoryRef Id="TARGETDIR">
|
||||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||||
|
<RegistryKey Root="HKCR" Key="auth47" Action="createAndRemoveOnUninstall">
|
||||||
|
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||||
|
<RegistryValue Type="string" Value="URL:Auth47 Authentication URI"/>
|
||||||
|
<RegistryKey Key="DefaultIcon">
|
||||||
|
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||||
|
</RegistryKey>
|
||||||
|
<RegistryKey Key="shell\open\command">
|
||||||
|
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||||
|
</RegistryKey>
|
||||||
|
</RegistryKey>
|
||||||
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
|
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
|
||||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||||
|
|
|
@ -4,6 +4,8 @@ import com.google.common.eventbus.Subscribe;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.Network;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.net.Auth47;
|
||||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
@ -27,7 +29,6 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
@ -47,6 +48,7 @@ import javafx.stage.Window;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.berndpruenster.netlayer.tor.Tor;
|
import org.berndpruenster.netlayer.tor.Tor;
|
||||||
import org.controlsfx.control.HyperlinkLabel;
|
import org.controlsfx.control.HyperlinkLabel;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -720,6 +722,14 @@ public class AppServices {
|
||||||
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
|
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Optional<ButtonType> showSuccessDialog(String title, String content, ButtonType... buttons) {
|
||||||
|
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||||
|
successGlyph.getStyleClass().add("success");
|
||||||
|
successGlyph.setFontSize(50);
|
||||||
|
|
||||||
|
return showAlertDialog(title, content, Alert.AlertType.INFORMATION, successGlyph, buttons);
|
||||||
|
}
|
||||||
|
|
||||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||||
return showAlertDialog(title, content, alertType, null, buttons);
|
return showAlertDialog(title, content, alertType, null, buttons);
|
||||||
}
|
}
|
||||||
|
@ -863,6 +873,8 @@ public class AppServices {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if("bitcoin".equals(uri.getScheme())) {
|
if("bitcoin".equals(uri.getScheme())) {
|
||||||
openBitcoinUri(uri);
|
openBitcoinUri(uri);
|
||||||
|
} else if(("auth47").equals(uri.getScheme())) {
|
||||||
|
openAuth47Uri(uri);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -885,7 +897,7 @@ public class AppServices {
|
||||||
private static void openBitcoinUri(URI uri) {
|
private static void openBitcoinUri(URI uri) {
|
||||||
try {
|
try {
|
||||||
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
||||||
Wallet wallet = selectWallet(null, "pay from");
|
Wallet wallet = selectWallet(null, null, "pay from");
|
||||||
|
|
||||||
if(wallet != null) {
|
if(wallet != null) {
|
||||||
final Wallet sendingWallet = wallet;
|
final Wallet sendingWallet = wallet;
|
||||||
|
@ -897,11 +909,30 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
|
public static void openAuth47Uri(URI uri) {
|
||||||
|
try {
|
||||||
|
Auth47 auth47 = new Auth47(uri);
|
||||||
|
Wallet wallet = selectWallet(null, Boolean.TRUE, "authenticate using your payment code");
|
||||||
|
|
||||||
|
if(wallet != null) {
|
||||||
|
try {
|
||||||
|
auth47.sendResponse(wallet);
|
||||||
|
showSuccessDialog("Successful authentication", "Successfully authenticated to " + auth47.getCallback() + ".");
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error authenticating auth47 URI", e);
|
||||||
|
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
showErrorDialog("Not a valid auth47 URI", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Wallet selectWallet(ScriptType scriptType, Boolean hasPaymentCode, String actionDescription) {
|
||||||
Wallet wallet = null;
|
Wallet wallet = null;
|
||||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
|
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> (scriptType == null || w.getScriptType() == scriptType) && (hasPaymentCode == null || w.hasPaymentCode())).collect(Collectors.toList());
|
||||||
if(wallets.isEmpty()) {
|
if(wallets.isEmpty()) {
|
||||||
showErrorDialog("No wallet available", "Open a" + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
showErrorDialog("No wallet available", "Open a" + (hasPaymentCode == null ? "" : " software") + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
||||||
} else if(wallets.size() == 1) {
|
} else if(wallets.size() == 1) {
|
||||||
wallet = wallets.iterator().next();
|
wallet = wallets.iterator().next();
|
||||||
} else {
|
} else {
|
||||||
|
|
246
src/main/java/com/sparrowwallet/sparrow/net/Auth47.java
Normal file
246
src/main/java/com/sparrowwallet/sparrow/net/Auth47.java
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class Auth47 {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Auth47.class);
|
||||||
|
|
||||||
|
public static final String SCHEME = "auth47";
|
||||||
|
public static final String VERSION = "1.0";
|
||||||
|
public static final String HTTPS_PROTOCOL = "https://";
|
||||||
|
public static final String SRBN_PROTOCOL = "srbn://";
|
||||||
|
|
||||||
|
private final String nonce;
|
||||||
|
private final URL callback;
|
||||||
|
private final String expiry;
|
||||||
|
private boolean srbn;
|
||||||
|
private String srbnName;
|
||||||
|
private String resource;
|
||||||
|
|
||||||
|
public Auth47(URI uri) throws MalformedURLException {
|
||||||
|
this.nonce = uri.getHost();
|
||||||
|
|
||||||
|
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||||
|
String query = uri.getRawQuery();
|
||||||
|
if(query == null) {
|
||||||
|
throw new IllegalArgumentException("No callback parameter provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(query.startsWith("?")) {
|
||||||
|
query = query.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] pairs = query.split("&");
|
||||||
|
for(String pair : pairs) {
|
||||||
|
int idx = pair.indexOf("=");
|
||||||
|
if(idx < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parameterMap.put(pair.substring(0, idx), pair.substring(idx + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
String strCallback = parameterMap.get("c");
|
||||||
|
if(strCallback == null) {
|
||||||
|
throw new IllegalArgumentException("No callback parameter provided.");
|
||||||
|
}
|
||||||
|
if(strCallback.startsWith(SRBN_PROTOCOL)) {
|
||||||
|
String srbnCallback = HTTPS_PROTOCOL + strCallback.substring(SRBN_PROTOCOL.length());
|
||||||
|
URL srbnUrl = new URL(srbnCallback);
|
||||||
|
this.srbn = true;
|
||||||
|
this.srbnName = srbnUrl.getUserInfo();
|
||||||
|
this.callback = new URL(HTTPS_PROTOCOL + srbnUrl.getHost());
|
||||||
|
} else {
|
||||||
|
this.callback = new URL(strCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.expiry = parameterMap.get("e");
|
||||||
|
this.resource = parameterMap.get("r");
|
||||||
|
if(resource == null) {
|
||||||
|
if(srbn) {
|
||||||
|
this.resource = "srbn";
|
||||||
|
} else if(strCallback.startsWith("http")) {
|
||||||
|
this.resource = strCallback;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid callback parameter (not http/s or srbn): " + strCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendResponse(Wallet wallet) throws IOException, Auth47Exception {
|
||||||
|
if(!wallet.hasPaymentCode()) {
|
||||||
|
throw new Auth47Exception("A software wallet is required to authenticate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(srbn) {
|
||||||
|
sendSorobanResponse(wallet);
|
||||||
|
} else {
|
||||||
|
sendHttpResponse(wallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendHttpResponse(Wallet wallet) throws IOException, Auth47Exception {
|
||||||
|
String json = getJsonResponse(wallet);
|
||||||
|
|
||||||
|
send(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendSorobanResponse(Wallet wallet) throws IOException, Auth47Exception {
|
||||||
|
String json = getJsonResponse(wallet);
|
||||||
|
|
||||||
|
SorobanResponse sorobanResponse = new SorobanResponse(srbnName, json);
|
||||||
|
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||||
|
String jsonRpc = gson.toJson(sorobanResponse);
|
||||||
|
|
||||||
|
send(jsonRpc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getJsonResponse(Wallet wallet) {
|
||||||
|
String challenge = getChallenge();
|
||||||
|
String signature = sign(wallet, challenge);
|
||||||
|
|
||||||
|
Response response = new Response(VERSION, challenge, signature, wallet.getPaymentCode().toString(), null);
|
||||||
|
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||||
|
return gson.toJson(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send(String json) throws IOException, Auth47Exception {
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.debug("Sending " + json + " to " + callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
Proxy proxy = AppServices.getProxy();
|
||||||
|
if(proxy == null && callback.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX)) {
|
||||||
|
throw new Auth47Exception("A Tor proxy must be configured to authenticate this resource.");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection connection = proxy == null ? (HttpURLConnection) callback.openConnection() : (HttpURLConnection) callback.openConnection(proxy);
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
|
try(OutputStream os = connection.getOutputStream()) {
|
||||||
|
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||||
|
os.write(jsonBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
int statusCode = connection.getResponseCode();
|
||||||
|
if(statusCode == 404) {
|
||||||
|
throw new Auth47Exception("Could not authenticate. Callback URL of " + callback + " returned a 404 response.");
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder res = new StringBuilder();
|
||||||
|
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String responseLine;
|
||||||
|
while((responseLine = br.readLine()) != null) {
|
||||||
|
res.append(responseLine.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.debug("Received " + statusCode + " " + res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(statusCode < 200 || statusCode >= 300) {
|
||||||
|
throw new Auth47Exception("Could not authenticate. Server returned " + res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sign(Wallet wallet, String challenge) {
|
||||||
|
try {
|
||||||
|
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||||
|
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||||
|
ExtendedKey extendedPrivateKey = notificationWallet.getKeystores().get(0).getBip47ExtendedPrivateKey();
|
||||||
|
List<ChildNumber> derivation = new ArrayList<>();
|
||||||
|
derivation.add(extendedPrivateKey.getKeyChildNumber());
|
||||||
|
derivation.addAll(notificationNode.getDerivation());
|
||||||
|
ECKey privKey = extendedPrivateKey.getKey(derivation);
|
||||||
|
return privKey.signMessage(challenge, ScriptType.P2PKH);
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error signing auth47 challenge", e);
|
||||||
|
throw new IllegalStateException("Error signing auth47 challenge: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getChallenge() {
|
||||||
|
return SCHEME + "://" + nonce + "?r=" + resource + (expiry == null ? "" : "&e=" + expiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public URL getCallback() {
|
||||||
|
return callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Response {
|
||||||
|
public Response(String version, String challenge, String signature, String paymentCode, String address) {
|
||||||
|
this.auth47_response = version;
|
||||||
|
this.challenge = challenge;
|
||||||
|
this.signature = signature;
|
||||||
|
this.nym = paymentCode;
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String auth47_response;
|
||||||
|
public String challenge;
|
||||||
|
public String signature;
|
||||||
|
public String nym;
|
||||||
|
public String address;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SorobanResponse {
|
||||||
|
public SorobanResponse(String name, String response) {
|
||||||
|
params.add(new SorobanParam(name, response));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String jsonrpc = "2.0";
|
||||||
|
public int id = 0;
|
||||||
|
public String method = "directory.Add";
|
||||||
|
public List<SorobanParam> params = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SorobanParam {
|
||||||
|
public SorobanParam(String name, String entry) {
|
||||||
|
Name = name;
|
||||||
|
Entry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String Name;
|
||||||
|
public String Entry;
|
||||||
|
public String Mode = "short";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Auth47Exception extends Exception {
|
||||||
|
public Auth47Exception(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Auth47Exception(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Auth47Exception(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Auth47Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue