001/* 002 * Copyright (c) 2009 The openGion Project. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the specific language 014 * governing permissions and limitations under the License. 015 */ 016package org.opengion.hayabusa.servlet.multipart; 017 018import java.io.IOException; 019import java.util.List; 020import java.util.ArrayList; 021import java.util.Locale ; 022 023import jakarta.servlet.http.HttpServletRequest; 024import jakarta.servlet.ServletInputStream; 025 026import org.opengion.fukurou.util.StringUtil; // 6.9.0.0 (2018/01/31) 027import org.opengion.fukurou.system.Closer ; 028 029import static org.opengion.fukurou.system.HybsConst.CR ; // 6.9.0.0 (2018/01/31) 030import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE; // 6.1.0.0 (2014/12/26) refactoring 031 032/** 033 * ファイルアップロード時のマルチパート処理のパーサーです。 034 * 035 * @og.group その他機能 036 * 037 * @version 4.0 038 * @author Kazuhiko Hasegawa 039 * @since JDK5.0, 040 */ 041public class MultipartParser { 042 private final ServletInputStream in; 043 private final String boundary; 044 private FilePart lastFilePart; 045 private final byte[] buf = new byte[8 * 1024]; 046 private static final String DEFAULT_ENCODING = "MS932"; 047 private String encoding = DEFAULT_ENCODING; 048 049 /** 050 * マルチパート処理のパーサーオブジェクトを構築する、コンストラクター 051 * 052 * @og.rev 5.3.7.0 (2011/07/01) 最大容量オーバー時のエラーメッセージ変更 053 * @og.rev 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限 054 * @og.rev 6.9.0.0 (2018/01/31) multipart 判定方法の変更 055 * @og.rev 7.4.2.0 (2021/04/30) Microsoft Edge は、boundary の取り方が違うので、その対応。 056 * 057 * @param req HttpServletRequestオブジェクト 058 * @param maxSize 最大容量(0,またはマイナスで無制限) 059 * @throws IOException 入出力エラーが発生したとき 060 */ 061 public MultipartParser( final HttpServletRequest req, final int maxSize ) throws IOException { 062// String type = null; 063 final String type1 = req.getHeader("Content-Type"); 064 final String type2 = req.getContentType(); 065 066 final String type = type1 != null && type2 != null && type1.length() < type2.length() 067 ? type2 068 : StringUtil.nval( type1,type2 ); 069 070 // 6.9.0.0 (2018/01/31) multipart 判定方法の変更 071// if( type1 == null && type2 != null ) { 072// type = type2; 073// } 074// else if( type2 == null && type1 != null ) { 075// type = type1; 076// } 077// else if( type1 != null && type2 != null ) { 078// type = (type1.length() > type2.length() ? type1 : type2); 079// } 080 081 // 6.9.0.0 (2018/01/31) multipart 判定方法の変更 082 if( type == null || !type.toLowerCase(Locale.JAPAN).startsWith("multipart/form-data") ) { 083// throw new IOException("Posted content type isn't multipart/form-data"); 084 final String errMsg = "Posted content type isn't multipart/form-data" + CR 085 + "Content-Type=" + type ; 086 throw new IOException( errMsg ); 087 } 088 089 final int length = req.getContentLength(); 090 // 5.5.2.6 (2012/05/25) maxSize で、0,またはマイナスで無制限 091 if( maxSize > 0 && length > maxSize ) { 092 throw new IOException("登録したファイルサイズが上限(" + ( maxSize / 1024 / 1024 ) + "MB)を越えています。" 093 + " 登録ファイル=" + ( length / 1024 / 1024 ) + "MB" ); // 5.3.7.0 (2011/07/01) 094 } 095 096 // 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser 097 // 7.4.2.0 (2021/04/30) Microsoft Edge は、boundary の取り方が違うので、その対応。 098 String bound = extractBoundary(type); 099// final String bound = extractBoundary(type); 100// if( bound == null ) { 101// throw new IOException("Separation boundary was not specified"); 102// } 103 104 this.in = req.getInputStream(); 105 // this.boundary = bound; // 7.4.2.0 (2021/04/30) 106 107 final String line = readLine(); 108 if( line == null ) { 109 throw new IOException("Corrupt form data: premature ending"); 110 } 111 // 7.4.2.0 (2021/04/30) Microsoft Edge は、boundary の取り方が違うので、その対応。 112 else if( bound == null && line.contains( "WebKitFormBoundary" ) ) { 113 bound = line; 114 } 115 116 this.boundary = bound; // 7.4.2.0 (2021/04/30) 117 118 if( !line.startsWith(boundary) ) { 119 throw new IOException("Corrupt form data: no leading boundary: " + 120 line + " != " + boundary); 121 } 122 } 123 124 /** 125 * エンコードを設定します。 126 * 127 * @param encoding エンコード 128 */ 129 public void setEncoding( final String encoding ) { 130 this.encoding = encoding; 131 } 132 133 /** 134 * 次のパートを読み取ります。 135 * 136 * @og.rev 3.5.6.2 (2004/07/05) 文字列の連結にStringBuilderを使用します。 137 * 138 * @return 次のパート 139 * @throws IOException 入出力エラーが発生したとき 140 */ 141 public Part readNextPart() throws IOException { 142 if( lastFilePart != null ) { 143 Closer.ioClose( lastFilePart.getInputStream() ); // 4.0.0 (2006/01/31) close 処理時の IOException を無視 144 lastFilePart = null; 145 } 146 147 String line = readLine(); 148 if( line == null || line.isEmpty() ) { return null; } 149 150 final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE ); // 6.1.0.0 (2014/12/26) refactoring 151 final List<String> headers = new ArrayList<>(); 152 while( line != null && line.length() > 0 ) { 153 String nextLine = null; 154 boolean getNextLine = true; 155 buf.setLength(0); // 6.1.0.0 (2014/12/26) refactoring 156 buf.append( line ); 157 while( getNextLine ) { 158 nextLine = readLine(); 159 160 // 6.1.0.0 (2014/12/26) refactoring 161 if( nextLine != null && nextLine.length() > 0 && ( nextLine.charAt(0) == ' ' || nextLine.charAt(0) == '\t' ) ) { 162 buf.append( nextLine ); 163 } 164 else { 165 getNextLine = false; 166 } 167 } 168 169 headers.add(buf.toString()); 170 line = nextLine; 171 } 172 173 if( line == null ) { 174 return null; 175 } 176 177 String name = null; 178 String filename = null; 179 String origname = null; 180 String contentType = "text/plain"; 181 182 for( final String headerline : headers ) { 183 if( headerline.toLowerCase(Locale.JAPAN).startsWith("content-disposition:") ) { 184 final String[] dispInfo = extractDispositionInfo(headerline); 185 186 name = dispInfo[1]; 187 filename = dispInfo[2]; 188 origname = dispInfo[3]; 189 } 190 else if( headerline.toLowerCase(Locale.JAPAN).startsWith("content-type:") ) { 191 final String type = extractContentType(headerline); 192 if( type != null ) { 193 contentType = type; 194 } 195 } 196 } 197 198 if( filename == null ) { 199 return new ParamPart(name, in, boundary, encoding); 200 } 201 else { 202 if( "".equals( filename ) ) { 203 filename = null; 204 } 205 lastFilePart = new FilePart(name,in,boundary,contentType,filename,origname); 206 return lastFilePart; 207 } 208 } 209 210 /** 211 * ローカル変数「境界」アクセス可能なフィールドを返します。 212 * 213 * @og.rev 6.9.0.0 (2018/01/31) HttpConnect 使用時のファイル名の文字化け対策 214 * 215 * @param line 1行 216 * 217 * @return 境界文字列 218 * @see org.opengion.hayabusa.servlet.multipart.MultipartParser 219 */ 220 private String extractBoundary( final String line ) { 221 // 4.0.0 (2005/01/31) The local variable "boundary" shadows an accessible field with the same name and compatible type in class org.opengion.hayabusa.servlet.multipart.MultipartParser 222 int index = line.lastIndexOf("boundary="); 223 if( index == -1 ) { 224 return null; 225 } 226 String bound = line.substring(index + 9); 227 if( bound.charAt(0) == '"' ) { 228 index = bound.lastIndexOf('"'); 229 bound = bound.substring(1, index); 230 } 231 232 // 6.9.0.0 (2018/01/31) HttpConnect 使用時のファイル名の文字化け対策 233 // HttpConnect で、MultipartEntityBuilder でファイルをアップロードするとき、 234 // 日本語ファイル名が文字化けするため、setCharset で、UTF-8 指定しますが、 235 // "; charset=UTF-8" という文字列がMIME変換文字にセットされる(バグ?) 236 // のような動きをしており、強制的に削除しています。 237 final int ad = bound.indexOf( "; charset=UTF-8" ); 238 if( ad >= 0 ) { bound=bound.substring( 0,ad ); } 239 240 bound = "--" + bound; 241 242 return bound; 243 } 244 245 /** 246 * コンテンツの情報を返します。 247 * 248 * @param origline 元の行 249 * 250 * @return コンテンツの情報配列 251 * @throws IOException 入出力エラーが発生したとき 252 */ 253 private String[] extractDispositionInfo( final String origline ) throws IOException { 254 255 final String line = origline.toLowerCase(Locale.JAPAN); 256 257 int start = line.indexOf( "content-disposition: " ); 258 int end = line.indexOf(';'); 259 if( start == -1 || end == -1 ) { 260 throw new IOException( "Content disposition corrupt: " + origline ); 261 } 262 final String disposition = line.substring( start + 21, end ); 263 if( !"form-data".equals(disposition) ) { 264 throw new IOException("Invalid content disposition: " + disposition); 265 } 266 267 start = line.indexOf("name=\"", end); // start at last semicolon 268 end = line.indexOf( '"', start + 7); // 6.0.2.5 (2014/10/31) refactoring skip name=\" 269 if( start == -1 || end == -1 ) { 270 throw new IOException("Content disposition corrupt: " + origline); 271 } 272 final String name = origline.substring(start + 6, end); 273 274 String filename = null; 275 String origname = null; 276 start = line.indexOf("filename=\"", end + 2); // start after name 277 end = line.indexOf( '"', start + 10); // skip filename=\" 278 if( start != -1 && end != -1 ) { // note the != 279 filename = origline.substring(start + 10, end); 280 origname = filename; 281 final int slash = 282 Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); 283 if( slash > -1 ) { 284 filename = filename.substring(slash + 1); // past last slash 285 } 286 } 287 288 String[] retval = new String[4]; // 6.1.0.0 (2014/12/26) refactoring 289 retval[0] = disposition; 290 retval[1] = name; 291 retval[2] = filename; 292 retval[3] = origname; 293 return retval; 294 } 295 296 /** 297 * コンテンツタイプの情報を返します。 298 * 299 * @param origline 元の行 300 * 301 * @return コンテンツタイプの情報 302 * @throws IOException 入出力エラーが発生したとき 303 */ 304 private String extractContentType( final String origline ) throws IOException { 305 String contentType = null; 306 307 final String line = origline.toLowerCase(Locale.JAPAN); 308 309 if( line.startsWith("content-type") ) { 310 final int start = line.indexOf(' '); 311 if( start == -1 ) { 312 throw new IOException("Content type corrupt: " + origline); 313 } 314 contentType = line.substring(start + 1); 315 } 316 else if( line.length() > 0 ) { // no content type, so should be empty 317 throw new IOException("Malformed line after disposition: " + origline); 318 } 319 320 return contentType; 321 } 322 323 /** 324 * 行を読み取ります。 325 * 326 * @return 読み取られた1行分 327 * @throws IOException 入出力エラーが発生したとき 328 */ 329 private String readLine() throws IOException { 330 final StringBuilder sbuf = new StringBuilder( BUFFER_MIDDLE ); 331 int result; 332 333 do { 334 result = in.readLine(buf, 0, buf.length); 335 if( result != -1 ) { 336 sbuf.append(new String(buf, 0, result, encoding)); 337 } 338 } while( result == buf.length ); 339 340 if( sbuf.length() == 0 ) { 341 return null; 342 } 343 344 // 4.0.0 (2005/01/31) The method StringBuilder.setLength() should be avoided in favor of creating a new StringBuilder. 345 String rtn = sbuf.toString(); 346 final int len = sbuf.length(); 347 if( len >= 2 && sbuf.charAt(len - 2) == '\r' ) { 348 rtn = rtn.substring(0,len - 2); 349 } 350 else if( len >= 1 && sbuf.charAt(len - 1) == '\n' ) { 351 rtn = rtn.substring(0,len - 1); 352 } 353 354 return rtn ; 355 } 356}