View Javadoc

1   /**********************************************************************
2    * HttpChannel.java
3    * created on 01.04.2006 by netseeker
4    * $Source$
5    * $Date$
6    * $Revision$
7    *
8    * ====================================================================
9    *
10   *  Copyright 2006 netseeker aka Michael Manske
11   *
12   *  Licensed under the Apache License, Version 2.0 (the "License");
13   *  you may not use this file except in compliance with the License.
14   *  You may obtain a copy of the License at
15   *
16   *      http://www.apache.org/licenses/LICENSE-2.0
17   *
18   *  Unless required by applicable law or agreed to in writing, software
19   *  distributed under the License is distributed on an "AS IS" BASIS,
20   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21   *  See the License for the specific language governing permissions and
22   *  limitations under the License.
23   * ====================================================================
24   *
25   * This file is part of the de.netseeker.ejoe.io framework.
26   * For more information on the author, please see
27   * <http://www.manskes.de/>.
28   *
29   *********************************************************************/
30  
31  package de.netseeker.ejoe.io;
32  
33  import java.io.IOException;
34  import java.io.UnsupportedEncodingException;
35  import java.net.SocketTimeoutException;
36  import java.net.URLDecoder;
37  import java.nio.ByteBuffer;
38  import java.nio.channels.ClosedChannelException;
39  import java.nio.channels.SelectionKey;
40  import java.nio.channels.SocketChannel;
41  import java.text.ParseException;
42  import java.util.logging.Level;
43  import java.util.logging.Logger;
44  
45  import de.netseeker.ejoe.ConnectionHeader;
46  import de.netseeker.ejoe.EJConstants;
47  import de.netseeker.ejoe.cache.ByteBufferAllocator;
48  import de.netseeker.ejoe.http.HttpHeaderParser;
49  import de.netseeker.ejoe.http.HttpRequest;
50  import de.netseeker.ejoe.http.HttpRequestParser;
51  import de.netseeker.ejoe.http.HttpResponse;
52  import de.netseeker.ejoe.http.HttpResponseParser;
53  
54  /***
55   * @author netseeker
56   * @since 0.3.9.1
57   */
58  class HttpChannel extends DataChannel
59  {
60      private static final Logger logger      = Logger.getLogger( HttpChannel.class.getName() );
61  
62      private static HttpChannel  dataChannel = new HttpChannel();
63  
64      /***
65       * Singleton with hidden constructor
66       */
67      private HttpChannel()
68      {
69          super();
70      }
71  
72      /***
73       * @return
74       */
75      public static DataChannel getInstance()
76      {
77          return dataChannel;
78      }
79  
80      /***
81       * @param header
82       * @param channel
83       * @param timeout
84       * @param magicBuf
85       * @return
86       * @throws IOException
87       * @throws ParseException
88       */
89      public ConnectionHeader handshake( final ConnectionHeader header, SocketChannel channel, byte[] prereadHead,
90                                         long timeout ) throws IOException, ParseException
91      {
92          ConnectionHeader receiverHeader;
93          String host = null;
94          ByteBuffer magicBuf = null;
95          ByteBuffer hBuffer = null;
96  
97          // shall we act as clientside and initialize the handshake?
98          if ( header.isClient() )
99          {
100             // create a new HTTP HEAD operation and send our client header and the requested adapter (if any)
101             hBuffer = new HttpRequest( header, HttpRequest.HTTP_HEAD ).toByteBuffer();
102             semiBlockingWrite( channel, hBuffer, timeout );
103             // something strange happened - at least the handshake data must be written in one operation
104             if ( hBuffer.hasRemaining() ) return null;
105         }
106 
107         // read and parse the HTTP header - this can also preread one or more bytes of none-header-data (content)
108         HttpHeaderParser parser = readHttpHeader( channel, prereadHead, timeout, header.isClient() );
109 
110         // little bit paranoia
111         if ( parser == null )
112         {
113             return null;
114         }
115 
116         // get the read ByteBuffer
117         magicBuf = parser.getByteHeader();
118 
119         if ( header.isClient() )
120         {
121             // did the HTTP parser already read the content? if so the ByteBuffer has already the required size.
122             if ( magicBuf == null )
123             {
124                 // allocate an appropiate ByteBuffer
125                 magicBuf = ByteBufferAllocator.allocate( parser.getContentLength() );
126             }
127 
128             // did the HTTP parser already read the complete content (the eight bits of the header byte)?
129             if ( magicBuf.position() < (parser.getContentLength() - 1) )
130             {
131                 // read the data part
132                 semiBlockingRead( channel, magicBuf, timeout );
133                 if ( magicBuf.hasRemaining() ) return null;
134             }
135 
136             magicBuf.flip();
137 
138             if ( logger.isLoggable( Level.FINEST ) )
139             {
140                 logger.log( Level.FINEST, "HTTP-Header read: "
141                         + IOUtil.bBitsToSBits( IOUtil.byteToBBits( magicBuf.get() ) ) );
142                 magicBuf.rewind();
143             }
144 
145             receiverHeader = new ConnectionHeader( channel, host, header.isClient(), magicBuf.get() );
146         }
147         // shall we act as server side and answer to the handshake request?
148         else
149         {
150             receiverHeader = new ConnectionHeader( channel, host, header.isClient() );
151             // the HTTP header must contain the eight header bits in the requested URI
152             receiverHeader.fromString( ((HttpRequestParser) parser).getUri() );
153 
154             // does the http header overwrite the compression setting?
155             receiverHeader.setCompression( header.hasCompression() && parser.hasCompression() );
156 
157             // does the http header overwrite the setting for using a persistent connection?
158             receiverHeader.setPersistent( parser.isPersistentConnection() && header.isPersistent() );
159 
160             // answer to the client request and send our serverside headerbyte
161             // clients as browsers (IE, Firefox, Opera) or similiar doesn't understand our handshake
162             // hence we send our handshake response only when the client tells us that it is able
163             // to parse our response
164             if ( receiverHeader.isHandshakeResponseAware() )
165             {
166                 // create a HTTP response with just our server header byte as content
167                 HttpResponse response = new HttpResponse( header, HttpResponse.HTTP_OK );
168                 response.addData( header.toByte() );
169                 logger.log( Level.FINEST, "Sending server headerbyte: " + header );
170                 hBuffer = response.toByteBuffer();
171                 semiBlockingWrite( channel, hBuffer, timeout );
172                 if ( hBuffer.hasRemaining() ) return null;
173             }
174             // ok, a unknown client which might have already send some content with it's first request
175             else if ( parser.hasPrereadContent() )
176             {
177                 receiverHeader.setWaitingBuffer( parser.getByteHeader() );
178             }
179         }
180 
181         receiverHeader.setHttp( true );
182         receiverHeader.setConnected( true );
183 
184         return receiverHeader;
185     }
186 
187     /***
188      * Reads a HTTP header from a socket channel and validates and parses the header Reading HTTP headers is much more
189      * tricky then reading the usual EJOE headers because in case of HTTP we can't deal with fix-sized headers. We have
190      * to read until the stream ends or we have detected a complete HTTP header. Doing so can result in preread content
191      * which we must be able to handle at later time.
192      * 
193      * @param channel
194      * @param timeout
195      * @param isRequest
196      * @return
197      * @throws IOException
198      * @throws ParseException
199      * @throws ConnectionTimeoutException
200      */
201     private HttpHeaderParser readHttpHeader( SocketChannel channel, byte[] preReadData, long timeout, boolean isRequest )
202             throws IOException, ParseException
203     {
204         HttpHeaderParser httpHeaderParser = null;
205         // prepare a byte buffer which will receive the HTTP header [ + preread content ]
206         ByteBuffer headerBuf = ByteBufferAllocator.allocate( EJConstants.HTTP_BYTEBUFFER_PREALLOC );
207 
208         // do we already have some preread content?
209         if ( preReadData != null )
210         {
211             // if so put it at the beginning of the byte buffer
212             headerBuf.put( preReadData );
213         }
214 
215         int readControl = 0;
216         int limit = -1;
217 
218         long timestamp = System.currentTimeMillis();
219         long timePeriod = -1;
220         do
221         {
222             limit = headerBuf.limit();
223             // increase the byte buffer if it's free memory falls below 20 percent
224             if ( headerBuf.remaining() <= (limit / 5) )
225             {
226                 logger.log( Level.FINEST, "Allocating additional " + (limit / 2) + "b for buffer with " + limit + "b" );
227                 headerBuf = ByteBufferAllocator.reAllocate( headerBuf, limit + (limit / 2) );
228             }
229 
230             readControl = channel.read( headerBuf );
231             timePeriod = System.currentTimeMillis() - timestamp;
232         }
233         while ( readControl > -1 && !HttpHeaderParser.isComplete( headerBuf ) && (timePeriod < timeout) );
234 
235         // timeout occured?
236         if ( timePeriod >= timeout )
237         {
238             throw new SocketTimeoutException();
239         }
240         else if ( readControl == -1 && headerBuf.position() == 0 )
241         {
242             throw new ClosedChannelException();
243         }
244         // stream ended but HTTP header isn't complete?
245         else if ( !HttpHeaderParser.isComplete( headerBuf ) )
246         {
247             throw new ParseException( "Received HTTP Header missing finalizing line terminators (//r//n//r//n)!",
248                                       readControl );
249         }
250 
251         if ( logger.isLoggable( Level.FINEST ) )
252         {
253             logger.log( Level.FINEST, "HTTP Header read: complete=" + HttpHeaderParser.isComplete( headerBuf )
254                     + ", readControl=" + readControl + ", bytes read: " + headerBuf.position() );
255         }
256 
257         headerBuf.flip();
258 
259         if ( logger.isLoggable( Level.FINE ) )
260         {
261             logger.log( Level.FINE, IOUtil.decodeToString( headerBuf ) );
262             headerBuf.position( 0 );
263         }
264 
265         // instantiate an appropiate HTTP header parser depending on whether the data are a HTTP request or a response
266         if ( isRequest )
267         {
268             httpHeaderParser = new HttpResponseParser( headerBuf );
269         }
270         else
271         {
272             httpHeaderParser = new HttpRequestParser( headerBuf );
273         }
274 
275         // validate the data
276         if ( !httpHeaderParser.isValid() )
277         {
278             headerBuf.rewind();
279             throw new ParseException( httpHeaderParser.getClass().getName() + ": Invalid HTTP header detected!!!\n"
280                     + IOUtil.decodeToString( headerBuf ), 0 );
281         }
282 
283         return httpHeaderParser;
284     }
285 
286     /*
287      * (non-Javadoc)
288      * 
289      * @see de.netseeker.ejoe.io.DataChannel#readHeader(de.netseeker.ejoe.ConnectionHeader, long)
290      */
291     public int readHeader( ConnectionHeader header, long timeout ) throws IOException
292     {
293         HttpHeaderParser parser = null;
294         try
295         {
296             parser = readHttpHeader( header.getChannel(), null, timeout, header.isClient() );
297         }
298         catch ( ParseException e )
299         {
300             throw new IOException( e.getMessage() );
301         }
302 
303         if ( parser.hasPrereadContent() )
304         {
305             header.setWaitingBuffer( parser.getByteHeader() );
306         }
307 
308         return parser.getContentLength();
309     }
310 
311     /*
312      * (non-Javadoc)
313      * 
314      * @see de.netseeker.ejoe.io.DataChannel#writeHeader(de.netseeker.ejoe.ConnectionHeader, java.nio.ByteBuffer, long)
315      */
316     public void writeHeader( ConnectionHeader header, ByteBuffer buffer, long timeout ) throws IOException
317     {
318         ByteBuffer headerBuf = null;
319         SocketChannel channel = header.getChannel();
320         boolean noBuffer = (buffer == null);
321         int length = !noBuffer ? buffer.limit() : 0;
322         if ( !noBuffer ) buffer.mark();
323 
324         if ( header.isClient() )
325         {
326             HttpRequest request = new HttpRequest( header, (header.getAttachementInfo() != null) ? header
327                     .getAttachementInfo().toString() : HttpRequest.HTTP_POST );
328             if ( !noBuffer ) request.addData( buffer );
329             headerBuf = request.toByteBuffer();
330             if ( logger.isLoggable( Level.FINEST ) )
331             {
332                 logger.log( Level.FINEST, "Preparing to write client request with " + headerBuf.limit() + " bytes:\n"
333                         + IOUtil.decodeToString( headerBuf ) );
334             }
335         }
336         else
337         {
338             HttpResponse response = new HttpResponse( header, (header.getAttachementInfo() != null) ? header
339                     .getAttachementInfo().toString() : HttpResponse.HTTP_OK );
340             if ( !noBuffer ) response.addData( buffer );
341             headerBuf = response.toByteBuffer();
342             if ( logger.isLoggable( Level.FINEST ) )
343             {
344                 logger.log( Level.FINEST, "Preparing to write server response with " + headerBuf.limit() + " bytes:\n"
345                         + IOUtil.decodeToString( headerBuf ) );
346             }
347         }
348 
349         try
350         {
351             semiBlockingWrite( channel, headerBuf, timeout );
352             IOUtil.setSendBufferSize( channel.socket(), length );
353         }
354         catch ( IncompleteIOException ioe )
355         {
356             logger.log( Level.FINEST, "Incomplete header write detected, skip this request." );
357             throw new IncompleteIOException( null, SelectionKey.OP_WRITE );
358         }
359         finally
360         {
361             ByteBufferAllocator.collect( headerBuf );
362             if ( !noBuffer ) buffer.reset();
363         }
364     }
365 
366     /*
367      * (non-Javadoc)
368      * 
369      * @see de.netseeker.ejoe.io.DataChannel#decode(java.nio.ByteBuffer)
370      */
371     public ByteBuffer decode( ByteBuffer buffer ) throws UnsupportedEncodingException
372     {
373         byte[] ejdata = IOUtil.encodeToBytes( EJConstants.HTTP_PARAM_NAME );
374 
375         if ( buffer.limit() > ejdata.length )
376         {
377             for ( int i = 0; i < ejdata.length; i++ )
378             {
379                 if ( buffer.get( i ) != ejdata[i] )
380                 {
381                     return buffer;
382                 }
383             }
384 
385             buffer.position( ejdata.length + 1 );
386             buffer.compact();
387             buffer.flip();
388             String request = IOUtil.decodeToString( buffer );
389             request = URLDecoder.decode( request, EJConstants.EJOE_DEFAULT_CHARSET );
390             buffer = IOUtil.encodeToByteBuffer( request );
391         }
392 
393         return buffer;
394     }
395 }