├── src ├── test │ ├── resources │ │ ├── server │ │ │ └── libe │ │ │ │ ├── libe.srl │ │ │ │ ├── libe.der │ │ │ │ ├── libe.jks │ │ │ │ ├── libe.p12 │ │ │ │ ├── libe.csr │ │ │ │ ├── libe.key.unprotected │ │ │ │ ├── libe.crt │ │ │ │ ├── libe.key │ │ │ │ └── libe.pem │ │ ├── clients │ │ │ └── libe │ │ │ │ ├── libe.der │ │ │ │ ├── libe.jks │ │ │ │ ├── libe.p12 │ │ │ │ ├── libe.key.der │ │ │ │ ├── libe.csr │ │ │ │ ├── libe.key │ │ │ │ ├── libe.crt │ │ │ │ └── libe.pem │ │ ├── scripts │ │ │ ├── checkClientJks │ │ │ ├── checkServerJks │ │ │ ├── cleanAll │ │ │ ├── mkca │ │ │ ├── vars │ │ │ ├── mkclient │ │ │ └── mkserver │ │ ├── test.sh │ │ ├── form.sh │ │ └── ca │ │ │ ├── fakeCa.key │ │ │ └── fakeCa.crt │ └── java │ │ └── org │ │ └── toilelibre │ │ └── libe │ │ ├── outside │ │ ├── curl │ │ │ ├── CurlWebTest.java │ │ │ ├── CurlFakeTest.java │ │ │ └── CurlTest.java │ │ └── monitor │ │ │ ├── StupidHttpServer.java │ │ │ └── RequestMonitor.java │ │ └── curl │ │ ├── CachedArgumentsTest.java │ │ ├── HttpRequestProviderTest.java │ │ └── ArgumentsBuilderGeneratorTest.java └── main │ └── java │ └── org │ └── toilelibre │ └── libe │ └── curl │ ├── Version.java │ ├── UglyVersionDisplay.java │ ├── HttpClientBuilder.java │ ├── AfterResponse.java │ ├── AuthMethodHandler.java │ ├── PemReader.java │ ├── HttpClientProvider.java │ ├── PayloadReader.java │ ├── DerReader.java │ ├── ReadArguments.java │ ├── IOUtils.java │ ├── CertFormat.java │ ├── InterceptorsBinder.java │ ├── MockNetworkAccess.java │ ├── Arguments.java │ ├── HttpRequestProvider.java │ ├── Curl.java │ └── SSLMaterialCreator.java ├── .gitignore ├── .github ├── main.workflow ├── workflows │ └── maven.yml ├── ISSUE_TEMPLATE │ ├── Parameter_request.md │ └── Bug_report.md └── dependabot.yml ├── LICENSE ├── pom.xml └── README.md /src/test/resources/server/libe/libe.srl: -------------------------------------------------------------------------------- 1 | 8B6EAE37D557AA0E 2 | -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/clients/libe/libe.der -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/clients/libe/libe.jks -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/clients/libe/libe.p12 -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/server/libe/libe.der -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/server/libe/libe.jks -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/server/libe/libe.p12 -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/libetl/curl/HEAD/src/test/resources/clients/libe/libe.key.der -------------------------------------------------------------------------------- /src/test/resources/scripts/checkClientJks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . ./vars 3 | keytool -keystore ../clients/$client_alias/$client_alias.jks -list -storepass $client_pass 4 | -------------------------------------------------------------------------------- /src/test/resources/scripts/checkServerJks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . ./vars 3 | keytool -keystore ../server/$server_alias/$server_alias.jks -list -storepass $server_pass 4 | -------------------------------------------------------------------------------- /src/test/resources/scripts/cleanAll: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -rf ../ca 2>/dev/null || true 4 | rm -rf ../server 2>/dev/null || true 5 | rm -rf ../clients 2>/dev/null || true 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings/ 4 | target/ 5 | bin/ 6 | .idea 7 | *.iml 8 | *.versionsBackup 9 | *.releaseBackup 10 | release.properties 11 | classes/ 12 | .vscode 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/Version.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | final class Version { 4 | static String NUMBER = "0.0.45-SNAPSHOT"; 5 | static String BUILD_TIME = "2025-12-04T11:13:04Z"; 6 | } 7 | -------------------------------------------------------------------------------- /src/test/resources/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -k --cert clients/libe/libe.pem:mylibepass https://localhost:$(netstat -tlp 2>&1 | grep java | tr ' ' '\n' | sort | uniq | grep -e '^\[::\]:[0-9]' | sed 's/[^0-9]//g')/public/ 3 | echo 4 | -------------------------------------------------------------------------------- /src/test/resources/form.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | curl -k -E clients/libe/libe.pem https://localhost:$(netstat -tlp 2>&1 | grep java | tr ' ' '\n' | sort | uniq | grep -e '^\[::\]:[0-9]' | sed 's/[^0-9]//g')/public/form -F 'toto=titi' -F 'tata=tutu;foo=bar' 3 | echo 4 | -------------------------------------------------------------------------------- /src/test/resources/scripts/mkca: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./vars 3 | 4 | mkdir -p ../ca/ 5 | openssl genrsa -des3 -passout pass:$ca_pass -out ../ca/fakeCa.key 1024 6 | openssl req -new -x509 -passin pass:$ca_pass -key ../ca/fakeCa.key -out ../ca/fakeCa.crt -subj "$subject" -days 36000 7 | -------------------------------------------------------------------------------- /src/test/resources/scripts/vars: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | subject="//dummy=1/C=FR/ST=IDF/L=PARIS/O=TOILE-LIBRE/OU=LIBE/CN=localhost/emailAddress=server@localdomain" 4 | 5 | server_alias=libe 6 | client_alias=libe 7 | 8 | ca_pass=mycapass 9 | server_pass=myserverpass 10 | client_pass=my"$client_alias"pass 11 | -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "build" { 2 | resolves = ["GitHub Action for Maven & JDK 14"] 3 | on = "push" 4 | } 5 | 6 | workflow "pull request" { 7 | resolves = ["GitHub Action for Maven & JDK 14"] 8 | on = "pull_request" 9 | } 10 | 11 | action "GitHub Action for Maven & JDK 14" { 12 | uses = "xlui/action-maven-cli/jdk14@master" 13 | args = "test" 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up JDK 17 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 17 16 | - name: Build with Maven 17 | run: mvn -B package --file pom.xml 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Parameter_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Parameter request 3 | about: Ask me a new parameter, or a missing requirement in a parameter 4 | 5 | --- 6 | 7 | **Short name of the parameter / Long name of the parameter ** 8 | Ex: -d / --data 9 | 10 | **What is the purpose of that parameter ? (in case of a new one)** 11 | A clear and concise description of what is the role of the parameter (Ex: allows to pass a request body) 12 | 13 | **Needs argument ? Y/N** 14 | Ex : Y 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the parameter request here. 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/UglyVersionDisplay.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import static org.toilelibre.libe.curl.Version.*; 4 | 5 | final class UglyVersionDisplay { 6 | 7 | 8 | static void stopAndDisplayVersionIfThe (boolean isTrue) { 9 | if (!isTrue) { 10 | return; 11 | } 12 | 13 | System.out.println (Curl.class.getPackage ().getName () + " version " + NUMBER + ", build-time : " + BUILD_TIME); 14 | 15 | throw new Curl.CurlException ( 16 | new IllegalArgumentException ( 17 | "You asked me to display the version. Probably not a production-ready code")); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIByDCCATECAQAwgYcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAMBgNV 3 | BAcMBVBBUklTMRQwEgYDVQQKDAtUT0lMRS1MSUJSRTENMAsGA1UECwwETElCRTES 4 | MBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAbG9jYWxk 5 | b21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKkdyrS373dPGgJU3wCW 6 | IuUgTXrogktmNfleSd/XL3jDRRoQFc9KieMKLPYORhUskgzixxeXGk3QakIe3tMx 7 | xxw43ZPiaiqjlfagXQUJTAkSWoGKf4MzgeLagWilNt24wXQH2MfMnGl4X3ZjlOum 8 | ift9/0zMOYEYS82egxNu1wg5AgMBAAGgADANBgkqhkiG9w0BAQsFAAOBgQB3Cbzo 9 | jyqDaUVyTeWfTh/IdKsK6t+ZP8YiJG/RT5r0fG7hpjBQuwF6lTkjMvGWE9XTuJXB 10 | 2cZhY5KyNtGYtPYAL0sBH7kWwYfPuzbrL7QIK0Btc5oIIhANFnAV7KO9oKwROH5y 11 | +AtNbaYHAe454iKcbe1eF2cFARmNxCvkQG83+Q== 12 | -----END CERTIFICATE REQUEST----- 13 | -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIByDCCATECAQAwgYcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAMBgNV 3 | BAcMBVBBUklTMRQwEgYDVQQKDAtUT0lMRS1MSUJSRTENMAsGA1UECwwETElCRTES 4 | MBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAbG9jYWxk 5 | b21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALDBERTzuhDRQUz7gvqs 6 | w1q3nrlBEpySNGOwDQjR0BxfxlnJk/mkLJA3VrWJz8h/qH0aVclVHATYbvGRNZLR 7 | eRODPSMnM+CUsh/Rmt+W7SI/gRsmPc76azYRW6o8LZW315WH0hZXDDUeHo3NhVpy 8 | pMZ52Ar6q58SIBCK+LlQnHdBAgMBAAGgADANBgkqhkiG9w0BAQsFAAOBgQCcNi57 9 | fAuPdolbq9J9hfpXoj+c6ufUWMrNYkyLDih3fcdbed1lw+eOpaWxm8B5sChADCpF 10 | IJp6tY0if1darmcxpmTbPQDPmOPt9HW3XwrUPiTttQ51lReYli6nqXj2fKRgvuJs 11 | vR3gK+Vw6XS7riLdf++shhEfxu7b1qJVVQzq7g== 12 | -----END CERTIFICATE REQUEST----- 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to improve the reliability of the library 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Environment** 11 | Special configuration in /etc/hosts ? 12 | Reverse proxy ? 13 | VPN ? Y/N 14 | Proxy ? Y/N 15 | 16 | **Curl command line** 17 | Please provide (if allowed) the full curl command line 18 | And (if possible) give me the access to hit the endpoint myself. 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen while using the real cURL command. 22 | 23 | **Stacktrace** 24 | If applicable, add a full stacktrace including the wrapping exceptions / nested causes. 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: junit:junit 11 | versions: 12 | - 4.13.2 13 | - dependency-name: org.springframework.boot:spring-boot-starter 14 | versions: 15 | - 2.4.3 16 | - 2.4.4 17 | - dependency-name: org.springframework.boot:spring-boot-starter-web 18 | versions: 19 | - 2.4.3 20 | - 2.4.4 21 | - dependency-name: org.springframework.boot:spring-boot-starter-jetty 22 | versions: 23 | - 2.4.4 24 | - dependency-name: org.springframework.boot:spring-boot-starter-security 25 | versions: 26 | - 2.4.4 27 | - dependency-name: org.springframework:spring-context 28 | versions: 29 | - 5.3.4 30 | - 5.3.5 31 | -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.key.unprotected: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXwIBAAKBgQCwwREU87oQ0UFM+4L6rMNat565QRKckjRjsA0I0dAcX8ZZyZP5 3 | pCyQN1a1ic/If6h9GlXJVRwE2G7xkTWS0XkTgz0jJzPglLIf0Zrflu0iP4EbJj3O 4 | +ms2EVuqPC2Vt9eVh9IWVww1Hh6NzYVacqTGedgK+qufEiAQivi5UJx3QQIDAQAB 5 | AoGBALCVkb0+6crXY8fHFkndw6WkAJzPmAp4PoLBZaRECQfv3zfgnUcwb5z8zFG9 6 | hqqmbuvdHYtwzfdBS0VnbwBo6e2cCSy2ggaFT16z9oqapj0LRI5JBSlxi/C1WpCa 7 | zQlHLGW8z1hLbbIoyffokk2RZz5nbQqZU1ENwFZy2eqFfithAkEA53Pz7Lc2z4m8 8 | ZYOGnOI3EVT3EfKGnwehAxGGIlIzxK/OMNrM4Gj5ukDISCaoPywvel3GNJbcdo/g 9 | hSqSDvrZ1QJBAMOABMKE3rql89G6t/ncCCGtg9vqLmBjaMJEdMQG9GxreNK+LodX 10 | SWjG1USkzwq/6ncFxokkFfCtaOZkLjfokb0CQQDcBHq87FE7KkigC058dgI5BJa1 11 | ReG47QyfGRp0J8Y0U5bjFCsrrD6wUgLRq4XGP5+GZ/wRY9OBFTYuAJDFkqpdAkEA 12 | o81cr5rif6LSgnSe94PrcERRCe7SAsLoIHV5HFHzc/AsYO8mTxHMmTSXtpccPpaE 13 | nktBiOCqXWj+TqO7se50RQJBAMmK6kdLuWxAadauldJKBhjkPrlpxIEAwXRYT+tk 14 | kqRpodtCTMiv20G09iruj2C2Qa6n3pcmim5VujaxMz5WFEI= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKkdyrS373dPGgJU 3 | 3wCWIuUgTXrogktmNfleSd/XL3jDRRoQFc9KieMKLPYORhUskgzixxeXGk3QakIe 4 | 3tMxxxw43ZPiaiqjlfagXQUJTAkSWoGKf4MzgeLagWilNt24wXQH2MfMnGl4X3Zj 5 | lOumift9/0zMOYEYS82egxNu1wg5AgMBAAECgYBEFUpWFvbBAYdnCOP+SHMj6y1G 6 | HfTWhf+UtPDovKjP5U0cgP1GxqtcS+xt4rE0NQ1XQrUEOkDMsusUU3lBbPjLLFBs 7 | D5tRKNmeGpXg8+dULJp/rnLSpz/KBYCf4XMSjO9KUFt6nuVW1jGScLxtoLeFj9JA 8 | WSbIjq8STAu42BZeQQJBANDqA+rbvSONR0NQWupGnI9B5upTMkzF9h+jbO5ZN6mS 9 | Tj3j50Z3Cld0DPKoSNErq6VLk8pIRF1H0RyoPUzwUjMCQQDPO4PbimJIMjkuSCq5 10 | kMGMbHkBfOC3XEf2iIvDGgAaCJONC0f433SYiEgKeSYy7nlhjYfW3r8VcyH4BBWX 11 | RkfjAkEAzxzxPUkTvcc2OxnSyCew1qVzFCPe3hXz53HRcIqqkEYno1vp0QX7DrBS 12 | lc8YQaaVyI2guw3iGMg2G77+5uPp2QJBALp5N2Fp2J5WcxWuYqOwGjmdshUgpaTZ 13 | KPgyTnE1CDpk1UDpkc3kZSqMFyc1dKYH6LlbfYxfy1oly789DaCd4TkCQAsGJfrx 14 | wlZIwTY98e+pNxMBylkM/mAb8A47+R6TBc8YkA7nS9ZgWMo88yJBZH5okLeOqN6f 15 | HsNsr6oyH8dWJg0= 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICiTCCAfICCQCLbq431VeqDjANBgkqhkiG9w0BAQsFADCBhzELMAkGA1UEBhMC 3 | RlIxDDAKBgNVBAgMA0lERjEOMAwGA1UEBwwFUEFSSVMxFDASBgNVBAoMC1RPSUxF 4 | LUxJQlJFMQ0wCwYDVQQLDARMSUJFMRIwEAYDVQQDDAlsb2NhbGhvc3QxITAfBgkq 5 | hkiG9w0BCQEWEnNlcnZlckBsb2NhbGRvbWFpbjAgFw0xNjA4MTUxNjAzNTVaGA8y 6 | MTE1MDMxMDE2MDM1NVowgYcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAM 7 | BgNVBAcMBVBBUklTMRQwEgYDVQQKDAtUT0lMRS1MSUJSRTENMAsGA1UECwwETElC 8 | RTESMBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAbG9j 9 | YWxkb21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKkdyrS373dPGgJU 10 | 3wCWIuUgTXrogktmNfleSd/XL3jDRRoQFc9KieMKLPYORhUskgzixxeXGk3QakIe 11 | 3tMxxxw43ZPiaiqjlfagXQUJTAkSWoGKf4MzgeLagWilNt24wXQH2MfMnGl4X3Zj 12 | lOumift9/0zMOYEYS82egxNu1wg5AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAeMoA 13 | rmOM52l39FJM0p4SmtbEUWMdLe9Ba+UMPn0cC/9/D2Rt75PCu49rB8MbtqcSS2nb 14 | yJ0t9qsjENnyvM/xz1Hp9ken4k+GX9AkaqUymBNDOJVy0FXlMTwdk4Hteb2F1Njc 15 | vkkuQthHF7gCa+elsH+gPTOVgJLiZQyOVfXpVjA= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICiTCCAfICCQCLbq431VeqDTANBgkqhkiG9w0BAQsFADCBhzELMAkGA1UEBhMC 3 | RlIxDDAKBgNVBAgMA0lERjEOMAwGA1UEBwwFUEFSSVMxFDASBgNVBAoMC1RPSUxF 4 | LUxJQlJFMQ0wCwYDVQQLDARMSUJFMRIwEAYDVQQDDAlsb2NhbGhvc3QxITAfBgkq 5 | hkiG9w0BCQEWEnNlcnZlckBsb2NhbGRvbWFpbjAgFw0xNjA4MTUxNjAzNTJaGA8y 6 | MTE1MDMxMDE2MDM1MlowgYcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAM 7 | BgNVBAcMBVBBUklTMRQwEgYDVQQKDAtUT0lMRS1MSUJSRTENMAsGA1UECwwETElC 8 | RTESMBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAbG9j 9 | YWxkb21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALDBERTzuhDRQUz7 10 | gvqsw1q3nrlBEpySNGOwDQjR0BxfxlnJk/mkLJA3VrWJz8h/qH0aVclVHATYbvGR 11 | NZLReRODPSMnM+CUsh/Rmt+W7SI/gRsmPc76azYRW6o8LZW315WH0hZXDDUeHo3N 12 | hVpypMZ52Ar6q58SIBCK+LlQnHdBAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAXgkN 13 | XNGSIlhzRtFnMmLQ01JX9mMt3CIozINh2EzLBtCMfWKS8sYwFduX3tw1dn06untT 14 | bvYQRp7B6TkoEfJzGA6yEth8Tz0FqIjAm3q6khwUHOrIJrBoUCHqxYJEzWPDpbtb 15 | bi4BerrBSrOHc8rpscd64jxt+qyAUdGkYJGxasA= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /src/test/resources/ca/fakeCa.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,701940FD1E30E4FB 4 | 5 | RMkNpf8OzWV8+S9HDZVcgTojkq7GMuckPZvRMG5YFxoTM0fIfAtjEZ4M865eII8G 6 | SRYgyMILjl+/T6SIxKgQumQlFS/mOtIK1esEJQvgj0kFZ5FrgofLlsOYvtg6V2U8 7 | c0K800TtkOLcTs8PyTlUjtH+iQPtFjXQWIYY+W9awOduah7kmPv76TVNqYTe6zwu 8 | Q2tX0RYRlv8/pvXhYY5PVRs4u9KIwBqWc/fFOKPz7BpVK9WaDOw3UnJcaZVq6dKK 9 | E22HV58QWPh678vEJ12Qy6rD7gk9ucmVw0RWZYg7iRFV9GcuJjkL2wTA3Xy2Bcxh 10 | OrqFTi6g7ln9q+pxiYclkAvTtJGyiFWezChF60QOnyJoFmr03+X2fBg9jirw83/8 11 | kHAek6dgBQO7ZEcfZLrbgWkD0iB7mgTlL8LZ9e8ojxDluToZrUWWQnD9oe3Wu55a 12 | qlQgRdJsqGLyGtJm3rIiVmySWObGwnrAlPQLzFT+R9RZB2ZuSrYl1emue8kWMaSO 13 | DLoQPEsXmIsmEGatBg3DlpKpmH1YJw4M+XpDbh06/G3w0E5Dd6YEEI0v7OvuFFDv 14 | qEvzw8ObrmA10Da8KbX/i2HGdu9OpUYEj6qnYdkUHcamujX4vAExdCvX9Kliqz0C 15 | 301c47lukjrBDTQ0JoncggIScChOV6oF1M4lIQ76qnQvV6mVHUv0qIqDsi3BosG7 16 | 0+TpD5L5/6TxPvdInzQmveeSWS6lHcy5ZKxJW59DNhDECVZ6sx5gpiEjZMbBuQDs 17 | +uoT6tF4Ts7dux6DZ1EFqM0DHJP0TLlPmUVR7JPginslHywwf5hnpw== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,41CE3C7D84B69709 4 | 5 | kg3Ucypj7WpLwFYl+qurkxfVJyCZJIr+6OommCUKAnIY9Rh/WGjDZsoU/sdta4H3 6 | sY4RhcpKu8e5+hXOoEXM2Yv0rviECymkOvJxbu+Xxm4Eu4MF/5RS8nlzVfw9d2fU 7 | sy6lrkr2busSadAcV7p0TQyKkka1vHK3phS3X10yZX0NUYF6gBcQ3uF3hYOAtpfl 8 | biN+TfxXFMIF5Ca4EW8WO3FXQn0+fm1jC5F+1AJz6tfoZhrglZLvCucd+oVo10ds 9 | rT99gcXzagF5EKptX2+NCj7e7gtdIDAAVSpqjpUv+4rLUwSgRl0gGL8l+hCllTxs 10 | XD0entaEl7mr3CQY1acp1XAiasyPvlN3cvfWwjtP6HRcWNfbUG8AetbIaZbYvIfP 11 | o2xGbIqz2PEw0GUBtd4diZeIxrsifXPBLJZjllltm9CVfwfIuw5KsW/MMPXhView 12 | FuXvavlhPVoTvxZxr06MCqF9OYR33m8m63unA5ynIzJAi6nqecLJhQDWi/SeL9QA 13 | /ermJU0BPvukLO/jwUXCq5FE82Yn3ogj5TX4Uw/yCVomrsMqR/xKafevi6aCmo2r 14 | ToeWCD+qBmewC3QrFGaICRt1hsWFcX1r/bRF6cYyZjbOtlqfUOU3pjCdtLh74Vw1 15 | mMQpLEcSPrdq0F0wXBXlR1kFEaayacbuwlW/FsbLZpmrlWUun9qn/Csrk7zfSdGd 16 | hA3DeNn33RDP39w+Lc4q9MO6cxuFmodFwGPhfLynJOHq2l9bq/dVvomKT8TKWyv7 17 | k2IkYmjJ+Ed7Y9Bgws8e1LNqigP0KQCl2PMA9kfwRQNYQNmawrIQ3w== 18 | -----END RSA PRIVATE KEY----- 19 | -------------------------------------------------------------------------------- /src/test/resources/ca/fakeCa.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC4DCCAkmgAwIBAgIJALQslmMLVfd3MA0GCSqGSIb3DQEBCwUAMIGHMQswCQYD 3 | VQQGEwJGUjEMMAoGA1UECAwDSURGMQ4wDAYDVQQHDAVQQVJJUzEUMBIGA1UECgwL 4 | VE9JTEUtTElCUkUxDTALBgNVBAsMBExJQkUxEjAQBgNVBAMMCWxvY2FsaG9zdDEh 5 | MB8GCSqGSIb3DQEJARYSc2VydmVyQGxvY2FsZG9tYWluMCAXDTE2MDgxNTE2MDM1 6 | MFoYDzIxMTUwMzEwMTYwMzUwWjCBhzELMAkGA1UEBhMCRlIxDDAKBgNVBAgMA0lE 7 | RjEOMAwGA1UEBwwFUEFSSVMxFDASBgNVBAoMC1RPSUxFLUxJQlJFMQ0wCwYDVQQL 8 | DARMSUJFMRIwEAYDVQQDDAlsb2NhbGhvc3QxITAfBgkqhkiG9w0BCQEWEnNlcnZl 9 | ckBsb2NhbGRvbWFpbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArTFygQ1S 10 | 56QXIdZKguBu6eflTCmcWm4XclHglz5/9mHqK4y5j137w5wjB4Qr/72YMhLln477 11 | h4GtIRWplWuwQaCN+xeyFRi4IInR/mT9ZaSg4a1Qx7+VPmzoY2QFe0ZLlPX06v1w 12 | h7w6dNbUvpBf6pPxB2xHu2G4r297+yTsd80CAwEAAaNQME4wHQYDVR0OBBYEFMEa 13 | QMeoJZrDOGQJa9wpITQ6W2blMB8GA1UdIwQYMBaAFMEaQMeoJZrDOGQJa9wpITQ6 14 | W2blMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAX6daxkgvXwFd7XBk 15 | YtQVCKo4oVuTl4LI2nc5K8e+XM4nzAiSr9E4pD7CzQk5QGa5oxlDH/sHw5AH8+Pp 16 | g4TUDW/YSBbdC4mz+XK4d8atTZuDisemO75srmu5hkc0QDCZh+DLVLQSUPJpISvo 17 | D0czuDR0TQ0WjfsKTNEWqKltTMw= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/outside/curl/CurlWebTest.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.outside.curl; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.apache.hc.core5.http.ClassicHttpResponse; 5 | import org.junit.jupiter.api.Assertions; 6 | import org.junit.jupiter.api.Disabled; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.io.IOException; 10 | import java.util.regex.Pattern; 11 | 12 | import static org.toilelibre.libe.curl.Curl.curl; 13 | 14 | public class CurlWebTest { 15 | @Test 16 | public void wrongHost () { 17 | curl ("-k https://wrong.host.badssl.com/"); 18 | } 19 | 20 | @Disabled // keeps failing 21 | @Test 22 | public void proxyWithAuthentication () throws IOException { 23 | ClassicHttpResponse response = curl ("http://httpbin.org/get -x http://204.133.187.66:3128 -U user:password"); 24 | String body = IOUtils.toString (response.getEntity ().getContent ()); 25 | Assertions.assertTrue (body.contains ("Host\": \"httpbin.org\"")); 26 | Assertions.assertTrue (Pattern.compile ("\"origin\": \"[a-zA-Z0-9.]+, [0-9.]+\"").matcher (body).find ()); 27 | Assertions.assertFalse (body.contains ("Proxy-Authorization")); 28 | } 29 | 30 | @Disabled // this domain no longer exists (as of 2025) 31 | @Test 32 | public void sslTest (){ 33 | curl ("curl -k https://lenovo.prod.ondemandconnectivity.com"); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/HttpClientBuilder.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.hc.client5.http.classic.ExecChain; 4 | import org.apache.hc.client5.http.classic.ExecChainHandler; 5 | import org.apache.hc.core5.http.ClassicHttpRequest; 6 | import org.apache.hc.core5.http.ClassicHttpResponse; 7 | import org.apache.hc.core5.http.HttpException; 8 | import org.apache.hc.core5.http.config.NamedElementChain; 9 | import org.apache.hc.core5.http.protocol.HttpContext; 10 | 11 | import java.io.IOException; 12 | import java.util.function.Consumer; 13 | 14 | class HttpClientBuilder extends org.apache.hc.client5.http.impl.classic.HttpClientBuilder { 15 | 16 | private Consumer contextTester; 17 | 18 | public static HttpClientBuilder create() { 19 | return new HttpClientBuilder(); 20 | } 21 | 22 | public void setContextTester(Consumer contextTester) { 23 | this.contextTester = contextTester; 24 | } 25 | 26 | @Override 27 | protected void customizeExecChain(NamedElementChain execChainDefinition) { 28 | execChainDefinition.addLast( 29 | (request, scope, chain) -> { 30 | if (contextTester != null) { 31 | contextTester.accept(scope.clientContext); 32 | } 33 | return chain.proceed(request, scope); 34 | }, 35 | "context-tester" 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/scripts/mkclient: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./vars 3 | 4 | mkdir -p ../clients/$client_alias 5 | openssl req -new -newkey rsa:1024 -passout pass:$client_pass -nodes -subj "$subject" -out ../clients/$client_alias/$client_alias.csr -keyout ../clients/$client_alias/$client_alias.key 6 | openssl x509 -req -days 36000 -passin pass:$ca_pass -in ../clients/$client_alias/$client_alias.csr -CA ../ca/fakeCa.crt -CAkey ../ca/fakeCa.key -CAserial ../server/$server_alias/$server_alias.srl -out ../clients/$client_alias/$client_alias.crt 7 | cat ../clients/$client_alias/$client_alias.key ../clients/$client_alias/$client_alias.crt > ../clients/$client_alias/$client_alias.pem 8 | openssl pkcs12 -export -in ../clients/$client_alias/$client_alias.crt -inkey ../clients/$client_alias/$client_alias.key -CAfile ../ca/fakeCa.cert -passout pass:$client_pass -out ../clients/$client_alias/$client_alias.p12 -name $client_alias 9 | 10 | openssl x509 -outform der -in ../clients/$client_alias/$client_alias.pem -out ../clients/$client_alias/$client_alias.der 11 | openssl pkcs8 -topk8 -inform PEM -outform DER -in ../clients/$client_alias/$client_alias.pem -out ../clients/$client_alias/$client_alias.key.der -v2 aes-256-cbc -passout pass:$client_pass 12 | openssl rsa -in ../clients/$client_alias/$client_alias.pem -inform PEM -outform DER -out ../clients/$client_alias/$client_alias.key.der 13 | 14 | keytool -import -noprompt -alias $client_alias -keystore ../server/$server_alias/$server_alias.jks -file ../clients/$client_alias/$client_alias.crt -storepass myserverpass 15 | keytool -importkeystore -alias $client_alias -destalias privateKey -srcstorepass $client_pass -deststorepass $client_pass -destkeystore ../clients/$client_alias/$client_alias.jks -srckeystore ../clients/$client_alias/$client_alias.p12 -srcstoretype PKCS12 16 | -------------------------------------------------------------------------------- /src/test/resources/scripts/mkserver: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . ./vars 3 | 4 | mkdir -p ../server/$server_alias/ 5 | openssl genrsa -passout pass:$server_pass -des3 -out ../server/$server_alias/$server_alias.key 1024 6 | openssl req -new -passin pass:$server_pass -key ../server/$server_alias/$server_alias.key -out ../server/$server_alias/$server_alias.csr -subj "$subject" 7 | openssl rsa -in ../server/$server_alias/$server_alias.key -passin pass:$server_pass -out ../server/$server_alias/$server_alias.key.unprotected 8 | openssl x509 -req -passin pass:$ca_pass -days 36000 -in ../server/$server_alias/$server_alias.csr -CA ../ca/fakeCa.crt -CAkey ../ca/fakeCa.key -CAcreateserial -out ../server/$server_alias/$server_alias.crt 9 | cat ../server/$server_alias/$server_alias.key ../server/$server_alias/$server_alias.crt > ../server/$server_alias/$server_alias.pem 10 | mv ../scripts/.srl ../server/$server_alias/$server_alias.srl 11 | openssl x509 -outform der -in ../server/$server_alias/$server_alias.pem -out ../server/$server_alias/$server_alias.der 12 | openssl pkcs12 -export -in ../server/$server_alias/$server_alias.crt -inkey ../server/$server_alias/$server_alias.key -CAfile ../ca/fakeCa.cert -passin pass:$server_pass -passout pass:$server_pass -out ../server/$server_alias/$server_alias.p12 -name $server_alias 13 | rm ../server/$server_alias/$server_alias.jks 2>/dev/null || true 14 | keytool -import -noprompt -alias ca -trustcacerts -keystore ../server/$server_alias/$server_alias.jks -file ../ca/fakeCa.crt -storepass $server_pass 15 | keytool -import -noprompt -alias serverCert -keystore ../server/$server_alias/$server_alias.jks -file ../server/$server_alias/$server_alias.crt -storepass $server_pass 16 | keytool -alias $server_alias -destalias serverKey -importkeystore -srcstorepass $server_pass -deststorepass $server_pass -destkeystore ../server/$server_alias/$server_alias.jks -srckeystore ../server/$server_alias/$server_alias.p12 -srcstoretype PKCS12 17 | -------------------------------------------------------------------------------- /src/test/resources/clients/libe/libe.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKkdyrS373dPGgJU 3 | 3wCWIuUgTXrogktmNfleSd/XL3jDRRoQFc9KieMKLPYORhUskgzixxeXGk3QakIe 4 | 3tMxxxw43ZPiaiqjlfagXQUJTAkSWoGKf4MzgeLagWilNt24wXQH2MfMnGl4X3Zj 5 | lOumift9/0zMOYEYS82egxNu1wg5AgMBAAECgYBEFUpWFvbBAYdnCOP+SHMj6y1G 6 | HfTWhf+UtPDovKjP5U0cgP1GxqtcS+xt4rE0NQ1XQrUEOkDMsusUU3lBbPjLLFBs 7 | D5tRKNmeGpXg8+dULJp/rnLSpz/KBYCf4XMSjO9KUFt6nuVW1jGScLxtoLeFj9JA 8 | WSbIjq8STAu42BZeQQJBANDqA+rbvSONR0NQWupGnI9B5upTMkzF9h+jbO5ZN6mS 9 | Tj3j50Z3Cld0DPKoSNErq6VLk8pIRF1H0RyoPUzwUjMCQQDPO4PbimJIMjkuSCq5 10 | kMGMbHkBfOC3XEf2iIvDGgAaCJONC0f433SYiEgKeSYy7nlhjYfW3r8VcyH4BBWX 11 | RkfjAkEAzxzxPUkTvcc2OxnSyCew1qVzFCPe3hXz53HRcIqqkEYno1vp0QX7DrBS 12 | lc8YQaaVyI2guw3iGMg2G77+5uPp2QJBALp5N2Fp2J5WcxWuYqOwGjmdshUgpaTZ 13 | KPgyTnE1CDpk1UDpkc3kZSqMFyc1dKYH6LlbfYxfy1oly789DaCd4TkCQAsGJfrx 14 | wlZIwTY98e+pNxMBylkM/mAb8A47+R6TBc8YkA7nS9ZgWMo88yJBZH5okLeOqN6f 15 | HsNsr6oyH8dWJg0= 16 | -----END PRIVATE KEY----- 17 | -----BEGIN CERTIFICATE----- 18 | MIICiTCCAfICCQCLbq431VeqDjANBgkqhkiG9w0BAQsFADCBhzELMAkGA1UEBhMC 19 | RlIxDDAKBgNVBAgMA0lERjEOMAwGA1UEBwwFUEFSSVMxFDASBgNVBAoMC1RPSUxF 20 | LUxJQlJFMQ0wCwYDVQQLDARMSUJFMRIwEAYDVQQDDAlsb2NhbGhvc3QxITAfBgkq 21 | hkiG9w0BCQEWEnNlcnZlckBsb2NhbGRvbWFpbjAgFw0xNjA4MTUxNjAzNTVaGA8y 22 | MTE1MDMxMDE2MDM1NVowgYcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAM 23 | BgNVBAcMBVBBUklTMRQwEgYDVQQKDAtUT0lMRS1MSUJSRTENMAsGA1UECwwETElC 24 | RTESMBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAbG9j 25 | YWxkb21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKkdyrS373dPGgJU 26 | 3wCWIuUgTXrogktmNfleSd/XL3jDRRoQFc9KieMKLPYORhUskgzixxeXGk3QakIe 27 | 3tMxxxw43ZPiaiqjlfagXQUJTAkSWoGKf4MzgeLagWilNt24wXQH2MfMnGl4X3Zj 28 | lOumift9/0zMOYEYS82egxNu1wg5AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAeMoA 29 | rmOM52l39FJM0p4SmtbEUWMdLe9Ba+UMPn0cC/9/D2Rt75PCu49rB8MbtqcSS2nb 30 | yJ0t9qsjENnyvM/xz1Hp9ken4k+GX9AkaqUymBNDOJVy0FXlMTwdk4Hteb2F1Njc 31 | vkkuQthHF7gCa+elsH+gPTOVgJLiZQyOVfXpVjA= 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /src/test/resources/server/libe/libe.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,41CE3C7D84B69709 4 | 5 | kg3Ucypj7WpLwFYl+qurkxfVJyCZJIr+6OommCUKAnIY9Rh/WGjDZsoU/sdta4H3 6 | sY4RhcpKu8e5+hXOoEXM2Yv0rviECymkOvJxbu+Xxm4Eu4MF/5RS8nlzVfw9d2fU 7 | sy6lrkr2busSadAcV7p0TQyKkka1vHK3phS3X10yZX0NUYF6gBcQ3uF3hYOAtpfl 8 | biN+TfxXFMIF5Ca4EW8WO3FXQn0+fm1jC5F+1AJz6tfoZhrglZLvCucd+oVo10ds 9 | rT99gcXzagF5EKptX2+NCj7e7gtdIDAAVSpqjpUv+4rLUwSgRl0gGL8l+hCllTxs 10 | XD0entaEl7mr3CQY1acp1XAiasyPvlN3cvfWwjtP6HRcWNfbUG8AetbIaZbYvIfP 11 | o2xGbIqz2PEw0GUBtd4diZeIxrsifXPBLJZjllltm9CVfwfIuw5KsW/MMPXhView 12 | FuXvavlhPVoTvxZxr06MCqF9OYR33m8m63unA5ynIzJAi6nqecLJhQDWi/SeL9QA 13 | /ermJU0BPvukLO/jwUXCq5FE82Yn3ogj5TX4Uw/yCVomrsMqR/xKafevi6aCmo2r 14 | ToeWCD+qBmewC3QrFGaICRt1hsWFcX1r/bRF6cYyZjbOtlqfUOU3pjCdtLh74Vw1 15 | mMQpLEcSPrdq0F0wXBXlR1kFEaayacbuwlW/FsbLZpmrlWUun9qn/Csrk7zfSdGd 16 | hA3DeNn33RDP39w+Lc4q9MO6cxuFmodFwGPhfLynJOHq2l9bq/dVvomKT8TKWyv7 17 | k2IkYmjJ+Ed7Y9Bgws8e1LNqigP0KQCl2PMA9kfwRQNYQNmawrIQ3w== 18 | -----END RSA PRIVATE KEY----- 19 | -----BEGIN CERTIFICATE----- 20 | MIICiTCCAfICCQCLbq431VeqDTANBgkqhkiG9w0BAQsFADCBhzELMAkGA1UEBhMC 21 | RlIxDDAKBgNVBAgMA0lERjEOMAwGA1UEBwwFUEFSSVMxFDASBgNVBAoMC1RPSUxF 22 | LUxJQlJFMQ0wCwYDVQQLDARMSUJFMRIwEAYDVQQDDAlsb2NhbGhvc3QxITAfBgkq 23 | hkiG9w0BCQEWEnNlcnZlckBsb2NhbGRvbWFpbjAgFw0xNjA4MTUxNjAzNTJaGA8y 24 | MTE1MDMxMDE2MDM1MlowgYcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDANJREYxDjAM 25 | BgNVBAcMBVBBUklTMRQwEgYDVQQKDAtUT0lMRS1MSUJSRTENMAsGA1UECwwETElC 26 | RTESMBAGA1UEAwwJbG9jYWxob3N0MSEwHwYJKoZIhvcNAQkBFhJzZXJ2ZXJAbG9j 27 | YWxkb21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALDBERTzuhDRQUz7 28 | gvqsw1q3nrlBEpySNGOwDQjR0BxfxlnJk/mkLJA3VrWJz8h/qH0aVclVHATYbvGR 29 | NZLReRODPSMnM+CUsh/Rmt+W7SI/gRsmPc76azYRW6o8LZW315WH0hZXDDUeHo3N 30 | hVpypMZ52Ar6q58SIBCK+LlQnHdBAgMBAAEwDQYJKoZIhvcNAQELBQADgYEAXgkN 31 | XNGSIlhzRtFnMmLQ01JX9mMt3CIozINh2EzLBtCMfWKS8sYwFduX3tw1dn06untT 32 | bvYQRp7B6TkoEfJzGA6yEth8Tz0FqIjAm3q6khwUHOrIJrBoUCHqxYJEzWPDpbtb 33 | bi4BerrBSrOHc8rpscd64jxt+qyAUdGkYJGxasA= 34 | -----END CERTIFICATE----- 35 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/curl/CachedArgumentsTest.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.commons.lang3.reflect.*; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.lang.reflect.*; 8 | import java.util.*; 9 | 10 | import static java.util.Arrays.*; 11 | import static java.util.Collections.emptyList; 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | public class CachedArgumentsTest { 15 | 16 | private static int containsKeyCallsCounter = 0; 17 | private static int getCallsCounter = 0; 18 | 19 | private static final HashMap> SHARED_CACHE = new HashMap> (){ 20 | 21 | @Override 22 | public boolean containsKey (Object o) { 23 | containsKeyCallsCounter++; 24 | return super.containsKey (o); 25 | } 26 | 27 | @Override 28 | public List get (Object o) { 29 | getCallsCounter++; 30 | return super.get (o); 31 | } 32 | }; 33 | 34 | @Test 35 | public void curlCommandMatchesShouldBeSavedInCache () { 36 | String args = "-X POST -H 'Content-Type:application/json' " + 37 | "-d '{\"test\":{\"name\":\"TEST_NAME\",\"value\":\"TEST_VALUE\"}}'"; 38 | 39 | CommandLine result1 = ReadArguments.getCommandLineFromRequest (args, 40 | emptyList (), SHARED_CACHE); 41 | 42 | assertEquals (1, containsKeyCallsCounter); 43 | assertEquals (0, getCallsCounter); 44 | 45 | CommandLine result2 = ReadArguments.getCommandLineFromRequest (args, 46 | emptyList (), SHARED_CACHE); 47 | 48 | assertEquals (2, containsKeyCallsCounter); 49 | assertEquals (1, getCallsCounter); 50 | 51 | assertArrayEquals (result1.getArgs (), result2.getArgs ()); 52 | 53 | assertEquals (1, SHARED_CACHE.size ()); 54 | assertTrue (SHARED_CACHE.containsKey (args + " ")); 55 | assertEquals (asList ("-X", "POST", "-H", "'Content-Type:application/json'", "-d", "'{\"test\":{\"name" + 56 | "\":\"TEST_NAME\",\"value\":\"TEST_VALUE\"}}'"), SHARED_CACHE.get (args + " ")); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/outside/monitor/StupidHttpServer.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.outside.monitor; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.Random; 6 | 7 | import org.springframework.boot.Banner; 8 | import org.springframework.boot.SpringApplication; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.boot.builder.SpringApplicationBuilder; 11 | import org.springframework.context.ConfigurableApplicationContext; 12 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 13 | 14 | @SpringBootApplication 15 | @EnableWebMvc 16 | public class StupidHttpServer { 17 | 18 | private static ConfigurableApplicationContext context; 19 | 20 | private static int managementPort; 21 | 22 | private static int port; 23 | 24 | public static void main (final String [] args) { 25 | StupidHttpServer.start (args); 26 | } 27 | 28 | public static int port () { 29 | return StupidHttpServer.port; 30 | } 31 | 32 | public static int [] start () { 33 | return StupidHttpServer.start (new String [0]); 34 | } 35 | 36 | public static int [] start (final String [] args) { 37 | final Random random = new Random (); 38 | StupidHttpServer.port = random.nextInt (32767) + 32768; 39 | StupidHttpServer.managementPort = random.nextInt (32767) + 32768; 40 | StupidHttpServer.start (StupidHttpServer.port, StupidHttpServer.managementPort, args); 41 | return new int [] { StupidHttpServer.port, StupidHttpServer.managementPort }; 42 | } 43 | 44 | public static void start (final int port, final int managementPort, final String [] args) { 45 | Map properties = new HashMap (); 46 | properties.put ("server.port", port); 47 | properties.put ("management.port", managementPort); 48 | 49 | StupidHttpServer.context = new SpringApplicationBuilder ().sources (StupidHttpServer.class).bannerMode (Banner.Mode.OFF).addCommandLineProperties (true).properties (properties).run (args); 50 | } 51 | 52 | public static void stop () { 53 | SpringApplication.exit (StupidHttpServer.context, () -> 0); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/AfterResponse.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.hc.core5.http.ClassicHttpResponse; 5 | import org.apache.hc.core5.http.HttpEntity; 6 | import org.apache.hc.core5.http.HttpResponse; 7 | import org.toilelibre.libe.curl.Curl.*; 8 | 9 | import java.io.*; 10 | import java.util.logging.*; 11 | 12 | final class AfterResponse { 13 | 14 | private static Logger LOGGER = Logger.getLogger (AfterResponse.class.getName ()); 15 | 16 | static void handle (final CommandLine commandLine, final HttpResponse response) { 17 | 18 | if (!commandLine.hasOption (Arguments.OUTPUT.getOpt ())) return; 19 | 20 | File file = createTheOutputFile (commandLine.getOptionValue (Arguments.OUTPUT.getOpt ())); 21 | FileOutputStream outputStream = getOutputStreamFromFile (file); 22 | writeTheResponseEntityInsideStream (outputStream, ((ClassicHttpResponse) response).getEntity ()); 23 | } 24 | 25 | private static void writeTheResponseEntityInsideStream (FileOutputStream outputStream, HttpEntity httpEntity) { 26 | try { 27 | if (httpEntity.getContentLength () >= 0) { 28 | outputStream.write (IOUtils.toByteArray (httpEntity.getContent (), (int) httpEntity.getContentLength ())); 29 | } 30 | else { 31 | outputStream.write (IOUtils.toByteArray (httpEntity.getContent ())); 32 | } 33 | } catch (final IOException e) { 34 | throw new CurlException (e); 35 | } finally { 36 | try { 37 | outputStream.close (); 38 | } catch (IOException e) { 39 | LOGGER.log (Level.WARNING, "Cannot flush the file in output"); 40 | } 41 | } 42 | } 43 | 44 | private static FileOutputStream getOutputStreamFromFile (File file) { 45 | try { 46 | return new FileOutputStream (file); 47 | } catch (final FileNotFoundException e) { 48 | throw new CurlException (e); 49 | } 50 | } 51 | 52 | private static File createTheOutputFile (String fileName) { 53 | final File file = new File (fileName); 54 | try { 55 | if (!file.createNewFile ()){ 56 | throw new CurlException (new IOException ("Could not create the file. Does it already exist ?")); 57 | } 58 | } catch (IOException e) { 59 | LOGGER.log (Level.WARNING, "Cannot flush the output file"); 60 | throw new CurlException (e); 61 | } 62 | return file; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/AuthMethodHandler.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.hc.client5.http.auth.AuthSchemeFactory; 5 | import org.apache.hc.client5.http.auth.AuthScope; 6 | import org.apache.hc.client5.http.auth.NTCredentials; 7 | import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; 8 | import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; 9 | import org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider; 10 | import org.apache.hc.core5.http.HttpHost; 11 | 12 | import java.net.MalformedURLException; 13 | import java.net.URI; 14 | import java.net.URISyntaxException; 15 | import java.util.Collections; 16 | 17 | final class AuthMethodHandler { 18 | 19 | private static final AuthScope ANY = new AuthScope(null, null, -1, null, null); 20 | 21 | static HttpClientBuilder handleAuthMethod (final CommandLine commandLine, HttpClientBuilder executor, 22 | final String hostname) { 23 | if (commandLine.getOptionValue (Arguments.AUTH.getOpt ()) != null) { 24 | final String[] authValue = commandLine.getOptionValue (Arguments.AUTH.getOpt ()).split ("(? 1 ? authValue[1].toCharArray() : null)); 39 | return (HttpClientBuilder) executor.setDefaultCredentialsProvider (basicCredentialsProvider); 40 | } catch (URISyntaxException | MalformedURLException e) { 41 | throw new Curl.CurlException(e); 42 | } 43 | } 44 | return executor; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/PemReader.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import java.io.*; 4 | import java.util.*; 5 | 6 | /** 7 | * A generic PEM reader, based on the format outlined in RFC 1421 8 | */ 9 | final class PemReader extends BufferedReader { 10 | 11 | static class PemObject { 12 | 13 | private final byte [] content; 14 | private final String type; 15 | 16 | /** 17 | * Generic constructor for object with headers. 18 | * 19 | * @param type 20 | * pem object type. 21 | * @param content 22 | * the binary content of the object. 23 | */ 24 | PemObject (final String type, final byte [] content) { 25 | this.type = type; 26 | this.content = content; 27 | } 28 | 29 | byte [] getContent () { 30 | return this.content; 31 | } 32 | 33 | String getType () { 34 | return this.type; 35 | } 36 | 37 | } 38 | 39 | private static final String BEGIN = "-----BEGIN "; 40 | 41 | private static final String END = "-----END "; 42 | 43 | PemReader (final Reader reader) { 44 | super (reader); 45 | } 46 | 47 | private PemObject loadObject (final String type) throws IOException { 48 | String line; 49 | final String endMarker = PemReader.END + type; 50 | final StringBuilder stringBuffer = new StringBuilder (); 51 | 52 | while ((line = this.readLine ()) != null) { 53 | if (line.contains (":")) { 54 | //there is an header. But we don't need them 55 | continue; 56 | } 57 | 58 | if (line.contains (endMarker)) { 59 | break; 60 | } 61 | 62 | stringBuffer.append (line.trim ()); 63 | } 64 | 65 | if (line == null) { 66 | throw new IOException (endMarker + " not found"); 67 | } 68 | 69 | return new PemObject (type, Base64.getDecoder ().decode (stringBuffer.toString ())); 70 | } 71 | 72 | PemObject readPemObject () throws IOException { 73 | String line = this.readLine (); 74 | 75 | while ((line != null) && !line.startsWith (PemReader.BEGIN)) { 76 | line = this.readLine (); 77 | } 78 | 79 | if (line != null) { 80 | line = line.substring (PemReader.BEGIN.length ()); 81 | final int index = line.indexOf ('-'); 82 | final String type = line.substring (0, index); 83 | 84 | if (index > 0) { 85 | return this.loadObject (type); 86 | } 87 | } 88 | 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/HttpClientProvider.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.hc.client5.http.classic.HttpClient; 5 | import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; 6 | import org.apache.hc.client5.http.io.HttpClientConnectionManager; 7 | import org.apache.hc.core5.http.ClassicHttpResponse; 8 | import org.apache.hc.core5.http.HttpRequest; 9 | import org.apache.hc.core5.http.protocol.HttpContext; 10 | import org.toilelibre.libe.curl.Curl.CurlException; 11 | 12 | import java.net.InetAddress; 13 | import java.net.UnknownHostException; 14 | import java.util.List; 15 | import java.util.function.BiFunction; 16 | import java.util.function.Consumer; 17 | import java.util.function.Supplier; 18 | 19 | import static org.toilelibre.libe.curl.AuthMethodHandler.handleAuthMethod; 20 | import static org.toilelibre.libe.curl.HttpRequestProvider.getConnectionConfig; 21 | import static org.toilelibre.libe.curl.HttpRequestProvider.getRoutePlanner; 22 | 23 | final class HttpClientProvider { 24 | 25 | static HttpClient prepareHttpClient (final CommandLine commandLine, 26 | List, ClassicHttpResponse>> additionalInterceptors, 27 | HttpClientConnectionManager connectionManager, 28 | Consumer httpClientCustomizer, 29 | Consumer contextTester) throws CurlException { 30 | HttpClientBuilder executor = HttpClientBuilder.create (); 31 | 32 | if (httpClientCustomizer != null) { 33 | httpClientCustomizer.accept(executor); 34 | } 35 | 36 | if (!commandLine.hasOption (Arguments.COMPRESSED.getOpt ())){ 37 | executor.disableContentCompression (); 38 | } 39 | 40 | final HttpClientConnectionManager chosenConnectionManager = connectionManager != null 41 | ? connectionManager 42 | : PoolingHttpClientConnectionManagerBuilder.create() 43 | .setSSLSocketFactory( 44 | SSLMaterialCreator.buildConnectionFactory (commandLine) 45 | ).setConnectionConfigResolver ( 46 | route -> getConnectionConfig (commandLine) 47 | ).build(); 48 | 49 | executor.setConnectionManager (chosenConnectionManager); 50 | executor.setRoutePlanner (getRoutePlanner(commandLine)); 51 | 52 | final String hostname; 53 | try { 54 | hostname = InetAddress.getLocalHost ().getHostName (); 55 | } catch (final UnknownHostException e1) { 56 | throw new Curl.CurlException (e1); 57 | } 58 | 59 | executor = handleAuthMethod (commandLine, executor, hostname); 60 | 61 | if (! commandLine.hasOption (Arguments.FOLLOW_REDIRECTS.getOpt ())) { 62 | executor.disableRedirectHandling (); 63 | } 64 | 65 | InterceptorsBinder.handleInterceptors (commandLine, executor, additionalInterceptors); 66 | 67 | executor.setContextTester(contextTester); 68 | 69 | return executor.build (); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/curl/HttpRequestProviderTest.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.hc.core5.http.ClassicHttpRequest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.Collections; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | public class HttpRequestProviderTest { 12 | 13 | @Test 14 | public void curlWithoutVerbAndWithoutDataShouldBeTransformedAsGetRequest () { 15 | //given 16 | CommandLine commandLine = ReadArguments.getCommandLineFromRequest ( 17 | "curl -H'Accept: application/json' http://localhost/user/byId/1", Collections.emptyList ()); 18 | 19 | //when 20 | ClassicHttpRequest request = HttpRequestProvider.prepareRequest (commandLine); 21 | 22 | //then 23 | assertEquals ("GET", request.getMethod ()); 24 | } 25 | 26 | @Test 27 | public void curlWithAPlaceholder () { 28 | //given 29 | CommandLine commandLine = ReadArguments.getCommandLineFromRequest ( 30 | "curl -H $curl_placeholder_0 http://localhost/user/byId/1", 31 | Collections.singletonList ("Accept: application/json")); 32 | 33 | //when 34 | ClassicHttpRequest request = HttpRequestProvider.prepareRequest (commandLine); 35 | 36 | //then 37 | assertEquals ("GET", request.getMethod ()); 38 | } 39 | 40 | 41 | @Test 42 | public void curlWithoutVerbAndWithDataShouldBeTransformedAsPostRequest () { 43 | //given 44 | CommandLine commandLine = ReadArguments.getCommandLineFromRequest ( 45 | "curl -H'Accept: application/json' -d'{\"id\":1,\"name\":\"John Doe\"}' http://localhost/user/", 46 | Collections.emptyList ()); 47 | 48 | //when 49 | ClassicHttpRequest request = HttpRequestProvider.prepareRequest (commandLine); 50 | 51 | //then 52 | assertEquals ("POST", request.getMethod ()); 53 | } 54 | 55 | @Test 56 | public void proxyWithAuthentication () { 57 | //given 58 | CommandLine commandLine = ReadArguments.getCommandLineFromRequest ( 59 | "http://httpbin.org/get -x http://87.98.174.157:3128/ -U user:password", 60 | Collections.emptyList ()); 61 | 62 | //when 63 | ClassicHttpRequest request = HttpRequestProvider.prepareRequest (commandLine); 64 | 65 | //then 66 | assertEquals (request.getFirstHeader ("Proxy-Authorization").getValue (), 67 | "Basic dXNlcjpwYXNzd29yZA=="); 68 | } 69 | 70 | @Test 71 | public void proxyWithAuthentication2 () { 72 | //given 73 | CommandLine commandLine = ReadArguments.getCommandLineFromRequest ( 74 | "-x http://localhost:80/ -U jack:insecure http://www.google.com/", 75 | Collections.emptyList ()); 76 | 77 | //when 78 | ClassicHttpRequest request = HttpRequestProvider.prepareRequest (commandLine); 79 | 80 | //then 81 | assertEquals (request.getFirstHeader ("Proxy-Authorization").getValue (), 82 | "Basic amFjazppbnNlY3VyZQ=="); 83 | } 84 | 85 | @Test 86 | public void proxyWithAuthentication3 () { 87 | //given 88 | CommandLine commandLine = ReadArguments.getCommandLineFromRequest ( 89 | "-x http://jack:insecure@localhost:80/ http://www.google.com/", 90 | Collections.emptyList ()); 91 | 92 | //when 93 | ClassicHttpRequest request = HttpRequestProvider.prepareRequest (commandLine); 94 | 95 | //then 96 | assertEquals (request.getFirstHeader ("Proxy-Authorization").getValue (), 97 | "Basic amFjazppbnNlY3VyZQ=="); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/PayloadReader.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.hc.core5.http.ContentType; 5 | import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; 6 | import org.apache.hc.core5.http.io.entity.InputStreamEntity; 7 | import org.apache.hc.core5.http.io.entity.StringEntity; 8 | 9 | import java.io.*; 10 | import java.nio.charset.*; 11 | import java.util.*; 12 | import java.util.regex.*; 13 | import java.util.stream.*; 14 | 15 | import static java.net.URLEncoder.*; 16 | import static java.util.Arrays.*; 17 | import static org.toilelibre.libe.curl.IOUtils.*; 18 | 19 | final class PayloadReader { 20 | private static final Pattern CONTENT_TYPE_ENCODING = 21 | Pattern.compile ("\\s*content-type\\s*:[^;]+;\\s*charset\\s*=\\s*(.*)", Pattern.CASE_INSENSITIVE); 22 | 23 | static AbstractHttpEntity getData (final CommandLine commandLine) { 24 | if (commandLine.hasOption (Arguments.DATA.getOpt ())) { 25 | return simpleDataFrom (commandLine); 26 | } 27 | if (commandLine.hasOption (Arguments.DATA_BINARY.getOpt ())) { 28 | return binaryDataFrom (commandLine); 29 | } 30 | if (commandLine.hasOption (Arguments.DATA_URLENCODE.getOpt ())) { 31 | return new StringEntity(stream (commandLine.getOptionValues (Arguments.DATA_URLENCODE.getOpt ())) 32 | .map (PayloadReader::urlEncodedDataFrom) 33 | .collect (Collectors.joining ("&"))); 34 | } 35 | return null; 36 | } 37 | 38 | private static StringEntity simpleDataFrom (CommandLine commandLine) { 39 | try { 40 | Charset encoding = charsetReadFromThe (commandLine).orElse (StandardCharsets.UTF_8); 41 | return new StringEntity (commandLine.getOptionValue (Arguments.DATA.getOpt ()), encoding); 42 | } catch (final IllegalArgumentException e) { 43 | throw new Curl.CurlException (e); 44 | } 45 | } 46 | 47 | private static InputStreamEntity binaryDataFrom (CommandLine commandLine) { 48 | final String value = commandLine.getOptionValue (Arguments.DATA_BINARY.getOpt ()); 49 | if (value.indexOf ('@') == 0) { 50 | return new InputStreamEntity (new ByteArrayInputStream (dataBehind (value)), ContentType.APPLICATION_OCTET_STREAM); 51 | } 52 | return new InputStreamEntity (new ByteArrayInputStream (value.getBytes ()), ContentType.APPLICATION_OCTET_STREAM); 53 | } 54 | 55 | private static Optional charsetReadFromThe (CommandLine commandLine) { 56 | 57 | return stream (Optional.ofNullable (commandLine.getOptionValues (Arguments.HEADER.getOpt ())).orElse (new String[0])) 58 | .filter (header -> header != null && CONTENT_TYPE_ENCODING.asPredicate ().test (header)) 59 | .findFirst ().map (correctHeader -> { 60 | final Matcher matcher = CONTENT_TYPE_ENCODING.matcher (correctHeader); 61 | if (!matcher.find ()) return null; 62 | return Charset.forName (matcher.group (1));}); 63 | } 64 | 65 | private static String urlEncodedDataFrom (String value) { 66 | if (value.startsWith ("=")) { 67 | value = value.substring (1); 68 | } 69 | if (value.indexOf ('=') != -1) { 70 | return value.substring (0, value.indexOf ('=') + 1) + encodeOrFail (value.substring (value.indexOf ('=') + 1), Charset.defaultCharset ()); 71 | } 72 | if (value.indexOf ('@') == 0) { 73 | return encodeOrFail (new String (dataBehind (value)), Charset.defaultCharset ()); 74 | } 75 | if (value.indexOf ('@') != -1) { 76 | return value.substring (0, value.indexOf ('@')) + '=' + encodeOrFail (new String (dataBehind (value.substring (value.indexOf ('@')))), Charset.defaultCharset ()); 77 | } 78 | return encodeOrFail (value, Charset.defaultCharset ()); 79 | } 80 | 81 | private static String encodeOrFail (String value, Charset encoding) { 82 | try { 83 | return encode (value, encoding.name ()); 84 | } catch (UnsupportedEncodingException e) { 85 | throw new Curl.CurlException (e); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/DerReader.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import java.io.*; 4 | import java.math.*; 5 | import java.security.spec.*; 6 | 7 | final class DerReader { 8 | static class Asn1Object { 9 | private static final byte LOWER_5_BITS = (byte) 0x1F; 10 | private final int tag; 11 | private final int type; 12 | 13 | private final byte [] value; 14 | 15 | Asn1Object (final int tag, final byte [] value) { 16 | this.tag = tag; 17 | this.type = tag & Asn1Object.LOWER_5_BITS; 18 | this.value = value; 19 | } 20 | 21 | BigInteger getInteger () throws IOException { 22 | if (this.type != DerReader.INTEGER) { 23 | throw new IOException ("Invalid DER: object is not integer"); 24 | } 25 | 26 | return new BigInteger (this.value); 27 | } 28 | 29 | KeySpec getKeySpec () throws IOException { 30 | 31 | final DerReader parser = this.getReader (); 32 | 33 | parser.read (); 34 | final BigInteger modulus = parser.read ().getInteger (); 35 | final BigInteger publicExp = parser.read ().getInteger (); 36 | final BigInteger privateExp = parser.read ().getInteger (); 37 | final BigInteger prime1 = parser.read ().getInteger (); 38 | final BigInteger prime2 = parser.read ().getInteger (); 39 | final BigInteger exp1 = parser.read ().getInteger (); 40 | final BigInteger exp2 = parser.read ().getInteger (); 41 | final BigInteger crtCoef = parser.read ().getInteger (); 42 | 43 | return new RSAPrivateCrtKeySpec (modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef); 44 | } 45 | 46 | DerReader getReader () throws IOException { 47 | if (!this.isConstructed ()) { 48 | throw new IOException ("Invalid DER: can't parse primitive entity"); 49 | } 50 | 51 | return new DerReader (this.value); 52 | } 53 | 54 | boolean isConstructed () { 55 | return (this.tag & DerReader.CONSTRUCTED) == DerReader.CONSTRUCTED; 56 | } 57 | } 58 | 59 | 60 | private static final int BYTE_MAX = 0xFF; 61 | private static final int CONSTRUCTED = 0x20; 62 | private static final int INTEGER = 0x02; 63 | private static final byte LOWER_7_BITS = (byte) 0x7F; 64 | private static final int MAX_NUMBER_OF_BYTES = 4; 65 | 66 | private final InputStream in; 67 | 68 | DerReader (final byte [] bytes) { 69 | this (new ByteArrayInputStream (bytes)); 70 | } 71 | 72 | private DerReader (final InputStream in) { 73 | this.in = in; 74 | } 75 | 76 | private int getLength () throws IOException { 77 | 78 | final int i = this.in.read (); 79 | if (i == -1) { 80 | throw new IOException ("Invalid DER: length missing"); 81 | } 82 | 83 | if ((i & ~DerReader.LOWER_7_BITS) == 0) { 84 | return i; 85 | } 86 | 87 | final int num = i & DerReader.LOWER_7_BITS; 88 | 89 | if ((i >= DerReader.BYTE_MAX) || (num > DerReader.MAX_NUMBER_OF_BYTES)) { 90 | throw new IOException ("Invalid DER: length field too big (" + i + ")"); 91 | } 92 | 93 | final byte [] bytes = new byte [num]; 94 | final int n = this.in.read (bytes); 95 | if (n < num) { 96 | throw new IOException ("Invalid DER: length too short"); 97 | } 98 | 99 | return new BigInteger (1, bytes).intValue (); 100 | } 101 | 102 | Asn1Object read () throws IOException { 103 | final int tag = this.in.read (); 104 | 105 | if (tag == -1) { 106 | throw new IOException ("Invalid DER: stream too short, missing tag"); 107 | } 108 | 109 | final int length = this.getLength (); 110 | 111 | final byte [] value = new byte [length]; 112 | final int n = this.in.read (value); 113 | if (n < length) { 114 | throw new IOException ("Invalid DER: stream too short, missing value"); 115 | } 116 | 117 | return new Asn1Object (tag, value); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/outside/curl/CurlFakeTest.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.outside.curl; 2 | 3 | import org.apache.hc.client5.http.auth.AuthExchange; 4 | import org.apache.hc.core5.http.ClassicHttpResponse; 5 | import org.apache.hc.core5.http.HttpHost; 6 | import org.apache.hc.core5.http.HttpRequest; 7 | import org.apache.hc.core5.http.protocol.HttpContext; 8 | import org.apache.hc.core5.util.VersionInfo; 9 | import org.junit.jupiter.api.Test; 10 | import org.toilelibre.libe.curl.Curl; 11 | 12 | import java.util.Map; 13 | import java.util.function.Consumer; 14 | 15 | import static org.junit.jupiter.api.Assertions.assertEquals; 16 | 17 | /** 18 | * This is a cheap test suite because this does not need any web server to run. 19 | * It mocks the connection backend of apache httpclient to proceed to some assertions. 20 | * If you need to write a fast test you can start here. 21 | * 22 | * Keep in mind that the same fake response will be sent. Don't assert anything on 23 | * the response as it does not make any sense. 24 | */ 25 | public class CurlFakeTest { 26 | 27 | @Test 28 | public void curlVerifyHost () { 29 | this.curl ("https://put.anything.in.this.url:1337", 30 | context -> 31 | assertEquals ("https://put.anything.in.this.url:1337", 32 | (((Map) 33 | context.getAttribute ("http.auth.exchanges")) 34 | .keySet().iterator().next()).toString()) 35 | ); 36 | } 37 | 38 | @Test 39 | public void curlVerifyHeader () { 40 | this.curl ("-H 'Titi: toto' https://put.anything.in.this.url:1337", 41 | context -> 42 | assertEquals ("toto", 43 | ((HttpRequest) 44 | context.getAttribute ("http.request")) 45 | .getLastHeader ("Titi").getValue ())); 46 | } 47 | 48 | @Test 49 | public void curlVerifyUserAgent () { 50 | this.curl ("-H 'User-Agent: toto' https://put.anything.in.this.url:1337", 51 | context -> 52 | assertEquals ("toto", 53 | ((HttpRequest) 54 | context.getAttribute ("http.request")) 55 | .getLastHeader ("User-Agent").getValue ())); 56 | } 57 | 58 | @Test 59 | public void curlVerifyUserAgentAndAOptionTogether () { 60 | this.curl ("-H 'User-Agent: toto' -A titi https://put.anything.in.this.url:1337", 61 | context -> 62 | assertEquals ("toto", 63 | ((HttpRequest) 64 | context.getAttribute ("http.request")) 65 | .getLastHeader ("User-Agent").getValue ())); 66 | } 67 | 68 | @Test 69 | public void curlVerifyAOptionAlone () { 70 | this.curl ("-A titi https://put.anything.in.this.url:1337", 71 | context -> 72 | assertEquals ("titi", 73 | ((HttpRequest) 74 | context.getAttribute ("http.request")) 75 | .getLastHeader ("User-Agent").getValue ())); 76 | } 77 | 78 | @Test 79 | public void curlWithDefaultUserAgent () { 80 | this.curl ("https://put.anything.in.this.url:1337", 81 | context -> 82 | assertEquals (Curl.class.getPackage ().getName () + "/" + Curl.getVersion () + 83 | VersionInfo.getSoftwareInfo (", Apache-HttpClient", 84 | "org.apache.http.client", CurlFakeTest.class), 85 | ((HttpRequest) context.getAttribute ("http.request")) 86 | .getLastHeader ("User-Agent").getValue ())); 87 | } 88 | 89 | private ClassicHttpResponse curl (final String requestCommand, Consumer assertions) { 90 | return org.toilelibre.libe.curl.Curl.curl (requestCommand, 91 | Curl.CurlArgumentsBuilder.CurlJavaOptions.with ().mockedNetworkAccess () 92 | .contextTester(httpContext -> assertions.accept(httpContext)).build()); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/ReadArguments.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.toilelibre.libe.curl.Curl.*; 5 | 6 | import java.util.*; 7 | import java.util.regex.*; 8 | import java.util.stream.*; 9 | 10 | import static java.lang.Integer.*; 11 | import static java.util.Optional.*; 12 | 13 | final class ReadArguments { 14 | private static final Pattern PLACEHOLDER_REGEX = Pattern.compile ("^\\$curl_placeholder_[0-9]+$"); 15 | private static final Map> CACHED_ARGS_MATCHES = new HashMap<> (); 16 | 17 | static CommandLine getCommandLineFromRequest (final String requestCommand, final List placeholderValues) { 18 | return getCommandLineFromRequest (requestCommand, placeholderValues, CACHED_ARGS_MATCHES); 19 | } 20 | 21 | static CommandLine getCommandLineFromRequest (final String requestCommand, final List placeholderValues, 22 | final boolean wantsOnlySimpleArgsParsing) { 23 | return getCommandLineFromRequest(requestCommand, placeholderValues, CACHED_ARGS_MATCHES, wantsOnlySimpleArgsParsing); 24 | } 25 | 26 | static CommandLine getCommandLineFromRequest (final String requestCommand, final List placeholderValues, 27 | final Map> argMatches) { 28 | return getCommandLineFromRequest(requestCommand, placeholderValues, argMatches, false); 29 | } 30 | 31 | static CommandLine getCommandLineFromRequest (final String requestCommand, final List placeholderValues, 32 | final Map> argMatches, 33 | final boolean wantsOnlySimpleArgsParsing) { 34 | 35 | // configure a parser 36 | final DefaultParser parser = new DefaultParser (); 37 | 38 | final String requestCommandWithoutBasename = requestCommand.replaceAll ("^[ ]*curl[ ]*", " ") + " "; 39 | final String[] args = ReadArguments.getArgsFromCommand (requestCommandWithoutBasename, placeholderValues, argMatches, 40 | wantsOnlySimpleArgsParsing); 41 | final CommandLine commandLine; 42 | try { 43 | commandLine = parser.parse (Arguments.ALL_OPTIONS, args); 44 | } catch (final ParseException e) { 45 | new HelpFormatter ().printHelp ("curl [options] url", Arguments.ALL_OPTIONS); 46 | throw new CurlException (e); 47 | } 48 | return commandLine; 49 | } 50 | 51 | private static List asMatches (Pattern regex, String input) { 52 | Matcher matcher = regex.matcher (input); 53 | List result = new ArrayList<> (); 54 | while (matcher.find ()){ 55 | result.add (matcher.group (1)); 56 | } 57 | return result; 58 | } 59 | 60 | 61 | private static String[] getArgsFromCommand (final String requestCommandWithoutBasename, 62 | final List placeholderValues, 63 | final Map> argMatches, 64 | final boolean wantsOnlySimpleArgsParsing) { 65 | final String requestCommandInput = requestCommandWithoutBasename.replaceAll ("\\s+-([a-zA-Z0-9])\\s+", " -$1 "); 66 | final List matches; 67 | if (argMatches.containsKey (requestCommandInput)) { 68 | matches = argMatches.get (requestCommandInput); 69 | }else{ 70 | matches = asMatches (wantsOnlySimpleArgsParsing 71 | ? Arguments.CHEAPER_ARGS_SPLIT_REGEX 72 | : Arguments.ARGS_SPLIT_REGEX, requestCommandInput); 73 | argMatches.put (requestCommandInput, matches); 74 | } 75 | 76 | return ofNullable (matches).map (List :: stream).orElse (Stream.empty ()).map (match -> { 77 | String argument = ReadArguments.removeSlashes (match.trim ()); 78 | if (PLACEHOLDER_REGEX.matcher (argument).matches ()) 79 | return placeholderValues.get (parseInt (argument.substring ("$curl_placeholder_".length ()))); 80 | else return argument; 81 | 82 | }).toArray (String[] ::new); 83 | } 84 | 85 | private static String removeSlashes (final String arg) { 86 | if (arg.length () == 0) { 87 | return arg; 88 | } 89 | if (arg.charAt (0) == '\"') { 90 | return arg.substring (1, arg.length () - 1).replaceAll ("\\\"", "\""); 91 | } 92 | if (arg.charAt (0) == '\'') { 93 | return arg.substring (1, arg.length () - 1).replaceAll ("\\\'", "\'"); 94 | } 95 | return arg; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/IOUtils.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.hc.core5.http.HttpEntity; 4 | 5 | import java.io.*; 6 | import java.nio.charset.*; 7 | import java.util.*; 8 | 9 | final class IOUtils { 10 | 11 | private static class StringBuilderWriter extends Writer implements Serializable { 12 | 13 | /** 14 | * 15 | */ 16 | private static final long serialVersionUID = 8461966367767048539L; 17 | private final StringBuilder builder; 18 | 19 | StringBuilderWriter () { 20 | this.builder = new StringBuilder (); 21 | } 22 | 23 | @Override 24 | public Writer append (final char value) { 25 | this.builder.append (value); 26 | return this; 27 | } 28 | 29 | @Override 30 | public Writer append (final CharSequence value) { 31 | this.builder.append (value); 32 | return this; 33 | } 34 | 35 | @Override 36 | public Writer append (final CharSequence value, final int start, final int end) { 37 | this.builder.append (value, start, end); 38 | return this; 39 | } 40 | 41 | @Override 42 | public void close () { 43 | } 44 | 45 | @Override 46 | public void flush () { 47 | } 48 | 49 | @Override 50 | public String toString () { 51 | return this.builder.toString (); 52 | } 53 | 54 | @Override 55 | public void write (final char [] value, final int offset, final int length) { 56 | this.builder.append (value, offset, length); 57 | } 58 | 59 | @Override 60 | public void write (final String value) { 61 | this.builder.append (value); 62 | } 63 | } 64 | 65 | private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; 66 | 67 | private static final int EOF = -1; 68 | 69 | private static void copy (final InputStream input, final Writer output, final Charset encoding) throws IOException { 70 | final InputStreamReader in = new InputStreamReader (input, encoding); 71 | IOUtils.copy (in, output); 72 | } 73 | 74 | private static int copy (final Reader input, final Writer output) throws IOException { 75 | final long count = IOUtils.copyLarge (input, output); 76 | if (count > Integer.MAX_VALUE) { 77 | return -1; 78 | } 79 | return (int) count; 80 | } 81 | 82 | private static long copyLarge (final Reader input, final Writer output) throws IOException { 83 | return IOUtils.copyLarge (input, output, new char [IOUtils.DEFAULT_BUFFER_SIZE]); 84 | } 85 | 86 | private static long copyLarge (final Reader input, final Writer output, final char [] buffer) throws IOException { 87 | long count = 0; 88 | int n; 89 | while (IOUtils.EOF != (n = input.read (buffer))) { 90 | output.write (buffer, 0, n); 91 | count += n; 92 | } 93 | return count; 94 | } 95 | 96 | static byte [] toByteArray (final File fileObject) throws IOException { 97 | final FileInputStream fis = new FileInputStream (fileObject); 98 | return IOUtils.toByteArray (fis, (int) fileObject.length ()); 99 | 100 | } 101 | 102 | static byte [] toByteArray (final InputStream fis, final int length) throws IOException { 103 | final byte [] result = new byte [length]; 104 | final DataInputStream dis = new DataInputStream (fis); 105 | dis.readFully (result); 106 | dis.close (); 107 | fis.close (); 108 | return result; 109 | } 110 | 111 | static byte [] toByteArray (final InputStream fis) throws IOException { 112 | byte[] chunk = new byte[4096]; 113 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); 114 | int read; 115 | while ((read = fis.read (chunk)) != -1) { 116 | byteArrayOutputStream.write (chunk, 0, read); 117 | } 118 | return byteArrayOutputStream.toByteArray (); 119 | } 120 | 121 | static String quietToString (final HttpEntity entity) { 122 | try { 123 | if (entity == null || entity.getContent () == null) return null; 124 | return IOUtils.toString (entity.getContent (), Charset.defaultCharset ()); 125 | } catch (IOException e) { 126 | throw new Curl.CurlException (e); 127 | } 128 | } 129 | 130 | static File getFile (final String filePath) { 131 | final File file = new File (filePath); 132 | if (file.exists ()) { 133 | return file; 134 | } 135 | return new File (System.getProperty ("user.dir") + File.separator + filePath); 136 | } 137 | 138 | static byte [] dataBehind (final String ref) { 139 | try { 140 | return IOUtils.toByteArray (new File (ref.substring (1).trim ())); 141 | } catch (final IOException e) { 142 | throw new Curl.CurlException (e); 143 | } 144 | } 145 | 146 | static boolean isFile (final String ref) { 147 | final String fileName = (Optional.ofNullable (ref).orElse ("").trim () + " "); 148 | if (fileName.charAt (0) != '@') { 149 | return false; 150 | } 151 | 152 | final File file = new File (fileName.substring (1).trim ()); 153 | return file.exists () && file.isFile (); 154 | } 155 | 156 | private static String toString (final InputStream input, final Charset encoding) throws IOException { 157 | final StringBuilderWriter sw = new StringBuilderWriter (); 158 | IOUtils.copy (input, sw, encoding); 159 | return sw.toString (); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/CertFormat.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.toilelibre.libe.curl.Curl.*; 4 | import org.toilelibre.libe.curl.DerReader.*; 5 | import org.toilelibre.libe.curl.PemReader.*; 6 | 7 | import java.io.*; 8 | import java.security.*; 9 | import java.security.cert.*; 10 | import java.security.spec.*; 11 | import java.util.*; 12 | import java.util.logging.*; 13 | 14 | enum CertFormat { 15 | 16 | DER ( (kind, content, passwordAsCharArray) -> { 17 | try { 18 | if (kind == Kind.CERTIFICATE) { 19 | final CertificateFactory certificateFactory = CertificateFactory.getInstance ("X.509"); 20 | return Collections.singletonList (certificateFactory.generateCertificate (new ByteArrayInputStream (content))); 21 | } 22 | if (kind == Kind.PRIVATE_KEY) { 23 | final DerReader derReader = new DerReader (content); 24 | final Asn1Object asn1 = derReader.read (); 25 | final KeyFactory keyFactory = KeyFactory.getInstance ("RSA"); 26 | final KeySpec keySpec = asn1.getKeySpec (); 27 | return Collections.singletonList (keyFactory.generatePrivate (keySpec)); 28 | } 29 | return null; 30 | } catch (CertificateException | NoSuchAlgorithmException | InvalidKeySpecException | IOException e) { 31 | throw new CurlException (e); 32 | } 33 | }), ENG ( (content, kind, passwordAsCharArray) -> { 34 | try { 35 | return KeyStore.getInstance ("pkcs12"); 36 | } catch (final KeyStoreException e) { 37 | throw new CurlException (e); 38 | } 39 | }), JKS ( (kind, content, passwordAsCharArray) -> { 40 | try { 41 | return CertFormat.readFromKeystoreType ("jks", content, kind, passwordAsCharArray); 42 | } catch (NoSuchAlgorithmException | CertificateException | IOException | KeyStoreException | UnrecoverableKeyException e) { 43 | throw new CurlException (e); 44 | } 45 | }), P12 ( (kind, content, passwordAsCharArray) -> { 46 | try { 47 | return CertFormat.readFromKeystoreType ("pkcs12", content, kind, passwordAsCharArray); 48 | } catch (NoSuchAlgorithmException | CertificateException | IOException | KeyStoreException | UnrecoverableKeyException e) { 49 | throw new CurlException (e); 50 | } 51 | }), PEM ( (kind, content, passwordAsCharArray) -> { 52 | PemReader pemReader = null; 53 | final List result = new ArrayList<> (); 54 | try { 55 | pemReader = new PemReader (new InputStreamReader (new ByteArrayInputStream (content))); 56 | PKCS8EncodedKeySpec privateKeySpec = null; 57 | PemObject pemObject; 58 | while ((pemObject = pemReader.readPemObject ()) != null) { 59 | final Kind readKind = Kind.fromValue (pemObject.getType ()); 60 | if (kind != readKind) { 61 | continue; 62 | } 63 | switch (kind) { 64 | case PRIVATE_KEY : 65 | privateKeySpec = new PKCS8EncodedKeySpec (pemObject.getContent ()); 66 | final KeyFactory keyFactory = KeyFactory.getInstance ("RSA"); 67 | result.add (keyFactory.generatePrivate (privateKeySpec)); 68 | break; 69 | case CERTIFICATE : 70 | final CertificateFactory certificateFactory = CertificateFactory.getInstance ("X.509"); 71 | result.add (certificateFactory.generateCertificate (new ByteArrayInputStream (pemObject.getContent ()))); 72 | break; 73 | default: 74 | break; 75 | } 76 | } 77 | return result; 78 | } catch (NoSuchAlgorithmException | CertificateException | IOException | InvalidKeySpecException e) { 79 | throw new CurlException (e); 80 | } finally { 81 | if (pemReader != null) { 82 | try { 83 | pemReader.close (); 84 | } catch (final IOException e) { 85 | logProblemWithPemReader (e); 86 | } 87 | } 88 | } 89 | }); 90 | 91 | private static Logger LOGGER = Logger.getLogger (AfterResponse.class.getName ()); 92 | private KeystoreFromFileGenerator generator; 93 | 94 | CertFormat (final KeystoreFromFileGenerator generator1) { 95 | this.generator = generator1; 96 | } 97 | 98 | @SuppressWarnings ("unchecked") 99 | List generateCredentialsFromFileAndPassword (final Kind kind, final byte [] content, final char [] passwordAsCharArray) { 100 | return (List) this.generator.generate (kind, content, passwordAsCharArray); 101 | } 102 | 103 | private static void logProblemWithPemReader (IOException e) { 104 | LOGGER.log (Level.WARNING, "Problem with PEM reader", e); 105 | } 106 | 107 | @FunctionalInterface 108 | interface KeystoreFromFileGenerator { 109 | Object generate (Kind kind, byte [] content, char [] passwordAsCharArray); 110 | } 111 | 112 | enum Kind { 113 | CERTIFICATE, PRIVATE_KEY; 114 | static Kind fromValue (final String value) { 115 | try { 116 | return Kind.valueOf (value.toUpperCase ().replace (' ', '_')); 117 | } catch (final IllegalArgumentException iae) { 118 | return null; 119 | } 120 | } 121 | } 122 | 123 | private static List readFromKeystoreType (final String type, final byte [] content, final Kind kind, final char [] passwordAsCharArray) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException { 124 | final KeyStore keyStore = KeyStore.getInstance (type); 125 | keyStore.load (new ByteArrayInputStream (content), passwordAsCharArray); 126 | final Enumeration aliases = keyStore.aliases (); 127 | final List result = new ArrayList<> (); 128 | while (aliases.hasMoreElements ()) { 129 | final String alias = aliases.nextElement (); 130 | if ((keyStore.getCertificate (alias) != null) && (kind == Kind.CERTIFICATE)) { 131 | result.add (keyStore.getCertificate (alias)); 132 | } 133 | if ((keyStore.getKey (alias, passwordAsCharArray) != null) && (kind == Kind.PRIVATE_KEY)) { 134 | result.add (keyStore.getKey (alias, passwordAsCharArray)); 135 | } 136 | } 137 | return result; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/InterceptorsBinder.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 5 | import org.apache.hc.core5.http.*; 6 | import org.apache.hc.core5.http.impl.io.HttpRequestExecutor; 7 | import org.apache.hc.core5.http.io.HttpClientConnection; 8 | import org.apache.hc.core5.http.io.HttpResponseInformationCallback; 9 | import org.apache.hc.core5.http.protocol.HttpContext; 10 | 11 | import java.io.*; 12 | import java.lang.reflect.*; 13 | import java.lang.reflect.Method; 14 | import java.util.*; 15 | import java.util.function.*; 16 | 17 | import static java.util.Arrays.*; 18 | import static java.util.stream.Collectors.*; 19 | import static java.util.stream.Stream.*; 20 | 21 | final class InterceptorsBinder { 22 | 23 | private static final BiFunction, ClassicHttpResponse> EXAMPLE 24 | = ((request, responseSupplier) -> responseSupplier.get ()); 25 | 26 | private static Type EXAMPLE_TYPE; 27 | 28 | static { 29 | try { 30 | EXAMPLE_TYPE = InterceptorsBinder.class.getDeclaredField ("EXAMPLE").getGenericType (); 31 | } catch (NoSuchFieldException e) { 32 | throw new Curl.CurlException (new IllegalArgumentException (e)); 33 | } 34 | } 35 | 36 | @SuppressWarnings ("unchecked") 37 | static void handleInterceptors (CommandLine commandLine, HttpClientBuilder executor, List, ClassicHttpResponse>> additionalInterceptors) { 38 | final List, ClassicHttpResponse>> interceptors = 39 | concat (stream (Optional.ofNullable (commandLine.getOptionValues (Arguments.INTERCEPTOR.getOpt ())).orElse (new String[0])) 40 | .map (methodName -> { 41 | final Class targetClass; 42 | try { 43 | targetClass = Class.forName (methodName.split ("::")[0]); 44 | } catch (ClassNotFoundException e) { 45 | return null; 46 | } 47 | Object newInstance; 48 | try { 49 | newInstance = targetClass.newInstance (); 50 | } catch (InstantiationException | IllegalAccessException e) { 51 | newInstance = null; 52 | } 53 | final Object finalNewInstance = newInstance; 54 | try { 55 | final BiFunction, ClassicHttpResponse> candidate = 56 | stream (targetClass.getDeclaredFields ()).filter (f -> 57 | EXAMPLE_TYPE.equals (f.getGenericType ())) 58 | .findFirst () 59 | .map (f -> { 60 | try { 61 | f.setAccessible (true); 62 | return (BiFunction, ClassicHttpResponse>) 63 | f.get (finalNewInstance); 64 | } catch (IllegalAccessException e) { 65 | return null; 66 | } 67 | }).orElse (null); 68 | if (candidate != null) return candidate; 69 | final Method targetMethod = stream (targetClass.getDeclaredMethods ()).filter (m -> 70 | methodName.split ("::")[1].equals (m.getName ())).findFirst ().orElse (null); 71 | if (targetMethod == null) return null; 72 | return (BiFunction, ClassicHttpResponse>) 73 | (request, subsequentCall) -> { 74 | try { 75 | return (ClassicHttpResponse) targetMethod.invoke (finalNewInstance, 76 | request, 77 | subsequentCall); 78 | } catch (IllegalAccessException | InvocationTargetException e) { 79 | throw new Curl.CurlException (e); 80 | } 81 | }; 82 | } catch (ClassCastException e) { 83 | return null; 84 | } 85 | }) 86 | .filter (Objects::nonNull), additionalInterceptors.stream ()) 87 | .collect (toList ()); 88 | executor.setRequestExecutor (new HttpRequestExecutor () { 89 | @Override 90 | public ClassicHttpResponse execute (ClassicHttpRequest request, 91 | HttpClientConnection connection, 92 | HttpResponseInformationCallback callback, 93 | HttpContext context) { 94 | Supplier executor = () -> { 95 | try { 96 | return super.execute (request, connection, callback, context); 97 | } catch (IOException | HttpException e) { 98 | throw new Curl.CurlException (e); 99 | } 100 | }; 101 | return loop (request, executor, interceptors); 102 | } 103 | 104 | ClassicHttpResponse loop (HttpRequest request, Supplier realCall, 105 | List, ClassicHttpResponse>> remainingInterceptors) { 106 | if (remainingInterceptors.size () > 0) { 107 | BiFunction, ClassicHttpResponse> nextInterceptor = 108 | remainingInterceptors.get (0); 109 | return nextInterceptor.apply (request, () -> this.loop (request, realCall, 110 | remainingInterceptors.subList (1, remainingInterceptors.size ()))); 111 | } else return realCall.get (); 112 | } 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/MockNetworkAccess.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.hc.client5.http.HttpRoute; 4 | import org.apache.hc.client5.http.io.ConnectionEndpoint; 5 | import org.apache.hc.client5.http.io.HttpClientConnectionManager; 6 | import org.apache.hc.client5.http.io.LeaseRequest; 7 | import org.apache.hc.core5.http.ClassicHttpRequest; 8 | import org.apache.hc.core5.http.ClassicHttpResponse; 9 | import org.apache.hc.core5.http.EndpointDetails; 10 | import org.apache.hc.core5.http.HttpException; 11 | import org.apache.hc.core5.http.ParseException; 12 | import org.apache.hc.core5.http.ProtocolVersion; 13 | import org.apache.hc.core5.http.impl.BasicEndpointDetails; 14 | import org.apache.hc.core5.http.impl.io.HttpRequestExecutor; 15 | import org.apache.hc.core5.http.io.HttpClientConnection; 16 | import org.apache.hc.core5.http.io.entity.StringEntity; 17 | import org.apache.hc.core5.http.message.BasicClassicHttpResponse; 18 | import org.apache.hc.core5.http.message.BasicHeader; 19 | import org.apache.hc.core5.http.protocol.HttpContext; 20 | import org.apache.hc.core5.io.CloseMode; 21 | import org.apache.hc.core5.util.TimeValue; 22 | import org.apache.hc.core5.util.Timeout; 23 | 24 | import javax.net.ssl.SSLSession; 25 | import java.io.IOException; 26 | import java.net.InetSocketAddress; 27 | import java.net.SocketAddress; 28 | 29 | class MockNetworkAccess implements HttpClientConnectionManager { 30 | public static HttpClientConnection mockConnection = new HttpClientConnection() { 31 | @Override 32 | public boolean isConsistent() { 33 | return true; 34 | } 35 | 36 | @Override 37 | public void sendRequestHeader(ClassicHttpRequest request) { 38 | 39 | } 40 | 41 | @Override 42 | public void terminateRequest(ClassicHttpRequest request) { 43 | 44 | } 45 | 46 | @Override 47 | public void sendRequestEntity(ClassicHttpRequest request) { 48 | 49 | } 50 | 51 | @Override 52 | public ClassicHttpResponse receiveResponseHeader() { 53 | ClassicHttpResponse response = new BasicClassicHttpResponse(200); 54 | response.setEntity(new StringEntity("{\"status\":\"ok\"}")); 55 | response.setHeader(new BasicHeader("Content-Type", "application/json")); 56 | return response; 57 | } 58 | 59 | @Override 60 | public void receiveResponseEntity(ClassicHttpResponse response) { 61 | 62 | } 63 | 64 | @Override 65 | public boolean isDataAvailable(Timeout timeout) { 66 | return true; 67 | } 68 | 69 | @Override 70 | public boolean isStale() { 71 | return false; 72 | } 73 | 74 | @Override 75 | public void flush() { 76 | 77 | } 78 | 79 | @Override 80 | public void close() { 81 | 82 | } 83 | 84 | @Override 85 | public EndpointDetails getEndpointDetails() { 86 | return new BasicEndpointDetails( 87 | InetSocketAddress.createUnresolved("localhost", 80), 88 | InetSocketAddress.createUnresolved("localhost", 80), 89 | null, 90 | Timeout.ofSeconds(60) 91 | 92 | ); 93 | } 94 | 95 | @Override 96 | public SocketAddress getLocalAddress() { 97 | return getEndpointDetails().getLocalAddress(); 98 | } 99 | 100 | @Override 101 | public SocketAddress getRemoteAddress() { 102 | return getEndpointDetails().getRemoteAddress(); 103 | } 104 | 105 | @Override 106 | public ProtocolVersion getProtocolVersion() { 107 | try { 108 | return ProtocolVersion.parse("http/2"); 109 | } catch (ParseException e) { 110 | throw new RuntimeException(e); 111 | } 112 | } 113 | 114 | @Override 115 | public SSLSession getSSLSession() { 116 | return null; 117 | } 118 | 119 | @Override 120 | public boolean isOpen() { 121 | return false; 122 | } 123 | 124 | @Override 125 | public Timeout getSocketTimeout() { 126 | return getEndpointDetails().getSocketTimeout(); 127 | } 128 | 129 | @Override 130 | public void setSocketTimeout(Timeout timeout) { 131 | 132 | } 133 | 134 | @Override 135 | public void close(CloseMode closeMode) { 136 | 137 | } 138 | }; 139 | @Override 140 | public LeaseRequest lease(String s, HttpRoute httpRoute, Timeout timeout, Object o) { 141 | return new LeaseRequest() { 142 | @Override 143 | public boolean cancel() { 144 | return true; 145 | } 146 | 147 | public ConnectionEndpoint get(Timeout timeout) { 148 | return new ConnectionEndpoint() { 149 | @Override 150 | public ClassicHttpResponse execute(String id, ClassicHttpRequest request, RequestExecutor requestExecutor, HttpContext context) throws IOException, HttpException { 151 | return requestExecutor.execute(request, mockConnection, context); 152 | } 153 | 154 | @Override 155 | public ClassicHttpResponse execute(String s, 156 | ClassicHttpRequest classicHttpRequest, 157 | HttpRequestExecutor httpRequestExecutor, 158 | HttpContext httpContext) { 159 | return null; 160 | } 161 | 162 | @Override 163 | public boolean isConnected() { 164 | return false; 165 | } 166 | 167 | @Override 168 | public void setSocketTimeout(Timeout timeout) { 169 | 170 | } 171 | 172 | @Override 173 | public void close(CloseMode closeMode) { 174 | 175 | } 176 | 177 | @Override 178 | public void close() throws IOException { 179 | 180 | } 181 | }; 182 | } 183 | }; 184 | } 185 | 186 | @Override 187 | public void release(ConnectionEndpoint connectionEndpoint, Object o, TimeValue timeValue) { 188 | 189 | } 190 | 191 | @Override 192 | public void connect(ConnectionEndpoint connectionEndpoint, TimeValue timeValue, HttpContext httpContext) throws IOException { 193 | } 194 | 195 | @Override 196 | public void upgrade(ConnectionEndpoint connectionEndpoint, HttpContext httpContext) throws IOException { 197 | 198 | } 199 | 200 | @Override 201 | public void close(CloseMode closeMode) { 202 | 203 | } 204 | 205 | @Override 206 | public void close() throws IOException { 207 | 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/Arguments.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | 5 | import java.util.regex.*; 6 | 7 | final class Arguments { 8 | 9 | final static Options ALL_OPTIONS = new Options (); 10 | 11 | final static Pattern ARGS_SPLIT_REGEX = Pattern.compile ("((?:(?:([^'\"\\s]+)(?=[\\s\"']))|(?:\"((?:[^\"]|(?<=\\\\)\")*)\"|(?:'((?:[^']|'(?! )[^']*')*)')|^[^\\s]+$|^[^\\s]+(?= )|(?<= )[^ ]+$)[^\\s\"']*))"); 12 | final static Pattern CHEAPER_ARGS_SPLIT_REGEX = Pattern.compile("(\"[^\"]*\"|'[^']*'|\\S+)"); 13 | 14 | final static Option AUTH = Arguments.add (Option.builder ("u").longOpt ("username").desc ("credentials").required (false).hasArg (true).desc ("user:password").build ()); 15 | 16 | final static Option CA_CERT = Arguments.add (Option.builder ("cacert").longOpt ("cacert").desc ("CA certificate").required (false).hasArg (true).desc ("CA_CERT").build ()); 17 | 18 | final static Option CERT = Arguments.add (Option.builder ("E").longOpt ("cert").desc ("client certificate").required (false).hasArg (true).desc ("CERT[:password]").build ()); 19 | 20 | final static Option CERT_TYPE = Arguments.add (Option.builder ("ct").longOpt ("cert-type").desc ("certificate type").required (false).hasArg (true).desc ("PEM|P12|JKS|DER|ENG").build ()); 21 | 22 | final static Option COMPRESSED = Arguments.add (Option.builder ("compressed").longOpt ("compressed").desc ("Request compressed response").required (false).hasArg (false).build ()); 23 | 24 | final static Option CONNECT_TIMEOUT = Arguments.add (Option.builder ("cti").longOpt ("connect-timeout").desc ("Maximum time allowed for connection").required (false).hasArg (true).argName ("seconds").build ()); 25 | 26 | final static Option DATA = Arguments.add (Option.builder ("d").longOpt ("data").desc ("Data").required (false).hasArg ().argName ("payload").build ()); 27 | 28 | final static Option DATA_BINARY = Arguments.add (Option.builder ("databinary").longOpt ("data-binary").desc ("http post binary data").required (false).hasArg ().argName ("payload").build ()); 29 | 30 | final static Option DATA_URLENCODE = Arguments.add (Option.builder ("dataurlencode").longOpt ("data-urlencode").desc ("Data to URLEncode").required (false).hasArg ().argName ("payload").build ()); 31 | 32 | final static Option FOLLOW_REDIRECTS = Arguments.add (Option.builder ("L").longOpt ("location").desc ("follow redirects").required (false).hasArg (false).build ()); 33 | 34 | final static Option FORM = Arguments.add (Option.builder ("F").longOpt ("form").desc ("http multipart post data").required (false).hasArg (true).build ()); 35 | 36 | final static Option GET = Arguments.add (Option.builder ("G").longOpt ("get").desc ("forces to use GET request method").required (false).hasArg (false).build ()); 37 | 38 | final static Option HEADER = Arguments.add (Option.builder ("H").longOpt ("header").desc ("Header").required (false).hasArg ().argName ("headerValue").build ()); 39 | 40 | final static Option HTTP_METHOD = Arguments.add (Option.builder ("X").longOpt ("request").desc ("Http Method").required (false).hasArg ().argName ("method").build ()); 41 | 42 | final static Option KEY = Arguments.add (Option.builder ("key").longOpt ("key").desc ("key").required (false).hasArg (true).desc ("KEY").build ()); 43 | 44 | final static Option KEY_TYPE = Arguments.add (Option.builder ("kt").longOpt ("key-type").desc ("key type").required (false).hasArg (true).desc ("PEM|P12|JKS|DER|ENG").build ()); 45 | 46 | final static Option MAX_TIME = Arguments.add (Option.builder ("m").longOpt ("max-time").desc ("Maximum time allowed for the transfer").required (false).hasArg (true).argName ("seconds").build ()); 47 | 48 | final static Option NO_KEEPALIVE = Arguments.add (Option.builder ("nokeepalive").longOpt ("no-keepalive").desc ("Disable TCP keepalive on the connection").required (false).hasArg (false).build ()); 49 | 50 | final static Option NTLM = Arguments.add (Option.builder ("ntlm").longOpt ("ntlm").desc ("NTLM auth").required (false).hasArg (false).build ()); 51 | 52 | final static Option OUTPUT = Arguments.add (Option.builder ("o").longOpt ("output").desc ("write to file").required (false).hasArg (true).argName ("FILE").build ()); 53 | 54 | final static Option PROXY = Arguments.add (Option.builder ("x").longOpt ("proxy").desc ("use the specified HTTP proxy").required (false).hasArg (true).argName ("<[protocol://][user:password@]proxyhost[:port]>").build ()); 55 | 56 | final static Option PROXY_USER = Arguments.add (Option.builder ("U").longOpt ("proxy-user").desc ("authentication for proxy").required (false).hasArg (true).argName ("user[:password]").build ()); 57 | 58 | final static Option TLS_V1 = Arguments.add (Option.builder ("1").longOpt ("tlsv1").desc ("use > = TLSv1 (SSL)").required (false).hasArg (false).build ()); 59 | 60 | final static Option TLS_V10 = Arguments.add (Option.builder ("tlsv10").longOpt ("tlsv1.0").desc ("use TLSv1.0 (SSL)").required (false).hasArg (false).build ()); 61 | 62 | final static Option TLS_V11 = Arguments.add (Option.builder ("tlsv11").longOpt ("tlsv1.1").desc ("use TLSv1.1 (SSL)").required (false).hasArg (false).build ()); 63 | 64 | final static Option TLS_V12 = Arguments.add (Option.builder ("tlsv12").longOpt ("tlsv1.2").desc ("use TLSv1.2 (SSL)").required (false).hasArg (false).build ()); 65 | 66 | final static Option SSL_V2 = Arguments.add (Option.builder ("2").longOpt ("sslv2").desc ("use SSLv2 (SSL)").required (false).hasArg (false).build ()); 67 | 68 | final static Option SSL_V3 = Arguments.add (Option.builder ("3").longOpt ("sslv3").desc ("use SSLv3 (SSL)").required (false).hasArg (false).build ()); 69 | 70 | final static Option TRUST_INSECURE = Arguments.add (Option.builder ("k").longOpt ("insecure").desc ("trust insecure").required (false).hasArg (false).build ()); 71 | 72 | final static Option USER_AGENT = Arguments.add (Option.builder ("A").longOpt ("user-agent").desc ("user agent").required (false).hasArg (true).build ()); 73 | 74 | final static Option VERSION = Arguments.add (Option.builder ("V").longOpt ("version").desc ("get the version of this library").required (false).hasArg (false).build ()); 75 | 76 | final static Option INTERCEPTOR = Arguments.add (Option.builder ("interceptor").longOpt ("interceptor").desc ("interceptor field or method (syntax is classname::fieldname). Must be a BiFunction, HttpResponse> or will be discarded").required (false).hasArg (true).build ()); 77 | 78 | private static Option add (final Option option) { 79 | Arguments.ALL_OPTIONS.addOption (option); 80 | return option; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/HttpRequestProvider.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.CommandLine; 4 | import org.apache.hc.client5.http.config.ConnectionConfig; 5 | import org.apache.hc.client5.http.entity.mime.FileBody; 6 | import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; 7 | import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; 8 | import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; 9 | import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; 10 | import org.apache.hc.client5.http.routing.HttpRoutePlanner; 11 | import org.apache.hc.core5.http.ClassicHttpRequest; 12 | import org.apache.hc.core5.http.HttpEntity; 13 | import org.apache.hc.core5.http.HttpHost; 14 | import org.apache.hc.core5.http.message.BasicClassicHttpRequest; 15 | import org.apache.hc.core5.http.message.BasicHeader; 16 | import org.apache.hc.core5.util.Timeout; 17 | import org.apache.hc.core5.util.VersionInfo; 18 | import org.toilelibre.libe.curl.Curl.CurlException; 19 | 20 | import java.net.URI; 21 | import java.net.URISyntaxException; 22 | import java.time.Duration; 23 | import java.util.Base64; 24 | import java.util.List; 25 | import java.util.Objects; 26 | import java.util.Optional; 27 | import java.util.stream.Collectors; 28 | 29 | import static java.util.Arrays.asList; 30 | import static java.util.Arrays.stream; 31 | import static java.util.stream.Collectors.toList; 32 | import static org.toilelibre.libe.curl.IOUtils.isFile; 33 | import static org.toilelibre.libe.curl.PayloadReader.getData; 34 | 35 | final class HttpRequestProvider { 36 | 37 | static ClassicHttpRequest prepareRequest (final CommandLine commandLine) throws CurlException { 38 | 39 | final String method = getMethod (commandLine); 40 | final BasicClassicHttpRequest request = wrapInRequestBuilder (method, commandLine.getArgs ()[0]); 41 | 42 | if (asList ("DELETE", "PATCH", "POST", "PUT").contains (method.toUpperCase ())) { 43 | request.setEntity (getData (commandLine)); 44 | 45 | if (request.getEntity () == null) { 46 | request.setEntity (HttpRequestProvider.getForm (commandLine)); 47 | } 48 | } 49 | 50 | HttpRequestProvider.setHeaders (commandLine, request); 51 | 52 | return request; 53 | 54 | } 55 | 56 | private static String getMethod (final CommandLine cl) throws CurlException { 57 | return cl.getOptionValue (Arguments.HTTP_METHOD.getOpt ()) == null ? determineVerbWithoutArgument (cl) : cl.getOptionValue (Arguments.HTTP_METHOD.getOpt ()); 58 | } 59 | 60 | private static BasicClassicHttpRequest wrapInRequestBuilder (String method, String uriAsString) { 61 | try { 62 | return new BasicClassicHttpRequest (method, new URI (uriAsString)); 63 | } catch (URISyntaxException e) { 64 | throw new CurlException (e); 65 | } 66 | } 67 | 68 | private static String determineVerbWithoutArgument (CommandLine commandLine) { 69 | if (commandLine.getOptionValue (Arguments.GET.getOpt()) != null) 70 | return "GET"; 71 | if (commandLine.hasOption (Arguments.DATA.getOpt ()) || 72 | commandLine.hasOption (Arguments.DATA_URLENCODE.getOpt ()) || 73 | commandLine.hasOption (Arguments.FORM.getOpt ())) { 74 | return "POST"; 75 | } 76 | return "GET"; 77 | } 78 | 79 | 80 | private static HttpEntity getForm (final CommandLine commandLine) { 81 | final String [] forms = Optional.ofNullable (commandLine.getOptionValues (Arguments.FORM.getOpt ())).orElse (new String [0]); 82 | 83 | if (forms.length == 0) { 84 | return null; 85 | } 86 | 87 | final MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create (); 88 | 89 | stream (forms).forEach (arg -> { 90 | if (arg.indexOf ('=') == -1) { 91 | throw new IllegalArgumentException ("option -F: is badly used here"); 92 | } 93 | }); 94 | 95 | final List fileForms = stream (forms).filter (arg -> isFile (arg.substring (arg.indexOf ('=') + 1))).collect (toList ()); 96 | final List textForms = stream (forms).filter (form -> !fileForms.contains (form)).collect (toList ()); 97 | 98 | fileForms.forEach (arg -> multiPartBuilder.addPart (arg.substring (0, arg.indexOf ('=')), 99 | new FileBody (IOUtils.getFile ( (arg.substring (arg.indexOf ("=@") + 2)))))); 100 | textForms.forEach (arg -> multiPartBuilder.addTextBody (arg.substring (0, arg.indexOf ('=')), arg.substring (arg.indexOf ('=') + 1))); 101 | 102 | return multiPartBuilder.build (); 103 | 104 | } 105 | 106 | private static void setHeaders (final CommandLine commandLine, final BasicClassicHttpRequest request) { 107 | final String [] headers = Optional.ofNullable (commandLine.getOptionValues (Arguments.HEADER.getOpt ())).orElse (new String [0]); 108 | 109 | List basicHeaders = 110 | stream (headers).filter (optionAsString -> optionAsString.indexOf (':') != -1).map (optionAsString -> optionAsString.split (":")) 111 | .map (optionAsArray -> new BasicHeader (optionAsArray [0].trim ().replaceAll ("^\"", "").replaceAll ("\\\"$", "").replaceAll ("^\\'", "").replaceAll ("\\'$", ""), 112 | String.join (":", asList (optionAsArray).subList (1, optionAsArray.length)).trim ())).collect (Collectors.toList ()); 113 | 114 | basicHeaders.forEach (request::addHeader); 115 | 116 | if (basicHeaders.stream ().noneMatch (h -> Objects.equals (h.getName ().toLowerCase (), "user-agent")) && 117 | commandLine.hasOption (Arguments.USER_AGENT.getOpt ())) { 118 | request.addHeader ("User-Agent", commandLine.getOptionValue (Arguments.USER_AGENT.getOpt ())); 119 | } 120 | 121 | if (basicHeaders.stream ().noneMatch (h -> Objects.equals (h.getName ().toLowerCase (), "user-agent")) && 122 | !commandLine.hasOption (Arguments.USER_AGENT.getOpt ())) { 123 | 124 | request.addHeader ("User-Agent", 125 | Curl.class.getPackage ().getName () + "/" + Version.NUMBER + 126 | VersionInfo.getSoftwareInfo( 127 | "Apache-HttpClient", "org.apache.hc.client5", HttpRequestProvider.class)); 128 | } 129 | 130 | if (commandLine.hasOption (Arguments.DATA_URLENCODE.getOpt ())) { 131 | request.addHeader ("Content-Type", "application/x-www-form-urlencoded"); 132 | } 133 | 134 | if (commandLine.hasOption (Arguments.NO_KEEPALIVE.getOpt ())){ 135 | request.addHeader ("Connection", "close"); 136 | } 137 | 138 | if (commandLine.hasOption (Arguments.PROXY_USER.getOpt ())) { 139 | request.addHeader ("Proxy-Authorization", "Basic " + Base64.getEncoder ().encodeToString ( 140 | commandLine.getOptionValue (Arguments.PROXY_USER.getOpt ()).getBytes ())); 141 | }else if (commandLine.hasOption (Arguments.PROXY.getOpt ()) && 142 | commandLine.getOptionValue (Arguments.PROXY.getOpt ()).contains ("@")){ 143 | request.addHeader ("Proxy-Authorization", "Basic " + Base64.getEncoder ().encodeToString ( 144 | commandLine.getOptionValue (Arguments.PROXY.getOpt ()) 145 | .replaceFirst ("^[^/]+/+", "").split ("@")[0].getBytes ())); 146 | } 147 | } 148 | 149 | static HttpRoutePlanner getRoutePlanner (final CommandLine commandLine) { 150 | 151 | if (commandLine.hasOption(Arguments.PROXY.getOpt())) { 152 | String hostWithoutTrailingSlash = commandLine.getOptionValue(Arguments.PROXY.getOpt()) 153 | .replaceFirst("\\s*/\\s*$", "") 154 | .replaceFirst("^[^@]+@", ""); 155 | try { 156 | return new DefaultProxyRoutePlanner(HttpHost.create(hostWithoutTrailingSlash)); 157 | } catch (URISyntaxException e) { 158 | throw new CurlException(e); 159 | } 160 | } 161 | return new DefaultRoutePlanner(new DefaultSchemePortResolver()); 162 | } 163 | 164 | static ConnectionConfig getConnectionConfig (final CommandLine commandLine) { 165 | ConnectionConfig.Builder connectionConfig = ConnectionConfig.custom(); 166 | if (commandLine.hasOption (Arguments.CONNECT_TIMEOUT.getOpt ())) { 167 | connectionConfig.setConnectTimeout (Timeout.of (Duration.ofMillis ((int) (Float.parseFloat ( 168 | commandLine.getOptionValue (Arguments.CONNECT_TIMEOUT.getOpt ())) * 1000)))); 169 | } 170 | 171 | if (commandLine.hasOption (Arguments.MAX_TIME.getOpt ())) { 172 | connectionConfig.setSocketTimeout (Timeout.of (Duration.ofMillis ((int) (Float.parseFloat ( 173 | commandLine.getOptionValue (Arguments.MAX_TIME.getOpt ())) * 1000)))); 174 | } 175 | 176 | return connectionConfig.build (); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/curl/ArgumentsBuilderGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import javassist.CannotCompileException; 4 | import javassist.ClassPool; 5 | import javassist.CtClass; 6 | import javassist.CtMethod; 7 | import javassist.NotFoundException; 8 | import org.apache.commons.cli.Option; 9 | import org.apache.commons.io.IOUtils; 10 | import org.junit.jupiter.api.Test; 11 | import org.toilelibre.libe.curl.Curl.CurlArgumentsBuilder; 12 | 13 | import java.io.File; 14 | import java.io.FileInputStream; 15 | import java.io.FileNotFoundException; 16 | import java.io.FileWriter; 17 | import java.io.IOException; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.Objects; 21 | import java.util.regex.Matcher; 22 | import java.util.regex.Pattern; 23 | import java.util.stream.IntStream; 24 | 25 | import static java.util.Arrays.stream; 26 | import static java.util.stream.Collectors.joining; 27 | 28 | public class ArgumentsBuilderGeneratorTest { 29 | 30 | private static final Pattern WORD_SEPARATOR = Pattern.compile ("-([a-zA-Z])"); 31 | private static final Pattern DIGITS_PATTERN = Pattern.compile ("-([0-9]+)"); 32 | private static final List DIGITS = Arrays.asList ("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"); 33 | 34 | @Test 35 | public void addOptionsToArgumentsBuilder () throws NotFoundException, CannotCompileException, IOException { 36 | final ClassPool pool = ClassPool.getDefault (); 37 | final CtClass argsBuilderClass = pool.get (CurlArgumentsBuilder.class.getName ()); 38 | final CtClass stringType = pool.get (String.class.getName ()); 39 | argsBuilderClass.defrost (); 40 | 41 | final String methodName = this.methodNameOf (Arguments.ALL_OPTIONS.getOptions ().iterator ().next ().getLongOpt ()); 42 | if (argsBuilderClass.getDeclaredMethods (methodName).length > 0) { 43 | return; 44 | } 45 | 46 | for (final Option option : Arguments.ALL_OPTIONS.getOptions ()) { 47 | final String shortMethodName = this.methodNameOf (option.getOpt ()); 48 | final String longMethodName = this.methodNameOf (option.getLongOpt ()); 49 | argsBuilderClass.addMethod (this.builderOptionMethod (argsBuilderClass, stringType, longMethodName, option.getLongOpt (), option.hasArg ())); 50 | if (!shortMethodName.equals (longMethodName)) { 51 | argsBuilderClass.addMethod (this.builderOptionMethod (argsBuilderClass, stringType, shortMethodName, option.getOpt (), option.hasArg ())); 52 | } 53 | } 54 | 55 | argsBuilderClass.writeFile ("target/classes"); 56 | } 57 | 58 | @Test 59 | public void addOptionsToReadmeMarkdown () throws IOException { 60 | final int shortNamesMaxLength = Arguments.ALL_OPTIONS.getOptions ().stream ().map (Option::getOpt) 61 | .map (String::length).max (Integer::compareTo).orElse (0); 62 | final int longNamesMaxLength = Arguments.ALL_OPTIONS.getOptions ().stream ().map (Option::getLongOpt) 63 | .map (String::length).max (Integer::compareTo).orElse (0); 64 | final int descriptionMaxLength = Arguments.ALL_OPTIONS.getOptions ().stream ().map (Option::getDescription) 65 | .map (String::length).max (Integer::compareTo).orElse (0); 66 | 67 | StringBuilder tableBuilder = new StringBuilder (); 68 | tableBuilder.append ("| Short Name ").append (IntStream.range (1, Math.max (0, shortNamesMaxLength - 9)) 69 | .mapToObj (i -> " ").collect (joining ())); 70 | tableBuilder.append ("| Long Name ").append (IntStream.range (1, Math.max (0, longNamesMaxLength - 8)) 71 | .mapToObj (i -> " ").collect (joining ())); 72 | tableBuilder.append ("| Argument Required "); 73 | tableBuilder.append ("| Description ").append (IntStream.range (0, Math.max (0, descriptionMaxLength - 11)) 74 | .mapToObj (i -> " ").collect (joining ())).append ("|\n"); 75 | tableBuilder.append ("| ").append (IntStream.range (1, Math.max (0, shortNamesMaxLength) + 1) 76 | .mapToObj (i -> "-").collect (joining ())).append (' '); 77 | tableBuilder.append ("| ").append (IntStream.range (1, Math.max (0, longNamesMaxLength) + 1) 78 | .mapToObj (i -> "-").collect (joining ())).append (' '); 79 | tableBuilder.append ("| ----------------- "); 80 | tableBuilder.append ("| ").append (IntStream.range (1, Math.max (0, descriptionMaxLength) + 1) 81 | .mapToObj (i -> "-").collect (joining ())).append (" |\n"); 82 | 83 | for (final Option option : Arguments.ALL_OPTIONS.getOptions ()) { 84 | tableBuilder.append ("| ").append (option.getOpt ()) 85 | .append (IntStream.range (1, Math.max (0, shortNamesMaxLength - option.getOpt ().length () + 2)) 86 | .mapToObj (i -> " ").collect (joining ())); 87 | tableBuilder.append ("| ").append (option.getLongOpt ()) 88 | .append (IntStream.range (1, Math.max (0, longNamesMaxLength - option.getLongOpt ().length () + 2)) 89 | .mapToObj (i -> " ").collect (joining ())); 90 | tableBuilder.append ("| ").append (option.hasArg ()) 91 | .append (IntStream.range (1, Math.max (0, 19 - (option.hasArg () ? 4 : 5))) 92 | .mapToObj (i -> " ").collect (joining ())); 93 | tableBuilder.append ("| ").append (option.getDescription ().replace ('|', ',')) 94 | .append (IntStream.range (1, Math.max (0, descriptionMaxLength - option.getDescription ().length () + 2)) 95 | .mapToObj (i -> " ").collect (joining ())).append ("|\n"); 96 | } 97 | final File readme = stream (Objects.requireNonNull (new File (ArgumentsBuilderGeneratorTest.class 98 | .getProtectionDomain ().getCodeSource ().getLocation ().getFile ()).getParentFile ().getParentFile () 99 | .listFiles ())).filter (f -> "README.md".equalsIgnoreCase (f.getName ())) 100 | .findFirst ().orElseThrow (() -> new FileNotFoundException ("README.md")); 101 | final String readmeContent = IOUtils.toString (new FileInputStream (readme)); 102 | 103 | final String newReadmeContent = Pattern.compile ( 104 | "Supported arguments \\(so far\\) :") 105 | .splitAsStream (readmeContent).findFirst () 106 | .orElse ("") + "Supported arguments (so far) :\n\n" + tableBuilder.toString (); 107 | 108 | FileWriter writer = new FileWriter (readme); 109 | writer.write (newReadmeContent); 110 | writer.close (); 111 | } 112 | 113 | private CtMethod builderOptionMethod (final CtClass ctClass, final CtClass stringType, final String methodName, final String optName, final boolean hasArg) throws CannotCompileException { 114 | final CtMethod method = new CtMethod (ctClass, methodName, hasArg ? new CtClass [] { stringType } : new CtClass [0], ctClass); 115 | method.setBody ("{curlCommand.append (\"-" + (optName.length () == 1 ? "" : "-") + optName + " \"" + (hasArg ? " + $1 + \" \"" : "") + ");\n" + " " + "return $0;}"); 116 | return method; 117 | } 118 | 119 | private String methodNameOf (final String opt) { 120 | if ((opt.length () == 1) && (opt.charAt (0) >= 'A') && (opt.charAt (0) <= 'Z')) { 121 | return opt.toLowerCase () + "UpperCase"; 122 | } 123 | 124 | final String lowerCased = ("" + Character.toLowerCase (opt.charAt (0)) + opt.substring (1)).replace ('.', '-'); 125 | final String notStartingWithADigitAndLowerCased = this.removeLeadingDigits ('-' + lowerCased); 126 | return this.capitalizeParts (notStartingWithADigitAndLowerCased.replaceAll ("-", "")); 127 | } 128 | 129 | private String removeLeadingDigits (final String lowerCased) { 130 | final StringBuffer result = new StringBuffer (); 131 | final Matcher matcher = ArgumentsBuilderGeneratorTest.DIGITS_PATTERN.matcher (lowerCased); 132 | 133 | while (matcher.find ()) { 134 | final StringBuilder replacement = new StringBuilder (); 135 | for (int i = 0; i < matcher.group (1).length (); i++) { 136 | replacement.append (ArgumentsBuilderGeneratorTest.DIGITS.get (matcher.group (1).charAt (i) - '0')); 137 | } 138 | matcher.appendReplacement (result, replacement.toString ()); 139 | } 140 | matcher.appendTail (result); 141 | return result.toString (); 142 | } 143 | 144 | private String capitalizeParts (final String notStartingWithADigitAndLowerCased) { 145 | final StringBuffer result = new StringBuffer (); 146 | final Matcher matcher = ArgumentsBuilderGeneratorTest.WORD_SEPARATOR.matcher (notStartingWithADigitAndLowerCased); 147 | 148 | while (matcher.find ()) { 149 | matcher.appendReplacement (result, matcher.group (1).toUpperCase ()); 150 | } 151 | matcher.appendTail (result); 152 | return result.toString (); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | org.toile-libre.libe 4 | curl 5 | 0.0.46-SNAPSHOT 6 | curl 7 | Curl 8 | https://github.com/libetl/curl 9 | 10 | 11 | The Unlicence 12 | https://raw.githubusercontent.com/libetl/curl/master/LICENSE 13 | 14 | 15 | 16 | 17 | libetl 18 | libe4@free.fr 19 | toile-libre 20 | http://libe.toile-libre.org 21 | 22 | 23 | 24 | scm:git:git@github.com:libetl/curl.git 25 | scm:git:git@github.com:libetl/curl.git 26 | git@github.com:libetl/curl.git 27 | ${project.version} 28 | 29 | 30 | 31 | ossrh 32 | https://oss.sonatype.org/content/repositories/snapshots 33 | 34 | 35 | ossrh 36 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 37 | 38 | 39 | 40 | src/main/java 41 | src/test/java 42 | 43 | 44 | src/test/resources 45 | 46 | **/*.java 47 | 48 | 49 | 50 | 51 | 52 | maven-compiler-plugin 53 | 3.14.1 54 | 55 | 1.8 56 | 1.8 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-source-plugin 62 | 3.4.0 63 | 64 | 65 | attach-sources 66 | 67 | jar-no-fork 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-javadoc-plugin 75 | 3.12.0 76 | 77 | 8 78 | 79 | 80 | 81 | attach-javadocs 82 | 83 | jar 84 | 85 | 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-gpg-plugin 91 | 3.2.8 92 | 93 | 94 | sign-artifacts 95 | verify 96 | 97 | sign 98 | 99 | 100 | 101 | 102 | 103 | org.sonatype.plugins 104 | nexus-staging-maven-plugin 105 | 1.7.0 106 | true 107 | 108 | ossrh 109 | https://oss.sonatype.org/ 110 | true 111 | 112 | 113 | 114 | org.apache.maven.plugins 115 | maven-release-plugin 116 | 3.3.0 117 | 118 | true 119 | false 120 | release 121 | deploy 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-antrun-plugin 126 | 3.2.0 127 | 128 | 129 | 130 | run 131 | 132 | generate-sources 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | commons-cli 155 | commons-cli 156 | 1.11.0 157 | 158 | 159 | org.apache.httpcomponents.client5 160 | httpclient5 161 | 5.5.1 162 | 163 | 164 | org.javassist 165 | javassist 166 | 3.30.2-GA 167 | test 168 | 169 | 170 | org.junit.jupiter 171 | junit-jupiter-api 172 | 6.0.1 173 | test 174 | 175 | 176 | org.junit.jupiter 177 | junit-jupiter-engine 178 | 6.0.1 179 | test 180 | 181 | 182 | org.junit.platform 183 | junit-platform-engine 184 | 6.0.1 185 | test 186 | 187 | 188 | org.assertj 189 | assertj-core 190 | 3.27.6 191 | test 192 | 193 | 194 | org.springframework.boot 195 | spring-boot-starter-security 196 | 4.0.0 197 | test 198 | 199 | 200 | ch.qos.logback 201 | logback-classic 202 | 203 | 204 | 205 | 206 | org.springframework.boot 207 | spring-boot-starter-web 208 | 4.0.0 209 | test 210 | 211 | 212 | ch.qos.logback 213 | logback-classic 214 | 215 | 216 | 217 | 218 | org.springframework.boot 219 | spring-boot-starter-jetty 220 | 4.0.0 221 | test 222 | 223 | 224 | org.springframework.boot 225 | spring-boot-starter 226 | 4.0.0 227 | test 228 | 229 | 230 | ch.qos.logback 231 | logback-classic 232 | 233 | 234 | 235 | 236 | org.springframework 237 | spring-context 238 | 7.0.1 239 | test 240 | 241 | 242 | org.mock-server 243 | mockserver-netty 244 | 5.15.0 245 | test 246 | 247 | 248 | com.google.guava 249 | guava 250 | 251 | 252 | com.jayway.jsonpath 253 | json-path 254 | 255 | 256 | com.nimbusds 257 | nimbus-jose-jwt 258 | 259 | 260 | commons-collections 261 | commons-collections 262 | 263 | 264 | 265 | 266 | com.google.guava 267 | guava 268 | 33.5.0-jre 269 | test 270 | 271 | 272 | tools.jackson.core 273 | jackson-core 274 | 3.0.3 275 | test 276 | 277 | 278 | tools.jackson.core 279 | jackson-databind 280 | 3.0.3 281 | test 282 | 283 | 284 | jakarta.servlet 285 | jakarta.servlet-api 286 | 6.1.0 287 | test 288 | 289 | 290 | 291 | UTF-8 292 | 293 | 294 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # curl [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) 2 | curl command in java (using Apache libs : HttpClient 5 and commons-cli) 3 | 4 | Setup with maven 5 | 6 | `` 7 | 8 |     `org.toile-libre.libe` 9 | 10 |     `curl` 11 | 12 |     ``![LATEST](https://img.shields.io/maven-central/v/org.toile-libre.libe/curl?label=%20&style=for-the-badge)`` 13 | 14 | `` 15 | 16 | Usage 17 | ```java 18 | org.apache.hc.core5.http.ClassicHttpResponse org.toilelibre.libe.curl.Curl.curl (String curlParams); 19 | String org.toilelibre.libe.curl.Curl.$ (String curlCommand); //Returns responseBody 20 | ``` 21 | 22 | You can import static these methods : 23 | ```java 24 | import static org.toilelibre.libe.curl.Curl.curl; 25 | import static org.toilelibre.libe.curl.Curl.$; 26 | ``` 27 | 28 | Examples : 29 | ```java 30 | $("curl https://localhost:8443/public/"); 31 | $("curl -k https://localhost:8443/public/"); 32 | curl("-k https://localhost:8443/public/"); 33 | curl("-k --cert src/test/resources/client.p12:password https://localhost:8443/public/"); 34 | curl("-k https://localhost:8443/public/redirection"); 35 | curl("-k https://localhost:8443/public/unauthorized"); 36 | curl("-k -L https://localhost:8443/public/redirection"); 37 | curl("-k -H'Host: localhost' -H'Authorization: 00000000-0000-0000-0000-000000000000' https://localhost:8443/public/v1/coverage/sncf/journeys?from=admin:7444extern"); 38 | curl("-k -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:8443/public/curlCommand1?param1=value1¶m2=value2'"); 39 | curl("-k -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' -u foo:bar 'https://localhost:8443/private/login'"); 40 | curl("-L -k -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' -u user:password 'https://localhost:8443/private/login'"); 41 | curl("-k -X POST 'https://localhost:8443/public/json' -d '{\"var1\":\"val1\",\"var2\":\"val2\"}'"); 42 | new Curl(with().simpleArgsParsing().build()).curl_("-k -G \"http://localhost:8200/v1/secret/booking-service/auth\"") 43 | ``` 44 | 45 | It also works with a builder 46 | 47 | ```java 48 | HttpResponse response = curl().k().xUpperCase("POST").d("{\"var1\":\"val1\",\"var2\":\"val2\"}").run("https://localhost:8443/public/json"); 49 | ``` 50 | 51 | How to get Google Homepage with this lib : 52 | ```java 53 | public String getGoogleHomepage (){ 54 | //-L is passed to follow the redirects 55 | return curl ().lUpperCase ().$ ("https://www.google.com/"); 56 | } 57 | ``` 58 | 59 | You can also specify five additional curl options using jvm code : 60 | * javaOptions.interceptor can be used to surround the call with a custom 61 | handling 62 | * javaOptions.placeHolders allows to define substitution variables 63 | (useful mostly for long payloads to avoid StackOverflowErrors) 64 | * javaOptions.connectionManager allows to specify your own connection 65 | manager for pooling purposes or optimization purposes 66 | (warning, this will break the trust insecure behavior) 67 | * javaOptions.httpClientCustomizer lets you manipulate the HttpClientBuilder 68 | * javaOptions.contextTester allows to inspect the request resolved information (it is a Consumer of HttpContext) 69 | * javaOptions.simpleArgsParsing uses an alternative regex based args parsing 70 | to avoid StackOverflowErrors with very long payloads 71 | 72 | ```java 73 | curl() 74 | .javaOptions(with().interceptor(((request, responseSupplier) -> { 75 | LOGGER.info("I log something before the call"); 76 | HttpResponse response = responseSupplier.get(); 77 | LOGGER.info("I log something after the call, status code is {}", 78 | response.getStatusLine().getStatusCode()); 79 | return response;})) 80 | .connectionManager(new PoolingHttpClientConnectionManager ()) 81 | .placeHolders(asList("fr-FR", "text/html")).build()) 82 | .hUpperCase("'Accept-Language: $curl_placeholder_0'") 83 | .hUpperCase("'Accept: $curl_placeholder_1'") 84 | .run("http://www.google.com"); 85 | ``` 86 | 87 | Supported arguments (so far) : 88 | 89 | | Short Name | Long Name | Argument Required | Description | 90 | | ------------- | --------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | 91 | | u | username | true | user:password | 92 | | cacert | cacert | true | CA_CERT | 93 | | E | cert | true | CERT[:password] | 94 | | ct | cert-type | true | PEM,P12,JKS,DER,ENG | 95 | | compressed | compressed | false | Request compressed response | 96 | | cti | connect-timeout | true | Maximum time allowed for connection | 97 | | d | data | true | Data | 98 | | databinary | data-binary | true | http post binary data | 99 | | dataurlencode | data-urlencode | true | Data to URLEncode | 100 | | L | location | false | follow redirects | 101 | | F | form | true | http multipart post data | 102 | | G | get | false | forces to use GET request method | 103 | | H | header | true | Header | 104 | | X | request | true | Http Method | 105 | | key | key | true | KEY | 106 | | kt | key-type | true | PEM,P12,JKS,DER,ENG | 107 | | m | max-time | true | Maximum time allowed for the transfer | 108 | | nokeepalive | no-keepalive | false | Disable TCP keepalive on the connection | 109 | | ntlm | ntlm | false | NTLM auth | 110 | | o | output | true | write to file | 111 | | x | proxy | true | use the specified HTTP proxy | 112 | | U | proxy-user | true | authentication for proxy | 113 | | 1 | tlsv1 | false | use > = TLSv1 (SSL) | 114 | | tlsv10 | tlsv1.0 | false | use TLSv1.0 (SSL) | 115 | | tlsv11 | tlsv1.1 | false | use TLSv1.1 (SSL) | 116 | | tlsv12 | tlsv1.2 | false | use TLSv1.2 (SSL) | 117 | | 2 | sslv2 | false | use SSLv2 (SSL) | 118 | | 3 | sslv3 | false | use SSLv3 (SSL) | 119 | | k | insecure | false | trust insecure | 120 | | A | user-agent | true | user agent | 121 | | V | version | false | get the version of this library | 122 | | interceptor | interceptor | true | interceptor field or method (syntax is classname::fieldname). Must be a BiFunction, HttpResponse> or will be discarded | 123 | -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/Curl.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 5 | import org.apache.hc.client5.http.io.HttpClientConnectionManager; 6 | import org.apache.hc.core5.http.ClassicHttpResponse; 7 | import org.apache.hc.core5.http.HttpRequest; 8 | import org.apache.hc.core5.http.HttpResponse; 9 | import org.apache.hc.core5.http.protocol.HttpContext; 10 | 11 | import java.io.*; 12 | import java.util.*; 13 | import java.util.concurrent.*; 14 | import java.util.function.*; 15 | 16 | import static org.toilelibre.libe.curl.Curl.CurlArgumentsBuilder.CurlJavaOptions.*; 17 | import static org.toilelibre.libe.curl.UglyVersionDisplay.*; 18 | 19 | public final class Curl { 20 | 21 | private static final CurlArgumentsBuilder.CurlJavaOptions NO_JAVA_OPTIONS = with().build(); 22 | private static final Curl DEFAULT = new Curl(NO_JAVA_OPTIONS); 23 | private final CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions; 24 | 25 | public Curl(CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) { 26 | this.curlJavaOptions = curlJavaOptions; 27 | } 28 | 29 | public static String $(final String requestCommand) throws CurlException { 30 | return $(requestCommand, NO_JAVA_OPTIONS); 31 | } 32 | 33 | public static CompletableFuture $Async(final String requestCommand) throws CurlException { 34 | return $Async(requestCommand, NO_JAVA_OPTIONS); 35 | } 36 | 37 | public static CompletableFuture $Async(final String requestCommand, 38 | CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) throws CurlException { 39 | return Curl.curlAsync(requestCommand, curlJavaOptions).thenApply((httpResponse) -> IOUtils.quietToString(httpResponse.getEntity())); 40 | } 41 | 42 | public static CurlArgumentsBuilder curl() { 43 | return new CurlArgumentsBuilder(); 44 | } 45 | 46 | public static CompletableFuture curlAsync(final String requestCommand) throws CurlException { 47 | return curlAsync(requestCommand, NO_JAVA_OPTIONS); 48 | } 49 | 50 | public static String $(final String requestCommand, CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) throws CurlException { 51 | try { 52 | return IOUtils.quietToString(DEFAULT.curl_(requestCommand, curlJavaOptions).getEntity()); 53 | } catch (final UnsupportedOperationException e) { 54 | throw new CurlException(e); 55 | } 56 | } 57 | 58 | public static CompletableFuture curlAsync(final String requestCommand, 59 | CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) throws CurlException { 60 | return CompletableFuture.supplyAsync(() -> { 61 | try { 62 | return DEFAULT.curl_(requestCommand, curlJavaOptions); 63 | } catch (IllegalArgumentException e) { 64 | throw new CurlException(e); 65 | } 66 | }).toCompletableFuture(); 67 | } 68 | 69 | public static ClassicHttpResponse curl(final String requestCommand) throws CurlException { 70 | return DEFAULT.curl_(requestCommand, null); 71 | } 72 | 73 | public ClassicHttpResponse curl_(final String requestCommand) throws CurlException { 74 | return this.curl_(requestCommand, null); 75 | } 76 | 77 | public static ClassicHttpResponse curl (final String requestCommand, 78 | CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) throws CurlException { 79 | return DEFAULT.curl_(requestCommand, curlJavaOptions); 80 | } 81 | 82 | public ClassicHttpResponse curl_ (final String requestCommand, 83 | CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) throws CurlException { 84 | try { 85 | final CurlArgumentsBuilder.CurlJavaOptions usedJavaOptions = curlJavaOptions != null 86 | ? curlJavaOptions 87 | : this.curlJavaOptions; 88 | final CommandLine commandLine = ReadArguments.getCommandLineFromRequest (requestCommand, 89 | usedJavaOptions.getPlaceHolders (), usedJavaOptions.simpleArgsParsing ()); 90 | stopAndDisplayVersionIfThe (commandLine.hasOption (Arguments.VERSION.getOpt ())); 91 | final ClassicHttpResponse response = (ClassicHttpResponse) 92 | HttpClientProvider.prepareHttpClient (commandLine, usedJavaOptions.getInterceptors (), 93 | usedJavaOptions.getConnectionManager (), 94 | usedJavaOptions.getHttpClientCustomizer (), 95 | usedJavaOptions.getContextTester ()).execute ( 96 | HttpRequestProvider.prepareRequest (commandLine)); 97 | AfterResponse.handle (commandLine, response); 98 | return response; 99 | } catch (final IOException | IllegalArgumentException e) { 100 | throw new CurlException (e); 101 | } 102 | } 103 | 104 | public static String getVersion () { 105 | return Version.NUMBER; 106 | } 107 | 108 | public static String getVersionWithBuildTime () { 109 | return Version.NUMBER + " (Build time : " + Version.BUILD_TIME + ")"; 110 | } 111 | 112 | public static class CurlArgumentsBuilder { 113 | 114 | private final StringBuilder curlCommand = new StringBuilder ("curl "); 115 | private CurlJavaOptions curlJavaOptions = with ().build (); 116 | 117 | public static class CurlJavaOptions { 118 | private final List, ClassicHttpResponse>> interceptors; 119 | private final List placeHolders; 120 | private final HttpClientConnectionManager connectionManager; 121 | private final Consumer contextTester; 122 | private final Consumer httpClientCustomizer; 123 | private final boolean simpleArgsParsing; 124 | private CurlJavaOptions (Builder builder) { 125 | interceptors = builder.interceptors; 126 | placeHolders = builder.placeHolders; 127 | connectionManager = builder.connectionManager; 128 | contextTester = builder.contextTester; 129 | httpClientCustomizer = builder.httpClientCustomizer; 130 | simpleArgsParsing = builder.simpleArgsParsing; 131 | } 132 | 133 | public static Builder with () { 134 | return new Builder (); 135 | } 136 | 137 | public List, ClassicHttpResponse>> getInterceptors () { 138 | return interceptors; 139 | } 140 | 141 | public List getPlaceHolders () { 142 | return placeHolders; 143 | } 144 | 145 | public HttpClientConnectionManager getConnectionManager () { 146 | return connectionManager; 147 | } 148 | 149 | public Consumer getContextTester () { 150 | return contextTester; 151 | } 152 | 153 | public Consumer getHttpClientCustomizer () { 154 | return httpClientCustomizer; 155 | } 156 | 157 | public boolean simpleArgsParsing () { 158 | return simpleArgsParsing; 159 | } 160 | 161 | public static final class Builder { 162 | private List, ClassicHttpResponse>> interceptors 163 | = new ArrayList<> (); 164 | private List placeHolders; 165 | private HttpClientConnectionManager connectionManager; 166 | 167 | private Consumer contextTester; 168 | private Consumer httpClientCustomizer; 169 | private boolean simpleArgsParsing = false; 170 | 171 | private Builder () { 172 | } 173 | 174 | public Builder interceptor (BiFunction, ClassicHttpResponse> val) { 175 | interceptors.add (val); 176 | return this; 177 | } 178 | 179 | public Builder placeHolders (List val) { 180 | placeHolders = val; 181 | return this; 182 | } 183 | 184 | public Builder connectionManager (HttpClientConnectionManager val) { 185 | connectionManager = val; 186 | return this; 187 | } 188 | 189 | public Builder mockedNetworkAccess () { 190 | connectionManager = new MockNetworkAccess(); 191 | return this; 192 | } 193 | 194 | public Builder contextTester (Consumer val) { 195 | contextTester = val; 196 | return this; 197 | } 198 | 199 | public Builder httpClientCustomizer (Consumer val) { 200 | httpClientCustomizer = val; 201 | return this; 202 | } 203 | 204 | public Builder simpleArgsParsing () { 205 | simpleArgsParsing = true; 206 | return this; 207 | } 208 | 209 | public CurlJavaOptions build () { 210 | return new CurlJavaOptions (this); 211 | } 212 | } 213 | } 214 | 215 | CurlArgumentsBuilder () { 216 | } 217 | 218 | public CurlArgumentsBuilder javaOptions (CurlJavaOptions curlJavaOptions) { 219 | this.curlJavaOptions = curlJavaOptions; 220 | return this; 221 | } 222 | 223 | public String $ (final String url) throws CurlException { 224 | this.curlCommand.append (url).append (" "); 225 | return Curl.$ (this.curlCommand.toString (), curlJavaOptions); 226 | } 227 | 228 | public CompletableFuture $Async (final String url) throws CurlException { 229 | this.curlCommand.append (url).append (" "); 230 | return Curl.$Async (this.curlCommand.toString (), curlJavaOptions); 231 | } 232 | 233 | public HttpResponse run (final String url) throws CurlException { 234 | this.curlCommand.append (url).append (" "); 235 | return DEFAULT.curl (this.curlCommand.toString (), curlJavaOptions); 236 | } 237 | 238 | public CompletableFuture runAsync (final String url) throws CurlException { 239 | this.curlCommand.append (url).append (" "); 240 | return Curl.curlAsync (this.curlCommand.toString (), curlJavaOptions); 241 | } 242 | 243 | } 244 | 245 | public static class CurlException extends RuntimeException { 246 | 247 | /** 248 | * 249 | */ 250 | private static final long serialVersionUID = 1L; 251 | 252 | CurlException (final Throwable arg0) { 253 | super (arg0); 254 | } 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/outside/monitor/RequestMonitor.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.outside.monitor; 2 | 3 | import java.io.IOException; 4 | import java.util.Collection; 5 | import java.util.Enumeration; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.Random; 9 | 10 | import jakarta.servlet.ServletException; 11 | import jakarta.servlet.http.HttpServletRequest; 12 | import jakarta.servlet.http.HttpServletResponse; 13 | import jakarta.servlet.http.Part; 14 | 15 | import org.apache.commons.io.IOUtils; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.boot.Banner; 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.boot.builder.SpringApplicationBuilder; 22 | import org.springframework.context.ConfigurableApplicationContext; 23 | import org.springframework.context.annotation.Bean; 24 | import org.springframework.context.annotation.Configuration; 25 | import org.springframework.http.HttpStatus; 26 | import org.springframework.http.MediaType; 27 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 28 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 29 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 30 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 31 | import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; 32 | import org.springframework.security.core.Authentication; 33 | import org.springframework.security.core.userdetails.User; 34 | import org.springframework.security.core.userdetails.UserDetails; 35 | import org.springframework.security.core.userdetails.UserDetailsService; 36 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 37 | import org.springframework.security.web.SecurityFilterChain; 38 | import org.springframework.stereotype.Controller; 39 | import org.springframework.web.bind.annotation.RequestBody; 40 | import org.springframework.web.bind.annotation.RequestMapping; 41 | import org.springframework.web.bind.annotation.RequestMethod; 42 | import org.springframework.web.bind.annotation.ResponseBody; 43 | import org.springframework.web.bind.annotation.ResponseStatus; 44 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 45 | 46 | import com.fasterxml.jackson.databind.ObjectMapper; 47 | 48 | @SpringBootApplication 49 | @EnableWebMvc 50 | @EnableWebSecurity 51 | public class RequestMonitor { 52 | @Controller 53 | @RequestMapping ("/**") 54 | static class MonitorController { 55 | 56 | @RequestMapping (value = "/public/noContent", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, method = RequestMethod.GET) 57 | @ResponseStatus (code = HttpStatus.NO_CONTENT) 58 | @ResponseBody 59 | public String emptyResponse () { 60 | return null; 61 | } 62 | 63 | @RequestMapping (value = "/public/data", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE, method = RequestMethod.POST) 64 | @ResponseStatus (code = HttpStatus.OK) 65 | @ResponseBody 66 | public byte[] data (final HttpServletRequest request) throws IOException { 67 | return this.logRequest (request, IOUtils.toString (request.getInputStream ())).getBytes (); 68 | } 69 | 70 | @RequestMapping (value = "/public/form", produces = MediaType.TEXT_PLAIN_VALUE, method = RequestMethod.POST) 71 | @ResponseStatus (code = HttpStatus.OK) 72 | @ResponseBody 73 | public String form (final HttpServletRequest request) throws ServletException, IOException { 74 | final Collection parts = request.getParts (); 75 | RequestMonitor.LOGGER.info (parts.toString ()); 76 | return this.logRequest (request, ""); 77 | } 78 | 79 | @RequestMapping (value = "/public/json", produces = MediaType.TEXT_PLAIN_VALUE) 80 | @ResponseStatus (code = HttpStatus.OK) 81 | @ResponseBody 82 | public String json (final HttpServletRequest request, @RequestBody (required = true) final String body) throws IOException { 83 | @SuppressWarnings ("unchecked") 84 | final Map map = new ObjectMapper ().readValue (body, Map.class); 85 | RequestMonitor.LOGGER.info (map.toString ()); 86 | return this.logRequest (request, body); 87 | } 88 | 89 | @RequestMapping (value = "/public/tooLong", produces = MediaType.TEXT_PLAIN_VALUE) 90 | @ResponseStatus (code = HttpStatus.OK) 91 | @ResponseBody 92 | public String tooLong () throws InterruptedException { 93 | Thread.sleep (1000); 94 | RequestMonitor.LOGGER.info ("Finally !"); 95 | return "...Finally."; 96 | } 97 | 98 | @RequestMapping (value = "/private/login", produces = MediaType.TEXT_PLAIN_VALUE) 99 | @ResponseStatus (code = HttpStatus.FOUND) 100 | @ResponseBody 101 | public String login (final HttpServletRequest request, final HttpServletResponse response, @RequestBody (required = false) final String body, final Authentication auth) { 102 | response.setHeader ("Location", this.serverLocation (request) + "/private/logged"); 103 | this.logRequest (request, body); 104 | return ""; 105 | } 106 | 107 | private String logRequest (final HttpServletRequest request, final String body) { 108 | final StringBuffer curlLog = new StringBuffer ("curl"); 109 | 110 | curlLog.append (" -k "); 111 | curlLog.append ("-E src/test/resources/clients/libe/libe.pem"); 112 | curlLog.append (" -X "); 113 | curlLog.append (request.getMethod ()); 114 | 115 | for (final Enumeration headerNameEnumeration = request.getHeaderNames (); headerNameEnumeration.hasMoreElements ();) { 116 | final String headerName = headerNameEnumeration.nextElement (); 117 | final String headerValue = request.getHeader (headerName); 118 | curlLog.append (" -H '"); 119 | curlLog.append (headerName); 120 | curlLog.append (": "); 121 | curlLog.append (headerValue); 122 | curlLog.append ("'"); 123 | 124 | } 125 | 126 | if (body != null) { 127 | curlLog.append (" -d '"); 128 | curlLog.append (body.replace ("'", "''")); 129 | curlLog.append ("'"); 130 | } 131 | 132 | curlLog.append (" "); 133 | curlLog.append (" '"); 134 | curlLog.append (this.serverLocation (request) + request.getServletPath () + (request.getQueryString () == null ? "" : "?" + request.getQueryString ())); 135 | curlLog.append ("'"); 136 | RequestMonitor.LOGGER.info (curlLog.toString ()); 137 | return curlLog.toString (); 138 | } 139 | 140 | @RequestMapping (produces = "text/plain;charset=utf-8") 141 | @ResponseStatus (code = HttpStatus.OK) 142 | @ResponseBody 143 | public String receiveRequest (final HttpServletRequest request, @RequestBody (required = false) final String body) { 144 | return this.logRequest (request, body); 145 | } 146 | 147 | @RequestMapping (value = "/public/redirection", produces = MediaType.TEXT_PLAIN_VALUE) 148 | @ResponseStatus (code = HttpStatus.FOUND) 149 | @ResponseBody 150 | public String redirection (final HttpServletRequest request, final HttpServletResponse response, @RequestBody (required = false) final String body) { 151 | response.setHeader ("Location", this.serverLocation (request) + "/public/redirectedThere"); 152 | this.logRequest (request, body); 153 | return ""; 154 | } 155 | 156 | private String serverLocation (final HttpServletRequest request) { 157 | return request.getScheme () + "://" + request.getServerName () + ":" + RequestMonitor.port (); 158 | } 159 | 160 | @RequestMapping (value = "/public/unauthorized", produces = MediaType.TEXT_PLAIN_VALUE) 161 | @ResponseStatus (code = HttpStatus.UNAUTHORIZED) 162 | @ResponseBody 163 | public String unauthorized (final HttpServletRequest request, final HttpServletResponse response, @RequestBody (required = false) final String body) { 164 | response.setHeader ("Location", this.serverLocation (request) + "/public/tryagain"); 165 | this.logRequest (request, body); 166 | return ""; 167 | } 168 | } 169 | 170 | @Configuration 171 | @EnableWebSecurity 172 | static class WebSecurityConfig { 173 | 174 | @Bean 175 | public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService users) throws Exception { 176 | http.authorizeHttpRequests (request -> request.requestMatchers ("/private").permitAll ().anyRequest ().authenticated ()) 177 | .userDetailsService(users) 178 | .httpBasic (request -> request.realmName ("basic")) 179 | .logout (LogoutConfigurer::permitAll); 180 | return http.build(); 181 | } 182 | 183 | @Bean 184 | public WebSecurityCustomizer configure () { 185 | return (web) -> web.ignoring ().requestMatchers ("/public/**"); 186 | } 187 | 188 | @Bean 189 | public UserDetailsService users() { 190 | UserDetails user = User.builder() 191 | .username("user") 192 | .password("{noop}password") 193 | .roles("USER") 194 | .build(); 195 | return new InMemoryUserDetailsManager(user); 196 | } 197 | } 198 | 199 | private static ConfigurableApplicationContext context; 200 | private static final Logger LOGGER = LoggerFactory.getLogger (RequestMonitor.class); 201 | 202 | private static int managementPort; 203 | 204 | private static int port; 205 | 206 | public static void main (final String [] args) { 207 | RequestMonitor.start (args); 208 | } 209 | 210 | public static int port () { 211 | return RequestMonitor.port; 212 | } 213 | 214 | public static int [] start () { 215 | return RequestMonitor.start (new String [0]); 216 | } 217 | 218 | public static int [] start (final boolean withSsl, final String [] args) { 219 | final Random random = new Random (); 220 | RequestMonitor.port = random.nextInt (32767) + 32768; 221 | RequestMonitor.managementPort = random.nextInt (32767) + 32768; 222 | RequestMonitor.start (RequestMonitor.port, RequestMonitor.managementPort, withSsl, args); 223 | return new int [] { RequestMonitor.port, RequestMonitor.managementPort }; 224 | } 225 | 226 | public static void start (final int port, final int managementPort, final boolean withSsl, final String [] args) { 227 | Map properties = new HashMap(); 228 | properties.put ("server.port", port); 229 | properties.put ("management.port", managementPort); 230 | if (withSsl) { 231 | properties.put ("server.ssl.key-store", "classpath:server/libe/libe.jks"); 232 | properties.put ("server.ssl.key-store-password", "myserverpass"); 233 | properties.put ("server.ssl.trust-store", "classpath:server/libe/libe.jks"); 234 | properties.put ("server.ssl.trust-store-password", "myserverpass"); 235 | properties.put ("server.ssl.client-auth", "need"); 236 | properties.put ("server.ssl.enabled-protocols","SSLv2,SSLv3,TLSv1.0,TLSv1.1,TLSv1.2"); 237 | } 238 | RequestMonitor.context = new SpringApplicationBuilder () 239 | .sources (RequestMonitor.class) 240 | .bannerMode (Banner.Mode.OFF) 241 | .addCommandLineProperties (true) 242 | .properties (properties) 243 | .run (args); 244 | } 245 | 246 | public static int [] start (final String [] args) { 247 | return RequestMonitor.start (true, args); 248 | } 249 | 250 | public static void stop () { 251 | SpringApplication.exit (RequestMonitor.context, () -> 0); 252 | } 253 | } -------------------------------------------------------------------------------- /src/main/java/org/toilelibre/libe/curl/SSLMaterialCreator.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.curl; 2 | 3 | import org.apache.commons.cli.*; 4 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 5 | import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; 6 | import org.apache.hc.client5.http.io.HttpClientConnectionManager; 7 | import org.apache.hc.client5.http.socket.ConnectionSocketFactory; 8 | import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory; 9 | import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; 10 | import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; 11 | import org.apache.hc.core5.http.config.RegistryBuilder; 12 | import org.apache.hc.core5.ssl.SSLContextBuilder; 13 | 14 | import java.io.*; 15 | import java.security.*; 16 | import java.security.cert.Certificate; 17 | import java.security.cert.*; 18 | import java.util.*; 19 | import java.util.stream.*; 20 | 21 | import static java.util.Arrays.*; 22 | import static java.util.Optional.*; 23 | import static java.util.stream.Collectors.*; 24 | import static org.apache.hc.client5.http.ssl.HttpsSupport.getDefaultHostnameVerifier; 25 | import static org.toilelibre.libe.curl.Arguments.*; 26 | import static org.toilelibre.libe.curl.IOUtils.*; 27 | 28 | final class SSLMaterialCreator { 29 | 30 | private final static Map>, SSLConnectionSocketFactory> cachedSSLFactoriesForPerformance = 31 | new HashMap<> (); 32 | 33 | 34 | 35 | static SSLConnectionSocketFactory buildConnectionFactory (final CommandLine commandLine) throws Curl.CurlException { 36 | 37 | Map> input = inputExtractedFrom (commandLine); 38 | final SSLConnectionSocketFactory foundInCache = cachedSSLFactoriesForPerformance.get (input); 39 | 40 | if (foundInCache != null) { 41 | return foundInCache; 42 | } 43 | 44 | final SSLContextBuilder builder = new SSLContextBuilder (); 45 | builder.setProtocol (protocolFromCommandLine (commandLine)); 46 | 47 | if (commandLine.hasOption (TRUST_INSECURE.getOpt ())) { 48 | sayTrustInsecure (builder); 49 | } 50 | 51 | final CertFormat certFormat = commandLine.hasOption (CERT_TYPE.getOpt ()) ? 52 | CertFormat.valueOf (commandLine.getOptionValue (CERT_TYPE.getOpt ()).toUpperCase ()) : 53 | CertFormat.PEM; 54 | final SSLMaterialCreator.CertPlusKeyInfo.Builder certAndKeysBuilder = 55 | SSLMaterialCreator.CertPlusKeyInfo.newBuilder () 56 | .cacert (commandLine.getOptionValue (CA_CERT.getOpt ())) 57 | .certFormat (certFormat) 58 | .keyFormat (commandLine.hasOption (KEY.getOpt ()) ? 59 | commandLine.hasOption (KEY_TYPE.getOpt ()) ? 60 | CertFormat.valueOf (commandLine.getOptionValue (KEY_TYPE.getOpt ()).toUpperCase ()) : CertFormat.PEM : certFormat); 61 | 62 | 63 | if (commandLine.hasOption (CERT.getOpt ())) { 64 | final String entireOption = commandLine.getOptionValue (CERT.getOpt ()); 65 | final int certSeparatorIndex = getSslSeparatorIndex (entireOption); 66 | final String cert = certSeparatorIndex == - 1 ? entireOption : entireOption.substring (0, certSeparatorIndex); 67 | certAndKeysBuilder.cert (cert) 68 | .certPassphrase (certSeparatorIndex == - 1 ? "" : entireOption.substring (certSeparatorIndex + 1)) 69 | .key (cert); 70 | } 71 | 72 | if (commandLine.hasOption (KEY.getOpt ())) { 73 | final String entireOption = commandLine.getOptionValue (KEY.getOpt ()); 74 | final int keySeparatorIndex = getSslSeparatorIndex (entireOption); 75 | final String key = keySeparatorIndex == - 1 ? entireOption : entireOption.substring (0, keySeparatorIndex); 76 | certAndKeysBuilder.key (key) 77 | .keyPassphrase (keySeparatorIndex == - 1 ? "" : entireOption.substring (keySeparatorIndex + 1)); 78 | } 79 | if (commandLine.hasOption (CERT.getOpt ()) || commandLine.hasOption (KEY.getOpt ())) { 80 | addClientCredentials (builder, certAndKeysBuilder.build ()); 81 | } 82 | 83 | try { 84 | final SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory (builder.build (), 85 | commandLine.hasOption (TRUST_INSECURE.getOpt ()) ? NoopHostnameVerifier.INSTANCE : 86 | getDefaultHostnameVerifier ()); 87 | cachedSSLFactoriesForPerformance.put (input, sslSocketFactory); 88 | return sslSocketFactory; 89 | } catch (NoSuchAlgorithmException | KeyManagementException e) { 90 | throw new Curl.CurlException (e); 91 | } 92 | } 93 | 94 | private static Map> inputExtractedFrom (CommandLine commandLine) { 95 | return Stream.of (TRUST_INSECURE, CERT_TYPE, CA_CERT, KEY, KEY_TYPE, 96 | CERT, TLS_V1, TLS_V10, TLS_V11, TLS_V12, SSL_V2, SSL_V3) 97 | .filter (option -> 98 | commandLine.getOptionValues (option.getOpt ()) != null || 99 | commandLine.hasOption (option.getOpt ()) 100 | ) 101 | .collect (toMap (Option::getOpt, option -> 102 | asList (ofNullable (commandLine.getOptionValues (option.getOpt ())) 103 | .orElse (new String[] {"true"})))); 104 | } 105 | 106 | private static void addClientCredentials (final SSLContextBuilder builder, 107 | final SSLMaterialCreator.CertPlusKeyInfo certPlusKeyInfo) throws Curl.CurlException { 108 | try { 109 | final String keyPassword = certPlusKeyInfo.getKeyPassphrase () == null ? 110 | certPlusKeyInfo.getCertPassphrase () : certPlusKeyInfo.getKeyPassphrase (); 111 | final KeyStore keyStore = generateKeyStore (certPlusKeyInfo); 112 | builder.loadKeyMaterial (keyStore, keyPassword == null ? null : keyPassword.toCharArray ()); 113 | } catch (GeneralSecurityException | IOException e) { 114 | throw new Curl.CurlException (e); 115 | } 116 | } 117 | 118 | private static KeyStore generateKeyStore (final SSLMaterialCreator.CertPlusKeyInfo certPlusKeyInfo) 119 | throws KeyStoreException, NoSuchAlgorithmException, java.security.cert.CertificateException, IOException, 120 | Curl.CurlException { 121 | final CertFormat certFormat = certPlusKeyInfo.getCertFormat (); 122 | final File caCertFileObject = certPlusKeyInfo.getCacert () == null ? null : getFile (certPlusKeyInfo.getCacert ()); 123 | final File certFileObject = getFile (certPlusKeyInfo.getCert ()); 124 | final CertFormat keyFormat = certPlusKeyInfo.getKeyFormat (); 125 | final File keyFileObject = getFile (certPlusKeyInfo.getKey ()); 126 | final char[] certPasswordAsCharArray = certPlusKeyInfo.getCertPassphrase () == null ? null : 127 | certPlusKeyInfo.getCertPassphrase ().toCharArray (); 128 | final char[] keyPasswordAsCharArray = certPlusKeyInfo.getKeyPassphrase () == null ? certPasswordAsCharArray : 129 | certPlusKeyInfo.getKeyPassphrase ().toCharArray (); 130 | final List caCertificatesNotFiltered = caCertFileObject == null ? 131 | Collections.emptyList () : 132 | certFormat.generateCredentialsFromFileAndPassword (CertFormat.Kind.CERTIFICATE, 133 | IOUtils.toByteArray (caCertFileObject), keyPasswordAsCharArray); 134 | final List caCertificatesFiltered = 135 | caCertificatesNotFiltered.stream ().filter ((certificate) -> (certificate instanceof X509Certificate) && (((X509Certificate) certificate).getBasicConstraints () != - 1)).collect (toList ()); 136 | final List certificates = 137 | certFormat.generateCredentialsFromFileAndPassword (CertFormat.Kind.CERTIFICATE, 138 | IOUtils.toByteArray (certFileObject), certPasswordAsCharArray); 139 | final List privateKeys = 140 | keyFormat.generateCredentialsFromFileAndPassword (CertFormat.Kind.PRIVATE_KEY, 141 | IOUtils.toByteArray (keyFileObject), keyPasswordAsCharArray); 142 | 143 | final KeyStore keyStore = KeyStore.getInstance ("JKS"); 144 | keyStore.load (null); 145 | final java.security.cert.Certificate[] certificatesAsArray = 146 | certificates.toArray (new java.security.cert.Certificate[0]); 147 | IntStream.range (0, certificates.size ()).forEach (i -> setCertificateEntry (keyStore, certificates, i)); 148 | IntStream.range (0, caCertificatesFiltered.size ()).forEach (i -> setCaCertificateEntry (keyStore, 149 | caCertificatesFiltered, i)); 150 | IntStream.range (0, privateKeys.size ()).forEach (i -> setPrivateKeyEntry (keyStore, privateKeys, 151 | keyPasswordAsCharArray, certificatesAsArray, i)); 152 | return keyStore; 153 | } 154 | 155 | private static int getSslSeparatorIndex (String entireOption) { 156 | return entireOption.matches ("^[A-Za-z]:\\\\") && entireOption.lastIndexOf (':') == 1 ? - 1 : 157 | entireOption.lastIndexOf (':'); 158 | } 159 | 160 | private static String protocolFromCommandLine (final CommandLine commandLine) { 161 | if (commandLine.hasOption (TLS_V1.getOpt ())) { 162 | return "TLSv1"; 163 | } 164 | if (commandLine.hasOption (TLS_V10.getOpt ())) { 165 | return "TLSv1.0"; 166 | } 167 | if (commandLine.hasOption (TLS_V11.getOpt ())) { 168 | return "TLSv1.1"; 169 | } 170 | if (commandLine.hasOption (TLS_V12.getOpt ())) { 171 | return "TLSv1.2"; 172 | } 173 | if (commandLine.hasOption (SSL_V2.getOpt ())) { 174 | return "SSLv2"; 175 | } 176 | if (commandLine.hasOption (SSL_V3.getOpt ())) { 177 | return "SSLv3"; 178 | } 179 | return "TLS"; 180 | } 181 | 182 | private static void sayTrustInsecure (final SSLContextBuilder builder) throws Curl.CurlException { 183 | try { 184 | builder.loadTrustMaterial (null, (chain, authType) -> true); 185 | } catch (NoSuchAlgorithmException | KeyStoreException e) { 186 | throw new Curl.CurlException (e); 187 | } 188 | } 189 | 190 | private static void setCaCertificateEntry (final KeyStore keyStore, 191 | final List certificates, final int i) { 192 | try { 193 | keyStore.setCertificateEntry ("ca-cert-alias-" + i, certificates.get (i)); 194 | } catch (final KeyStoreException e) { 195 | throw new Curl.CurlException (e); 196 | } 197 | } 198 | 199 | private static void setCertificateEntry (final KeyStore keyStore, 200 | final List certificates, final int i) { 201 | try { 202 | keyStore.setCertificateEntry ("cert-alias-" + i, certificates.get (i)); 203 | } catch (final KeyStoreException e) { 204 | throw new Curl.CurlException (e); 205 | } 206 | } 207 | 208 | private static void setPrivateKeyEntry (final KeyStore keyStore, final List privateKeys, 209 | final char[] passwordAsCharArray, final Certificate[] certificatesAsArray 210 | , final int i) { 211 | try { 212 | keyStore.setKeyEntry ("key-alias-" + i, privateKeys.get (i), passwordAsCharArray, certificatesAsArray); 213 | } catch (final KeyStoreException e) { 214 | throw new Curl.CurlException (e); 215 | } 216 | } 217 | 218 | static class CertPlusKeyInfo { 219 | 220 | private final CertFormat certFormat; 221 | private final CertFormat keyFormat; 222 | private final String cert; 223 | private final String certPassphrase; 224 | private final String cacert; 225 | private final String key; 226 | private final String keyPassphrase; 227 | 228 | private CertPlusKeyInfo (Builder builder) { 229 | certFormat = builder.certFormat; 230 | keyFormat = builder.keyFormat; 231 | cert = builder.cert; 232 | certPassphrase = builder.certPassphrase; 233 | cacert = builder.cacert; 234 | key = builder.key; 235 | keyPassphrase = builder.keyPassphrase; 236 | } 237 | 238 | static Builder newBuilder () { 239 | return new Builder (); 240 | } 241 | 242 | CertFormat getCertFormat () { 243 | return certFormat; 244 | } 245 | 246 | CertFormat getKeyFormat () { 247 | return keyFormat; 248 | } 249 | 250 | String getCert () { 251 | return cert; 252 | } 253 | 254 | String getCertPassphrase () { 255 | return certPassphrase; 256 | } 257 | 258 | String getCacert () { 259 | return cacert; 260 | } 261 | 262 | String getKey () { 263 | return key; 264 | } 265 | 266 | String getKeyPassphrase () { 267 | return keyPassphrase; 268 | } 269 | 270 | 271 | static final class Builder { 272 | private CertFormat certFormat; 273 | private CertFormat keyFormat; 274 | private String cert; 275 | private String certPassphrase; 276 | private String cacert; 277 | private String key; 278 | private String keyPassphrase; 279 | 280 | private Builder () {} 281 | 282 | Builder certFormat (CertFormat val) { 283 | certFormat = val; 284 | return this; 285 | } 286 | 287 | Builder keyFormat (CertFormat val) { 288 | keyFormat = val; 289 | return this; 290 | } 291 | 292 | Builder cert (String val) { 293 | cert = val; 294 | return this; 295 | } 296 | 297 | Builder certPassphrase (String val) { 298 | certPassphrase = val; 299 | return this; 300 | } 301 | 302 | Builder cacert (String val) { 303 | cacert = val; 304 | return this; 305 | } 306 | 307 | Builder key (String val) { 308 | key = val; 309 | return this; 310 | } 311 | 312 | Builder keyPassphrase (String val) { 313 | keyPassphrase = val; 314 | return this; 315 | } 316 | 317 | CertPlusKeyInfo build () { 318 | return new CertPlusKeyInfo (this); 319 | } 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/test/java/org/toilelibre/libe/outside/curl/CurlTest.java: -------------------------------------------------------------------------------- 1 | package org.toilelibre.libe.outside.curl; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.apache.hc.client5.http.ClientProtocolException; 5 | import org.apache.hc.client5.http.ConnectTimeoutException; 6 | import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; 7 | import org.apache.hc.client5.http.socket.ConnectionSocketFactory; 8 | import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; 9 | import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; 10 | import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; 11 | import org.apache.hc.core5.http.ClassicHttpResponse; 12 | import org.apache.hc.core5.http.HttpRequest; 13 | import org.apache.hc.core5.http.HttpStatus; 14 | import org.apache.hc.core5.http.config.RegistryBuilder; 15 | import org.apache.hc.core5.ssl.SSLContextBuilder; 16 | import org.assertj.core.api.Assertions; 17 | import org.junit.jupiter.api.AfterAll; 18 | import org.junit.jupiter.api.BeforeAll; 19 | import org.junit.jupiter.api.Disabled; 20 | import org.junit.jupiter.api.Test; 21 | import org.mockserver.integration.ClientAndServer; 22 | import org.toilelibre.libe.curl.Curl; 23 | import org.toilelibre.libe.curl.Curl.CurlException; 24 | import org.toilelibre.libe.outside.monitor.RequestMonitor; 25 | import org.toilelibre.libe.outside.monitor.StupidHttpServer; 26 | 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.net.SocketTimeoutException; 30 | import java.nio.charset.StandardCharsets; 31 | import java.security.*; 32 | import java.security.cert.*; 33 | import java.util.Objects; 34 | import java.util.Random; 35 | import java.util.concurrent.CompletableFuture; 36 | import java.util.concurrent.ExecutionException; 37 | import java.util.function.BiFunction; 38 | import java.util.function.Supplier; 39 | import java.util.logging.Level; 40 | import java.util.logging.Logger; 41 | 42 | import static java.util.Arrays.asList; 43 | import static org.junit.jupiter.api.Assertions.assertThrows; 44 | import static org.toilelibre.libe.curl.Curl.CurlArgumentsBuilder.CurlJavaOptions.with; 45 | 46 | public class CurlTest { 47 | 48 | private static final Integer proxyPort = Math.abs (new Random ().nextInt ()) % 20000 + 1024; 49 | private static final Logger LOGGER = Logger.getLogger (CurlTest.class.getName ()); 50 | private static ClientAndServer proxy; 51 | 52 | @BeforeAll 53 | public static void startRequestMonitor () { 54 | if (System.getProperty ("skipServer") == null) { 55 | RequestMonitor.start (); 56 | StupidHttpServer.start (); 57 | proxy = ClientAndServer.startClientAndServer (proxyPort); 58 | } 59 | } 60 | 61 | @AfterAll 62 | public static void stopRequestMonitor () { 63 | if (System.getProperty ("skipServer") == null) { 64 | RequestMonitor.stop (); 65 | StupidHttpServer.stop (); 66 | proxy.stop (); 67 | } 68 | } 69 | 70 | private String $ (final String requestCommand) { 71 | return Curl.$ (String.format (requestCommand, RequestMonitor.port ())); 72 | } 73 | 74 | private CompletableFuture $Async (final String requestCommand) { 75 | return Curl.$Async (String.format (requestCommand, RequestMonitor.port ())); 76 | } 77 | 78 | private ClassicHttpResponse curl (final String requestCommand) { 79 | return curl (requestCommand, with ().build ()); 80 | } 81 | 82 | private ClassicHttpResponse curl (final String requestCommand, Curl.CurlArgumentsBuilder.CurlJavaOptions curlJavaOptions) { 83 | return Curl.curl (String.format (requestCommand, RequestMonitor.port ()), curlJavaOptions); 84 | } 85 | 86 | private CompletableFuture curlAsync (final String requestCommand) { 87 | return Curl.curlAsync (String.format (requestCommand, RequestMonitor.port ())); 88 | } 89 | 90 | private void assertFound (final ClassicHttpResponse curlResponse) { 91 | Assertions.assertThat (curlResponse).isNotNull (); 92 | Assertions.assertThat (this.statusCodeOf (curlResponse)).isEqualTo (HttpStatus.SC_MOVED_TEMPORARILY); 93 | } 94 | 95 | private void assertOk (final ClassicHttpResponse curlResponse) { 96 | Assertions.assertThat (curlResponse).isNotNull (); 97 | Assertions.assertThat (this.statusCodeOf (curlResponse)).isEqualTo (HttpStatus.SC_OK); 98 | } 99 | 100 | private void assertUnauthorized (final ClassicHttpResponse curlResponse) { 101 | Assertions.assertThat (curlResponse).isNotNull (); 102 | Assertions.assertThat (this.statusCodeOf (curlResponse)).isEqualTo (HttpStatus.SC_UNAUTHORIZED); 103 | } 104 | 105 | private int statusCodeOf (final ClassicHttpResponse response) { 106 | return response.getCode(); 107 | } 108 | 109 | @Test 110 | public void displayVersion () { 111 | assertThrows(CurlException.class, () -> this.assertOk (this.curl ("-V"))); 112 | } 113 | 114 | @Test 115 | public void curlRoot () { 116 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/")); 117 | } 118 | 119 | @Test 120 | public void curlCompressed () { 121 | this.assertOk (this.curl ("-k --compressed -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/")); 122 | } 123 | 124 | @Test 125 | public void curlNoKeepAlive () { 126 | this.assertOk (this.curl ("-k --no-keepalive -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/")); 127 | } 128 | 129 | @Test 130 | public void curlTlsV12 () { 131 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --tlsv1.2")); 132 | } 133 | 134 | @Test 135 | public void curlBadHeaderFormatIgnored () { 136 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -H 'toto' https://localhost:%d/public/")); 137 | } 138 | 139 | @Test 140 | public void theSkyIsBlueInIvritWithTheWrongEncoding () throws IOException { 141 | ClassicHttpResponse response = this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ -H 'Content-Type: text/plain; charset=ISO-8859-1' -d \"השמים כחולים\""); 142 | Assertions.assertThat (IOUtils.toString (response.getEntity ().getContent (), StandardCharsets.UTF_8)).contains ("'????? ??????'"); 143 | } 144 | 145 | @Test 146 | public void theSkyIsBlueInIvritWithoutEncoding () throws IOException { 147 | ClassicHttpResponse response = this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ -d \"השמים כחולים\""); 148 | Assertions.assertThat (IOUtils.toString (response.getEntity ().getContent (), StandardCharsets.UTF_8)).contains ("'השמים כחולים'"); 149 | } 150 | 151 | @Test 152 | public void theSkyIsBlueInIvritWithUTF8Encoding () throws IOException { 153 | ClassicHttpResponse response = this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ -H 'Content-Type: text/plain; charset=UTF-8' -d \"השמים כחולים\""); 154 | Assertions.assertThat (IOUtils.toString (response.getEntity ().getContent (), StandardCharsets.UTF_8)).contains ("'השמים כחולים'"); 155 | } 156 | 157 | @Test 158 | public void curlDER () { 159 | this.assertOk (this.curl ("-k --cert-type DER --cert src/test/resources/clients/libe/libe.der:mylibepass --key src/test/resources/clients/libe/libe.key.der --key-type DER https://localhost:%d/public/")); 160 | } 161 | 162 | @Test 163 | public void curlHalfPemHalfPKCS12 () { 164 | this.assertOk (this.curl ("-k --cert-type P12 --cert src/test/resources/clients/libe/libe.p12:mylibepass --key-type PEM --key src/test/resources/clients/libe/libe.pem https://localhost:%d/public/")); 165 | } 166 | 167 | @Test 168 | public void curlWithPlaceholders () { 169 | this.assertOk (this.curl ("-k --cert-type $curl_placeholder_0 --cert $curl_placeholder_1 --key-type $curl_placeholder_2 --key $curl_placeholder_3 https://localhost:%d/public/", 170 | with ().placeHolders (asList ("P12", "src/test/resources/clients/libe/libe.p12:mylibepass", "PEM", "src/test/resources/clients/libe/libe.pem")).build ())); 171 | } 172 | @Test 173 | public void curlWithConnectionManager () throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, UnrecoverableKeyException, IOException, CertificateException { 174 | KeyStore keystore = KeyStore.getInstance ("JKS"); 175 | keystore.load (Thread.currentThread ().getContextClassLoader ().getResourceAsStream ("clients/libe/libe.jks"), "mylibepass".toCharArray ()); 176 | this.assertOk (this.curl ("https://localhost:%d/public/", 177 | with ().connectionManager (new PoolingHttpClientConnectionManager (RegistryBuilder.create () 178 | .register ("https", new SSLConnectionSocketFactory (SSLContextBuilder.create () 179 | .loadTrustMaterial (null, new TrustSelfSignedStrategy()) 180 | .loadKeyMaterial (keystore, "mylibepass".toCharArray ()) 181 | .build (), NoopHostnameVerifier.INSTANCE)) 182 | .build ())).build ())); 183 | } 184 | 185 | @Test 186 | public void curlJKS () { 187 | this.assertOk (this.curl ("-k --cert-type JKS --cert src/test/resources/clients/libe/libe.jks:mylibepass https://localhost:%d/public/")); 188 | } 189 | 190 | @Test 191 | public void curlOfReadCurlOfReadCurl () { 192 | this.assertOk (this.curl (this.$ (this.$ (this.$ (this.$ ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/")))))); 193 | } 194 | 195 | @Test 196 | public void curlPKCS12 () { 197 | this.assertOk (this.curl ("-k --cert-type P12 --cert src/test/resources/clients/libe/libe.p12:mylibepass https://localhost:%d/public/")); 198 | } 199 | 200 | @Test 201 | public void curlToRedirectionWithFollowRedirectParam () { 202 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -L https://localhost:%d/public/redirection")); 203 | } 204 | 205 | @Test 206 | public void curlToRedirectionWithoutFollowRedirectParam () { 207 | this.assertFound (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/redirection")); 208 | } 209 | 210 | @Test 211 | public void curlToUnauthorized () { 212 | this.assertUnauthorized (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/unauthorized")); 213 | } 214 | 215 | @Test 216 | public void curlWithCaCert () { 217 | this.assertOk (this.curl ("-k --cacert src/test/resources/ca/fakeCa.crt --cert-type PEM --cert src/test/resources/clients/libe/libe.pem:mylibepass https://localhost:%d/public/")); 218 | } 219 | 220 | @Test 221 | public void curlWithFullSslChain () { 222 | this.assertOk (this.curl ("-k --cacert src/test/resources/ca/fakeCa.crt --cert-type PEM --cert src/test/resources/clients/libe/libe.pem:mylibepass --key-type P12 --key src/test/resources/clients/libe/libe.p12:mylibepass https://localhost:%d/public/")); 223 | 224 | try { 225 | // correct cert password and wrong key password 226 | this.curl ("-k --cacert src/test/resources/ca/fakeCa.crt --cert-type PEM --cert src/test/resources/clients/libe/libe.pem:mylibepass --key-type P12 --key src/test/resources/clients/libe/libe.p12:mylibepass2 https://localhost:%d/public/"); 227 | Assertions.fail ("This curl is not supposed to work and should fail with a IOException"); 228 | }catch (CurlException curlException){ 229 | Assertions.assertThat (curlException.getCause ().getClass ().getName ()) 230 | .isEqualTo (IOException.class.getName ()); 231 | Assertions.assertThat (curlException.getCause ().getMessage ()) 232 | .isEqualTo ( 233 | "keystore password was incorrect"); 234 | } 235 | } 236 | 237 | @Test 238 | public void curlWithHeaders () { 239 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -H'Host: localhost' -H'Authorization: 45e03eb2-8954-40a3-8068-c926f0461182' https://localhost:%d/public/v1/coverage/sncf/journeys?from=admin:7444extern")); 240 | } 241 | 242 | @Test 243 | public void curlWithHeadersContainingColon () { 244 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -H'Host: localhost' -H'SOAPAction: action1:action2:action3' https://localhost:%d/public/test")); 245 | } 246 | 247 | @Test 248 | public void curlWithOnlyALogin () { 249 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -u user https://localhost:%d/public/")); 250 | } 251 | 252 | @Test 253 | public void loginCorrectLoginCurlCommand () { 254 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -L -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' -u user:password 'https://localhost:%d/private/login'")); 255 | } 256 | 257 | @Test 258 | public void loginWithIncorrectLoginCurlCommand () { 259 | this.assertUnauthorized (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' -u foo:bar 'https://localhost:%d/private/login'")); 260 | } 261 | 262 | @Test 263 | public void readCurlCommand () { 264 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public/curlCommand1?param1=value1¶m2=value2'")); 265 | } 266 | 267 | @Test 268 | public void readCurlOfCurlCommand () { 269 | this.assertOk (this.curl (this.$ ("-k -E src/test/resources/clients/libe/libe.pem -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public/curlCommand2?param1=value1¶m2=value2'"))); 270 | } 271 | 272 | @Test 273 | public void readCurlPublicRoot () { 274 | this.assertOk (this.curl (this.$ ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/"))); 275 | } 276 | 277 | @Disabled 278 | @Test 279 | public void curlWithTooLowRequestTimeout () { 280 | try { 281 | this.curl (this.$("-k -E src/test/resources/clients/libe/libe.pem --connect-timeout 0.001 --max-time 10 https://localhost:%d/public/tooLong")); 282 | Assertions.fail ("This curl is not supposed to work and should fail with a ConnectTimeoutException"); 283 | }catch (CurlException curlException){ 284 | Assertions.assertThat ( 285 | asList (ConnectTimeoutException.class.getName (), ClientProtocolException.class.getName ()) 286 | .contains (curlException.getCause ().getClass ().getName ())).isTrue (); 287 | } 288 | } 289 | 290 | @Test 291 | public void curlWithMaxTime () { 292 | try { 293 | this.curl (this.$("-k -E src/test/resources/clients/libe/libe.pem --connect-timeout 10 --max-time 0.001 https://localhost:%d/public/tooLong")); 294 | Assertions.fail ("This curl is not supposed to work and should fail with a SocketTimeoutException"); 295 | }catch (CurlException curlException){ 296 | Assertions.assertThat (curlException.getCause ().getClass ().getName ()) 297 | .isEqualTo (SocketTimeoutException.class.getName ()); 298 | } 299 | } 300 | 301 | @Test 302 | public void readCurlWithHeaders () { 303 | this.assertOk (this.curl (this.$ ("-k -E src/test/resources/clients/libe/libe.pem -H'Host: localhost' -H'Authorization: 45e03eb2-8954-40a3-8068-c926f0461182' https://localhost:%d/public/v1/coverage/sncf/journeys?from=admin:7444extern"))); 304 | } 305 | 306 | @Test 307 | public void tryToLoginAnonymouslyWithCurlCommand () { 308 | this.assertUnauthorized (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X GET -H 'User-Agent: curl/7.49.1' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/private/login'")); 309 | } 310 | 311 | @Test 312 | public void withForm () { 313 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -F 'toto=titi;foo=bar' -F 'tutu=tata' -X POST -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public/form'")); 314 | } 315 | 316 | @Test 317 | public void withJsonBody () { 318 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X POST 'https://localhost:%d/public/json' -d '{\"var1\":\"val1\",\"var2\":\"val2\"}'")); 319 | } 320 | 321 | @Test 322 | public void withSimpleArgsParsing () { 323 | this.assertOk (curl ("-k -E src/test/resources/clients/libe/libe.pem -X POST 'https://localhost:%d/public/json' -d '{\"var1\":\"val1\",\"var2\":\"val2\"}'", 324 | with ().simpleArgsParsing ().build ())); 325 | } 326 | 327 | @Test 328 | public void withUrlEncodedData () { 329 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X POST 'https://localhost:%d/public/data' --data-urlencode 'message=hello world' --data-urlencode 'othermessage=how are you'")); 330 | } 331 | 332 | @Test 333 | public void withUrlEncodedData2 () { 334 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X POST 'https://localhost:%d/public/data' --data-urlencode '=hello world'")); 335 | } 336 | 337 | @Test 338 | public void withUrlEncodedData3 () { 339 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X POST 'https://localhost:%d/public/data' --data-urlencode 'message@src/test/resources/test.sh'")); 340 | } 341 | 342 | @Test 343 | public void withBinaryData () throws IOException { 344 | ClassicHttpResponse response = this.curl ("-k -E src/test/resources/clients/libe/libe.pem --data-binary \"@src/test/resources/clients/libe/libe.der\" -X POST -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public/data'"); 345 | String expected = IOUtils.toString (Objects.requireNonNull (Thread.currentThread ().getContextClassLoader ().getResourceAsStream ("clients/libe/libe.der")), StandardCharsets.UTF_8); 346 | String fullCurl = IOUtils.toString (response.getEntity ().getContent (), StandardCharsets.UTF_8); 347 | String actual = fullCurl.substring (fullCurl.indexOf ("-d '") + 4, fullCurl.indexOf ("' 'https")); 348 | Assertions.assertThat (actual.length ()).isEqualTo (expected.length ()); 349 | } 350 | 351 | @Test 352 | public void withFileForm () { 353 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -F 'toto=titi' -F 'script=@src/test/resources/test.sh' -X POST -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public/form'")); 354 | } 355 | 356 | @Test 357 | public void withUserAgent () { 358 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X GET -A 'toto' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public'")); 359 | } 360 | 361 | @Test 362 | public void outputFile () { 363 | File file = new File ("target/classes/downloadedCurl"); 364 | 365 | boolean fileDeleted = file.delete (); 366 | LOGGER.log (Level.FINE, "output file deleted : " + fileDeleted); 367 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X GET -A 'toto' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public' -o target/classes/downloadedCurl")); 368 | Assertions.assertThat (new File ("target/classes/downloadedCurl").exists ()).isTrue (); 369 | } 370 | 371 | @Test 372 | public void outputFileWithSpaces () { 373 | File file = new File ("target/classes/downloaded Curl With Spaces"); 374 | 375 | boolean fileDeleted = file.delete (); 376 | LOGGER.log (Level.FINE, "output file deleted : " + fileDeleted); 377 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -X GET -A 'toto' -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public' -o 'target/classes/downloaded Curl With Spaces'")); 378 | Assertions.assertThat (new File ("target/classes/downloaded Curl With Spaces").exists ()).isTrue (); 379 | } 380 | 381 | @Test 382 | public void justTheVersion () { 383 | assertThrows (CurlException.class, () -> this.assertOk (this.curl ("-V"))); 384 | } 385 | 386 | @Test 387 | public void curlCertNotFound () { 388 | assertThrows (CurlException.class, () -> this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/toto.pem https://localhost:%d/public/"))); 389 | } 390 | 391 | @Test 392 | public void readHelp () { 393 | assertThrows (CurlException.class, () -> this.curl ("--help")); 394 | } 395 | 396 | @Test 397 | public void withBadForm () { 398 | assertThrows (CurlException.class, () -> 399 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem -F 'totoghghgh' -X POST -H 'Accept: */*' -H 'Host: localhost' 'https://localhost:%d/public/form'"))); 400 | } 401 | 402 | @Test 403 | public void curlRootWithoutClientCertificate () { 404 | assertThrows (CurlException.class, () -> this.$ ("curl -k https://localhost:%d/public/")); 405 | } 406 | 407 | @Test 408 | public void curlRootWithoutTrustingInsecure () { 409 | assertThrows (CurlException.class, () -> this.$ ("curl https://localhost:%d/public/")); 410 | } 411 | 412 | @Test 413 | @Disabled // tls v1.1 is now disabled in all recent versions of the jdk, so this test will always fail 414 | public void curlTlsV11 () { 415 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --tlsv1.1")); 416 | } 417 | 418 | @Test 419 | public void curlTlsV10 () { 420 | assertThrows (CurlException.class, () -> 421 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --tlsv1.0"))); 422 | } 423 | 424 | @Test 425 | public void curlTlsV1 () { 426 | assertThrows (CurlException.class, () -> 427 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ -1"))); 428 | } 429 | 430 | @Test 431 | public void curlSslV2 () { 432 | assertThrows (CurlException.class, () -> 433 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ -2"))); 434 | } 435 | 436 | @Test 437 | public void curlSslV3 () { 438 | assertThrows (CurlException.class, () -> 439 | this.assertOk (this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ -3"))); 440 | } 441 | 442 | @Test 443 | public void curlWithProxy () { 444 | this.assertOk (Curl.curl ("-x http://localhost:" + proxyPort + " http://localhost:" + StupidHttpServer.port () + "/public/foo")); 445 | } 446 | 447 | @Test 448 | public void curlAsync () throws InterruptedException, ExecutionException { 449 | this.$Async (this.$Async ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/pathAsync").get ()); 450 | } 451 | 452 | @Test 453 | public void twoCurlsInParallel () { 454 | final CompletableFuture future1 = this.curlAsync ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/path1"); 455 | final CompletableFuture future2 = this.curlAsync ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/path2"); 456 | 457 | try { 458 | CompletableFuture.allOf (future1, future2).get (); 459 | this.assertOk (future1.get ()); 460 | this.assertOk (future2.get ()); 461 | } catch (InterruptedException | ExecutionException e) { 462 | Assertions.fail (); 463 | } 464 | } 465 | 466 | @Test 467 | public void noContentShouldNotTriggerANullPointerException () { 468 | this.$ ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/noContent"); 469 | } 470 | 471 | @SuppressWarnings ("unused") 472 | public static class MyInterceptor { 473 | public ClassicHttpResponse intercept (HttpRequest request, Supplier responseSupplier){ 474 | LOGGER.info ("I log something before the call"); 475 | ClassicHttpResponse response = responseSupplier.get (); 476 | LOGGER.info ("I log something after the call... Bingo, the status of the response is " + 477 | response.getCode ()); 478 | return response; 479 | } 480 | } 481 | 482 | @SuppressWarnings ("unused") 483 | private final BiFunction, ClassicHttpResponse> mySecondInterceptor = 484 | (request, responseSupplier) -> { 485 | LOGGER.info ("I log something before the call (from a lambda)"); 486 | ClassicHttpResponse response = responseSupplier.get (); 487 | LOGGER.info ("I log something after the call (from a lambda)... Bingo, the status of the response is " + 488 | response.getCode ()); 489 | return response; 490 | }; 491 | 492 | @Test 493 | public void withAnInterceptor (){ 494 | this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --interceptor org.toilelibre.libe.outside.curl.CurlTest$MyInterceptor::intercept --interceptor org.toilelibre.libe.outside.curl.CurlTest::mySecondInterceptor"); 495 | } 496 | 497 | @Test 498 | public void withAnInlinedInterceptor (){ 499 | Curl.curl () 500 | .javaOptions (with ().interceptor (((request, responseSupplier) -> { 501 | LOGGER.info ("I log something before the call"); 502 | ClassicHttpResponse response = responseSupplier.get (); 503 | LOGGER.info ("I log something after the call... Bingo, the status of the response is " + 504 | response.getCode ()); 505 | return response; 506 | })).build ()) 507 | .run ("http://www.google.com"); 508 | } 509 | 510 | @Test 511 | public void nonExistingInterceptor1(){ 512 | this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --interceptor org.toilelibre.libe.outside.curl.CurlTest$ThatSoCalledInterceptor::intercept"); 513 | } 514 | 515 | @Test 516 | public void nonExistingInterceptor2(){ 517 | this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --interceptor org.toilelibre.libe.outside.curl.CurlTest.A_SO_CALLED_FIELDNAME"); 518 | } 519 | 520 | @Test 521 | public void nonInterceptorField (){ 522 | this.curl ("-k -E src/test/resources/clients/libe/libe.pem https://localhost:%d/public/ --interceptor org.toilelibre.libe.outside.curl.CurlTest.LOGGER"); 523 | } 524 | } 525 | --------------------------------------------------------------------------------