Validate the extra headers and log but otherwise ignore invalid headers. An empty http.extraHeader starts the list afresh. The http.userAgent is restricted to printable 7-bit ASCII, other characters are replaced by '.'. Moves a support method from the ssh.apache bundle to HttpSupport in the main JGit bundle. Bug:541500 Change-Id: Id2d8df12914e2cdbd936ff00dc824d8f871bd580 Signed-off-by: James Wynn <james@jameswynn.com> Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>tags/v5.10.0.202011041322-m2
import java.util.Iterator; | import java.util.Iterator; | ||||
import java.util.List; | import java.util.List; | ||||
import org.eclipse.jgit.util.HttpSupport; | |||||
/** | /** | ||||
* A basic parser for HTTP response headers. Handles status lines and | * A basic parser for HTTP response headers. Handles status lines and | ||||
* authentication headers (WWW-Authenticate, Proxy-Authenticate). | * authentication headers (WWW-Authenticate, Proxy-Authenticate). | ||||
int length = header.length(); | int length = header.length(); | ||||
for (int i = 0; i < length;) { | for (int i = 0; i < length;) { | ||||
int start = skipWhiteSpace(header, i); | int start = skipWhiteSpace(header, i); | ||||
int end = scanToken(header, start); | |||||
int end = HttpSupport.scanToken(header, start); | |||||
if (end <= start) { | if (end <= start) { | ||||
break; | break; | ||||
} | } | ||||
// optional legacy whitespace around the equals sign), where the | // optional legacy whitespace around the equals sign), where the | ||||
// value can be either a token or a quoted string. | // value can be either a token or a quoted string. | ||||
start = skipWhiteSpace(header, start); | start = skipWhiteSpace(header, start); | ||||
int end = scanToken(header, start); | |||||
int end = HttpSupport.scanToken(header, start); | |||||
if (end == start) { | if (end == start) { | ||||
// Nothing found. Either at end or on a comma. | // Nothing found. Either at end or on a comma. | ||||
if (start < header.length() && header.charAt(start) == ',') { | if (start < header.length() && header.charAt(start) == ',') { | ||||
challenge.addArgument(header.substring(start, end), value); | challenge.addArgument(header.substring(start, end), value); | ||||
start = nextEnd[0]; | start = nextEnd[0]; | ||||
} else { | } else { | ||||
int nextEnd = scanToken(header, nextStart); | |||||
int nextEnd = HttpSupport.scanToken(header, nextStart); | |||||
challenge.addArgument(header.substring(start, end), | challenge.addArgument(header.substring(start, end), | ||||
header.substring(nextStart, nextEnd)); | header.substring(nextStart, nextEnd)); | ||||
start = nextEnd; | start = nextEnd; | ||||
return i; | return i; | ||||
} | } | ||||
private static int scanToken(String header, int from) { | |||||
int length = header.length(); | |||||
int i = from; | |||||
while (i < length) { | |||||
char c = header.charAt(i); | |||||
switch (c) { | |||||
case '!': | |||||
case '#': | |||||
case '$': | |||||
case '%': | |||||
case '&': | |||||
case '\'': | |||||
case '*': | |||||
case '+': | |||||
case '-': | |||||
case '.': | |||||
case '^': | |||||
case '_': | |||||
case '`': | |||||
case '|': | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
i++; | |||||
break; | |||||
default: | |||||
if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { | |||||
i++; | |||||
break; | |||||
} | |||||
return i; | |||||
} | |||||
} | |||||
return i; | |||||
} | |||||
private static String scanQuotedString(String header, int from, int[] to) { | private static String scanQuotedString(String header, int from, int[] to) { | ||||
StringBuilder result = new StringBuilder(); | StringBuilder result = new StringBuilder(); | ||||
int length = header.length(); | int length = header.length(); |
private static final String DEFAULT = "[http]\n" + "\tpostBuffer = 1\n" | private static final String DEFAULT = "[http]\n" + "\tpostBuffer = 1\n" | ||||
+ "\tsslVerify= true\n" + "\tfollowRedirects = true\n" | + "\tsslVerify= true\n" + "\tfollowRedirects = true\n" | ||||
+ "\textraHeader = x: y\n" + "\tuserAgent = Test/0.1\n" | |||||
+ "\tmaxRedirects = 5\n\n"; | + "\tmaxRedirects = 5\n\n"; | ||||
private Config config; | private Config config; | ||||
new URIish("http://user@example.com/path")); | new URIish("http://user@example.com/path")); | ||||
assertEquals(1024, http.getPostBuffer()); | assertEquals(1024, http.getPostBuffer()); | ||||
} | } | ||||
@Test | |||||
public void testExtraHeaders() throws Exception { | |||||
config.fromText(DEFAULT + "[http \"http://example.com\"]\n" | |||||
+ "\textraHeader=foo: bar\n"); | |||||
HttpConfig http = new HttpConfig(config, | |||||
new URIish("http://example.com/")); | |||||
assertEquals(1, http.getExtraHeaders().size()); | |||||
assertEquals("foo: bar", http.getExtraHeaders().get(0)); | |||||
} | |||||
@Test | |||||
public void testExtraHeadersMultiple() throws Exception { | |||||
config.fromText(DEFAULT + "[http \"http://example.com\"]\n" | |||||
+ "\textraHeader=foo: bar\n" // | |||||
+ "\textraHeader=bar: foo\n"); | |||||
HttpConfig http = new HttpConfig(config, | |||||
new URIish("http://example.com/")); | |||||
assertEquals(2, http.getExtraHeaders().size()); | |||||
assertEquals("foo: bar", http.getExtraHeaders().get(0)); | |||||
assertEquals("bar: foo", http.getExtraHeaders().get(1)); | |||||
} | |||||
@Test | |||||
public void testExtraHeadersReset() throws Exception { | |||||
config.fromText(DEFAULT + "[http \"http://example.com\"]\n" | |||||
+ "\textraHeader=foo: bar\n" // | |||||
+ "\textraHeader=bar: foo\n" // | |||||
+ "\textraHeader=\n"); | |||||
HttpConfig http = new HttpConfig(config, | |||||
new URIish("http://example.com/")); | |||||
assertTrue(http.getExtraHeaders().isEmpty()); | |||||
} | |||||
@Test | |||||
public void testExtraHeadersResetAndMore() throws Exception { | |||||
config.fromText(DEFAULT + "[http \"http://example.com\"]\n" | |||||
+ "\textraHeader=foo: bar\n" // | |||||
+ "\textraHeader=bar: foo\n" // | |||||
+ "\textraHeader=\n" // | |||||
+ "\textraHeader=baz: something\n"); | |||||
HttpConfig http = new HttpConfig(config, | |||||
new URIish("http://example.com/")); | |||||
assertEquals(1, http.getExtraHeaders().size()); | |||||
assertEquals("baz: something", http.getExtraHeaders().get(0)); | |||||
} | |||||
@Test | |||||
public void testUserAgent() throws Exception { | |||||
config.fromText(DEFAULT + "[http \"http://example.com\"]\n" | |||||
+ "\tuserAgent=DummyAgent/4.0\n"); | |||||
HttpConfig http = new HttpConfig(config, | |||||
new URIish("http://example.com/")); | |||||
assertEquals("DummyAgent/4.0", http.getUserAgent()); | |||||
} | |||||
@Test | |||||
public void testUserAgentNonAscii() throws Exception { | |||||
config.fromText(DEFAULT + "[http \"http://example.com\"]\n" | |||||
+ "\tuserAgent= d ümmy Agent -5.10\n"); | |||||
HttpConfig http = new HttpConfig(config, | |||||
new URIish("http://example.com/")); | |||||
assertEquals("d.mmy.Agent.-5.10", http.getUserAgent()); | |||||
} | |||||
} | } |
import java.util.Arrays; | import java.util.Arrays; | ||||
import java.util.Collections; | import java.util.Collections; | ||||
import java.util.Date; | import java.util.Date; | ||||
import java.util.LinkedHashMap; | |||||
import java.util.LinkedHashSet; | import java.util.LinkedHashSet; | ||||
import java.util.Map; | |||||
import java.util.Set; | import java.util.Set; | ||||
import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile; | import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile; | ||||
cookieFile.exists()); | cookieFile.exists()); | ||||
} | } | ||||
} | } | ||||
private void assertHeaders(String expected, String... headersToAdd) { | |||||
HttpConnection fake = Mockito.mock(HttpConnection.class); | |||||
Map<String, String> headers = new LinkedHashMap<>(); | |||||
Mockito.doAnswer(invocation -> { | |||||
Object[] args = invocation.getArguments(); | |||||
headers.put(args[0].toString(), args[1].toString()); | |||||
return null; | |||||
}).when(fake).setRequestProperty(ArgumentMatchers.anyString(), | |||||
ArgumentMatchers.anyString()); | |||||
TransportHttp.addHeaders(fake, Arrays.asList(headersToAdd)); | |||||
Assert.assertEquals(expected, headers.toString()); | |||||
} | |||||
@Test | |||||
public void testAddHeaders() { | |||||
assertHeaders("{a=b, c=d}", "a: b", "c :d"); | |||||
} | |||||
@Test | |||||
public void testAddHeaderEmptyValue() { | |||||
assertHeaders("{a-x=b, c=, d=e}", "a-x: b", "c:", "d:e"); | |||||
} | |||||
@Test | |||||
public void testSkipHeaderWithEmptyKey() { | |||||
assertHeaders("{a=b, c=d}", "a: b", " : x", "c :d"); | |||||
assertHeaders("{a=b, c=d}", "a: b", ": x", "c :d"); | |||||
} | |||||
@Test | |||||
public void testSkipHeaderWithoutKey() { | |||||
assertHeaders("{a=b, c=d}", "a: b", "x", "c :d"); | |||||
} | |||||
@Test | |||||
public void testSkipHeaderWithInvalidKey() { | |||||
assertHeaders("{a=b, c=d}", "a: b", "q/p: x", "c :d"); | |||||
assertHeaders("{a=b, c=d}", "a: b", "ä: x", "c :d"); | |||||
} | |||||
@Test | |||||
public void testSkipHeaderWithNonAsciiValue() { | |||||
assertHeaders("{a=b, c=d}", "a: b", "q/p: x", "c :d"); | |||||
assertHeaders("{a=b, c=d}", "a: b", "x: ä", "c :d"); | |||||
} | |||||
} | } |
</message_arguments> | </message_arguments> | ||||
</filter> | </filter> | ||||
</resource> | </resource> | ||||
<resource path="src/org/eclipse/jgit/transport/HttpConfig.java" type="org.eclipse.jgit.transport.HttpConfig"> | |||||
<filter id="336658481"> | |||||
<message_arguments> | |||||
<message_argument value="org.eclipse.jgit.transport.HttpConfig"/> | |||||
<message_argument value="EXTRA_HEADER"/> | |||||
</message_arguments> | |||||
</filter> | |||||
<filter id="336658481"> | |||||
<message_arguments> | |||||
<message_argument value="org.eclipse.jgit.transport.HttpConfig"/> | |||||
<message_argument value="USER_AGENT"/> | |||||
</message_arguments> | |||||
</filter> | |||||
</resource> | |||||
</component> | </component> |
invalidGitdirRef = Invalid .git reference in file ''{0}'' | invalidGitdirRef = Invalid .git reference in file ''{0}'' | ||||
invalidGitModules=Invalid .gitmodules file | invalidGitModules=Invalid .gitmodules file | ||||
invalidGitType=invalid git type: {0} | invalidGitType=invalid git type: {0} | ||||
invalidHeaderFormat=Invalid header from git config http.extraHeader ignored: no colon or empty key in header ''{0}'' | |||||
invalidHeaderKey=Invalid header from git config http.extraHeader ignored: key contains illegal characters; see RFC 7230: ''{0}'' | |||||
invalidHeaderValue=Invalid header from git config http.extraHeader ignored: value should be 7bit-ASCII characters only: ''{0}'' | |||||
invalidHexString=Invalid hex string: {0} | invalidHexString=Invalid hex string: {0} | ||||
invalidHomeDirectory=Invalid home directory: {0} | invalidHomeDirectory=Invalid home directory: {0} | ||||
invalidHooksPath=Invalid git config core.hooksPath = {0} | invalidHooksPath=Invalid git config core.hooksPath = {0} |
/***/ public String invalidGitdirRef; | /***/ public String invalidGitdirRef; | ||||
/***/ public String invalidGitModules; | /***/ public String invalidGitModules; | ||||
/***/ public String invalidGitType; | /***/ public String invalidGitType; | ||||
/***/ public String invalidHeaderFormat; | |||||
/***/ public String invalidHeaderKey; | |||||
/***/ public String invalidHeaderValue; | |||||
/***/ public String invalidHexString; | /***/ public String invalidHexString; | ||||
/***/ public String invalidHomeDirectory; | /***/ public String invalidHomeDirectory; | ||||
/***/ public String invalidHooksPath; | /***/ public String invalidHooksPath; |
import java.io.IOException; | import java.io.IOException; | ||||
import java.net.URISyntaxException; | import java.net.URISyntaxException; | ||||
import java.text.MessageFormat; | import java.text.MessageFormat; | ||||
import java.util.Arrays; | |||||
import java.util.Collections; | |||||
import java.util.List; | |||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.function.Supplier; | import java.util.function.Supplier; | ||||
import org.eclipse.jgit.annotations.NonNull; | |||||
import org.eclipse.jgit.errors.ConfigInvalidException; | import org.eclipse.jgit.errors.ConfigInvalidException; | ||||
import org.eclipse.jgit.internal.JGitText; | import org.eclipse.jgit.internal.JGitText; | ||||
import org.eclipse.jgit.lib.Config; | import org.eclipse.jgit.lib.Config; | ||||
/** git config key for the "sslVerify" setting. */ | /** git config key for the "sslVerify" setting. */ | ||||
public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$ | public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$ | ||||
/** | |||||
* git config key for the "userAgent" setting. | |||||
* | |||||
* @since 5.10 | |||||
*/ | |||||
public static final String USER_AGENT = "userAgent"; //$NON-NLS-1$ | |||||
/** | |||||
* git config key for the "extraHeader" setting. | |||||
* | |||||
* @since 5.10 | |||||
*/ | |||||
public static final String EXTRA_HEADER = "extraHeader"; //$NON-NLS-1$ | |||||
/** | /** | ||||
* git config key for the "cookieFile" setting. | * git config key for the "cookieFile" setting. | ||||
* | * | ||||
private int maxRedirects; | private int maxRedirects; | ||||
private String userAgent; | |||||
private List<String> extraHeaders; | |||||
private String cookieFile; | private String cookieFile; | ||||
private boolean saveCookies; | private boolean saveCookies; | ||||
return maxRedirects; | return maxRedirects; | ||||
} | } | ||||
/** | |||||
* Get the "http.userAgent" setting | |||||
* | |||||
* @return the value of the "http.userAgent" setting | |||||
* @since 5.10 | |||||
*/ | |||||
public String getUserAgent() { | |||||
return userAgent; | |||||
} | |||||
/** | |||||
* Get the "http.extraHeader" setting | |||||
* | |||||
* @return the value of the "http.extraHeader" setting | |||||
* @since 5.10 | |||||
*/ | |||||
@NonNull | |||||
public List<String> getExtraHeaders() { | |||||
return extraHeaders == null ? Collections.emptyList() : extraHeaders; | |||||
} | |||||
/** | /** | ||||
* Get the "http.cookieFile" setting | * Get the "http.cookieFile" setting | ||||
* | * | ||||
if (redirectLimit < 0) { | if (redirectLimit < 0) { | ||||
redirectLimit = MAX_REDIRECTS; | redirectLimit = MAX_REDIRECTS; | ||||
} | } | ||||
String agent = config.getString(HTTP, null, USER_AGENT); | |||||
if (agent != null) { | |||||
agent = UserAgent.clean(agent); | |||||
} | |||||
userAgent = agent; | |||||
String[] headers = config.getStringList(HTTP, null, EXTRA_HEADER); | |||||
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpextraHeader | |||||
// "an empty value will reset the extra headers to the empty list." | |||||
int start = findLastEmpty(headers) + 1; | |||||
if (start > 0) { | |||||
headers = Arrays.copyOfRange(headers, start, headers.length); | |||||
} | |||||
extraHeaders = Arrays.asList(headers); | |||||
cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY); | cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY); | ||||
saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false); | saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false); | ||||
cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY, | cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY, | ||||
DEFAULT_COOKIE_FILE_CACHE_LIMIT); | DEFAULT_COOKIE_FILE_CACHE_LIMIT); | ||||
String match = findMatch(config.getSubsections(HTTP), uri); | String match = findMatch(config.getSubsections(HTTP), uri); | ||||
if (match != null) { | if (match != null) { | ||||
// Override with more specific items | // Override with more specific items | ||||
postBufferSize = config.getInt(HTTP, match, POST_BUFFER_KEY, | postBufferSize = config.getInt(HTTP, match, POST_BUFFER_KEY, | ||||
if (newMaxRedirects >= 0) { | if (newMaxRedirects >= 0) { | ||||
redirectLimit = newMaxRedirects; | redirectLimit = newMaxRedirects; | ||||
} | } | ||||
String uriSpecificUserAgent = config.getString(HTTP, match, | |||||
USER_AGENT); | |||||
if (uriSpecificUserAgent != null) { | |||||
userAgent = UserAgent.clean(uriSpecificUserAgent); | |||||
} | |||||
String[] uriSpecificExtraHeaders = config.getStringList(HTTP, match, | |||||
EXTRA_HEADER); | |||||
if (uriSpecificExtraHeaders.length > 0) { | |||||
start = findLastEmpty(uriSpecificExtraHeaders) + 1; | |||||
if (start > 0) { | |||||
uriSpecificExtraHeaders = Arrays.copyOfRange( | |||||
uriSpecificExtraHeaders, start, | |||||
uriSpecificExtraHeaders.length); | |||||
} | |||||
extraHeaders = Arrays.asList(uriSpecificExtraHeaders); | |||||
} | |||||
String urlSpecificCookieFile = config.getString(HTTP, match, | String urlSpecificCookieFile = config.getString(HTTP, match, | ||||
COOKIE_FILE_KEY); | COOKIE_FILE_KEY); | ||||
if (urlSpecificCookieFile != null) { | if (urlSpecificCookieFile != null) { | ||||
maxRedirects = redirectLimit; | maxRedirects = redirectLimit; | ||||
} | } | ||||
private int findLastEmpty(String[] values) { | |||||
for (int i = values.length - 1; i >= 0; i--) { | |||||
if (values[i] == null) { | |||||
return i; | |||||
} | |||||
} | |||||
return -1; | |||||
} | |||||
/** | /** | ||||
* Determines the best match from a set of subsection names (representing | * Determines the best match from a set of subsection names (representing | ||||
* prefix URLs) for the given {@link URIish}. | * prefix URLs) for the given {@link URIish}. |
import java.net.URI; | import java.net.URI; | ||||
import java.net.URISyntaxException; | import java.net.URISyntaxException; | ||||
import java.net.URL; | import java.net.URL; | ||||
import java.nio.charset.StandardCharsets; | |||||
import java.nio.file.InvalidPathException; | import java.nio.file.InvalidPathException; | ||||
import java.nio.file.Path; | import java.nio.file.Path; | ||||
import java.nio.file.Paths; | import java.nio.file.Paths; | ||||
conn.setRequestProperty(HDR_ACCEPT_ENCODING, ENCODING_GZIP); | conn.setRequestProperty(HDR_ACCEPT_ENCODING, ENCODING_GZIP); | ||||
} | } | ||||
conn.setRequestProperty(HDR_PRAGMA, "no-cache"); //$NON-NLS-1$ | conn.setRequestProperty(HDR_PRAGMA, "no-cache"); //$NON-NLS-1$ | ||||
if (UserAgent.get() != null) { | |||||
if (http.getUserAgent() != null) { | |||||
conn.setRequestProperty(HDR_USER_AGENT, http.getUserAgent()); | |||||
} else if (UserAgent.get() != null) { | |||||
conn.setRequestProperty(HDR_USER_AGENT, UserAgent.get()); | conn.setRequestProperty(HDR_USER_AGENT, UserAgent.get()); | ||||
} | } | ||||
int timeOut = getTimeout(); | int timeOut = getTimeout(); | ||||
conn.setConnectTimeout(effTimeOut); | conn.setConnectTimeout(effTimeOut); | ||||
conn.setReadTimeout(effTimeOut); | conn.setReadTimeout(effTimeOut); | ||||
} | } | ||||
addHeaders(conn, http.getExtraHeaders()); | |||||
// set cookie header if necessary | // set cookie header if necessary | ||||
if (!relevantCookies.isEmpty()) { | if (!relevantCookies.isEmpty()) { | ||||
setCookieHeader(conn); | setCookieHeader(conn); | ||||
return conn; | return conn; | ||||
} | } | ||||
/** | |||||
* Adds a list of header strings to the connection. Headers are expected to | |||||
* separate keys from values, i.e. "Key: Value". Headers without colon or | |||||
* key are ignored (and logged), as are headers with keys that are not RFC | |||||
* 7230 tokens or with non-ASCII values. | |||||
* | |||||
* @param conn | |||||
* The target HttpConnection | |||||
* @param headersToAdd | |||||
* A list of header strings | |||||
*/ | |||||
static void addHeaders(HttpConnection conn, List<String> headersToAdd) { | |||||
for (String header : headersToAdd) { | |||||
// Empty values are allowed according to | |||||
// https://tools.ietf.org/html/rfc7230 | |||||
int colon = header.indexOf(':'); | |||||
String key = null; | |||||
if (colon > 0) { | |||||
key = header.substring(0, colon).trim(); | |||||
} | |||||
if (key == null || key.isEmpty()) { | |||||
LOG.warn(MessageFormat.format( | |||||
JGitText.get().invalidHeaderFormat, header)); | |||||
} else if (HttpSupport.scanToken(key, 0) != key.length()) { | |||||
LOG.warn(MessageFormat.format(JGitText.get().invalidHeaderKey, | |||||
header)); | |||||
} else { | |||||
String value = header.substring(colon + 1).trim(); | |||||
if (!StandardCharsets.US_ASCII.newEncoder().canEncode(value)) { | |||||
LOG.warn(MessageFormat | |||||
.format(JGitText.get().invalidHeaderValue, header)); | |||||
} else { | |||||
conn.setRequestProperty(key, value); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
private void setCookieHeader(HttpConnection conn) { | private void setCookieHeader(HttpConnection conn) { | ||||
StringBuilder cookieHeaderValue = new StringBuilder(); | StringBuilder cookieHeaderValue = new StringBuilder(); | ||||
for (HttpCookie cookie : relevantCookies) { | for (HttpCookie cookie : relevantCookies) { |
return "unknown"; //$NON-NLS-1$ | return "unknown"; //$NON-NLS-1$ | ||||
} | } | ||||
private static String clean(String s) { | |||||
static String clean(String s) { | |||||
s = s.trim(); | s = s.trim(); | ||||
StringBuilder b = new StringBuilder(s.length()); | StringBuilder b = new StringBuilder(s.length()); | ||||
for (int i = 0; i < s.length(); i++) { | for (int i = 0; i < s.length(); i++) { |
} | } | ||||
} | } | ||||
/** | |||||
* Scan a RFC 7230 token as it appears in HTTP headers. | |||||
* | |||||
* @param header | |||||
* to scan in | |||||
* @param from | |||||
* index in {@code header} to start scanning at | |||||
* @return the index after the token, that is, on the first non-token | |||||
* character or {@code header.length} | |||||
* @throws IndexOutOfBoundsException | |||||
* if {@code from < 0} or {@code from > header.length()} | |||||
* | |||||
* @see <a href="https://tools.ietf.org/html/rfc7230#appendix-B">RFC 7230, | |||||
* Appendix B: Collected Grammar; "token" production</a> | |||||
* @since 5.10 | |||||
*/ | |||||
public static int scanToken(String header, int from) { | |||||
int length = header.length(); | |||||
int i = from; | |||||
if (i < 0 || i > length) { | |||||
throw new IndexOutOfBoundsException(); | |||||
} | |||||
while (i < length) { | |||||
char c = header.charAt(i); | |||||
switch (c) { | |||||
case '!': | |||||
case '#': | |||||
case '$': | |||||
case '%': | |||||
case '&': | |||||
case '\'': | |||||
case '*': | |||||
case '+': | |||||
case '-': | |||||
case '.': | |||||
case '^': | |||||
case '_': | |||||
case '`': | |||||
case '|': | |||||
case '~': | |||||
case '0': | |||||
case '1': | |||||
case '2': | |||||
case '3': | |||||
case '4': | |||||
case '5': | |||||
case '6': | |||||
case '7': | |||||
case '8': | |||||
case '9': | |||||
i++; | |||||
break; | |||||
default: | |||||
if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { | |||||
i++; | |||||
break; | |||||
} | |||||
return i; | |||||
} | |||||
} | |||||
return i; | |||||
} | |||||
private HttpSupport() { | private HttpSupport() { | ||||
// Utility class only. | // Utility class only. | ||||
} | } |