Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 32 additions & 24 deletions src/main/java/edu/ohio/ais/rundeck/HttpBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -429,38 +429,46 @@ String getAuthHeader(PluginStepContext pluginStepContext, Map<String, Object> o


public void setHeaders(String headers, RequestBuilder request){
//checking json
Gson gson = new Gson();
Map<String,String> map = new HashMap<>();
Map<String, Object> map = parseHeaders(headers);
if (map == null) {
log.log(0, "Error parsing the headers");
return;
}
for (Map.Entry<String, Object> entry : map.entrySet()) {
request.setHeader(entry.getKey(), headerValueToString(entry.getValue()));
}
Comment thread
ronaveva marked this conversation as resolved.
}

@SuppressWarnings("unchecked")
private static Map<String, Object> parseHeaders(String headers) {
try {
map = (Map<String,String>) gson.fromJson(headers, map.getClass());
} catch (Exception e) {
map = null;
Object parsed = new Gson().fromJson(headers, HashMap.class);
if (parsed instanceof Map) {
return (Map<String, Object>) parsed;
}
} catch (Exception ignored) {
// fall through to YAML
}

//checking yml
if(map == null) {
map = new HashMap<>();
Object object = null;
try {
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
map = yaml.load(headers);
} catch (Exception e) {
map = null;
try {
Object parsed = new Yaml(new SafeConstructor(new LoaderOptions())).load(headers);
if (parsed instanceof Map) {
return (Map<String, Object>) parsed;
}
} catch (Exception ignored) {
// both parsers failed; caller logs and skips
}
return null;
}

if(map == null){
log.log(0, "Error parsing the headers");
}else{
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();

request.setHeader(key, value);
static String headerValueToString(Object value) {
if (value instanceof Double) {
double d = (Double) value;
if (!Double.isInfinite(d) && !Double.isNaN(d) && d == Math.floor(d)
&& d >= Long.MIN_VALUE && d <= Long.MAX_VALUE) {
return Long.toString((long) d);
}
}
return String.valueOf(value);
}
Comment thread
ronaveva marked this conversation as resolved.

static void propertyResolver(String pluginType, String property, Map<String,Object> Configuration, PluginStepContext context, String SERVICE_PROVIDER_NAME) {
Expand Down
95 changes: 95 additions & 0 deletions src/test/java/edu/ohio/ais/rundeck/HttpBuilderTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package edu.ohio.ais.rundeck;

import com.dtolabs.rundeck.plugins.PluginLogger;
import org.apache.http.client.methods.RequestBuilder;
import org.junit.Before;
import org.junit.Test;

import java.util.HashMap;
Expand All @@ -8,9 +11,21 @@
import static edu.ohio.ais.rundeck.HttpBuilder.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;

public class HttpBuilderTest {

private HttpBuilder builder;
private RequestBuilder request;

@Before
public void setUp() {
builder = new HttpBuilder();
request = mock(RequestBuilder.class);
}

@Test
public void testGetStringOption() {
Map<String, Object> options = new HashMap<>();
Expand Down Expand Up @@ -75,4 +90,84 @@ public void testGetBooleanOption() {
assertEquals("Expected the value associated with the key", Boolean.FALSE, result);
}

// RUN-2569 / upstream issue #32: HttpBuilder.setHeaders previously threw
// ClassCastException when YAML or JSON header values parsed as non-String
// scalars (e.g. `Content-Length: 0`). These tests pin the fixed behavior.

@Test
public void setHeaders_yamlIntegerValue_setsStringifiedValue() {
builder.setHeaders("Content-Length: 0", request);
verify(request).setHeader("Content-Length", "0");
}

@Test
public void setHeaders_jsonIntegerValue_setsStringifiedValue() {
builder.setHeaders("{\"Content-Length\":0}", request);
verify(request).setHeader("Content-Length", "0");
}

@Test
public void setHeaders_yamlStringValue_setsValueUnchanged() {
builder.setHeaders("Authorization: Bearer abc", request);
verify(request).setHeader("Authorization", "Bearer abc");
}

@Test
public void setHeaders_jsonStringValue_setsValueUnchanged() {
builder.setHeaders("{\"X-Custom\":\"foo\"}", request);
verify(request).setHeader("X-Custom", "foo");
}

@Test
public void setHeaders_yamlMixedStringAndNumeric_setsBothCorrectly() {
builder.setHeaders("Content-Length: 0\nX-Custom: foo", request);
verify(request).setHeader("Content-Length", "0");
verify(request).setHeader("X-Custom", "foo");
}

@Test
public void setHeaders_yamlBooleanValue_setsStringifiedValue() {
builder.setHeaders("X-Debug: true", request);
verify(request).setHeader("X-Debug", "true");
}

@Test
public void setHeaders_yamlListValue_setsStringifiedValueWithoutThrowing() {
// Non-scalar values no longer throw ClassCastException; they are
// coerced to their default toString() representation. This is a
// deliberate behavior change relative to the pre-fix code, which
// crashed on any non-String value.
builder.setHeaders("Accept: [a, b]", request);
verify(request).setHeader("Accept", "[a, b]");
}

@Test
public void setHeaders_unparseableInput_logsAndDoesNotThrow() {
PluginLogger log = mock(PluginLogger.class);
builder.setLog(log);
// A bareword is neither valid JSON nor a YAML map; both parsers
// either throw or return a non-Map value, so setHeaders must log
// the parse error and skip touching the request.
builder.setHeaders("not a map", request);
verify(log).log(0, "Error parsing the headers");
verifyZeroInteractions(request);
}

@Test
public void headerValueToString_wholeNumberDouble_emitsIntegerString() {
// Gson parses all JSON numbers as Double by default; ensure that a
// whole-number Double like 0.0 is emitted as "0" rather than "0.0"
// so headers like Content-Length stay valid for strict HTTP servers.
assertEquals("0", headerValueToString(0.0));
assertEquals("1024", headerValueToString(1024.0));
assertEquals("-1", headerValueToString(-1.0));
}

@Test
public void headerValueToString_fractionalDouble_emitsDecimalString() {
// Genuinely fractional Doubles must keep their decimal form rather
// than being silently truncated to a long.
assertEquals("1.5", headerValueToString(1.5));
}

}
Loading