1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
19 package org.apache.catalina.connector;
20
21 import java.io.IOException;
22 import java.io.UnsupportedEncodingException;
23
24 import org.apache.catalina.CometEvent;
25 import org.apache.catalina.Context;
26 import org.apache.catalina.Globals;
27 import org.apache.catalina.Wrapper;
28 import org.apache.catalina.util.StringManager;
29 import org.apache.catalina.util.ServerInfo;
30 import org.apache.catalina.util.URLEncoder;
31 import org.apache.coyote.ActionCode;
32 import org.apache.coyote.Adapter;
33 import org.apache.juli.logging.Log;
34 import org.apache.juli.logging.LogFactory;
35 import org.apache.tomcat.util.buf.B2CConverter;
36 import org.apache.tomcat.util.buf.ByteChunk;
37 import org.apache.tomcat.util.buf.CharChunk;
38 import org.apache.tomcat.util.buf.MessageBytes;
39 import org.apache.tomcat.util.http.Cookies;
40 import org.apache.tomcat.util.http.ServerCookie;
41 import org.apache.tomcat.util.net.SocketStatus;
42
43
44 /**
45 * Implementation of a request processor which delegates the processing to a
46 * Coyote processor.
47 *
48 * @author Craig R. McClanahan
49 * @author Remy Maucherat
50 * @version $Revision: 896389 $ $Date: 2010-01-06 12:09:55 +0100 (Wed, 06 Jan 2010) $
51 */
52
53 public class CoyoteAdapter implements Adapter {
54
55 private static Log log = LogFactory.getLog(CoyoteAdapter.class);
56
57 // -------------------------------------------------------------- Constants
58
59 private static final String POWERED_BY = "Servlet/2.5 JSP/2.1 " +
60 "(" + ServerInfo.getServerInfo() + " Java/" +
61 System.getProperty("java.vm.vendor") + "/" +
62 System.getProperty("java.runtime.version") + ")";
63
64 public static final int ADAPTER_NOTES = 1;
65
66
67 protected static final boolean ALLOW_BACKSLASH =
68 Boolean.valueOf(System.getProperty("org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH", "false")).booleanValue();
69
70
71 // ----------------------------------------------------------- Constructors
72
73
74 /**
75 * Construct a new CoyoteProcessor associated with the specified connector.
76 *
77 * @param connector CoyoteConnector that owns this processor
78 */
79 public CoyoteAdapter(Connector connector) {
80
81 super();
82 this.connector = connector;
83
84 }
85
86
87 // ----------------------------------------------------- Instance Variables
88
89
90 /**
91 * The CoyoteConnector with which this processor is associated.
92 */
93 private Connector connector = null;
94
95
96 /**
97 * The match string for identifying a session ID parameter.
98 */
99 private static final String match =
100 ";" + Globals.SESSION_PARAMETER_NAME + "=";
101
102
103 /**
104 * The string manager for this package.
105 */
106 protected StringManager sm =
107 StringManager.getManager(Constants.Package);
108
109
110 /**
111 * Encoder for the Location URL in HTTP redirects.
112 */
113 protected static URLEncoder urlEncoder;
114
115
116 // ----------------------------------------------------- Static Initializer
117
118
119 /**
120 * The safe character set.
121 */
122 static {
123 urlEncoder = new URLEncoder();
124 urlEncoder.addSafeCharacter('-');
125 urlEncoder.addSafeCharacter('_');
126 urlEncoder.addSafeCharacter('.');
127 urlEncoder.addSafeCharacter('*');
128 urlEncoder.addSafeCharacter('/');
129 }
130
131
132 // -------------------------------------------------------- Adapter Methods
133
134
135 /**
136 * Event method.
137 *
138 * @return false to indicate an error, expected or not
139 */
140 public boolean event(org.apache.coyote.Request req,
141 org.apache.coyote.Response res, SocketStatus status) {
142
143 Request request = (Request) req.getNote(ADAPTER_NOTES);
144 Response response = (Response) res.getNote(ADAPTER_NOTES);
145
146 if (request.getWrapper() != null) {
147
148 boolean error = false;
149 boolean read = false;
150 try {
151 if (status == SocketStatus.OPEN) {
152 if (response.isClosed()) {
153 // The event has been closed asynchronously, so call end instead of
154 // read to cleanup the pipeline
155 request.getEvent().setEventType(CometEvent.EventType.END);
156 request.getEvent().setEventSubType(null);
157 } else {
158 try {
159 // Fill the read buffer of the servlet layer
160 if (request.read()) {
161 read = true;
162 }
163 } catch (IOException e) {
164 error = true;
165 }
166 if (read) {
167 request.getEvent().setEventType(CometEvent.EventType.READ);
168 request.getEvent().setEventSubType(null);
169 } else if (error) {
170 request.getEvent().setEventType(CometEvent.EventType.ERROR);
171 request.getEvent().setEventSubType(CometEvent.EventSubType.CLIENT_DISCONNECT);
172 } else {
173 request.getEvent().setEventType(CometEvent.EventType.END);
174 request.getEvent().setEventSubType(null);
175 }
176 }
177 } else if (status == SocketStatus.DISCONNECT) {
178 request.getEvent().setEventType(CometEvent.EventType.ERROR);
179 request.getEvent().setEventSubType(CometEvent.EventSubType.CLIENT_DISCONNECT);
180 error = true;
181 } else if (status == SocketStatus.ERROR) {
182 request.getEvent().setEventType(CometEvent.EventType.ERROR);
183 request.getEvent().setEventSubType(CometEvent.EventSubType.IOEXCEPTION);
184 error = true;
185 } else if (status == SocketStatus.STOP) {
186 request.getEvent().setEventType(CometEvent.EventType.END);
187 request.getEvent().setEventSubType(CometEvent.EventSubType.SERVER_SHUTDOWN);
188 } else if (status == SocketStatus.TIMEOUT) {
189 if (response.isClosed()) {
190 // The event has been closed asynchronously, so call end instead of
191 // read to cleanup the pipeline
192 request.getEvent().setEventType(CometEvent.EventType.END);
193 request.getEvent().setEventSubType(null);
194 } else {
195 request.getEvent().setEventType(CometEvent.EventType.ERROR);
196 request.getEvent().setEventSubType(CometEvent.EventSubType.TIMEOUT);
197 }
198 }
199
200 req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
201
202 // Calling the container
203 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
204
205 if (!error && !response.isClosed() && (request.getAttribute(Globals.EXCEPTION_ATTR) != null)) {
206 // An unexpected exception occurred while processing the event, so
207 // error should be called
208 request.getEvent().setEventType(CometEvent.EventType.ERROR);
209 request.getEvent().setEventSubType(null);
210 error = true;
211 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
212 }
213 if (response.isClosed() || !request.isComet()) {
214 if (status==SocketStatus.OPEN) {
215 //CometEvent.close was called during an event.
216 request.getEvent().setEventType(CometEvent.EventType.END);
217 request.getEvent().setEventSubType(null);
218 error = true;
219 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
220 }
221 res.action(ActionCode.ACTION_COMET_END, null);
222 } else if (!error && read && request.getAvailable()) {
223 // If this was a read and not all bytes have been read, or if no data
224 // was read from the connector, then it is an error
225 request.getEvent().setEventType(CometEvent.EventType.ERROR);
226 request.getEvent().setEventSubType(CometEvent.EventSubType.IOEXCEPTION);
227 error = true;
228 connector.getContainer().getPipeline().getFirst().event(request, response, request.getEvent());
229 }
230 return (!error);
231 } catch (Throwable t) {
232 if (!(t instanceof IOException)) {
233 log.error(sm.getString("coyoteAdapter.service"), t);
234 }
235 error = true;
236 return false;
237 } finally {
238 req.getRequestProcessor().setWorkerThreadName(null);
239 // Recycle the wrapper request and response
240 if (error || response.isClosed() || !request.isComet()) {
241 request.recycle();
242 request.setFilterChain(null);
243 response.recycle();
244 }
245 }
246
247 } else {
248 return false;
249 }
250 }
251
252
253 /**
254 * Service method.
255 */
256 public void service(org.apache.coyote.Request req,
257 org.apache.coyote.Response res)
258 throws Exception {
259
260 Request request = (Request) req.getNote(ADAPTER_NOTES);
261 Response response = (Response) res.getNote(ADAPTER_NOTES);
262
263 if (request == null) {
264
265 // Create objects
266 request = (Request) connector.createRequest();
267 request.setCoyoteRequest(req);
268 response = (Response) connector.createResponse();
269 response.setCoyoteResponse(res);
270
271 // Link objects
272 request.setResponse(response);
273 response.setRequest(request);
274
275 // Set as notes
276 req.setNote(ADAPTER_NOTES, request);
277 res.setNote(ADAPTER_NOTES, response);
278
279 // Set query string encoding
280 req.getParameters().setQueryStringEncoding
281 (connector.getURIEncoding());
282
283 }
284
285 if (connector.getXpoweredBy()) {
286 response.addHeader("X-Powered-By", POWERED_BY);
287 }
288
289 boolean comet = false;
290
291 try {
292
293 // Parse and set Catalina and configuration specific
294 // request parameters
295 req.getRequestProcessor().setWorkerThreadName(Thread.currentThread().getName());
296 if (postParseRequest(req, request, res, response)) {
297 // Calling the container
298 connector.getContainer().getPipeline().getFirst().invoke(request, response);
299
300 if (request.isComet()) {
301 if (!response.isClosed() && !response.isError()) {
302 if (request.getAvailable() || (request.getContentLength() > 0 && (!request.isParametersParsed()))) {
303 // Invoke a read event right away if there are available bytes
304 if (event(req, res, SocketStatus.OPEN)) {
305 comet = true;
306 res.action(ActionCode.ACTION_COMET_BEGIN, null);
307 }
308 } else {
309 comet = true;
310 res.action(ActionCode.ACTION_COMET_BEGIN, null);
311 }
312 } else {
313 // Clear the filter chain, as otherwise it will not be reset elsewhere
314 // since this is a Comet request
315 request.setFilterChain(null);
316 }
317 }
318
319 }
320
321 if (!comet) {
322 response.finishResponse();
323 req.action(ActionCode.ACTION_POST_REQUEST , null);
324 }
325
326 } catch (IOException e) {
327 ;
328 } catch (Throwable t) {
329 log.error(sm.getString("coyoteAdapter.service"), t);
330 } finally {
331 req.getRequestProcessor().setWorkerThreadName(null);
332 // Recycle the wrapper request and response
333 if (!comet) {
334 request.recycle();
335 response.recycle();
336 } else {
337 // Clear converters so that the minimum amount of memory
338 // is used by this processor
339 request.clearEncoders();
340 response.clearEncoders();
341 }
342 }
343
344 }
345
346
347 // ------------------------------------------------------ Protected Methods
348
349
350 /**
351 * Parse additional request parameters.
352 */
353 protected boolean postParseRequest(org.apache.coyote.Request req,
354 Request request,
355 org.apache.coyote.Response res,
356 Response response)
357 throws Exception {
358
359 // XXX the processor needs to set a correct scheme and port prior to this point,
360 // in ajp13 protocols dont make sense to get the port from the connector..
361 // XXX the processor may have set a correct scheme and port prior to this point,
362 // in ajp13 protocols dont make sense to get the port from the connector...
363 // otherwise, use connector configuration
364 if (! req.scheme().isNull()) {
365 // use processor specified scheme to determine secure state
366 request.setSecure(req.scheme().equals("https"));
367 } else {
368 // use connector scheme and secure configuration, (defaults to
369 // "http" and false respectively)
370 req.scheme().setString(connector.getScheme());
371 request.setSecure(connector.getSecure());
372 }
373
374 // FIXME: the code below doesnt belongs to here,
375 // this is only have sense
376 // in Http11, not in ajp13..
377 // At this point the Host header has been processed.
378 // Override if the proxyPort/proxyHost are set
379 String proxyName = connector.getProxyName();
380 int proxyPort = connector.getProxyPort();
381 if (proxyPort != 0) {
382 req.setServerPort(proxyPort);
383 }
384 if (proxyName != null) {
385 req.serverName().setString(proxyName);
386 }
387
388 // Parse session Id
389 parseSessionId(req, request);
390
391 // URI decoding
392 MessageBytes decodedURI = req.decodedURI();
393 decodedURI.duplicate(req.requestURI());
394
395 if (decodedURI.getType() == MessageBytes.T_BYTES) {
396 // Remove any path parameters
397 ByteChunk uriBB = decodedURI.getByteChunk();
398 int semicolon = uriBB.indexOf(';', 0);
399 if (semicolon > 0) {
400 decodedURI.setBytes
401 (uriBB.getBuffer(), uriBB.getStart(), semicolon);
402 }
403 // %xx decoding of the URL
404 try {
405 req.getURLDecoder().convert(decodedURI, false);
406 } catch (IOException ioe) {
407 res.setStatus(400);
408 res.setMessage("Invalid URI: " + ioe.getMessage());
409 return false;
410 }
411 // Normalization
412 if (!normalize(req.decodedURI())) {
413 res.setStatus(400);
414 res.setMessage("Invalid URI");
415 return false;
416 }
417 // Character decoding
418 convertURI(decodedURI, request);
419 // Check that the URI is still normalized
420 if (!checkNormalize(req.decodedURI())) {
421 res.setStatus(400);
422 res.setMessage("Invalid URI character encoding");
423 return false;
424 }
425 } else {
426 // The URL is chars or String, and has been sent using an in-memory
427 // protocol handler, we have to assume the URL has been properly
428 // decoded already
429 decodedURI.toChars();
430 // Remove any path parameters
431 CharChunk uriCC = decodedURI.getCharChunk();
432 int semicolon = uriCC.indexOf(';');
433 if (semicolon > 0) {
434 decodedURI.setChars
435 (uriCC.getBuffer(), uriCC.getStart(), semicolon);
436 }
437 }
438
439 // Set the remote principal
440 String principal = req.getRemoteUser().toString();
441 if (principal != null) {
442 request.setUserPrincipal(new CoyotePrincipal(principal));
443 }
444
445 // Set the authorization type
446 String authtype = req.getAuthType().toString();
447 if (authtype != null) {
448 request.setAuthType(authtype);
449 }
450
451 // Request mapping.
452 MessageBytes serverName;
453 if (connector.getUseIPVHosts()) {
454 serverName = req.localName();
455 if (serverName.isNull()) {
456 // well, they did ask for it
457 res.action(ActionCode.ACTION_REQ_LOCAL_NAME_ATTRIBUTE, null);
458 }
459 } else {
460 serverName = req.serverName();
461 }
462 connector.getMapper().map(serverName, decodedURI,
463 request.getMappingData());
464 request.setContext((Context) request.getMappingData().context);
465 request.setWrapper((Wrapper) request.getMappingData().wrapper);
466
467 // Filter trace method
468 if (!connector.getAllowTrace()
469 && req.method().equalsIgnoreCase("TRACE")) {
470 Wrapper wrapper = request.getWrapper();
471 String header = null;
472 if (wrapper != null) {
473 String[] methods = wrapper.getServletMethods();
474 if (methods != null) {
475 for (int i=0; i<methods.length; i++) {
476 if ("TRACE".equals(methods[i])) {
477 continue;
478 }
479 if (header == null) {
480 header = methods[i];
481 } else {
482 header += ", " + methods[i];
483 }
484 }
485 }
486 }
487 res.setStatus(405);
488 res.addHeader("Allow", header);
489 res.setMessage("TRACE method is not allowed");
490 return false;
491 }
492
493 // Possible redirect
494 MessageBytes redirectPathMB = request.getMappingData().redirectPath;
495 if (!redirectPathMB.isNull()) {
496 String redirectPath = urlEncoder.encode(redirectPathMB.toString());
497 String query = request.getQueryString();
498 if (request.isRequestedSessionIdFromURL()) {
499 // This is not optimal, but as this is not very common, it
500 // shouldn't matter
501 redirectPath = redirectPath + ";" + Globals.SESSION_PARAMETER_NAME + "="
502 + request.getRequestedSessionId();
503 }
504 if (query != null) {
505 // This is not optimal, but as this is not very common, it
506 // shouldn't matter
507 redirectPath = redirectPath + "?" + query;
508 }
509 response.sendRedirect(redirectPath);
510 return false;
511 }
512
513 // Parse session Id
514 parseSessionCookiesId(req, request);
515
516 return true;
517 }
518
519
520 /**
521 * Parse session id in URL.
522 */
523 protected void parseSessionId(org.apache.coyote.Request req, Request request) {
524
525 ByteChunk uriBC = req.requestURI().getByteChunk();
526 int semicolon = uriBC.indexOf(match, 0, match.length(), 0);
527
528 if (semicolon > 0) {
529 // What encoding to use? Some platforms, eg z/os, use a default
530 // encoding that doesn't give the expected result so be explicit
531 String enc = connector.getURIEncoding();
532 if (enc == null) {
533 enc = "ISO-8859-1";
534 }
535
536 // Parse session ID, and extract it from the decoded request URI
537 int start = uriBC.getStart();
538 int end = uriBC.getEnd();
539
540 int sessionIdStart = semicolon + match.length();
541 int semicolon2 = uriBC.indexOf(';', sessionIdStart);
542 try {
543 if (semicolon2 >= 0) {
544 request.setRequestedSessionId
545 (new String(uriBC.getBuffer(), start + sessionIdStart,
546 semicolon2 - sessionIdStart, enc));
547 // Extract session ID from request URI
548 byte[] buf = uriBC.getBuffer();
549 for (int i = 0; i < end - start - semicolon2; i++) {
550 buf[start + semicolon + i]
551 = buf[start + i + semicolon2];
552 }
553 uriBC.setBytes(buf, start,
554 end - start - semicolon2 + semicolon);
555 } else {
556 request.setRequestedSessionId
557 (new String(uriBC.getBuffer(), start + sessionIdStart,
558 (end - start) - sessionIdStart, enc));
559 uriBC.setEnd(start + semicolon);
560 }
561 request.setRequestedSessionURL(true);
562 } catch (UnsupportedEncodingException uee) {
563 // Make sure no session ID is returned
564 request.setRequestedSessionId(null);
565 request.setRequestedSessionURL(false);
566 log.warn(sm.getString("coyoteAdapter.parseSession", enc), uee);
567 }
568 } else {
569 request.setRequestedSessionId(null);
570 request.setRequestedSessionURL(false);
571 }
572
573 }
574
575
576 /**
577 * Parse session id in URL.
578 */
579 protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {
580
581 // If session tracking via cookies has been disabled for the current
582 // context, don't go looking for a session ID in a cookie as a cookie
583 // from a parent context with a session ID may be present which would
584 // overwrite the valid session ID encoded in the URL
585 Context context = (Context) request.getMappingData().context;
586 if (context != null && !context.getCookies())
587 return;
588
589 // Parse session id from cookies
590 Cookies serverCookies = req.getCookies();
591 int count = serverCookies.getCookieCount();
592 if (count <= 0)
593 return;
594
595 for (int i = 0; i < count; i++) {
596 ServerCookie scookie = serverCookies.getCookie(i);
597 if (scookie.getName().equals(Globals.SESSION_COOKIE_NAME)) {
598 // Override anything requested in the URL
599 if (!request.isRequestedSessionIdFromCookie()) {
600 // Accept only the first session id cookie
601 convertMB(scookie.getValue());
602 request.setRequestedSessionId
603 (scookie.getValue().toString());
604 request.setRequestedSessionCookie(true);
605 request.setRequestedSessionURL(false);
606 if (log.isDebugEnabled())
607 log.debug(" Requested cookie session id is " +
608 request.getRequestedSessionId());
609 } else {
610 if (!request.isRequestedSessionIdValid()) {
611 // Replace the session id until one is valid
612 convertMB(scookie.getValue());
613 request.setRequestedSessionId
614 (scookie.getValue().toString());
615 }
616 }
617 }
618 }
619
620 }
621
622
623 /**
624 * Character conversion of the URI.
625 */
626 protected void convertURI(MessageBytes uri, Request request)
627 throws Exception {
628
629 ByteChunk bc = uri.getByteChunk();
630 int length = bc.getLength();
631 CharChunk cc = uri.getCharChunk();
632 cc.allocate(length, -1);
633
634 String enc = connector.getURIEncoding();
635 if (enc != null) {
636 B2CConverter conv = request.getURIConverter();
637 try {
638 if (conv == null) {
639 conv = new B2CConverter(enc);
640 request.setURIConverter(conv);
641 }
642 } catch (IOException e) {
643 // Ignore
644 log.error("Invalid URI encoding; using HTTP default");
645 connector.setURIEncoding(null);
646 }
647 if (conv != null) {
648 try {
649 conv.convert(bc, cc);
650 uri.setChars(cc.getBuffer(), cc.getStart(),
651 cc.getLength());
652 return;
653 } catch (IOException e) {
654 log.error("Invalid URI character encoding; trying ascii");
655 cc.recycle();
656 }
657 }
658 }
659
660 // Default encoding: fast conversion
661 byte[] bbuf = bc.getBuffer();
662 char[] cbuf = cc.getBuffer();
663 int start = bc.getStart();
664 for (int i = 0; i < length; i++) {
665 cbuf[i] = (char) (bbuf[i + start] & 0xff);
666 }
667 uri.setChars(cbuf, 0, length);
668
669 }
670
671
672 /**
673 * Character conversion of the a US-ASCII MessageBytes.
674 */
675 protected void convertMB(MessageBytes mb) {
676
677 // This is of course only meaningful for bytes
678 if (mb.getType() != MessageBytes.T_BYTES)
679 return;
680
681 ByteChunk bc = mb.getByteChunk();
682 CharChunk cc = mb.getCharChunk();
683 int length = bc.getLength();
684 cc.allocate(length, -1);
685
686 // Default encoding: fast conversion
687 byte[] bbuf = bc.getBuffer();
688 char[] cbuf = cc.getBuffer();
689 int start = bc.getStart();
690 for (int i = 0; i < length; i++) {
691 cbuf[i] = (char) (bbuf[i + start] & 0xff);
692 }
693 mb.setChars(cbuf, 0, length);
694
695 }
696
697
698 /**
699 * Normalize URI.
700 * <p>
701 * This method normalizes "\", "//", "/./" and "/../". This method will
702 * return false when trying to go above the root, or if the URI contains
703 * a null byte.
704 *
705 * @param uriMB URI to be normalized
706 */
707 public static boolean normalize(MessageBytes uriMB) {
708
709 ByteChunk uriBC = uriMB.getByteChunk();
710 final byte[] b = uriBC.getBytes();
711 final int start = uriBC.getStart();
712 int end = uriBC.getEnd();
713
714 // An empty URL is not acceptable
715 if (start == end)
716 return false;
717
718 // URL * is acceptable
719 if ((end - start == 1) && b[start] == (byte) '*')
720 return true;
721
722 int pos = 0;
723 int index = 0;
724
725 // Replace '\' with '/'
726 // Check for null byte
727 for (pos = start; pos < end; pos++) {
728 if (b[pos] == (byte) '\\') {
729 if (ALLOW_BACKSLASH) {
730 b[pos] = (byte) '/';
731 } else {
732 return false;
733 }
734 }
735 if (b[pos] == (byte) 0) {
736 return false;
737 }
738 }
739
740 // The URL must start with '/'
741 if (b[start] != (byte) '/') {
742 return false;
743 }
744
745 // Replace "//" with "/"
746 for (pos = start; pos < (end - 1); pos++) {
747 if (b[pos] == (byte) '/') {
748 while ((pos + 1 < end) && (b[pos + 1] == (byte) '/')) {
749 copyBytes(b, pos, pos + 1, end - pos - 1);
750 end--;
751 }
752 }
753 }
754
755 // If the URI ends with "/." or "/..", then we append an extra "/"
756 // Note: It is possible to extend the URI by 1 without any side effect
757 // as the next character is a non-significant WS.
758 if (((end - start) >= 2) && (b[end - 1] == (byte) '.')) {
759 if ((b[end - 2] == (byte) '/')
760 || ((b[end - 2] == (byte) '.')
761 && (b[end - 3] == (byte) '/'))) {
762 b[end] = (byte) '/';
763 end++;
764 }
765 }
766
767 uriBC.setEnd(end);
768
769 index = 0;
770
771 // Resolve occurrences of "/./" in the normalized path
772 while (true) {
773 index = uriBC.indexOf("/./", 0, 3, index);
774 if (index < 0)
775 break;
776 copyBytes(b, start + index, start + index + 2,
777 end - start - index - 2);
778 end = end - 2;
779 uriBC.setEnd(end);
780 }
781
782 index = 0;
783
784 // Resolve occurrences of "/../" in the normalized path
785 while (true) {
786 index = uriBC.indexOf("/../", 0, 4, index);
787 if (index < 0)
788 break;
789 // Prevent from going outside our context
790 if (index == 0)
791 return false;
792 int index2 = -1;
793 for (pos = start + index - 1; (pos >= 0) && (index2 < 0); pos --) {
794 if (b[pos] == (byte) '/') {
795 index2 = pos;
796 }
797 }
798 copyBytes(b, start + index2, start + index + 3,
799 end - start - index - 3);
800 end = end + index2 - index - 3;
801 uriBC.setEnd(end);
802 index = index2;
803 }
804
805 return true;
806
807 }
808
809
810 /**
811 * Check that the URI is normalized following character decoding.
812 * <p>
813 * This method checks for "\", 0, "//", "/./" and "/../". This method will
814 * return false if sequences that are supposed to be normalized are still
815 * present in the URI.
816 *
817 * @param uriMB URI to be checked (should be chars)
818 */
819 public static boolean checkNormalize(MessageBytes uriMB) {
820
821 CharChunk uriCC = uriMB.getCharChunk();
822 char[] c = uriCC.getChars();
823 int start = uriCC.getStart();
824 int end = uriCC.getEnd();
825
826 int pos = 0;
827
828 // Check for '\' and 0
829 for (pos = start; pos < end; pos++) {
830 if (c[pos] == '\\') {
831 return false;
832 }
833 if (c[pos] == 0) {
834 return false;
835 }
836 }
837
838 // Check for "//"
839 for (pos = start; pos < (end - 1); pos++) {
840 if (c[pos] == '/') {
841 if (c[pos + 1] == '/') {
842 return false;
843 }
844 }
845 }
846
847 // Check for ending with "/." or "/.."
848 if (((end - start) >= 2) && (c[end - 1] == '.')) {
849 if ((c[end - 2] == '/')
850 || ((c[end - 2] == '.')
851 && (c[end - 3] == '/'))) {
852 return false;
853 }
854 }
855
856 // Check for "/./"
857 if (uriCC.indexOf("/./", 0, 3, 0) >= 0) {
858 return false;
859 }
860
861 // Check for "/../"
862 if (uriCC.indexOf("/../", 0, 4, 0) >= 0) {
863 return false;
864 }
865
866 return true;
867
868 }
869
870
871 // ------------------------------------------------------ Protected Methods
872
873
874 /**
875 * Copy an array of bytes to a different position. Used during
876 * normalization.
877 */
878 protected static void copyBytes(byte[] b, int dest, int src, int len) {
879 for (int pos = 0; pos < len; pos++) {
880 b[pos + dest] = b[pos + src];
881 }
882 }
883
884
885 }