Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Handle absolute, asterisk, and authority-form request targets #1470

Merged
merged 1 commit into from
Mar 10, 2017

Conversation

natemcmaster
Copy link
Contributor

@natemcmaster natemcmaster commented Mar 9, 2017

Improves compliance with RFC 7230 on the expected handling of requests that have URI or asterisk in the request target.

This means rejecting asterisk requests that are not OPTIONS and rejecting authority-form requests that are not CONNECT. "http 405 method not allowed" response requires you set "Allow: " with acceptable methods

This also means the server will handle the path and query on targets with absolute URIs as request-targets.

Resolves #666
Resolves #1279

@natemcmaster
Copy link
Contributor Author

Will follow up with comparison benchmarks when they finish running.

@davidfowl
Copy link
Member

PERFFFF

// TODO Validate that target is a correct host[:port] string.
// Reject as 400 if not. This is just a quick scan for invalid characters
// but doesn't check that the target fully matches the URI spec.
for (var i = 0; i < target.Length; i++)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fear repeated scanning of strings can be bad for performance numbers. I think we'll end up moving more validation into the parser.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last time we talked about this, we agreed we we're okay with sub-optimal performance on edge cases. This code shouldn't be hit unless we have a bad client or malformed requests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last time we talked about this

How long ago was that?

I'll wait for the benchmarks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about this the first time I implemented this #1246. We didn't end up merging that one because we want to prioritize @pakrym's work on pipelines.

for (var i = 0; i < target.Length; i++)
{
var ch = (char)target[i];
if (!char.IsLetterOrDigit(ch) && ch != ':' && ch != '.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using IsLetterOrDigit, can you use a subset of this:

https://github.com/aspnet/KestrelHttpServer/blob/dev/src/Microsoft.AspNetCore.Server.Kestrel/Internal/Http/KestrelHttpParser.cs#L505

We should move this a HttpUtilities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The "IsValidTokenChar" method includes characters not allowed in the request target for authority-form, like "*".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I'm not saying we should use it. I said a subset into HttpUtilities.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, thx. yes, this can move to the utils class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should rename IsValidTokenChar to IsValidMethodChar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and move to HttpUtlities

var ch = (char)target[i];
if (!char.IsLetterOrDigit(ch) && ch != ':' && ch != '.')
{
if (Log.IsEnabled(LogLevel.Information))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking IsEnabled all over the code before exceptions now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the extra work to avoid 😉 push it to right of throw and combine the two

private BadHttpRequestException GetRequestLineRejection(Span<byte> line)
{
    const int MaxRequestLineError = 32;
    if (Log.IsEnabled(LogLevel.Information))
    {
         var line = line.GetAsciiStringEscaped(MaxRequestLineError);
         return BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine, line);
    }
    else
    {
         return BadHttpRequestException.GetException(RequestRejectionReason.InvalidRequestLine);
    }
}

private void ThrowRequestLineRejection(Span<byte> line)
{
    throw GetRequestLineRejection(line)
}

Then

if (!char.IsLetterOrDigit(ch) && ch != ':' && ch != '.')
{
    ThrowRequestLineRejection(line);
}

Keep the main body tight with only code that's mostly always executed; push everything else out

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you have to pass a bucket of params to do it 😝

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will change when @CesarBS puts #1469 in.

@@ -162,6 +185,10 @@ public KestrelHttpParser(IKestrelTrace log)
RejectRequestLine(span);
}
}
else if (ch == 0)
{
RejectRequestLine(span);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail early when possible. \0 is never acceptable on the request line.

Copy link
Member

@davidfowl davidfowl Mar 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is we need to do that in a bunch of places and this probably doesn't handle all of those cases. Not just 0 but a bunch of other characters.

@natemcmaster
Copy link
Contributor Author

PR

                        Method |                                                          ParserType |      Mean |    StdErr |    StdDev | Scaled | Scaled-StdDev |        RPS | Allocated |
------------------------------ |-------------------------------------------------------------------- |---------- |---------- |---------- |------- |-------------- |----------- |---------- |
          PlaintextTechEmpower | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 1.3479 us | 0.0118 us | 0.0646 us |   1.00 |          0.00 | 741,905.06 |     353 B |
 PipelinedPlaintextTechEmpower | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 1.0828 us | 0.0046 us | 0.0251 us |   0.80 |          0.04 | 923,522.40 |     351 B |
                    LiveAspNet | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 3.4407 us | 0.0347 us | 0.1902 us |   2.56 |          0.18 | 290,634.41 |   1.41 kB |
           PipelinedLiveAspNet | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 3.1495 us | 0.0243 us | 0.1332 us |   2.34 |          0.14 | 317,512.69 |   1.39 kB |
                       Unicode | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 7.0485 us | 0.0673 us | 0.3684 us |   5.24 |          0.35 | 141,873.35 |   3.91 kB |
              UnicodePipelined | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 7.0970 us | 0.0602 us | 0.3298 us |   5.28 |          0.33 | 140,904.71 |   3.89 kB |

dev

                        Method |                                                          ParserType |      Mean |    StdErr |    StdDev | Scaled | Scaled-StdDev |        RPS | Allocated |
------------------------------ |-------------------------------------------------------------------- |---------- |---------- |---------- |------- |-------------- |----------- |---------- |
          PlaintextTechEmpower | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 1.2991 us | 0.0187 us | 0.1026 us |   1.00 |          0.00 | 769,784.52 |     353 B |
 PipelinedPlaintextTechEmpower | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 1.0524 us | 0.0051 us | 0.0282 us |   0.81 |          0.06 | 950,246.05 |     351 B |
                    LiveAspNet | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 2.6336 us | 0.0256 us | 0.1401 us |   2.04 |          0.17 | 379,709.69 |   1.12 kB |
           PipelinedLiveAspNet | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 2.3801 us | 0.0135 us | 0.0739 us |   1.84 |          0.13 | 420,148.35 |   1.11 kB |
                       Unicode | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 4.1385 us | 0.0238 us | 0.1304 us |   3.20 |          0.23 | 241,633.89 |   1.99 kB |
              UnicodePipelined | Microsoft.AspNetCore.Server.Kestrel.Internal.Http.KestrelHttpParser | 4.2248 us | 0.0318 us | 0.1744 us |   3.27 |          0.25 | 236,696.38 |   1.98 kB |

@davidfowl
Copy link
Member

davidfowl commented Mar 9, 2017

That doesn't look good. Profile and see where the time is spent

@natemcmaster
Copy link
Contributor Author

Looking into it. Correctness comes before perf, but I'll profile more to see what I can gain back

@natemcmaster
Copy link
Contributor Author

Uri.Create.

#1471


RawTarget = target.GetAsciiStringNonNullCharacters();

if (!Uri.TryCreate(RawTarget, UriKind.Absolute, out var uri))
Copy link
Member

@davidfowl davidfowl Mar 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to use Uri.TryCreate? Maybe we should roll our own.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uri is a garbage hole for performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is about correctness, not perf. For context, we talked about this in the first iteration of absolute-URI support. We're aware this is slow, but it's not on the hot path. In fact, it's not really on any commonly used code path. The HTTP spec requires we handle requests like GET http://localhost/path HTTP/1.1, but in practice, clients never do. Instead they use origin-form, GET /path HTTP/1.1. We debated whether to bother at all, but since our goal is edge-server capabilities, we will comply with the HTTP spec, including its edge-cases. Fast URI parsing is not a high priority.

// | | queryEnd
// | | |versionStart
// | | ||
// http://host/path?query=1 HTTP/1.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pathStart should be at the first / in the actual path. For the special cases http://host and http://host?query, make a span with / and pass that to OnStartLine.

{
if (target.Length == 0)
Copy link
Member

@davidfowl davidfowl Mar 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Target can't be empty here can it? Doesn't the parser check for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should. This could be a debug.assert instead.

@@ -66,6 +68,27 @@ public KestrelHttpParser(IKestrelTrace log)
span = startLineBuffer.ToSpan();
}

// pathStart
// | pathEnd and queryStart
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need pathEnd and queryStart? I couldn't think of a scenario when they are different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only if path had been decoded in place and query not moved (e.g. path could get shorter) - though this doesn't currently happen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have removed pathEnd in #1463

@natemcmaster
Copy link
Contributor Author

Here are the new comparisons with the #1471 fix in benchmarks.

dev

                        Method |      Mean |    StdErr |    StdDev |    Median | Scaled | Scaled-StdDev |        RPS | Allocated |
------------------------------ |---------- |---------- |---------- |---------- |------- |-------------- |----------- |---------- |
          PlaintextTechEmpower | 1.4258 us | 0.0287 us | 0.1572 us | 1.3617 us |   1.00 |          0.00 | 701,360.54 |     353 B |
          PlaintextAbsoluteUri | 1.4000 us | 0.0103 us | 0.0566 us | 1.3791 us |   0.99 |          0.11 | 714,310.21 |     387 B |
 PipelinedPlaintextTechEmpower | 1.0874 us | 0.0106 us | 0.0578 us | 1.0690 us |   0.77 |          0.09 | 919,605.26 |     351 B |
                    LiveAspNet | 2.5348 us | 0.0224 us | 0.1227 us | 2.5159 us |   1.80 |          0.20 | 394,501.35 |   1.08 kB |
           PipelinedLiveAspNet | 2.5066 us | 0.0393 us | 0.2150 us | 2.3992 us |   1.78 |          0.23 | 398,949.37 |   1.07 kB |
                       Unicode | 3.9702 us | 0.0447 us | 0.2448 us | 3.8809 us |   2.81 |          0.33 | 251,878.79 |   1.89 kB |
              UnicodePipelined | 3.8373 us | 0.0128 us | 0.0699 us | 3.8163 us |   2.72 |          0.27 | 260,600.24 |   1.86 kB |

PR

                        Method |       Mean |    StdErr |    StdDev | Scaled | Scaled-StdDev |        RPS | Allocated |
------------------------------ | ---------- |---------- |---------- |------- |-------------- |----------- |---------- |
          PlaintextTechEmpower |  1.3305 us | 0.0053 us | 0.0292 us |   1.00 |          0.00 | 751,581.09 |     353 B |
          PlaintextAbsoluteUri |  2.1944 us | 0.0153 us | 0.0837 us |   1.65 |          0.07 | 455,696.01 |     698 B |
 PipelinedPlaintextTechEmpower |  1.0833 us | 0.0040 us | 0.0220 us |   0.81 |          0.02 | 923,131.88 |     351 B |
                    LiveAspNet |  2.5116 us | 0.0147 us | 0.0805 us |   1.89 |          0.07 | 398,159.47 |   1.08 kB |
           PipelinedLiveAspNet |  2.3130 us | 0.0068 us | 0.0372 us |   1.74 |          0.05 | 432,341.14 |   1.07 kB |
                       Unicode |  4.2242 us | 0.0505 us | 0.2767 us |   3.18 |          0.22 | 236,730.41 |   1.89 kB |
              UnicodePipelined |  4.0434 us | 0.0663 us | 0.3632 us |   3.04 |          0.28 | 247,318.74 |   1.86 kB |

@natemcmaster natemcmaster force-pushed the namc/request-targets branch from 5a75fd9 to c67a838 Compare March 9, 2017 19:25
@natemcmaster
Copy link
Contributor Author

🆙 📅 had a bunch of merge conflicts with #1463. Re-running benchmarks.

(Will squash before merging)

@natemcmaster
Copy link
Contributor Author

dev

                        Method |      Mean |    StdErr |    StdDev |    Median | Scaled | Scaled-StdDev |        RPS | Allocated |
------------------------------ |---------- |---------- |---------- |---------- |------- |-------------- |----------- |---------- |
          PlaintextTechEmpower | 1.2376 us | 0.0152 us | 0.0834 us | 1.2097 us |   1.00 |          0.00 | 808,015.22 |     349 B |
          PlaintextAbsoluteUri | 1.2648 us | 0.0062 us | 0.0339 us | 1.2635 us |   1.03 |          0.07 | 790,612.89 |     381 B |
 PipelinedPlaintextTechEmpower | 1.0391 us | 0.0255 us | 0.1395 us | 0.9648 us |   0.84 |          0.12 | 962,407.67 |     348 B |
                    LiveAspNet | 2.4872 us | 0.0665 us | 0.3644 us | 2.4205 us |   2.02 |          0.32 | 402,063.77 |   1.07 kB |
           PipelinedLiveAspNet | 2.1457 us | 0.0075 us | 0.0410 us | 2.1392 us |   1.74 |          0.11 | 466,051.66 |   1.07 kB |
                       Unicode | 3.9496 us | 0.0641 us | 0.3508 us | 3.7639 us |   3.20 |          0.35 | 253,191.49 |   1.85 kB |
              UnicodePipelined | 3.8724 us | 0.0461 us | 0.2525 us | 3.7603 us |   3.14 |          0.28 | 258,234.69 |   1.86 kB |

PR

                        Method |          Mean |      StdDev |        Median | Scaled | Scaled-StdDev |          RPS | Allocated |
------------------------------ |-------------- |------------ |-------------- |------- |-------------- |------------- |---------- |
          PlaintextTechEmpower | 1,197.5448 ns |  22.3411 ns | 1,195.7931 ns |   1.00 |          0.00 |   835,041.84 |     349 B |
          PlaintextAbsoluteUri | 2,035.7134 ns |  41.9057 ns | 2,031.9959 ns |   1.70 |          0.05 |   491,228.28 |     692 B |
 PipelinedPlaintextTechEmpower |   990.8188 ns |  28.8461 ns |   984.0139 ns |   0.83 |          0.03 | 1,009,266.29 |     348 B |
                    LiveAspNet | 2,318.5341 ns |  42.4130 ns | 2,303.5764 ns |   1.94 |          0.05 |   431,307.01 |   1.07 kB |
           PipelinedLiveAspNet | 2,197.7497 ns |  55.2662 ns | 2,184.0570 ns |   1.84 |          0.06 |   455,010.87 |   1.07 kB |
                       Unicode | 3,762.2063 ns | 179.1014 ns | 3,661.2269 ns |   3.14 |          0.16 |   265,801.48 |   1.85 kB |
              UnicodePipelined | 3,645.7075 ns |  74.3930 ns | 3,624.3606 ns |   3.05 |          0.08 |   274,295.18 |   1.86 kB |

@davidfowl
Copy link
Member

Wait, you made it faster?

@@ -157,7 +162,7 @@ public KestrelHttpParser(IKestrelTrace log)
RejectRequestLine(data, length);
}

handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, pathEncoded);
handler.OnStartLine(method, httpVersion, targetBuffer, pathBuffer, query, customMethod, new Span<byte>(data, length), pathEncoded);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare a local

@natemcmaster
Copy link
Contributor Author

natemcmaster commented Mar 9, 2017

All but the PlaintextAbsoluteUri benchmark which is almost cut in half. But that's going from 800k RPS of incorrect handling to 480k RPS correct handling of the request.

@@ -17,6 +23,14 @@ private BadHttpRequestException(string message, int statusCode)

internal int StatusCode { get; }

internal void SetAdditionalHeaders(FrameResponseHeaders headers)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit strange having logic in exception and calling methods on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer subclass of BadHttpRequestException with RequiredMethod property and downcast check in ProduceEnd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BadHttpRequestException is sealed :-/

@@ -1179,6 +1185,15 @@ public void RejectRequest(RequestRejectionReason reason, string value)
throw BadHttpRequestException.GetException(reason, value);
}

private void RejectRequestLine(Span<byte> span)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename span to requestLine


namespace Microsoft.AspNetCore.Server.Kestrel
{
public sealed class BadHttpRequestException : IOException
{
// used in rare cases
private HttpMethod? _requiredMethod;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readonly and add second ctor.

@@ -1179,6 +1185,15 @@ public void RejectRequest(RequestRejectionReason reason, string value)
throw BadHttpRequestException.GetException(reason, value);
}

private void RejectRequestLine(Span<byte> span)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will change with #1469.

@@ -1216,8 +1232,51 @@ protected void ReportApplicationError(Exception ex)
Log.ApplicationError(ConnectionId, ex);
}

public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, Span<byte> line, bool pathEncoded)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1469 introduces rejection of request targets, so you won't have to pass the entire line here anymore.


// The authority-form of request-target is only used for CONNECT
// requests (https://tools.ietf.org/html/rfc7231#section-4.3.6).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: blank line

{
Unknown = -1,
Http = 0,
HttpSecure = 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Https

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How strong is your preference for this? I'm chose the name to make it super obvious that it's http + TLS, not plain-old http.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use https everywhere else

@@ -109,6 +109,11 @@ public KestrelHttpParser(IKestrelTrace log)

pathEncoded = true;
}
else if (ch == 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being checked here now? Should blow up when trying to get ASCII string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail fast. The null character is never valid in the start line.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are validating these during materialization of values, fail fast is less important then less checks in hot paths

@@ -15,7 +15,13 @@ public static class HttpUtilities
public const string Http10Version = "HTTP/1.0";
public const string Http11Version = "HTTP/1.1";

public const string HttpUriScheme = "http://";
public const string HttpSecureUriScheme = "https://";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HttpsUriScheme

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was intentionally trying to emphasize the "S" of https by expanding it to Secure. It's easy to miss that extra character when reviewing/writing code.

private void SetErrorResponseException(BadHttpRequestException ex)
{
SetErrorResponseHeaders(ex.StatusCode);
if (ex.AllowedHeader != default(StringValues))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StringValues.IsNullOrEmpty

cesarblum pushed a commit that referenced this pull request Mar 10, 2017
private BadHttpRequestException(string message, int statusCode)
:this(message, statusCode, null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: : this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Improves compliance with RFC 7230 on the expected handling of requests
that have URI or asterisk in the request target.

This means rejecting asterisk requests that are not OPTIONS and rejecting
authority-form requests taht are not CONNECT.

This also means the server will handle the path and query on targets
with absolute URIs as request-targets.
@natemcmaster natemcmaster force-pushed the namc/request-targets branch from eef051c to 49b328d Compare March 10, 2017 00:54
@natemcmaster
Copy link
Contributor Author

Squashed, formatted, benchmarked, etc. Awaiting sign off.

@natemcmaster natemcmaster merged commit 49b328d into dev Mar 10, 2017
@natemcmaster natemcmaster deleted the namc/request-targets branch March 10, 2017 16:37
cesarblum pushed a commit that referenced this pull request Mar 10, 2017
cesarblum pushed a commit that referenced this pull request Mar 12, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants