View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.util;
20  
21  import java.io.BufferedInputStream;
22  import java.io.BufferedOutputStream;
23  import java.io.ByteArrayInputStream;
24  import java.io.ByteArrayOutputStream;
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.FileOutputStream;
28  import java.io.FilterInputStream;
29  import java.io.IOException;
30  import java.io.InputStream;
31  import java.io.OutputStream;
32  import java.nio.charset.StandardCharsets;
33  import java.nio.file.Files;
34  import java.nio.file.Path;
35  import java.nio.file.StandardCopyOption;
36  import java.util.ArrayList;
37  import java.util.Collection;
38  import java.util.Collections;
39  import java.util.List;
40  import java.util.Locale;
41  
42  import javax.servlet.MultipartConfigElement;
43  import javax.servlet.http.Part;
44  
45  import org.eclipse.jetty.util.log.Log;
46  import org.eclipse.jetty.util.log.Logger;
47  
48  
49  
50  /**
51   * MultiPartInputStream
52   *
53   * Handle a MultiPart Mime input stream, breaking it up on the boundary into files and strings.
54   */
55  public class MultiPartInputStreamParser
56  {
57      private static final Logger LOG = Log.getLogger(MultiPartInputStreamParser.class);
58      public static final MultipartConfigElement  __DEFAULT_MULTIPART_CONFIG = new MultipartConfigElement(System.getProperty("java.io.tmpdir"));
59      protected InputStream _in;
60      protected MultipartConfigElement _config;
61      protected String _contentType;
62      protected MultiMap<Part> _parts;
63      protected Exception _err;
64      protected File _tmpDir;
65      protected File _contextTmpDir;
66      protected boolean _deleteOnExit;
67      protected boolean _writeFilesWithFilenames;
68  
69  
70  
71      public class MultiPart implements Part
72      {
73          protected String _name;
74          protected String _filename;
75          protected File _file;
76          protected OutputStream _out;
77          protected ByteArrayOutputStream2 _bout;
78          protected String _contentType;
79          protected MultiMap<String> _headers;
80          protected long _size = 0;
81          protected boolean _temporary = true;
82  
83          public MultiPart (String name, String filename)
84          throws IOException
85          {
86              _name = name;
87              _filename = filename;
88          }
89  
90          @Override
91          public String toString()
92          {
93              return String.format("Part{n=%s,fn=%s,ct=%s,s=%d,t=%b,f=%s}",_name,_filename,_contentType,_size,_temporary,_file);
94          }
95          protected void setContentType (String contentType)
96          {
97              _contentType = contentType;
98          }
99  
100 
101         protected void open()
102         throws IOException
103         {
104             //We will either be writing to a file, if it has a filename on the content-disposition
105             //and otherwise a byte-array-input-stream, OR if we exceed the getFileSizeThreshold, we
106             //will need to change to write to a file.
107             if (isWriteFilesWithFilenames() && _filename != null && _filename.trim().length() > 0)
108             {
109                 createFile();
110             }
111             else
112             {
113                 //Write to a buffer in memory until we discover we've exceed the
114                 //MultipartConfig fileSizeThreshold
115                 _out = _bout= new ByteArrayOutputStream2();
116             }
117         }
118 
119         protected void close()
120         throws IOException
121         {
122             _out.close();
123         }
124 
125 
126         protected void write (int b)
127         throws IOException
128         {
129             if (MultiPartInputStreamParser.this._config.getMaxFileSize() > 0 && _size + 1 > MultiPartInputStreamParser.this._config.getMaxFileSize())
130                 throw new IllegalStateException ("Multipart Mime part "+_name+" exceeds max filesize");
131 
132             if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + 1 > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file==null)
133                 createFile();
134 
135             _out.write(b);
136             _size ++;
137         }
138 
139         protected void write (byte[] bytes, int offset, int length)
140         throws IOException
141         {
142             if (MultiPartInputStreamParser.this._config.getMaxFileSize() > 0 && _size + length > MultiPartInputStreamParser.this._config.getMaxFileSize())
143                 throw new IllegalStateException ("Multipart Mime part "+_name+" exceeds max filesize");
144 
145             if (MultiPartInputStreamParser.this._config.getFileSizeThreshold() > 0 && _size + length > MultiPartInputStreamParser.this._config.getFileSizeThreshold() && _file==null)
146                 createFile();
147 
148             _out.write(bytes, offset, length);
149             _size += length;
150         }
151 
152         protected void createFile ()
153         throws IOException
154         {
155             _file = File.createTempFile("MultiPart", "", MultiPartInputStreamParser.this._tmpDir);
156 
157             if (_deleteOnExit)
158                 _file.deleteOnExit();
159             FileOutputStream fos = new FileOutputStream(_file);
160             BufferedOutputStream bos = new BufferedOutputStream(fos);
161 
162             if (_size > 0 && _out != null)
163             {
164                 //already written some bytes, so need to copy them into the file
165                 _out.flush();
166                 _bout.writeTo(bos);
167                 _out.close();
168                 _bout = null;
169             }
170             _out = bos;
171         }
172 
173 
174 
175         protected void setHeaders(MultiMap<String> headers)
176         {
177             _headers = headers;
178         }
179 
180         /**
181          * @see javax.servlet.http.Part#getContentType()
182          */
183         public String getContentType()
184         {
185             return _contentType;
186         }
187 
188         /**
189          * @see javax.servlet.http.Part#getHeader(java.lang.String)
190          */
191         public String getHeader(String name)
192         {
193             if (name == null)
194                 return null;
195             return _headers.getValue(name.toLowerCase(Locale.ENGLISH), 0);
196         }
197 
198         /**
199          * @see javax.servlet.http.Part#getHeaderNames()
200          */
201         public Collection<String> getHeaderNames()
202         {
203             return _headers.keySet();
204         }
205 
206         /**
207          * @see javax.servlet.http.Part#getHeaders(java.lang.String)
208          */
209         public Collection<String> getHeaders(String name)
210         {
211            return _headers.getValues(name);
212         }
213 
214         /**
215          * @see javax.servlet.http.Part#getInputStream()
216          */
217         public InputStream getInputStream() throws IOException
218         {
219            if (_file != null)
220            {
221                //written to a file, whether temporary or not
222                return new BufferedInputStream (new FileInputStream(_file));
223            }
224            else
225            {
226                //part content is in memory
227                return new ByteArrayInputStream(_bout.getBuf(),0,_bout.size());
228            }
229         }
230 
231 
232         /**
233          * @see javax.servlet.http.Part#getSubmittedFileName()
234          */
235         @Override
236         public String getSubmittedFileName()
237         {
238             return getContentDispositionFilename();
239         }
240 
241         public byte[] getBytes()
242         {
243             if (_bout!=null)
244                 return _bout.toByteArray();
245             return null;
246         }
247 
248         /**
249          * @see javax.servlet.http.Part#getName()
250          */
251         public String getName()
252         {
253            return _name;
254         }
255 
256         /**
257          * @see javax.servlet.http.Part#getSize()
258          */
259         public long getSize()
260         {
261             return _size;
262         }
263 
264         /**
265          * @see javax.servlet.http.Part#write(java.lang.String)
266          */
267         public void write(String fileName) throws IOException
268         {
269             if (_file == null)
270             {
271                 _temporary = false;
272 
273                 //part data is only in the ByteArrayOutputStream and never been written to disk
274                 _file = new File (_tmpDir, fileName);
275 
276                 BufferedOutputStream bos = null;
277                 try
278                 {
279                     bos = new BufferedOutputStream(new FileOutputStream(_file));
280                     _bout.writeTo(bos);
281                     bos.flush();
282                 }
283                 finally
284                 {
285                     if (bos != null)
286                         bos.close();
287                     _bout = null;
288                 }
289             }
290             else
291             {
292                 //the part data is already written to a temporary file, just rename it
293                 _temporary = false;
294 
295                 Path src = _file.toPath();
296                 Path target = src.resolveSibling(fileName);
297                 Files.move(src, target, StandardCopyOption.REPLACE_EXISTING);
298                 _file = target.toFile();
299             }
300         }
301 
302         /**
303          * Remove the file, whether or not Part.write() was called on it
304          * (ie no longer temporary)
305          * @see javax.servlet.http.Part#delete()
306          */
307         public void delete() throws IOException
308         {
309             if (_file != null && _file.exists())
310                 _file.delete();
311         }
312 
313         /**
314          * Only remove tmp files.
315          *
316          * @throws IOException if unable to delete the file
317          */
318         public void cleanUp() throws IOException
319         {
320             if (_temporary && _file != null && _file.exists())
321                 _file.delete();
322         }
323 
324 
325         /**
326          * Get the file
327          * @return the file, if any, the data has been written to.
328          */
329         public File getFile ()
330         {
331             return _file;
332         }
333 
334 
335         /**
336          * Get the filename from the content-disposition.
337          * @return null or the filename
338          */
339         public String getContentDispositionFilename ()
340         {
341             return _filename;
342         }
343     }
344 
345 
346 
347 
348     /**
349      * @param in Request input stream
350      * @param contentType Content-Type header
351      * @param config MultipartConfigElement
352      * @param contextTmpDir javax.servlet.context.tempdir
353      */
354     public MultiPartInputStreamParser (InputStream in, String contentType, MultipartConfigElement config, File contextTmpDir)
355     {
356         _in = new ReadLineInputStream(in);
357        _contentType = contentType;
358        _config = config;
359        _contextTmpDir = contextTmpDir;
360        if (_contextTmpDir == null)
361            _contextTmpDir = new File (System.getProperty("java.io.tmpdir"));
362 
363        if (_config == null)
364            _config = new MultipartConfigElement(_contextTmpDir.getAbsolutePath());
365     }
366 
367     /**
368      * Get the already parsed parts.
369      * @return the parts that were parsed
370      */
371     public Collection<Part> getParsedParts()
372     {
373         if (_parts == null)
374             return Collections.emptyList();
375 
376         Collection<List<Part>> values = _parts.values();
377         List<Part> parts = new ArrayList<>();
378         for (List<Part> o: values)
379         {
380             List<Part> asList = LazyList.getList(o, false);
381             parts.addAll(asList);
382         }
383         return parts;
384     }
385 
386     /**
387      * Delete any tmp storage for parts, and clear out the parts list.
388      *
389      * @throws MultiException if unable to delete the parts
390      */
391     public void deleteParts ()
392     throws MultiException
393     {
394         Collection<Part> parts = getParsedParts();
395         MultiException err = new MultiException();
396         for (Part p:parts)
397         {
398             try
399             {
400                 ((MultiPartInputStreamParser.MultiPart)p).cleanUp();
401             }
402             catch(Exception e)
403             {
404                 err.add(e);
405             }
406         }
407         _parts.clear();
408 
409         err.ifExceptionThrowMulti();
410     }
411 
412 
413     /**
414      * Parse, if necessary, the multipart data and return the list of Parts.
415      *
416      * @return the parts
417      * @throws IOException if unable to get the parts
418      */
419     public Collection<Part> getParts()
420     throws IOException
421     {
422         parse();
423         throwIfError();
424 
425         
426         Collection<List<Part>> values = _parts.values();
427         List<Part> parts = new ArrayList<>();
428         for (List<Part> o: values)
429         {
430             List<Part> asList = LazyList.getList(o, false);
431             parts.addAll(asList);
432         }
433         return parts;
434     }
435 
436 
437     /**
438      * Get the named Part.
439      *
440      * @param name the part name
441      * @return the parts
442      * @throws IOException if unable to get the part
443      */
444     public Part getPart(String name)
445     throws IOException
446     {
447         parse();
448         throwIfError();   
449         return _parts.getValue(name, 0);
450     }
451 
452     /**
453      * Throws an exception if one has been latched.
454      * 
455      * @throws IOException the exception (if present)
456      */
457     protected void throwIfError ()
458     throws IOException
459     {
460         if (_err != null)
461         {
462             if (_err instanceof IOException)
463                 throw (IOException)_err;
464             if (_err instanceof IllegalStateException)
465                 throw (IllegalStateException)_err;
466             throw new IllegalStateException(_err);
467         }
468     }
469 
470     /**
471      * Parse, if necessary, the multipart stream.
472      *
473      */
474     protected void parse ()
475     {
476         //have we already parsed the input?
477         if (_parts != null || _err != null)
478             return;
479 
480         //initialize
481         long total = 0; //keep running total of size of bytes read from input and throw an exception if exceeds MultipartConfigElement._maxRequestSize
482         _parts = new MultiMap<>();
483 
484         //if its not a multipart request, don't parse it
485         if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
486             return;
487 
488         try
489         {
490             //sort out the location to which to write the files
491 
492             if (_config.getLocation() == null)
493                 _tmpDir = _contextTmpDir;
494             else if ("".equals(_config.getLocation()))
495                 _tmpDir = _contextTmpDir;
496             else
497             {
498                 File f = new File (_config.getLocation());
499                 if (f.isAbsolute())
500                     _tmpDir = f;
501                 else
502                     _tmpDir = new File (_contextTmpDir, _config.getLocation());
503             }
504 
505             if (!_tmpDir.exists())
506                 _tmpDir.mkdirs();
507 
508             String contentTypeBoundary = "";
509             int bstart = _contentType.indexOf("boundary=");
510             if (bstart >= 0)
511             {
512                 int bend = _contentType.indexOf(";", bstart);
513                 bend = (bend < 0? _contentType.length(): bend);
514                 contentTypeBoundary = QuotedStringTokenizer.unquote(value(_contentType.substring(bstart,bend)).trim());
515             }
516 
517             String boundary="--"+contentTypeBoundary;
518             String lastBoundary=boundary+"--";
519             byte[] byteBoundary=lastBoundary.getBytes(StandardCharsets.ISO_8859_1);
520 
521             // Get first boundary
522             String line = null;
523             try
524             {
525                 line=((ReadLineInputStream)_in).readLine();
526             }
527             catch (IOException e)
528             {
529                 LOG.warn("Badly formatted multipart request");
530                 throw e;
531             }
532 
533             if (line == null)
534                 throw new IOException("Missing content for multipart request");
535 
536             boolean badFormatLogged = false;
537             line=line.trim();
538             while (line != null && !line.equals(boundary) && !line.equals(lastBoundary))
539             {
540                 if (!badFormatLogged)
541                 {
542                     LOG.warn("Badly formatted multipart request");
543                     badFormatLogged = true;
544                 }
545                 line=((ReadLineInputStream)_in).readLine();
546                 line=(line==null?line:line.trim());
547             }
548 
549             if (line == null)
550                 throw new IOException("Missing initial multi part boundary");
551 
552             // Empty multipart.
553             if (line.equals(lastBoundary))
554                 return;
555 
556             // Read each part
557             boolean lastPart=false;
558 
559             outer:while(!lastPart)
560             {
561                 String contentDisposition=null;
562                 String contentType=null;
563                 String contentTransferEncoding=null;
564 
565                 MultiMap<String> headers = new MultiMap<>();
566                 while(true)
567                 {
568                     line=((ReadLineInputStream)_in).readLine();
569 
570                     //No more input
571                     if(line==null)
572                         break outer;
573 
574                     //end of headers:
575                     if("".equals(line))
576                         break;
577 
578                     total += line.length();
579                     if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
580                         throw new IllegalStateException ("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
581 
582                     //get content-disposition and content-type
583                     int c=line.indexOf(':',0);
584                     if(c>0)
585                     {
586                         String key=line.substring(0,c).trim().toLowerCase(Locale.ENGLISH);
587                         String value=line.substring(c+1,line.length()).trim();
588                         headers.put(key, value);
589                         if (key.equalsIgnoreCase("content-disposition"))
590                             contentDisposition=value;
591                         if (key.equalsIgnoreCase("content-type"))
592                             contentType = value;
593                         if(key.equals("content-transfer-encoding"))
594                             contentTransferEncoding=value;
595                     }
596                 }
597 
598                 // Extract content-disposition
599                 boolean form_data=false;
600                 if(contentDisposition==null)
601                 {
602                     throw new IOException("Missing content-disposition");
603                 }
604 
605                 QuotedStringTokenizer tok=new QuotedStringTokenizer(contentDisposition,";", false, true);
606                 String name=null;
607                 String filename=null;
608                 while(tok.hasMoreTokens())
609                 {
610                     String t=tok.nextToken().trim();
611                     String tl=t.toLowerCase(Locale.ENGLISH);
612                     if(t.startsWith("form-data"))
613                         form_data=true;
614                     else if(tl.startsWith("name="))
615                         name=value(t);
616                     else if(tl.startsWith("filename="))
617                         filename=filenameValue(t);
618                 }
619 
620                 // Check disposition
621                 if(!form_data)
622                 {
623                     continue;
624                 }
625                 //It is valid for reset and submit buttons to have an empty name.
626                 //If no name is supplied, the browser skips sending the info for that field.
627                 //However, if you supply the empty string as the name, the browser sends the
628                 //field, with name as the empty string. So, only continue this loop if we
629                 //have not yet seen a name field.
630                 if(name==null)
631                 {
632                     continue;
633                 }
634 
635                 //Have a new Part
636                 MultiPart part = new MultiPart(name, filename);
637                 part.setHeaders(headers);
638                 part.setContentType(contentType);
639                 _parts.add(name, part);
640                 part.open();
641 
642                 InputStream partInput = null;
643                 if ("base64".equalsIgnoreCase(contentTransferEncoding))
644                 {
645                     partInput = new Base64InputStream((ReadLineInputStream)_in);
646                 }
647                 else if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding))
648                 {
649                     partInput = new FilterInputStream(_in)
650                     {
651                         @Override
652                         public int read() throws IOException
653                         {
654                             int c = in.read();
655                             if (c >= 0 && c == '=')
656                             {
657                                 int hi = in.read();
658                                 int lo = in.read();
659                                 if (hi < 0 || lo < 0)
660                                 {
661                                     throw new IOException("Unexpected end to quoted-printable byte");
662                                 }
663                                 char[] chars = new char[] { (char)hi, (char)lo };
664                                 c = Integer.parseInt(new String(chars),16);
665                             }
666                             return c;
667                         }
668                     };
669                 }
670                 else
671                     partInput = _in;
672 
673 
674                 try
675                 {
676                     int state=-2;
677                     int c;
678                     boolean cr=false;
679                     boolean lf=false;
680 
681                     // loop for all lines
682                     while(true)
683                     {
684                         int b=0;
685                         while((c=(state!=-2)?state:partInput.read())!=-1)
686                         {
687                             total ++;
688                             if (_config.getMaxRequestSize() > 0 && total > _config.getMaxRequestSize())
689                                 throw new IllegalStateException("Request exceeds maxRequestSize ("+_config.getMaxRequestSize()+")");
690 
691                             state=-2;
692 
693                             // look for CR and/or LF
694                             if(c==13||c==10)
695                             {
696                                 if(c==13)
697                                 {
698                                     partInput.mark(1);
699                                     int tmp=partInput.read();
700                                     if (tmp!=10)
701                                         partInput.reset();
702                                     else
703                                         state=tmp;
704                                 }
705                                 break;
706                             }
707 
708                             // Look for boundary
709                             if(b>=0&&b<byteBoundary.length&&c==byteBoundary[b])
710                             {
711                                 b++;
712                             }
713                             else
714                             {
715                                 // Got a character not part of the boundary, so we don't have the boundary marker.
716                                 // Write out as many chars as we matched, then the char we're looking at.
717                                 if(cr)
718                                     part.write(13);
719 
720                                 if(lf)
721                                     part.write(10);
722 
723                                 cr=lf=false;
724                                 if(b>0)
725                                     part.write(byteBoundary,0,b);
726 
727                                 b=-1;
728                                 part.write(c);
729                             }
730                         }
731 
732                         // Check for incomplete boundary match, writing out the chars we matched along the way
733                         if((b>0&&b<byteBoundary.length-2)||(b==byteBoundary.length-1))
734                         {
735                             if(cr)
736                                 part.write(13);
737 
738                             if(lf)
739                                 part.write(10);
740 
741                             cr=lf=false;
742                             part.write(byteBoundary,0,b);
743                             b=-1;
744                         }
745 
746                         // Boundary match. If we've run out of input or we matched the entire final boundary marker, then this is the last part.
747                         if(b>0||c==-1)
748                         {
749 
750                             if(b==byteBoundary.length)
751                                 lastPart=true;
752                             if(state==10)
753                                 state=-2;
754                             break;
755                         }
756 
757                         // handle CR LF
758                         if(cr)
759                             part.write(13);
760 
761                         if(lf)
762                             part.write(10);
763 
764                         cr=(c==13);
765                         lf=(c==10||state==10);
766                         if(state==10)
767                             state=-2;
768                     }
769                 }
770                 finally
771                 {
772                     part.close();
773                 }
774             }
775             if (lastPart)
776             {
777                 while(line!=null)
778                     line=((ReadLineInputStream)_in).readLine();
779             }
780             else
781                 throw new IOException("Incomplete parts");
782         }
783         catch (Exception e)
784         {
785             _err = e;
786         }
787     }
788 
789     public void setDeleteOnExit(boolean deleteOnExit)
790     {
791         _deleteOnExit = deleteOnExit;
792     }
793 
794     public void setWriteFilesWithFilenames (boolean writeFilesWithFilenames)
795     {
796         _writeFilesWithFilenames = writeFilesWithFilenames;
797     }
798     
799     public boolean isWriteFilesWithFilenames ()
800     {
801         return _writeFilesWithFilenames;
802     }
803 
804     public boolean isDeleteOnExit()
805     {
806         return _deleteOnExit;
807     }
808 
809 
810     /* ------------------------------------------------------------ */
811     private String value(String nameEqualsValue)
812     {
813         int idx = nameEqualsValue.indexOf('=');
814         String value = nameEqualsValue.substring(idx+1).trim();
815         return QuotedStringTokenizer.unquoteOnly(value);
816     }
817 
818 
819     /* ------------------------------------------------------------ */
820     private String filenameValue(String nameEqualsValue)
821     {
822         int idx = nameEqualsValue.indexOf('=');
823         String value = nameEqualsValue.substring(idx+1).trim();
824 
825         if (value.matches(".??[a-z,A-Z]\\:\\\\[^\\\\].*"))
826         {
827             //incorrectly escaped IE filenames that have the whole path
828             //we just strip any leading & trailing quotes and leave it as is
829             char first=value.charAt(0);
830             if (first=='"' || first=='\'')
831                 value=value.substring(1);
832             char last=value.charAt(value.length()-1);
833             if (last=='"' || last=='\'')
834                 value = value.substring(0,value.length()-1);
835 
836             return value;
837         }
838         else
839             //unquote the string, but allow any backslashes that don't
840             //form a valid escape sequence to remain as many browsers
841             //even on *nix systems will not escape a filename containing
842             //backslashes
843             return QuotedStringTokenizer.unquoteOnly(value, true);
844     }
845 
846 
847 
848     private static class Base64InputStream extends InputStream
849     {
850         ReadLineInputStream _in;
851         String _line;
852         byte[] _buffer;
853         int _pos;
854 
855 
856         public Base64InputStream(ReadLineInputStream rlis)
857         {
858             _in = rlis;
859         }
860 
861         @Override
862         public int read() throws IOException
863         {
864             if (_buffer==null || _pos>= _buffer.length)
865             {
866                 //Any CR and LF will be consumed by the readLine() call.
867                 //We need to put them back into the bytes returned from this
868                 //method because the parsing of the multipart content uses them
869                 //as markers to determine when we've reached the end of a part.
870                 _line = _in.readLine();
871                 if (_line==null)
872                     return -1;  //nothing left
873                 if (_line.startsWith("--"))
874                     _buffer=(_line+"\r\n").getBytes(); //boundary marking end of part
875                 else if (_line.length()==0)
876                     _buffer="\r\n".getBytes(); //blank line
877                 else
878                 {
879                     ByteArrayOutputStream baos = new ByteArrayOutputStream((4*_line.length()/3)+2);
880                     B64Code.decode(_line, baos);
881                     baos.write(13);
882                     baos.write(10);
883                     _buffer = baos.toByteArray();
884                 }
885 
886                 _pos=0;
887             }
888 
889             return _buffer[_pos++];
890         }
891     }
892 }