diff --git a/.gitignore b/.gitignore index faac27b..edb6e81 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ hs_err_pid* replay_pid* /.idea -*.iml \ No newline at end of file +*.iml +/jzlint-server/target/ diff --git a/jzlint-ca/src/main/java/de/mtg/jzlint/ca/CreateCertificate008.java b/jzlint-ca/src/main/java/de/mtg/jzlint/ca/CreateCertificate008.java index a59c231..73b86a9 100644 --- a/jzlint-ca/src/main/java/de/mtg/jzlint/ca/CreateCertificate008.java +++ b/jzlint-ca/src/main/java/de/mtg/jzlint/ca/CreateCertificate008.java @@ -1,11 +1,33 @@ package de.mtg.jzlint.ca; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Random; + import org.bouncycastle.asn1.ASN1Encoding; import org.bouncycastle.asn1.ASN1ObjectIdentifier; -import org.bouncycastle.asn1.DERPrintableString; -import org.bouncycastle.asn1.DERSet; import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AuthorityInformationAccess; +import org.bouncycastle.asn1.x509.CertificatePolicies; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; @@ -14,17 +36,6 @@ import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import java.io.IOException; -import java.math.BigInteger; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.security.*; -import java.security.cert.X509Certificate; -import java.security.spec.AlgorithmParameterSpec; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.util.*; - /** * Certificates for lint: e_aia_ca_issuers_must_have_http_only diff --git a/jzlint-server/pom.xml b/jzlint-server/pom.xml new file mode 100644 index 0000000..763222f --- /dev/null +++ b/jzlint-server/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + de.mtg + jzlint-server + 1.0.1 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + + + + + de.mtg + jzlint + ${project.version} + + + + org.bouncycastle + bcprov-jdk18on + 1.78.1 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/CliUtils.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/CliUtils.java new file mode 100644 index 0000000..a7acef0 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/CliUtils.java @@ -0,0 +1,88 @@ +package de.mtg.jzlint.server; + +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.util.List; + +import de.mtg.jzlint.Source; + +public class CliUtils { + + public static final String CHECK_APPLIES = "checkApplies"; + public static final String EXECUTE = "execute"; + + private CliUtils() { + // empty + } + + public static boolean isCertificateIssuerLint(Class lintClass) { + try { + lintClass.getMethod(CHECK_APPLIES, X509Certificate.class, X509Certificate.class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static boolean isCRLIssuerLint(Class lintClass) { + try { + lintClass.getMethod(CHECK_APPLIES, X509CRL.class, X509Certificate.class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static boolean isOCSPResponseIssuerLint(Class lintClass) { + try { + lintClass.getMethod(CHECK_APPLIES, byte[].class, X509Certificate.class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static boolean isCertificateLint(Class lintClass) { + try { + lintClass.getMethod(CHECK_APPLIES, X509Certificate.class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static boolean isCRLLint(Class lintClass) { + try { + lintClass.getMethod(CHECK_APPLIES, X509CRL.class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static boolean isOCSPResponseLint(Class lintClass) { + try { + lintClass.getMethod(CHECK_APPLIES, byte[].class); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + + public static boolean includeLint(Source lintSource, List includeSources, List excludeSources) { + + boolean includeIsEmpty = includeSources == null || includeSources.isEmpty(); + boolean excludeIsEmpty = excludeSources == null || excludeSources.isEmpty(); + + if (!includeIsEmpty) { + return includeSources.contains(lintSource.getSourceName()); + } + + if (!excludeIsEmpty) { + return !excludeSources.contains(lintSource.getSourceName()); + } + + return true; + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/JZLintServer.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/JZLintServer.java new file mode 100644 index 0000000..fc31a33 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/JZLintServer.java @@ -0,0 +1,17 @@ +package de.mtg.jzlint.server; + +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class JZLintServer { + + public static void main(String[] args) { + Security.addProvider(new BouncyCastleProvider()); + SpringApplication.run(JZLintServer.class, args); + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/LintController.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/LintController.java new file mode 100644 index 0000000..332c632 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/LintController.java @@ -0,0 +1,78 @@ +package de.mtg.jzlint.server; + +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; +import java.util.concurrent.ForkJoinPool; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.async.DeferredResult; + +import de.mtg.jzlint.LintJSONResults; +import de.mtg.jzlint.utils.ParsedDomainNameUtils; + +@RestController +public class LintController { + + @Value("${request.timeout:15000}") + private long requestTimeout; + + @PostMapping(value = "/certificate/lint", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public DeferredResult> lintCertificate(@RequestBody TBLCertificate tblCertificate) { + DeferredResult> response = new DeferredResult<>(requestTimeout, new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + + ForkJoinPool.commonPool().submit(() -> { + try { + byte[] rawPKIObject = tblCertificate.getCertificate().getBytes(StandardCharsets.US_ASCII); + LintJSONResults lint = ServerUtils.lint(rawPKIObject, null, tblCertificate.getIncludeNames(), tblCertificate.getIncludeSources(), tblCertificate.getExcludeNames(), tblCertificate.getExcludeSources()); + X509Certificate certificate = ServerUtils.getCertificate(rawPKIObject); + ParsedDomainNameUtils.cleanCacheEntry(certificate); + response.setResult(new ResponseEntity<>(ServerUtils.convertResultToResponse(lint), HttpStatus.OK)); + } catch (Exception ex) { + response.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + }); + + return response; + } + + @PostMapping("/crl/lint") + DeferredResult> lintCRL(@RequestBody TBLCRL tblCrl) { + DeferredResult> response = new DeferredResult<>(requestTimeout, new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + + ForkJoinPool.commonPool().submit(() -> { + try { + byte[] rawPKIObject = tblCrl.getCrl().getBytes(StandardCharsets.US_ASCII); + LintJSONResults lint = ServerUtils.lint(rawPKIObject, null, tblCrl.getIncludeNames(), tblCrl.getIncludeSources(), tblCrl.getExcludeNames(), tblCrl.getExcludeSources()); + response.setResult(new ResponseEntity<>(ServerUtils.convertResultToResponse(lint), HttpStatus.OK)); + } catch (Exception ex) { + response.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + }); + + return response; + } + + @PostMapping("/ocspresponse/lint") + DeferredResult> lintOCSP(@RequestBody TBLOCPResponse tblocpResponse) { + DeferredResult> response = new DeferredResult<>(requestTimeout, new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + + ForkJoinPool.commonPool().submit(() -> { + try { + byte[] rawPKIObject = tblocpResponse.getOcspResponse().getBytes(StandardCharsets.US_ASCII); + LintJSONResults lint = ServerUtils.lint(rawPKIObject, null, tblocpResponse.getIncludeNames(), tblocpResponse.getIncludeSources(), tblocpResponse.getExcludeNames(), tblocpResponse.getExcludeSources()); + response.setResult(new ResponseEntity<>(ServerUtils.convertResultToResponse(lint), HttpStatus.OK)); + } catch (Exception ex) { + response.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); + } + }); + + return response; + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/LintResponse.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/LintResponse.java new file mode 100644 index 0000000..3df3d77 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/LintResponse.java @@ -0,0 +1,34 @@ +package de.mtg.jzlint.server; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class LintResponse { + + private List warnings; + + private List errors; + + public LintResponse() { + // empty + } + + public List getWarnings() { + return warnings; + } + + public void setWarnings(List warnings) { + this.warnings = warnings; + } + + public List getErrors() { + return errors; + } + + public void setErrors(List errors) { + this.errors = errors; + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/ServerUtils.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/ServerUtils.java new file mode 100644 index 0000000..25881e0 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/ServerUtils.java @@ -0,0 +1,215 @@ +package de.mtg.jzlint.server; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.NoSuchProviderException; +import java.security.cert.CRLException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509CRL; +import java.security.cert.X509Certificate; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.bouncycastle.asn1.ocsp.OCSPResponse; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import de.mtg.jzlint.IneffectiveDate; +import de.mtg.jzlint.Lint; +import de.mtg.jzlint.LintClassesContainer; +import de.mtg.jzlint.LintJSONResult; +import de.mtg.jzlint.LintJSONResults; +import de.mtg.jzlint.LintResult; +import de.mtg.jzlint.Source; +import de.mtg.jzlint.Status; +import de.mtg.jzlint.utils.DateUtils; + +public final class ServerUtils { + + public static LintResponse convertResultToResponse(final LintJSONResults results) { + Set keySet = results.getResult().keySet(); + List errors = new ArrayList<>(); + List warnings = new ArrayList<>(); + LintResponse lintResponse = new LintResponse(); + for (String key : keySet) { + String result = results.getResult().get(key).get("result"); + if ("error".equalsIgnoreCase(result)) { + errors.add(key); + } + if ("warning".equalsIgnoreCase(result)) { + warnings.add(key); + } + } + lintResponse.setErrors(errors); + lintResponse.setWarnings(warnings); + return lintResponse; + } + + + public static X509Certificate getCertificate(byte[] input) { + try (InputStream inputStream = new ByteArrayInputStream(input)) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME); + return (X509Certificate) certificateFactory.generateCertificate(inputStream); + } catch (IOException | CertificateException | NoSuchProviderException ex) { + return null; + } + } + + public static X509CRL getCRL(byte[] input) { + try (InputStream inputStream = new ByteArrayInputStream(input)) { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME); + return (X509CRL) certificateFactory.generateCRL(inputStream); + } catch (IOException | CertificateException | NoSuchProviderException | CRLException ex) { + return null; + } + } + + public static OCSPResponse getOCSPResponse(byte[] input) { + try { + return OCSPResponse.getInstance(input); + } catch (Exception ex) { + return null; + } + } + + public static LintJSONResults lint( + byte[] pkiObject, + byte[] issuer, + List includeNames, + List includeSources, + List excludeSources, + List excludeNames) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { + + LintClassesContainer lintClassesContainer = LintClassesContainer.getInstance(); + List> lintClasses = lintClassesContainer.getLintClasses(); + + List result = new ArrayList<>(); + + boolean hasIssuer = (issuer != null && issuer.length > 0); + X509Certificate certificate = getCertificate(pkiObject); + X509CRL crl = getCRL(pkiObject); + boolean isCertificate = certificate != null; + boolean isCRL = crl != null; + OCSPResponse ocspResponse = getOCSPResponse(pkiObject); + boolean isOCSP = ocspResponse != null; + X509Certificate issuerCertificate = null; + if (hasIssuer) { + issuerCertificate = getCertificate(issuer); + } + + for (Class lintClass : lintClasses) { + + if (!lintClass.isAnnotationPresent(Lint.class)) { + continue; + } + + Lint lintAnnotation = lintClass.getAnnotation(Lint.class); + + String lintName = lintAnnotation.name(); + + if (includeNames != null && !includeNames.isEmpty() && !includeNames.contains(lintName)) { + continue; + } + + if (excludeNames != null && !excludeNames.isEmpty() && excludeNames.contains(lintName)) { + continue; + } + + Source source = lintAnnotation.source(); + if (!CliUtils.includeLint(source, includeSources, excludeSources)) { + continue; + } + + boolean isCertificateIssuerLint = CliUtils.isCertificateIssuerLint(lintClass); + boolean isCRLIssuerLint = CliUtils.isCRLIssuerLint(lintClass); + boolean isOCSPResponseIssuerLint = CliUtils.isOCSPResponseIssuerLint(lintClass); + + if (isCertificate) { + ZonedDateTime time = DateUtils.getNotBefore(certificate); + if (hasIssuer && isCertificateIssuerLint) { + result.add(getLintResult(certificate, issuerCertificate, time, X509Certificate.class, lintClass, lintAnnotation)); + } else if (CliUtils.isCertificateLint(lintClass)) { + result.add(getLintResult(certificate, null, time, X509Certificate.class, lintClass, lintAnnotation)); + } + } + + if (isCRL) { + ZonedDateTime time = DateUtils.getThisUpdate(crl); + if (hasIssuer && isCRLIssuerLint) { + result.add(getLintResult(crl, issuerCertificate, time, X509CRL.class, lintClass, lintAnnotation)); + } else if (CliUtils.isCRLLint(lintClass)) { + result.add(getLintResult(crl, null, time, X509CRL.class, lintClass, lintAnnotation)); + } + } + + if (isOCSP) { + ZonedDateTime time = DateUtils.getProducedAt(ocspResponse); + if (hasIssuer && isOCSPResponseIssuerLint) { + result.add(getLintResult(pkiObject, issuerCertificate, time, byte[].class, lintClass, lintAnnotation)); + } else if (CliUtils.isOCSPResponseLint(lintClass)) { + result.add(getLintResult(pkiObject, null, time, byte[].class, lintClass, lintAnnotation)); + } + } + } + + return new LintJSONResults(result); + } + + + public static LintJSONResult getLintResult( + Object pkiObject, + X509Certificate issuer, + ZonedDateTime time, + Class pkiObjectClass, + Class lintClass, + Lint lintAnnotation) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + + Method checkAppliesMethod; + Method executeMethod; + if (issuer == null) { + checkAppliesMethod = lintClass.getMethod(CliUtils.CHECK_APPLIES, pkiObjectClass); + executeMethod = lintClass.getMethod(CliUtils.EXECUTE, pkiObjectClass); + } else { + checkAppliesMethod = lintClass.getMethod(CliUtils.CHECK_APPLIES, pkiObjectClass, issuer.getClass()); + executeMethod = lintClass.getMethod(CliUtils.EXECUTE, pkiObjectClass, issuer.getClass()); + } + + Object object = lintClass.getDeclaredConstructor().newInstance(); + + boolean checkApplies; + if (issuer == null) { + checkApplies = (boolean) checkAppliesMethod.invoke(object, pkiObject); + } else { + checkApplies = (boolean) checkAppliesMethod.invoke(object, pkiObject, issuer); + } + + if (!checkApplies) { + return new LintJSONResult(lintAnnotation.name(), Status.NA); + } + + if (!DateUtils.isIssuedOnOrAfter(time, lintAnnotation.effectiveDate().getZonedDateTime())) { + return new LintJSONResult(lintAnnotation.name(), Status.NE); + } + + if (IneffectiveDate.EMPTY != lintAnnotation.ineffectiveDate() && + DateUtils.isIssuedOnOrAfter(time, lintAnnotation.ineffectiveDate().getZonedDateTime())) { + return new LintJSONResult(lintAnnotation.name(), Status.NE); + } + + LintResult lintResult; + if (issuer == null) { + lintResult = (LintResult) executeMethod.invoke(object, pkiObject); + } else { + lintResult = (LintResult) executeMethod.invoke(object, pkiObject, issuer); + } + + return new LintJSONResult(lintAnnotation.name(), lintResult.getStatus()); + + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLCRL.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLCRL.java new file mode 100644 index 0000000..b0afd19 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLCRL.java @@ -0,0 +1,62 @@ +package de.mtg.jzlint.server; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TBLCRL { + + private String crl; + + private List includeNames; + private List includeSources; + private List excludeSources; + private List excludeNames; + + + public TBLCRL() { + // empty + } + + public String getCrl() { + return crl; + } + + public void setCrl(String crl) { + this.crl = crl; + } + + public List getIncludeNames() { + return includeNames; + } + + public void setIncludeNames(List includeNames) { + this.includeNames = includeNames; + } + + public List getIncludeSources() { + return includeSources; + } + + public void setIncludeSources(List includeSources) { + this.includeSources = includeSources; + } + + public List getExcludeSources() { + return excludeSources; + } + + public void setExcludeSources(List excludeSources) { + this.excludeSources = excludeSources; + } + + public List getExcludeNames() { + return excludeNames; + } + + public void setExcludeNames(List excludeNames) { + this.excludeNames = excludeNames; + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLCertificate.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLCertificate.java new file mode 100644 index 0000000..e9b9a50 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLCertificate.java @@ -0,0 +1,61 @@ +package de.mtg.jzlint.server; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TBLCertificate { + + private String certificate; + + private List includeNames; + private List includeSources; + private List excludeSources; + private List excludeNames; + + public TBLCertificate() { + // empty + } + + public String getCertificate() { + return certificate; + } + + public void setCertificate(String certificate) { + this.certificate = certificate; + } + + public List getIncludeNames() { + return includeNames; + } + + public void setIncludeNames(List includeNames) { + this.includeNames = includeNames; + } + + public List getIncludeSources() { + return includeSources; + } + + public void setIncludeSources(List includeSources) { + this.includeSources = includeSources; + } + + public List getExcludeSources() { + return excludeSources; + } + + public void setExcludeSources(List excludeSources) { + this.excludeSources = excludeSources; + } + + public List getExcludeNames() { + return excludeNames; + } + + public void setExcludeNames(List excludeNames) { + this.excludeNames = excludeNames; + } + +} diff --git a/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLOCPResponse.java b/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLOCPResponse.java new file mode 100644 index 0000000..3a2b550 --- /dev/null +++ b/jzlint-server/src/main/java/de/mtg/jzlint/server/TBLOCPResponse.java @@ -0,0 +1,62 @@ +package de.mtg.jzlint.server; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class TBLOCPResponse { + + private String ocspResponse; + + private List includeNames; + private List includeSources; + private List excludeSources; + private List excludeNames; + + + public TBLOCPResponse() { + // empty + } + + public String getOcspResponse() { + return ocspResponse; + } + + public void setOcspResponse(String ocspResponse) { + this.ocspResponse = ocspResponse; + } + + public List getIncludeNames() { + return includeNames; + } + + public void setIncludeNames(List includeNames) { + this.includeNames = includeNames; + } + + public List getIncludeSources() { + return includeSources; + } + + public void setIncludeSources(List includeSources) { + this.includeSources = includeSources; + } + + public List getExcludeSources() { + return excludeSources; + } + + public void setExcludeSources(List excludeSources) { + this.excludeSources = excludeSources; + } + + public List getExcludeNames() { + return excludeNames; + } + + public void setExcludeNames(List excludeNames) { + this.excludeNames = excludeNames; + } + +} diff --git a/jzlint-server/src/main/resources/application.properties b/jzlint-server/src/main/resources/application.properties new file mode 100644 index 0000000..f5365fb --- /dev/null +++ b/jzlint-server/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.servlet.encoding.enabled=false +server.servlet.encoding.force=false +server.servlet.encoding.force-request=false +server.servlet.encoding.force-response=false \ No newline at end of file