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.nio.charset.Charset;
22 import java.nio.charset.StandardCharsets;
23
24
25
26 /* ------------------------------------------------------------ */
27 /** URI Holder.
28 * This class assists with the decoding and encoding or HTTP URI's.
29 * It differs from the java.net.URL class as it does not provide
30 * communications ability, but it does assist with query string
31 * formatting.
32 * <P>UTF-8 encoding is used by default for % encoded characters. This
33 * may be overridden with the org.eclipse.jetty.util.URI.charset system property.
34 * @see UrlEncoded
35 *
36 */
37 public class URIUtil
38 implements Cloneable
39 {
40 public static final String SLASH="/";
41 public static final String HTTP="http";
42 public static final String HTTP_COLON="http:";
43 public static final String HTTPS="https";
44 public static final String HTTPS_COLON="https:";
45
46 // Use UTF-8 as per http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars
47 public static final Charset __CHARSET;
48
49 static
50 {
51 String charset = System.getProperty("org.eclipse.jetty.util.URI.charset");
52 __CHARSET = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset);
53 }
54
55 private URIUtil()
56 {}
57
58 /* ------------------------------------------------------------ */
59 /** Encode a URI path.
60 * This is the same encoding offered by URLEncoder, except that
61 * the '/' character is not encoded.
62 * @param path The path the encode
63 * @return The encoded path
64 */
65 public static String encodePath(String path)
66 {
67 if (path==null || path.length()==0)
68 return path;
69
70 StringBuilder buf = encodePath(null,path);
71 return buf==null?path:buf.toString();
72 }
73
74 /* ------------------------------------------------------------ */
75 /** Encode a URI path.
76 * @param path The path the encode
77 * @param buf StringBuilder to encode path into (or null)
78 * @return The StringBuilder or null if no substitutions required.
79 */
80 public static StringBuilder encodePath(StringBuilder buf, String path)
81 {
82 byte[] bytes=null;
83 if (buf==null)
84 {
85 loop:
86 for (int i=0;i<path.length();i++)
87 {
88 char c=path.charAt(i);
89 switch(c)
90 {
91 case '%':
92 case '?':
93 case ';':
94 case '#':
95 case '\'':
96 case '"':
97 case '<':
98 case '>':
99 case ' ':
100 buf=new StringBuilder(path.length()*2);
101 break loop;
102 default:
103 if (c>127)
104 {
105 bytes=path.getBytes(URIUtil.__CHARSET);
106 buf=new StringBuilder(path.length()*2);
107 break loop;
108 }
109
110 }
111 }
112 if (buf==null)
113 return null;
114 }
115
116 synchronized(buf)
117 {
118 if (bytes!=null)
119 {
120 for (int i=0;i<bytes.length;i++)
121 {
122 byte c=bytes[i];
123 switch(c)
124 {
125 case '%':
126 buf.append("%25");
127 continue;
128 case '?':
129 buf.append("%3F");
130 continue;
131 case ';':
132 buf.append("%3B");
133 continue;
134 case '#':
135 buf.append("%23");
136 continue;
137 case '"':
138 buf.append("%22");
139 continue;
140 case '\'':
141 buf.append("%27");
142 continue;
143 case '<':
144 buf.append("%3C");
145 continue;
146 case '>':
147 buf.append("%3E");
148 continue;
149 case ' ':
150 buf.append("%20");
151 continue;
152 default:
153 if (c<0)
154 {
155 buf.append('%');
156 TypeUtil.toHex(c,buf);
157 }
158 else
159 buf.append((char)c);
160 continue;
161 }
162 }
163
164 }
165 else
166 {
167 for (int i=0;i<path.length();i++)
168 {
169 char c=path.charAt(i);
170 switch(c)
171 {
172 case '%':
173 buf.append("%25");
174 continue;
175 case '?':
176 buf.append("%3F");
177 continue;
178 case ';':
179 buf.append("%3B");
180 continue;
181 case '#':
182 buf.append("%23");
183 continue;
184 case '"':
185 buf.append("%22");
186 continue;
187 case '\'':
188 buf.append("%27");
189 continue;
190 case '<':
191 buf.append("%3C");
192 continue;
193 case '>':
194 buf.append("%3E");
195 continue;
196 case ' ':
197 buf.append("%20");
198 continue;
199 default:
200 buf.append(c);
201 continue;
202 }
203 }
204 }
205 }
206
207 return buf;
208 }
209
210 /* ------------------------------------------------------------ */
211 /** Encode a URI path.
212 * @param path The path the encode
213 * @param buf StringBuilder to encode path into (or null)
214 * @param encode String of characters to encode. % is always encoded.
215 * @return The StringBuilder or null if no substitutions required.
216 */
217 public static StringBuilder encodeString(StringBuilder buf,
218 String path,
219 String encode)
220 {
221 if (buf==null)
222 {
223 loop:
224 for (int i=0;i<path.length();i++)
225 {
226 char c=path.charAt(i);
227 if (c=='%' || encode.indexOf(c)>=0)
228 {
229 buf=new StringBuilder(path.length()<<1);
230 break loop;
231 }
232 }
233 if (buf==null)
234 return null;
235 }
236
237 synchronized(buf)
238 {
239 for (int i=0;i<path.length();i++)
240 {
241 char c=path.charAt(i);
242 if (c=='%' || encode.indexOf(c)>=0)
243 {
244 buf.append('%');
245 StringUtil.append(buf,(byte)(0xff&c),16);
246 }
247 else
248 buf.append(c);
249 }
250 }
251
252 return buf;
253 }
254
255 /* ------------------------------------------------------------ */
256 /* Decode a URI path and strip parameters
257 * @param path The path the encode
258 * @param buf StringBuilder to encode path into
259 */
260 public static String decodePath(String path)
261 {
262 if (path==null)
263 return null;
264 // Array to hold all converted characters
265 char[] chars=null;
266 int n=0;
267 // Array to hold a sequence of %encodings
268 byte[] bytes=null;
269 int b=0;
270
271 int len=path.length();
272
273 for (int i=0;i<len;i++)
274 {
275 char c = path.charAt(i);
276
277 if (c=='%' && (i+2)<len)
278 {
279 if (chars==null)
280 {
281 chars=new char[len];
282 bytes=new byte[len];
283 path.getChars(0,i,chars,0);
284 }
285 bytes[b++]=(byte)(0xff&TypeUtil.parseInt(path,i+1,2,16));
286 i+=2;
287 continue;
288 }
289 else if (c==';')
290 {
291 if (chars==null)
292 {
293 chars=new char[len];
294 path.getChars(0,i,chars,0);
295 n=i;
296 }
297 break;
298 }
299 else if (bytes==null)
300 {
301 n++;
302 continue;
303 }
304
305 // Do we have some bytes to convert?
306 if (b>0)
307 {
308 String s=new String(bytes,0,b,__CHARSET);
309 s.getChars(0,s.length(),chars,n);
310 n+=s.length();
311 b=0;
312 }
313
314 chars[n++]=c;
315 }
316
317 if (chars==null)
318 return path;
319
320 // if we have a remaining sequence of bytes
321 if (b>0)
322 {
323 String s=new String(bytes,0,b,__CHARSET);
324 s.getChars(0,s.length(),chars,n);
325 n+=s.length();
326 }
327
328 return new String(chars,0,n);
329 }
330
331 /* ------------------------------------------------------------ */
332 /* Decode a URI path and strip parameters.
333 * @param path The path the encode
334 * @param buf StringBuilder to encode path into
335 */
336 public static String decodePath(byte[] buf, int offset, int length)
337 {
338 byte[] bytes=null;
339 int n=0;
340
341 for (int i=0;i<length;i++)
342 {
343 byte b = buf[i + offset];
344
345 if (b=='%' && (i+2)<length)
346 {
347 b=(byte)(0xff&TypeUtil.parseInt(buf,i+offset+1,2,16));
348 i+=2;
349 }
350 else if (b==';')
351 {
352 length=i;
353 break;
354 }
355 else if (bytes==null)
356 {
357 n++;
358 continue;
359 }
360
361 if (bytes==null)
362 {
363 bytes=new byte[length];
364 for (int j=0;j<n;j++)
365 bytes[j]=buf[j + offset];
366 }
367
368 bytes[n++]=b;
369 }
370
371 if (bytes==null)
372 return new String(buf,offset,length,__CHARSET);
373 return new String(bytes,0,n,__CHARSET);
374 }
375
376
377 /* ------------------------------------------------------------ */
378 /** Add two URI path segments.
379 * Handles null and empty paths, path and query params (eg ?a=b or
380 * ;JSESSIONID=xxx) and avoids duplicate '/'
381 * @param p1 URI path segment (should be encoded)
382 * @param p2 URI path segment (should be encoded)
383 * @return Legally combined path segments.
384 */
385 public static String addPaths(String p1, String p2)
386 {
387 if (p1==null || p1.length()==0)
388 {
389 if (p1!=null && p2==null)
390 return p1;
391 return p2;
392 }
393 if (p2==null || p2.length()==0)
394 return p1;
395
396 int split=p1.indexOf(';');
397 if (split<0)
398 split=p1.indexOf('?');
399 if (split==0)
400 return p2+p1;
401 if (split<0)
402 split=p1.length();
403
404 StringBuilder buf = new StringBuilder(p1.length()+p2.length()+2);
405 buf.append(p1);
406
407 if (buf.charAt(split-1)=='/')
408 {
409 if (p2.startsWith(URIUtil.SLASH))
410 {
411 buf.deleteCharAt(split-1);
412 buf.insert(split-1,p2);
413 }
414 else
415 buf.insert(split,p2);
416 }
417 else
418 {
419 if (p2.startsWith(URIUtil.SLASH))
420 buf.insert(split,p2);
421 else
422 {
423 buf.insert(split,'/');
424 buf.insert(split+1,p2);
425 }
426 }
427
428 return buf.toString();
429 }
430
431 /* ------------------------------------------------------------ */
432 /** Return the parent Path.
433 * Treat a URI like a directory path and return the parent directory.
434 */
435 public static String parentPath(String p)
436 {
437 if (p==null || URIUtil.SLASH.equals(p))
438 return null;
439 int slash=p.lastIndexOf('/',p.length()-2);
440 if (slash>=0)
441 return p.substring(0,slash+1);
442 return null;
443 }
444
445 /* ------------------------------------------------------------ */
446 /** Convert a path to a cananonical form.
447 * All instances of "." and ".." are factored out. Null is returned
448 * if the path tries to .. above its root.
449 * @param path
450 * @return path or null.
451 */
452 public static String canonicalPath(String path)
453 {
454 if (path==null || path.length()==0)
455 return path;
456
457 int end=path.length();
458 int start = path.lastIndexOf('/', end);
459
460 search:
461 while (end>0)
462 {
463 switch(end-start)
464 {
465 case 2: // possible single dot
466 if (path.charAt(start+1)!='.')
467 break;
468 break search;
469 case 3: // possible double dot
470 if (path.charAt(start+1)!='.' || path.charAt(start+2)!='.')
471 break;
472 break search;
473 }
474
475 end=start;
476 start=path.lastIndexOf('/',end-1);
477 }
478
479 // If we have checked the entire string
480 if (start>=end)
481 return path;
482
483 StringBuilder buf = new StringBuilder(path);
484 int delStart=-1;
485 int delEnd=-1;
486 int skip=0;
487
488 while (end>0)
489 {
490 switch(end-start)
491 {
492 case 2: // possible single dot
493 if (buf.charAt(start+1)!='.')
494 {
495 if (skip>0 && --skip==0)
496 {
497 delStart=start>=0?start:0;
498 if(delStart>0 && delEnd==buf.length() && buf.charAt(delEnd-1)=='.')
499 delStart++;
500 }
501 break;
502 }
503
504 if(start<0 && buf.length()>2 && buf.charAt(1)=='/' && buf.charAt(2)=='/')
505 break;
506
507 if(delEnd<0)
508 delEnd=end;
509 delStart=start;
510 if (delStart<0 || delStart==0&&buf.charAt(delStart)=='/')
511 {
512 delStart++;
513 if (delEnd<buf.length() && buf.charAt(delEnd)=='/')
514 delEnd++;
515 break;
516 }
517 if (end==buf.length())
518 delStart++;
519
520 end=start--;
521 while (start>=0 && buf.charAt(start)!='/')
522 start--;
523 continue;
524
525 case 3: // possible double dot
526 if (buf.charAt(start+1)!='.' || buf.charAt(start+2)!='.')
527 {
528 if (skip>0 && --skip==0)
529 { delStart=start>=0?start:0;
530 if(delStart>0 && delEnd==buf.length() && buf.charAt(delEnd-1)=='.')
531 delStart++;
532 }
533 break;
534 }
535
536 delStart=start;
537 if (delEnd<0)
538 delEnd=end;
539
540 skip++;
541 end=start--;
542 while (start>=0 && buf.charAt(start)!='/')
543 start--;
544 continue;
545
546 default:
547 if (skip>0 && --skip==0)
548 {
549 delStart=start>=0?start:0;
550 if(delEnd==buf.length() && buf.charAt(delEnd-1)=='.')
551 delStart++;
552 }
553 }
554
555 // Do the delete
556 if (skip<=0 && delStart>=0 && delEnd>=delStart)
557 {
558 buf.delete(delStart,delEnd);
559 delStart=delEnd=-1;
560 if (skip>0)
561 delEnd=end;
562 }
563
564 end=start--;
565 while (start>=0 && buf.charAt(start)!='/')
566 start--;
567 }
568
569 // Too many ..
570 if (skip>0)
571 return null;
572
573 // Do the delete
574 if (delEnd>=0)
575 buf.delete(delStart,delEnd);
576
577 return buf.toString();
578 }
579
580 /* ------------------------------------------------------------ */
581 /** Convert a path to a compact form.
582 * All instances of "//" and "///" etc. are factored out to single "/"
583 * @param path
584 * @return path
585 */
586 public static String compactPath(String path)
587 {
588 if (path==null || path.length()==0)
589 return path;
590
591 int state=0;
592 int end=path.length();
593 int i=0;
594
595 loop:
596 while (i<end)
597 {
598 char c=path.charAt(i);
599 switch(c)
600 {
601 case '?':
602 return path;
603 case '/':
604 state++;
605 if (state==2)
606 break loop;
607 break;
608 default:
609 state=0;
610 }
611 i++;
612 }
613
614 if (state<2)
615 return path;
616
617 StringBuffer buf = new StringBuffer(path.length());
618 buf.append(path,0,i);
619
620 loop2:
621 while (i<end)
622 {
623 char c=path.charAt(i);
624 switch(c)
625 {
626 case '?':
627 buf.append(path,i,end);
628 break loop2;
629 case '/':
630 if (state++==0)
631 buf.append(c);
632 break;
633 default:
634 state=0;
635 buf.append(c);
636 }
637 i++;
638 }
639
640 return buf.toString();
641 }
642
643 /* ------------------------------------------------------------ */
644 /**
645 * @param uri URI
646 * @return True if the uri has a scheme
647 */
648 public static boolean hasScheme(String uri)
649 {
650 for (int i=0;i<uri.length();i++)
651 {
652 char c=uri.charAt(i);
653 if (c==':')
654 return true;
655 if (!(c>='a'&&c<='z' ||
656 c>='A'&&c<='Z' ||
657 (i>0 &&(c>='0'&&c<='9' ||
658 c=='.' ||
659 c=='+' ||
660 c=='-'))
661 ))
662 break;
663 }
664 return false;
665 }
666
667 /* ------------------------------------------------------------ */
668 /**
669 * Create a new URI from the arguments, handling IPv6 host encoding and default ports
670 * @param scheme
671 * @param server
672 * @param port
673 * @param path
674 * @param query
675 * @return A String URI
676 */
677 public static String newURI(String scheme,String server, int port,String path,String query)
678 {
679 StringBuilder builder = newURIBuilder(scheme, server, port);
680 builder.append(path);
681 if (query!=null && query.length()>0)
682 builder.append('?').append(query);
683 return builder.toString();
684 }
685
686 /* ------------------------------------------------------------ */
687 /**
688 * Create a new URI StringBuilder from the arguments, handling IPv6 host encoding and default ports
689 * @param scheme
690 * @param server
691 * @param port
692 * @return a StringBuilder containing URI prefix
693 */
694 public static StringBuilder newURIBuilder(String scheme,String server, int port)
695 {
696 StringBuilder builder = new StringBuilder();
697 appendSchemeHostPort(builder, scheme, server, port);
698 return builder;
699 }
700
701 /* ------------------------------------------------------------ */
702 /**
703 * Append scheme, host and port URI prefix, handling IPv6 address encoding and default ports</p>
704 * @param url StringBuilder to append to
705 * @param scheme
706 * @param server
707 * @param port
708 */
709 public static void appendSchemeHostPort(StringBuilder url,String scheme,String server, int port)
710 {
711 if (server.indexOf(':')>=0&&server.charAt(0)!='[')
712 url.append(scheme).append("://").append('[').append(server).append(']');
713 else
714 url.append(scheme).append("://").append(server);
715
716 if (port > 0)
717 {
718 switch(scheme)
719 {
720 case "http":
721 if (port!=80)
722 url.append(':').append(port);
723 break;
724
725 case "https":
726 if (port!=443)
727 url.append(':').append(port);
728 break;
729
730 default:
731 url.append(':').append(port);
732 }
733 }
734 }
735
736 /* ------------------------------------------------------------ */
737 /**
738 * Append scheme, host and port URI prefix, handling IPv6 address encoding and default ports</p>
739 * @param url StringBuffer to append to
740 * @param scheme
741 * @param server
742 * @param port
743 */
744 public static void appendSchemeHostPort(StringBuffer url,String scheme,String server, int port)
745 {
746 synchronized (url)
747 {
748 if (server.indexOf(':')>=0&&server.charAt(0)!='[')
749 url.append(scheme).append("://").append('[').append(server).append(']');
750 else
751 url.append(scheme).append("://").append(server);
752
753 if (port > 0)
754 {
755 switch(scheme)
756 {
757 case "http":
758 if (port!=80)
759 url.append(':').append(port);
760 break;
761
762 case "https":
763 if (port!=443)
764 url.append(':').append(port);
765 break;
766
767 default:
768 url.append(':').append(port);
769 }
770 }
771 }
772 }
773
774 public static boolean equalsIgnoreEncodings(String uriA, String uriB)
775 {
776 int lenA=uriA.length();
777 int lenB=uriB.length();
778 int a=0;
779 int b=0;
780
781 while (a<lenA && b<lenB)
782 {
783 int oa=uriA.charAt(a++);
784 int ca=oa;
785 if (ca=='%')
786 ca=TypeUtil.convertHexDigit(uriA.charAt(a++))*16+TypeUtil.convertHexDigit(uriA.charAt(a++));
787
788 int ob=uriB.charAt(b++);
789 int cb=ob;
790 if (cb=='%')
791 cb=TypeUtil.convertHexDigit(uriB.charAt(b++))*16+TypeUtil.convertHexDigit(uriB.charAt(b++));
792
793 if (ca=='/' && oa!=ob)
794 return false;
795
796 if (ca!=cb )
797 return URIUtil.decodePath(uriA).equals(URIUtil.decodePath(uriB));
798 }
799 return a==lenA && b==lenB;
800 }
801 }
802
803
804