forked from asit-asso/extract
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
asit-asso#303 - Géneration du QR code sans passer par l'API Image Charts
- Loading branch information
Showing
6 changed files
with
429 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
134 changes: 134 additions & 0 deletions
134
extract/src/main/java/ch/asit_asso/extract/utils/ImageUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package ch.asit_asso.extract.utils; | ||
|
||
import java.awt.image.BufferedImage; | ||
import java.io.ByteArrayInputStream; | ||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
import java.net.URL; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import java.util.regex.Matcher; | ||
import java.util.regex.Pattern; | ||
import javax.imageio.ImageIO; | ||
import javax.validation.constraints.NotNull; | ||
import io.micrometer.core.instrument.util.StringUtils; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public abstract class ImageUtils { | ||
|
||
private static final Logger logger = LoggerFactory.getLogger(ImageUtils.class); | ||
|
||
private static final String DATA_URL_PREFIX = "data:"; | ||
|
||
private static final Pattern DATA_URL_REGEX = Pattern.compile("^data:(?<mimeType>[^;]+);(?<encoding>[^,]+),(?<content>.+)$"); | ||
|
||
|
||
|
||
public static boolean checkUrl(@NotNull String url) { | ||
ImageUtils.logger.debug("Checking image URL {}", url); | ||
|
||
if (url.startsWith(ImageUtils.DATA_URL_PREFIX)) { | ||
ImageUtils.logger.debug("The URL is a data URL"); | ||
|
||
return ImageUtils.checkDataUrl(url); | ||
} | ||
|
||
try { | ||
BufferedImage image = ImageIO.read(new URL(url)); | ||
|
||
if (image != null) { | ||
ImageUtils.logger.debug("Image successfully read from the URL."); | ||
return true; | ||
} | ||
|
||
ImageUtils.logger.debug("Image read from the URL is null."); | ||
// TODO Check for SVG | ||
return false; | ||
|
||
} catch (IOException exception) { | ||
ImageUtils.logger.debug(String.format("Checking URL %s produced an error.", url), exception); | ||
return false; | ||
} | ||
} | ||
|
||
|
||
|
||
public static @NotNull String encodeToBase64(BufferedImage image) { | ||
|
||
byte[] bytes; | ||
|
||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { | ||
ImageIO.write(image, "PNG", outputStream); | ||
bytes = outputStream.toByteArray(); | ||
|
||
} catch (IOException ioException) { | ||
throw new RuntimeException("The 2FA registration QR code generation failed.", ioException); | ||
} | ||
|
||
return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); | ||
} | ||
|
||
|
||
|
||
|
||
private static boolean checkDataUrl(@NotNull String url) { | ||
Matcher dataMatcher = ImageUtils.DATA_URL_REGEX.matcher(url); | ||
|
||
if (!dataMatcher.find()) { | ||
ImageUtils.logger.debug("The URL passed is not a valid image data URL."); | ||
return false; | ||
} | ||
|
||
String mimeType = dataMatcher.group("mimeType"); | ||
|
||
if (mimeType == null || !mimeType.startsWith("image/")) { | ||
ImageUtils.logger.debug("The MIME type in the data URL is not an image."); | ||
return false; | ||
} | ||
|
||
String encoding = dataMatcher.group("encoding"); | ||
|
||
if (!"base64".equals(encoding)) { | ||
ImageUtils.logger.debug("The data in the URL is not Base64 encoded."); | ||
return false; | ||
} | ||
|
||
String base64Content = dataMatcher.group("content"); | ||
|
||
if (StringUtils.isEmpty(base64Content)) { | ||
ImageUtils.logger.debug("No content in data URL."); | ||
return false; | ||
} | ||
|
||
return ImageUtils.loadImageFromBase64(base64Content); | ||
} | ||
|
||
|
||
|
||
private static boolean loadImageFromBase64(@NotNull String base64String) { | ||
|
||
Base64.Decoder decoder = Base64.getDecoder(); | ||
byte[] imageByte; | ||
|
||
try { | ||
imageByte = decoder.decode(base64String); | ||
|
||
} catch (IllegalArgumentException invalidBase64Exception) { | ||
ImageUtils.logger.debug("The content is not a valid Base64-encoded string.", invalidBase64Exception); | ||
return false; | ||
} | ||
|
||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(imageByte)) { | ||
BufferedImage image = ImageIO.read(inputStream); | ||
|
||
ImageUtils.logger.debug("Image read from Base64 string is {}null", (image == null) ? "" : "NOT "); | ||
|
||
return (image != null); | ||
|
||
} catch (IOException exception) { | ||
ImageUtils.logger.debug("Checking data string produced an error.", exception); | ||
return false; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...est/java/ch/asit_asso/extract/unit/authentication/twofactor/TwoFactorApplicationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package ch.asit_asso.extract.unit.authentication.twofactor; | ||
|
||
import java.util.concurrent.atomic.AtomicReference; | ||
import ch.asit_asso.extract.authentication.twofactor.TwoFactorApplication; | ||
import ch.asit_asso.extract.authentication.twofactor.TwoFactorService; | ||
import ch.asit_asso.extract.domain.User; | ||
import ch.asit_asso.extract.unit.MockEnabledTest; | ||
import ch.asit_asso.extract.utils.ImageUtils; | ||
import ch.asit_asso.extract.utils.Secrets; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Tag; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mock; | ||
import org.mockito.Mockito; | ||
import org.mockito.stubbing.Answer; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; | ||
import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.mockito.ArgumentMatchers.anyString; | ||
import static org.mockito.Mockito.times; | ||
|
||
@Tag("unit") | ||
public class TwoFactorApplicationTest extends MockEnabledTest { | ||
|
||
private TwoFactorApplication application; | ||
|
||
@Mock | ||
private Secrets secrets; | ||
|
||
@Mock | ||
private TwoFactorService service; | ||
|
||
private User user; | ||
|
||
@BeforeEach | ||
public void setUp() { | ||
this.user = new User(1); | ||
this.user.setLogin("testUser"); | ||
|
||
Mockito.when(this.secrets.encrypt(anyString())).thenAnswer( | ||
(Answer<String>) invocationOnMock -> invocationOnMock.getArgument(0) | ||
); | ||
|
||
Mockito.when(this.secrets.decrypt(anyString())).thenAnswer( | ||
(Answer<String>) invocationOnMock -> invocationOnMock.getArgument(0) | ||
); | ||
|
||
this.application = new TwoFactorApplication(this.user, this.secrets, this.service); | ||
} | ||
|
||
@Test | ||
@DisplayName("Generate QR code") | ||
public void getQrCodeUrl() { | ||
this.user.setTwoFactorStatus(User.TwoFactorStatus.INACTIVE); | ||
this.application.enable(); | ||
|
||
AtomicReference<String> url = new AtomicReference<>(); | ||
|
||
assertDoesNotThrow(() -> url.set(this.application.getQrCodeUrl())); | ||
|
||
Mockito.verify(this.secrets, times(1)).decrypt(anyString()); | ||
assertNotNull(url); | ||
assertTrue(ImageUtils.checkUrl(url.get()), "The resulting URL could not be loaded as an image."); | ||
} | ||
} |
Oops, something went wrong.