一個(gè)有意思的Tomcat 異常
在公眾號(hào)后臺(tái),經(jīng)常能看到讀者的消息,其中一部分消息是關(guān)于Tomcat使用過(guò)程中遇到的問(wèn)題。但是,由于微信的「克制」,如果消息回復(fù)的比較晚,就會(huì)遇到「過(guò)期」的尷尬,我并不能主動(dòng)聯(lián)系到提問(wèn)的人。
后面有需要討論問(wèn)題的朋友,如果公眾號(hào)發(fā)消息未收到回復(fù),可以加我微信。
說(shuō)回正題,之前有位讀者留言,說(shuō)了一個(gè) Tomcat 異常的問(wèn)題。
即 Tomcat 各功能正常,不影響使用,但是偶爾的在日志中會(huì)看到類似于這樣的異常信息:
- INFO [https-apr-8443-exec-5] org.apache.coyote.http11.Http11Processor.service Error parsing HTTP request header
- Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level.
- java.lang.IllegalArgumentException: Invalid character (CR or LF) found in method name
- at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:443)
- at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:982)
為啥報(bào)這個(gè)呢?明明自己沒(méi)做什么操作。
順著異常信息我們往上看,首先這個(gè)提示是解析請(qǐng)求頭出現(xiàn)的錯(cuò)誤。更細(xì)節(jié)一些是解析請(qǐng)求頭中第一行,所謂的「Request Line」的時(shí)候出了問(wèn)題。
什么是「Request Line」呢? 就是HTTP 規(guī)范中指定的,以請(qǐng)求方法開(kāi)頭 再加上請(qǐng)求URI 等。具體看這個(gè)規(guī)范說(shuō)明
這里我們的異常信息提示我們是在解析 Method name的時(shí)候出了問(wèn)題??匆?guī)范里說(shuō)了「The Request-Line begins with a method token」也就是有固定的東西的,不是啥都能叫一個(gè)method name。我們熟悉的GET/POST/PUT/DELETE都是這里允許的。
我們?cè)賮?lái)看 Tomcat 的源碼,是如何判斷這里的 Requet Line 是不是一個(gè)包含一個(gè)合法的 method name。
順著異常的類和方法,輕車熟路,直接就能看到了。
- if (parsingRequestLinePhase == 2) {
- //
- // Reading the method name
- // Method name is a token
- //
- boolean space = false;
- while (!space) {
- // Read new bytes if needed
- if (byteBuffer.position() >= byteBuffer.limit()) {
- if (!fill(false)) // request line parsing
- return false;
- }
- // Spec says method name is a token followed by a single SP but
- // also be tolerant of multiple SP and/or HT.
- int pos = byteBuffer.position();
- byte chr = byteBuffer.get();
- if (chr == Constants.SP || chr == Constants.HT) {
- space = true;
- request.method().setBytes(byteBuffer.array(), parsingRequestLineStart,
- pos - parsingRequestLineStart);
- } else if (!HttpParser.isToken(chr)) {
- byteBuffer.position(byteBuffer.position() - 1);
- throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
- }
- }
- parsingRequestLinePhase = 3;
- }
我們注意紅色的異常就是上面產(chǎn)生的內(nèi)容。產(chǎn)生這個(gè)是由于讀取的byte 不是個(gè) SP 同時(shí)下面的 isToken 也不是true導(dǎo)致。
那Token都有誰(shuí)是怎么定義的?
這里挺有意思的,直接用一個(gè)boolean數(shù)組來(lái)存,前面我們傳進(jìn)來(lái)的byte,對(duì)應(yīng)的是這個(gè)數(shù)組的下標(biāo)。
- public static boolean isToken(int c) {
- // Fast for correct values, slower for incorrect ones
- try {
- return IS_TOKEN[c];
- } catch (ArrayIndexOutOfBoundsException ex) {
- return false;
- }
- }
這里的boolean數(shù)組,初始化時(shí)有幾個(gè)關(guān)聯(lián)的數(shù)組一起,長(zhǎng)度為128。
- private static final boolean[] IS_CONTROL = new boolean[ARRAY_SIZE];
- private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
- private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
- // Control> 0-31, 127
- if (i < 32 || i == 127) {
- IS_CONTROL[i] = true;
- }
- // Separator
- if ( i == '(' || i == ')' || i == '<' || i == '>' || i == '@' ||
- i == ',' || i == ';' || i == ':' || i == '\\' || i == '\"' ||
- i == '/' || i == '[' || i == ']' || i == '?' || i == '=' ||
- i == '{' || i == '}' || i == ' ' || i == '\t') {
- IS_SEPARATOR[i] = true;
- }
- // Token: Anything 0-127 that is not a control and not a separator
- if (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) {
- IS_TOKEN[i] = true;
- }
所以這里token的定義明確了,非控制字符,非分隔符,ascii 碼小于128 的都是 token。
所以問(wèn)題產(chǎn)生原因定位了,是由于我們的請(qǐng)求頭中傳遞了「非法」方法名稱,導(dǎo)致請(qǐng)求不能正確處理。
我們來(lái)看一個(gè)正常的請(qǐng)求信息
Request Line 就是上面看到的第一行內(nèi)容。 GET /a/ HTTP/1.1
那有問(wèn)題的內(nèi)容大概是這個(gè)樣子
誰(shuí)能從上面解析出來(lái)請(qǐng)求方法?
這時(shí)你可能會(huì)問(wèn),正常請(qǐng)求都好好的,你這個(gè)怎么搞的?
對(duì)。正常沒(méi)問(wèn)題,如果我們的Connector 是普通的此時(shí)可以響應(yīng)請(qǐng)求,如果你一直http://localhost:port/a ,可以正常響應(yīng),此時(shí)后臺(tái)收到一個(gè)https://localhost:port1/a,你要怎么響應(yīng)?
要知道這兩個(gè)編碼大不一樣。所以就出現(xiàn)了本文開(kāi)頭的問(wèn)題。
如果不想走尋常路,可以自己寫(xiě)個(gè)Socket ,連到 Tomcat Server上,發(fā)個(gè)不合法的請(qǐng)求,大概也是一個(gè)樣子。
那出現(xiàn)了這類問(wèn)題怎么排查呢? 別忘了 Tomcat 提供了一系列有用的 Valve ,其中一個(gè)查看請(qǐng)求的叫AccessLogValve(閥門(Valve)常打開(kāi),快發(fā)請(qǐng)求過(guò)來(lái) | Tomcat的AccessLogValve介紹)
在 Log里可以查看每個(gè)到達(dá)的請(qǐng)求來(lái)源IP,請(qǐng)求協(xié)議,響應(yīng)狀態(tài),請(qǐng)求方法等。但是如果上面的異常產(chǎn)生時(shí),請(qǐng)求方法這類有問(wèn)題的內(nèi)容也是拿不到的,此時(shí)的response status 是400 。但通過(guò)IP我們能看到是誰(shuí)在一直請(qǐng)求。如果判斷是非法請(qǐng)求后,可以再增加我們的過(guò)濾Valve,直接將其設(shè)置為Deny就OK了。
【本文為51CTO專欄作者“侯樹(shù)成”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)作者微信公眾號(hào)『Tomcat那些事兒』獲取授權(quán)】