diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d31d34..15fb3a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.64.1](https://github.com/cloudogu/ces-build-lib/releases/tag/1.64.0) - 2023-04-17 +### Fixed +- HttpClient escapes now the credentials in curl command to support credentials with characters like `$`. + ## [1.64.0](https://github.com/cloudogu/ces-build-lib/releases/tag/1.64.0) - 2023-04-11 -## Added +### Added - Add parameter to configure version for markdown link checker #100. ## [1.63.0](https://github.com/cloudogu/ces-build-lib/releases/tag/1.63.0) - 2023-02-16 -## Fixed +### Fixed - A bug with SonarCloud where an error was thrown because a private field was accessed (#99) ## [1.62.0](https://github.com/cloudogu/ces-build-lib/releases/tag/1.62.0) - 2023-01-30 -## Added +### Added - Function lintDockerfile to lint docker files #96. - Function shellCheck to lint shell scripts #96. diff --git a/pom.xml b/pom.xml index aef92a4..e2f08a4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,26 @@ - + 4.0.0 + + + jenkins-ci-releases + https://repo.jenkins-ci.org/releases/ + + + maven-central + https://repo1.maven.org/maven2 + + + com.cloudogu.ces ces-build-lib ces-build-lib - 1.64.0 + 1.64.1 + UTF-8 @@ -15,12 +28,6 @@ - - com.cloudbees - groovy-cps - 1.31 - - org.codehaus.groovy groovy-all @@ -30,7 +37,7 @@ junit junit - 4.13.1 + 4.13.2 test @@ -43,15 +50,15 @@ org.hamcrest - hamcrest-all - 1.3 + hamcrest + 2.2 test com.lesfurets jenkins-pipeline-unit - 1.1 + 1.17 test diff --git a/src/com/cloudogu/ces/cesbuildlib/HttpClient.groovy b/src/com/cloudogu/ces/cesbuildlib/HttpClient.groovy index bb88fb8..fd39377 100644 --- a/src/com/cloudogu/ces/cesbuildlib/HttpClient.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/HttpClient.groovy @@ -2,7 +2,7 @@ package com.cloudogu.ces.cesbuildlib /** * An HTTP client that calls curl on the shell. - * + * * Returns a map of * * httpCode (String) * * headers (Map) @@ -22,7 +22,7 @@ class HttpClient implements Serializable { Map get(String url, String contentType = '', def data = '') { return httpRequest('GET', url, contentType, data) } - + Map put(String url, String contentType = '', def data = '') { return httpRequest('PUT', url, contentType, data) } @@ -34,7 +34,7 @@ class HttpClient implements Serializable { } return httpRequest('PUT', url, contentType, filePath, command) } - + Map post(String url, String contentType = '', def data = '') { return httpRequest('POST', url, contentType, data) } @@ -50,53 +50,56 @@ class HttpClient implements Serializable { } } + private static String escapeSingleQuotes(String toEscape) { + return toEscape.replaceAll("'", "'\"'\"'") + } + protected String getCurlAuthParam() { - "-u ${script.env.CURL_USER}:${script.env.CURL_PASSWORD}" + "-u '" + escapeSingleQuotes(script.env.CURL_USER) + ":" + escapeSingleQuotes(script.env.CURL_PASSWORD) + "' " } private String getCurlCommand(String httpMethod, String url, String contentType, String data) { - return "curl -i -X ${httpMethod} " + - (credentials ? "${getCurlAuthParam()} " : '') + - (contentType ? "-H 'Content-Type: ${contentType}' " : '') + - (data ? "-d '" + data + "' " : '') + - "${url}" + return "curl -i -X '" + escapeSingleQuotes(httpMethod) + "' " + + (credentials ? getCurlAuthParam() : '') + + (contentType ? "-H 'Content-Type: " + escapeSingleQuotes(contentType) + "' " : '') + + (data ? "-d '" + escapeSingleQuotes(data) + "' " : '') + + "'" + escapeSingleQuotes(url) + "'" } private String getUploadFileCurlCommand(String httpMethod, String url, String contentType, String filePath) { - return "curl -i -X ${httpMethod} " + - (credentials ? "${getCurlAuthParam()} " : '') + - (contentType ? "-H 'Content-Type: ${contentType}' " : '') + - (filePath ? "-T '" + filePath + "' " : '') + - "${url}" + return "curl -i -X '" + escapeSingleQuotes(httpMethod) + "' " + + (credentials ? getCurlAuthParam() : '') + + (contentType ? "-H 'Content-Type: " + escapeSingleQuotes(contentType) + "' " : '') + + (filePath ? "-T '" + escapeSingleQuotes(filePath) + "' " : '') + + "'" + escapeSingleQuotes(url) + "'" } - + protected Map httpRequest(String httpMethod, String url, String contentType, def data, String customCommand = '') { String httpResponse def rawHeaders def body - + executeWithCredentials { String curlCommand if (customCommand.isEmpty()) { - String dataStr = data.toString().replaceAll("'", "'\"'\"'") - curlCommand = getCurlCommand(httpMethod, url, contentType, dataStr) + curlCommand = getCurlCommand(httpMethod, url, contentType, data.toString()) } else { curlCommand = customCommand } - + // Command must be run inside this closure, otherwise the credentials will not be masked (using '*') in the console httpResponse = sh.returnStdOut curlCommand } String[] responseLines = httpResponse.split("\n") - + // e.g. HTTP/2 301 - String httpCode = responseLines[0].split(" ")[1] + String httpCode = responseLines[0].split(" ")[1] def separatingLine = responseLines.findIndexOf { it.trim().isEmpty() } - + if (separatingLine > 0) { - rawHeaders = responseLines[1..(separatingLine -1)] - body = responseLines[separatingLine+1..-1].join('\n') + rawHeaders = responseLines[1..(separatingLine - 1)] + body = responseLines[separatingLine + 1..-1].join('\n') } else { // No body returned rawHeaders = responseLines[1..-1] @@ -104,11 +107,11 @@ class HttpClient implements Serializable { } def headers = [:] - for(String line: rawHeaders) { + for (String line : rawHeaders) { // e.g. cache-control: no-cache def splitLine = line.split(':', 2) headers[splitLine[0].trim()] = splitLine[1].trim() } - return [ httpCode: httpCode, headers: headers, body: body] + return [httpCode: httpCode, headers: headers, body: body] } } diff --git a/src/com/cloudogu/ces/cesbuildlib/K3d.groovy b/src/com/cloudogu/ces/cesbuildlib/K3d.groovy index fb0d6c6..71d25c7 100644 --- a/src/com/cloudogu/ces/cesbuildlib/K3d.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/K3d.groovy @@ -1,7 +1,5 @@ package com.cloudogu.ces.cesbuildlib -import com.cloudbees.groovy.cps.NonCPS - class K3d { /** * The image of the k3s version defining the targeted k8s version @@ -70,7 +68,6 @@ class K3d { * * @return new randomized cluster name */ - @NonCPS static String createClusterName() { String[] randomUUIDs = UUID.randomUUID().toString().split("-") String uuid_snippet = randomUUIDs[randomUUIDs.length - 1] diff --git a/test/com/cloudogu/ces/cesbuildlib/DockerMock.groovy b/test/com/cloudogu/ces/cesbuildlib/DockerMock.groovy index 4a13766..2736151 100644 --- a/test/com/cloudogu/ces/cesbuildlib/DockerMock.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/DockerMock.groovy @@ -21,7 +21,7 @@ class DockerMock { when(imageMock.mountJenkinsUser(anyBoolean())).thenReturn(imageMock) when(imageMock.mountDockerSocket()).thenReturn(imageMock) when(imageMock.mountDockerSocket(anyBoolean())).thenReturn(imageMock) - when(imageMock.inside(anyString(), any())).thenAnswer(new Answer() { + when(imageMock.inside(any(), any())).thenAnswer(new Answer() { @Override Object answer(InvocationOnMock invocation) throws Throwable { Closure closure = invocation.getArgument(1) diff --git a/test/com/cloudogu/ces/cesbuildlib/HttpClientTest.groovy b/test/com/cloudogu/ces/cesbuildlib/HttpClientTest.groovy index d1caa5e..91d5fed 100644 --- a/test/com/cloudogu/ces/cesbuildlib/HttpClientTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/HttpClientTest.groovy @@ -21,7 +21,7 @@ class HttpClientTest { assertThat(actualResponse.httpCode).isEqualTo('201') assertThat(actualResponse.headers['location']).isEqualTo('https://some:/url') assertThat(actualResponse.body).isEqualTo('') - assertThat(scriptMock.actualShMapArgs[0]).isEqualTo('curl -i -X GET http://url') + assertThat(scriptMock.actualShMapArgs[0]).isEqualTo("curl -i -X 'GET' 'http://url'") } @Test @@ -43,7 +43,7 @@ class HttpClientTest { assertThat(actualResponse.body).isEqualTo('') assertThat(scriptMock.actualShMapArgs[0]) - .isEqualTo('curl -i -X POST -H \'Content-Type: input\' -d \'{"title":"t","description":"d"}\' http://some-url' ) + .isEqualTo("curl -i -X 'POST' -H 'Content-Type: input' -d '{\"title\":\"t\",\"description\":\"d\"}' 'http://some-url'" ) } @Test @@ -60,7 +60,7 @@ class HttpClientTest { assertThat(actualResponse.body).isEqualTo('') assertThat(scriptMock.actualShMapArgs[0]) - .isEqualTo('curl -i -X PUT -H \'Content-Type: input\' -T \'/path/to/file\' http://some-url' ) + .isEqualTo("curl -i -X 'PUT' -H 'Content-Type: input' -T '/path/to/file' 'http://some-url'") } @Test @@ -82,7 +82,7 @@ class HttpClientTest { assertThat(actualResponse.body).isEqualTo(expectedBody1 + expectedBody2) assertThat(scriptMock.actualShMapArgs[0]) - .isEqualTo('curl -i -X POST http://some-url' ) + .isEqualTo("curl -i -X 'POST' 'http://some-url'") } @Test @@ -97,7 +97,49 @@ class HttpClientTest { http.get('http://url') - assertThat(scriptMock.actualShMapArgs[0]).isEqualTo('curl -i -X GET -u user:pw http://url') + assertThat(scriptMock.actualShMapArgs[0]).isEqualTo("curl -i -X 'GET' -u 'user:pw' 'http://url'") + } + + @Test + void "put request with single quotes"() { + http = new HttpClient(scriptMock, "credentialsID") + def expectedResponse = 'HTTP/1.1 203\n' + + 'cache-control: no-cache\n' + + 'content-type: output' + scriptMock.env.put("CURL_USER", "us'er") + scriptMock.env.put("CURL_PASSWORD", "p'w") + scriptMock.expectedDefaultShRetValue = expectedResponse + + def actualResponse = http.putFile('http://so\'me-url', 'in\'put', "/path/t\'o/file") + + assertThat(actualResponse.httpCode).isEqualTo('203') + assertThat(actualResponse.headers['content-type']).isEqualTo('output') + assertThat(actualResponse.body).isEqualTo('') + + assertThat(scriptMock.actualShMapArgs[0]) + .isEqualTo("curl -i -X 'PUT' -u 'us'\"'\"'er:p'\"'\"'w' -H 'Content-Type: in'\"'\"'put' -T '/path/t'\"'\"'o/file' 'http://so'\"'\"'me-url'" ) + } + + @Test + void "post request with single quotes"() { + def expectedResponse = 'HTTP/1.1 203\n' + + 'cache-control: no-cache\n' + + 'content-type: output' + scriptMock.expectedDefaultShRetValue = expectedResponse + + def dataJson = JsonOutput.toJson([ + title : 't', + description: 'd\'d' + ]) + + def actualResponse = http.post('http://some-url', 'input', dataJson) + + assertThat(actualResponse.httpCode).isEqualTo('203') + assertThat(actualResponse.headers['content-type']).isEqualTo('output') + assertThat(actualResponse.body).isEqualTo('') + + assertThat(scriptMock.actualShMapArgs[0]) + .isEqualTo("curl -i -X 'POST' -H 'Content-Type: input' -d '{\"title\":\"t\",\"description\":\"d'\"'\"'d\"}' 'http://some-url'" ) } }