Skip to content

Commit

Permalink
Improving http message robustness (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
therealryan authored Nov 17, 2022
1 parent c179ce0 commit 665ad03
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public abstract class HttpMsg<T extends HttpMsg<T>> extends AbstractMessage<T> {
/**
* Matches header lines
*/
protected static final Pattern HEADER_PATTERN = Pattern
protected static final Pattern HEADER_LINE_PATTERN = Pattern
.compile( "(?<name>[^:]*?):(?<value>.*)" );

private final Supplier<Map<String, Object>> basis;
Expand Down Expand Up @@ -172,7 +172,7 @@ public byte[] content() {
* @param wire <code>true</code> if we should try to create an accurate
* representation of the bytes that go on the wire,
* <code>false</code> for a more human-friendly output
* @return The conplete formatted body
* @return The complete formatted body
*/
protected String enchunken( String content, boolean wire ) {
if( wire && "chunked".equals( get( header( "Transfer-Encoding" ) ) ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import static java.util.stream.Collectors.toMap;

import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
Expand Down Expand Up @@ -38,12 +41,6 @@ public class HttpReq extends HttpMsg<HttpReq> {
*/
public static final String PATH_VAR_PRESUFIX = "%";

private static final Pattern REQ_PATTERN = Pattern
.compile( "^(?<method>\\w+?) (?<path>\\S+?) (?<version>\\S+?)\r\n"
+ "(?<headers>.*?)\r\n"
+ "\r\n"
+ "(?<body>.*)$", Pattern.DOTALL );

/**
* An empty request
*/
Expand All @@ -56,23 +53,36 @@ public HttpReq() {
*/
public HttpReq( byte[] content, Function<byte[], Message> bodyParse ) {

Matcher m = REQ_PATTERN.matcher( new String( content, UTF_8 ) );
if( m.matches() ) {
Deque<String> lines = new ArrayDeque<>(
Arrays.asList( new String( content, UTF_8 ).split( "\r\n" ) ) );

set( HttpReq.METHOD, m.group( "method" ).trim() );
set( HttpReq.PATH, m.group( "path" ).trim() );
set( VERSION, m.group( "version" ).trim() );
// start line
Deque<String> startFields = new ArrayDeque<>(
Arrays.asList( lines.removeFirst().split( " " ) ) );

for( String line : m.group( "headers" ).split( "\r\n" ) ) {
Matcher h = HEADER_PATTERN.matcher( line );
if( h.matches() ) {
set( HEADER_PRESUFIX + h.group( "name" ).trim() + HEADER_PRESUFIX,
h.group( "value" ).trim() );
}
}
if( !startFields.isEmpty() ) {
set( METHOD, startFields.removeFirst() );
}
if( !startFields.isEmpty() ) {
set( PATH, startFields.removeFirst() );
}
if( !startFields.isEmpty() ) {
set( VERSION, startFields.stream().collect( joining( " " ) ) );
}

set( BODY, bodyParse.apply( dechunken( m.group( "body" ) ).getBytes( UTF_8 ) ) );
// zero or more headers
String headerLine;
while( !lines.isEmpty() && !(headerLine = lines.removeFirst()).isEmpty() ) {
Matcher h = HEADER_LINE_PATTERN.matcher( headerLine );
if( h.matches() ) {
set( HEADER_PRESUFIX + h.group( "name" ).trim() + HEADER_PRESUFIX,
h.group( "value" ).trim() );
}
}

// body
String body = lines.stream().collect( joining( "\r\n" ) );
set( BODY, bodyParse.apply( dechunken( body ).getBytes( UTF_8 ) ) );
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import static java.util.stream.Collectors.joining;

import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import com.mastercard.test.flow.Message;
import com.mastercard.test.flow.util.Bytes;
Expand All @@ -19,11 +22,6 @@
*/
public class HttpRes extends HttpMsg<HttpRes> {

private static final Pattern RES_PATTERN = Pattern
.compile( "^(?<version>\\S+?) (?<status>\\S+) ?(?<text>.*?)\r\n"
+ "(?<headers>.*?)\r\n"
+ "\r\n"
+ "(?<body>.*)$", Pattern.DOTALL );
/**
* Use this as the field path to set the HTTP status code
*/
Expand All @@ -46,22 +44,36 @@ public HttpRes() {
public HttpRes( byte[] content, Function<byte[], Message> bodyParse ) {
this();

Matcher m = RES_PATTERN.matcher( new String( content, UTF_8 ) );
if( m.matches() ) {
set( VERSION, m.group( "version" ).trim() );
set( HttpRes.STATUS, m.group( "status" ).trim() );
set( HttpRes.STATUS_TEXT, m.group( "text" ).trim() );

for( String line : m.group( "headers" ).split( "\r\n" ) ) {
Matcher h = HEADER_PATTERN.matcher( line );
if( h.matches() ) {
set( HEADER_PRESUFIX + h.group( "name" ).trim() + HEADER_PRESUFIX,
h.group( "value" ).trim() );
}
}
Deque<String> lines = new ArrayDeque<>(
Arrays.asList( new String( content, UTF_8 ).split( "\r\n" ) ) );

// status line
Deque<String> statusFields = new ArrayDeque<>(
Arrays.asList( lines.removeFirst().split( " " ) ) );

if( !statusFields.isEmpty() ) {
set( VERSION, statusFields.removeFirst() );
}
if( !statusFields.isEmpty() ) {
set( STATUS, statusFields.removeFirst() );
}
if( !statusFields.isEmpty() ) {
set( STATUS_TEXT, statusFields.stream().collect( joining( " " ) ) );
}

set( BODY, bodyParse.apply( dechunken( m.group( "body" ) ).getBytes( UTF_8 ) ) );
// zero or more headers
String headerLine;
while( !lines.isEmpty() && !(headerLine = lines.removeFirst()).isEmpty() ) {
Matcher h = HEADER_LINE_PATTERN.matcher( headerLine );
if( h.matches() ) {
set( HEADER_PRESUFIX + h.group( "name" ).trim() + HEADER_PRESUFIX,
h.group( "value" ).trim() );
}
}

// body
String body = lines.stream().collect( joining( "\r\n" ) );
set( BODY, bodyParse.apply( dechunken( body ).getBytes( UTF_8 ) ) );
}

/**
Expand Down Expand Up @@ -122,18 +134,26 @@ protected boolean isHttpField( String field ) {

@Override
protected String serialise( String bodyContent, boolean wireFormat ) {
return String.format( ""
+ "%s %s%s%s\r\n" // response line
+ "%s" // headers (will supply their own line endings)
+ "\r\n" // empty line
+ "%s", // body,
version(), status(), statusText().isEmpty() ? "" : " ", statusText(),
headers().entrySet().stream()
.map( e -> String.format( "%s: %s\r\n",
e.getKey(),
e.getValue() ) )
.collect( joining() ),
enchunken( bodyContent, wireFormat ) );
StringBuilder sb = new StringBuilder();
// status
sb.append( Stream.of(
version(), status(), statusText() )
.collect( joining( " " ) ) )
.append( "\r\n" );

// headers
headers().entrySet().stream()
.map( e -> String.format( "%s: %s\r\n",
e.getKey(),
e.getValue() ) )
.forEach( sb::append );

sb.append( "\r\n" );

// body
sb.append( enchunken( bodyContent, wireFormat ) );

return sb.toString();
}

private String status() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.mastercard.test.flow.msg.http;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
* Generates combinations of list members
*
* @param <T> The combination member type
* @param <C> The collection type to return
*/
class Combinator<T, C extends Collection<T>> implements Iterator<C> {

private final boolean[] included;
private boolean fullSetReturned = false;
private final List<T> elements;
private final Supplier<C> collection;

/**
* @param collection How to build a collection of the desired return type
* @param members The members to combine
*/
@SafeVarargs
public Combinator( Supplier<C> collection, T... members ) {
elements = Arrays.asList( members );
included = new boolean[elements.size()];
this.collection = collection;
}

@Override
public boolean hasNext() {
return !fullSetReturned;
}

@Override
public C next() {

C result = collection.get();
for( int i = 0; i < included.length; i++ ) {
if( included[i] ) {
result.add( elements.get( i ) );
}
}

fullSetReturned = allSet();

for( int i = 0; i < included.length; i++ ) {
if( !included[i] ) {
included[i] = true;
break;
}
included[i] = false;
}
return result;
}

/**
* @return A stream of the combinations
*/
public Stream<C> stream() {
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize( this, Spliterator.ORDERED ),
false );
}

private boolean allSet() {
for( int i = 0; i < included.length; i++ ) {
if( !included[i] ) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.mastercard.test.flow.msg.http;

import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import java.util.TreeSet;

import org.junit.jupiter.api.Test;

/**
* Exercises {@link Combinator}
*/
@SuppressWarnings("static-method")
class CombinatorTest {

/**
* Demonstrates the combinations of zero items
*/
@Test
void empty() {
test( "[]" );
}

/**
* Demonstrates the combinations of zero items
*/
@Test
void single() {
test( "[]\n"
+ "[a]",
"a" );
}

/**
* Demonstrates the combinations of zero items
*/
@Test
void pair() {
test( "[]\n"
+ "[a]\n"
+ "[b]\n"
+ "[a, b]",
"a", "b" );
}

/**
* Demonstrates the combinations of three items
*/
@Test
void triple() {
test( ""
+ "[]\n"
+ "[a]\n"
+ "[b]\n"
+ "[a, b]\n"
+ "[c]\n"
+ "[a, c]\n"
+ "[b, c]\n"
+ "[a, b, c]",
"a", "b", "c" );
}

private static void test( String expected, String... items ) {
assertEquals(
expected,
new Combinator<>( TreeSet::new, items )
.stream()
.map( String::valueOf )
.collect( joining( "\n" ) ),
"Combinations of " + Arrays.toString( items ) );
}
}
Loading

0 comments on commit 665ad03

Please sign in to comment.