一個有意思的Tomcat 異常
在公眾號后臺,經(jīng)常能看到讀者的消息,其中一部分消息是關(guān)于Tomcat使用過程中遇到的問題。但是,由于微信的「克制」,如果消息回復的比較晚,就會遇到「過期」的尷尬,我并不能主動聯(lián)系到提問的人。
后面有需要討論問題的朋友,如果公眾號發(fā)消息未收到回復,可以加我微信。
說回正題,之前有位讀者留言,說了一個 Tomcat 異常的問題。
即 Tomcat 各功能正常,不影響使用,但是偶爾的在日志中會看到類似于這樣的異常信息:
- 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)
為啥報這個呢?明明自己沒做什么操作。
順著異常信息我們往上看,首先這個提示是解析請求頭出現(xiàn)的錯誤。更細節(jié)一些是解析請求頭中第一行,所謂的「Request Line」的時候出了問題。
什么是「Request Line」呢? 就是HTTP 規(guī)范中指定的,以請求方法開頭 再加上請求URI 等。具體看這個規(guī)范說明
這里我們的異常信息提示我們是在解析 Method name的時候出了問題。看規(guī)范里說了「The Request-Line begins with a method token」也就是有固定的東西的,不是啥都能叫一個method name。我們熟悉的GET/POST/PUT/DELETE都是這里允許的。
我們再來看 Tomcat 的源碼,是如何判斷這里的 Requet Line 是不是一個包含一個合法的 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)生這個是由于讀取的byte 不是個 SP 同時下面的 isToken 也不是true導致。
那Token都有誰是怎么定義的?
這里挺有意思的,直接用一個boolean數(shù)組來存,前面我們傳進來的byte,對應的是這個數(shù)組的下標。
- 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ù)組,初始化時有幾個關(guān)聯(lián)的數(shù)組一起,長度為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。
所以問題產(chǎn)生原因定位了,是由于我們的請求頭中傳遞了「非法」方法名稱,導致請求不能正確處理。
我們來看一個正常的請求信息
Request Line 就是上面看到的第一行內(nèi)容。 GET /a/ HTTP/1.1
那有問題的內(nèi)容大概是這個樣子
誰能從上面解析出來請求方法?
這時你可能會問,正常請求都好好的,你這個怎么搞的?
對。正常沒問題,如果我們的Connector 是普通的此時可以響應請求,如果你一直http://localhost:port/a ,可以正常響應,此時后臺收到一個https://localhost:port1/a,你要怎么響應?
要知道這兩個編碼大不一樣。所以就出現(xiàn)了本文開頭的問題。
如果不想走尋常路,可以自己寫個Socket ,連到 Tomcat Server上,發(fā)個不合法的請求,大概也是一個樣子。
那出現(xiàn)了這類問題怎么排查呢? 別忘了 Tomcat 提供了一系列有用的 Valve ,其中一個查看請求的叫AccessLogValve(閥門(Valve)常打開,快發(fā)請求過來 | Tomcat的AccessLogValve介紹)
在 Log里可以查看每個到達的請求來源IP,請求協(xié)議,響應狀態(tài),請求方法等。但是如果上面的異常產(chǎn)生時,請求方法這類有問題的內(nèi)容也是拿不到的,此時的response status 是400 。但通過IP我們能看到是誰在一直請求。如果判斷是非法請求后,可以再增加我們的過濾Valve,直接將其設置為Deny就OK了。
【本文為51CTO專欄作者“侯樹成”的原創(chuàng)稿件,轉(zhuǎn)載請通過作者微信公眾號『Tomcat那些事兒』獲取授權(quán)】