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
98 if ( header.isClient() )
99 {
100
101 hBuffer = new HttpRequest( header, HttpRequest.HTTP_HEAD ).toByteBuffer();
102 semiBlockingWrite( channel, hBuffer, timeout );
103
104 if ( hBuffer.hasRemaining() ) return null;
105 }
106
107
108 HttpHeaderParser parser = readHttpHeader( channel, prereadHead, timeout, header.isClient() );
109
110
111 if ( parser == null )
112 {
113 return null;
114 }
115
116
117 magicBuf = parser.getByteHeader();
118
119 if ( header.isClient() )
120 {
121
122 if ( magicBuf == null )
123 {
124
125 magicBuf = ByteBufferAllocator.allocate( parser.getContentLength() );
126 }
127
128
129 if ( magicBuf.position() < (parser.getContentLength() - 1) )
130 {
131
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
148 else
149 {
150 receiverHeader = new ConnectionHeader( channel, host, header.isClient() );
151
152 receiverHeader.fromString( ((HttpRequestParser) parser).getUri() );
153
154
155 receiverHeader.setCompression( header.hasCompression() && parser.hasCompression() );
156
157
158 receiverHeader.setPersistent( parser.isPersistentConnection() && header.isPersistent() );
159
160
161
162
163
164 if ( receiverHeader.isHandshakeResponseAware() )
165 {
166
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
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
206 ByteBuffer headerBuf = ByteBufferAllocator.allocate( EJConstants.HTTP_BYTEBUFFER_PREALLOC );
207
208
209 if ( preReadData != null )
210 {
211
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
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
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
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
266 if ( isRequest )
267 {
268 httpHeaderParser = new HttpResponseParser( headerBuf );
269 }
270 else
271 {
272 httpHeaderParser = new HttpRequestParser( headerBuf );
273 }
274
275
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
288
289
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
313
314
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
368
369
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 }