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.http;
20
21 import java.io.UnsupportedEncodingException;
22 import java.net.URI;
23 import java.net.URISyntaxException;
24 import java.nio.charset.Charset;
25 import java.nio.charset.StandardCharsets;
26
27 import org.eclipse.jetty.util.MultiMap;
28 import org.eclipse.jetty.util.TypeUtil;
29 import org.eclipse.jetty.util.URIUtil;
30 import org.eclipse.jetty.util.UrlEncoded;
31
32
33 /* ------------------------------------------------------------ */
34 /** Http URI.
35 * Parse a HTTP URI from a string or byte array. Given a URI
36 * <code>http://user@host:port/path/info;param?query#fragment</code>
37 * this class will split it into the following undecoded optional elements:<ul>
38 * <li>{@link #getScheme()} - http:</li>
39 * <li>{@link #getAuthority()} - //name@host:port</li>
40 * <li>{@link #getHost()} - host</li>
41 * <li>{@link #getPort()} - port</li>
42 * <li>{@link #getPath()} - /path/info</li>
43 * <li>{@link #getParam()} - param</li>
44 * <li>{@link #getQuery()} - query</li>
45 * <li>{@link #getFragment()} - fragment</li>
46 * </ul>
47 *
48 * <p>Any parameters will be returned from {@link #getPath()}, but are excluded from the
49 * return value of {@link #getDecodedPath()}. If there are multiple parameters, the
50 * {@link #getParam()} method returns only the last one.
51 */
52 public class HttpURI
53 {
54 private enum State {
55 START,
56 HOST_OR_PATH,
57 SCHEME_OR_PATH,
58 HOST,
59 IPV6,
60 PORT,
61 PATH,
62 PARAM,
63 QUERY,
64 FRAGMENT,
65 ASTERISK};
66
67 private String _scheme;
68 private String _user;
69 private String _host;
70 private int _port;
71 private String _path;
72 private String _param;
73 private String _query;
74 private String _fragment;
75
76 String _uri;
77 String _decodedPath;
78
79 /* ------------------------------------------------------------ */
80 /**
81 * Construct a normalized URI.
82 * Port is not set if it is the default port.
83 * @param scheme the URI scheme
84 * @param host the URI hose
85 * @param port the URI port
86 * @param path the URI path
87 * @param param the URI param
88 * @param query the URI query
89 * @param fragment the URI fragment
90 * @return the normalized URI
91 */
92 public static HttpURI createHttpURI(String scheme, String host, int port, String path, String param, String query, String fragment)
93 {
94 if (port==80 && HttpScheme.HTTP.is(scheme))
95 port=0;
96 if (port==443 && HttpScheme.HTTPS.is(scheme))
97 port=0;
98 return new HttpURI(scheme,host,port,path,param,query,fragment);
99 }
100
101 /* ------------------------------------------------------------ */
102 public HttpURI()
103 {
104 }
105
106 /* ------------------------------------------------------------ */
107 public HttpURI(String scheme, String host, int port, String path, String param, String query, String fragment)
108 {
109 _scheme = scheme;
110 _host = host;
111 _port = port;
112 _path = path;
113 _param = param;
114 _query = query;
115 _fragment = fragment;
116 }
117
118 /* ------------------------------------------------------------ */
119 public HttpURI(HttpURI uri)
120 {
121 this(uri._scheme,uri._host,uri._port,uri._path,uri._param,uri._query,uri._fragment);
122 }
123
124 /* ------------------------------------------------------------ */
125 public HttpURI(String uri)
126 {
127 _port=-1;
128 parse(State.START,uri,0,uri.length());
129 }
130
131 /* ------------------------------------------------------------ */
132 public HttpURI(URI uri)
133 {
134 _uri=null;
135
136 _scheme=uri.getScheme();
137 _host=uri.getHost();
138 if (_host==null && uri.getRawSchemeSpecificPart().startsWith("//"))
139 _host="";
140 _port=uri.getPort();
141 _user = uri.getUserInfo();
142 _path=uri.getRawPath();
143
144 _decodedPath = uri.getPath();
145 if (_decodedPath != null)
146 {
147 int p = _decodedPath.lastIndexOf(';');
148 if (p >= 0)
149 _param = _decodedPath.substring(p + 1);
150 }
151 _query=uri.getRawQuery();
152 _fragment=uri.getFragment();
153
154 _decodedPath=null;
155 }
156
157 /* ------------------------------------------------------------ */
158 public HttpURI(String scheme, String host, int port, String pathQuery)
159 {
160 _uri=null;
161
162 _scheme=scheme;
163 _host=host;
164 _port=port;
165
166 parse(State.PATH,pathQuery,0,pathQuery.length());
167
168 }
169
170 /* ------------------------------------------------------------ */
171 public void parse(String uri)
172 {
173 clear();
174 _uri=uri;
175 parse(State.START,uri,0,uri.length());
176 }
177
178 /* ------------------------------------------------------------ */
179 public void parseConnect(String uri)
180 {
181 clear();
182 _uri=uri;
183 _path=uri;
184 }
185
186 /* ------------------------------------------------------------ */
187 public void parse(String uri, int offset, int length)
188 {
189 clear();
190 int end=offset+length;
191 _uri=uri.substring(offset,end);
192 parse(State.START,uri,offset,end);
193 }
194
195 /* ------------------------------------------------------------ */
196 private void parse(State state, final String uri, final int offset, final int end)
197 {
198 boolean encoded=false;
199 int mark=offset;
200 int path_mark=0;
201
202 for (int i=offset; i<end; i++)
203 {
204 char c=uri.charAt(i);
205
206 switch (state)
207 {
208 case START:
209 {
210 switch(c)
211 {
212 case '/':
213 mark = i;
214 state = State.HOST_OR_PATH;
215 break;
216 case ';':
217 mark=i+1;
218 state=State.PARAM;
219 break;
220 case '?':
221 // assume empty path (if seen at start)
222 _path = "";
223 mark=i+1;
224 state=State.QUERY;
225 break;
226 case '#':
227 mark=i+1;
228 state=State.FRAGMENT;
229 break;
230 case '*':
231 _path="*";
232 state=State.ASTERISK;
233 break;
234
235 default:
236 mark=i;
237 if (_scheme==null)
238 state=State.SCHEME_OR_PATH;
239 else
240 {
241 path_mark=i;
242 state=State.PATH;
243 }
244 }
245
246 continue;
247 }
248
249 case SCHEME_OR_PATH:
250 {
251 switch (c)
252 {
253 case ':':
254 // must have been a scheme
255 _scheme=uri.substring(mark,i);
256 // Start again with scheme set
257 state=State.START;
258 break;
259
260 case '/':
261 // must have been in a path and still are
262 state=State.PATH;
263 break;
264
265 case ';':
266 // must have been in a path
267 mark=i+1;
268 state=State.PARAM;
269 break;
270
271 case '?':
272 // must have been in a path
273 _path=uri.substring(mark,i);
274 mark=i+1;
275 state=State.QUERY;
276 break;
277
278 case '%':
279 // must have be in an encoded path
280 encoded=true;
281 state=State.PATH;
282 break;
283
284 case '#':
285 // must have been in a path
286 _path=uri.substring(mark,i);
287 state=State.FRAGMENT;
288 break;
289 }
290 continue;
291 }
292
293 case HOST_OR_PATH:
294 {
295 switch(c)
296 {
297 case '/':
298 _host="";
299 mark=i+1;
300 state=State.HOST;
301 break;
302
303 case '@':
304 case ';':
305 case '?':
306 case '#':
307 // was a path, look again
308 i--;
309 path_mark=mark;
310 state=State.PATH;
311 break;
312 default:
313 // it is a path
314 path_mark=mark;
315 state=State.PATH;
316 }
317 continue;
318 }
319
320 case HOST:
321 {
322 switch (c)
323 {
324 case '/':
325 _host = uri.substring(mark,i);
326 path_mark=mark=i;
327 state=State.PATH;
328 break;
329 case ':':
330 if (i > mark)
331 _host=uri.substring(mark,i);
332 mark=i+1;
333 state=State.PORT;
334 break;
335 case '@':
336 _user=uri.substring(mark,i);
337 mark=i+1;
338 break;
339
340 case '[':
341 state=State.IPV6;
342 break;
343 }
344 continue;
345 }
346
347 case IPV6:
348 {
349 switch (c)
350 {
351 case '/':
352 throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
353 case ']':
354 c = uri.charAt(++i);
355 _host=uri.substring(mark,i);
356 if (c == ':')
357 {
358 mark=i+1;
359 state=State.PORT;
360 }
361 else
362 {
363 path_mark=mark=i;
364 state=State.PATH;
365 }
366 break;
367 }
368
369 continue;
370 }
371
372 case PORT:
373 {
374 if (c=='/')
375 {
376 _port=TypeUtil.parseInt(uri,mark,i-mark,10);
377 path_mark=mark=i;
378 state=State.PATH;
379 }
380 continue;
381 }
382
383 case PATH:
384 {
385 switch (c)
386 {
387 case ';':
388 mark=i+1;
389 state=State.PARAM;
390 break;
391 case '?':
392 _path=uri.substring(path_mark,i);
393 mark=i+1;
394 state=State.QUERY;
395 break;
396 case '#':
397 _path=uri.substring(path_mark,i);
398 mark=i+1;
399 state=State.FRAGMENT;
400 break;
401 case '%':
402 encoded=true;
403 break;
404 }
405 continue;
406 }
407
408 case PARAM:
409 {
410 switch (c)
411 {
412 case '?':
413 _path=uri.substring(path_mark,i);
414 _param=uri.substring(mark,i);
415 mark=i+1;
416 state=State.QUERY;
417 break;
418 case '#':
419 _path=uri.substring(path_mark,i);
420 _param=uri.substring(mark,i);
421 mark=i+1;
422 state=State.FRAGMENT;
423 break;
424 case '/':
425 encoded=true;
426 // ignore internal params
427 state=State.PATH;
428 break;
429 case ';':
430 // multiple parameters
431 mark=i+1;
432 break;
433 }
434 continue;
435 }
436
437 case QUERY:
438 {
439 if (c=='#')
440 {
441 _query=uri.substring(mark,i);
442 mark=i+1;
443 state=State.FRAGMENT;
444 }
445 continue;
446 }
447
448 case ASTERISK:
449 {
450 throw new IllegalArgumentException("only '*'");
451 }
452
453 case FRAGMENT:
454 {
455 _fragment=uri.substring(mark,end);
456 i=end;
457 }
458 }
459 }
460
461
462 switch(state)
463 {
464 case START:
465 break;
466 case SCHEME_OR_PATH:
467 _path=uri.substring(mark,end);
468 break;
469
470 case HOST_OR_PATH:
471 _path=uri.substring(mark,end);
472 break;
473
474 case HOST:
475 if(end>mark)
476 _host=uri.substring(mark,end);
477 break;
478
479 case IPV6:
480 throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
481
482 case PORT:
483 _port=TypeUtil.parseInt(uri,mark,end-mark,10);
484 break;
485
486 case ASTERISK:
487 break;
488
489 case FRAGMENT:
490 _fragment=uri.substring(mark,end);
491 break;
492
493 case PARAM:
494 _path=uri.substring(path_mark,end);
495 _param=uri.substring(mark,end);
496 break;
497
498 case PATH:
499 _path=uri.substring(path_mark,end);
500 break;
501
502 case QUERY:
503 _query=uri.substring(mark,end);
504 break;
505 }
506
507 if (!encoded)
508 {
509 if (_param==null)
510 _decodedPath=_path;
511 else
512 _decodedPath=_path.substring(0,_path.length()-_param.length()-1);
513 }
514 }
515
516 /* ------------------------------------------------------------ */
517 public String getScheme()
518 {
519 return _scheme;
520 }
521
522 /* ------------------------------------------------------------ */
523 public String getHost()
524 {
525 // Return null for empty host to retain compatibility with java.net.URI
526 if (_host!=null && _host.length()==0)
527 return null;
528 return _host;
529 }
530
531 /* ------------------------------------------------------------ */
532 public int getPort()
533 {
534 return _port;
535 }
536
537 /* ------------------------------------------------------------ */
538 /**
539 * The parsed Path.
540 *
541 * @return the path as parsed on valid URI. null for invalid URI.
542 */
543 public String getPath()
544 {
545 return _path;
546 }
547
548 /* ------------------------------------------------------------ */
549 public String getDecodedPath()
550 {
551 if (_decodedPath==null && _path!=null)
552 _decodedPath=URIUtil.decodePath(_path);
553 return _decodedPath;
554 }
555
556 /* ------------------------------------------------------------ */
557 public String getParam()
558 {
559 return _param;
560 }
561
562 /* ------------------------------------------------------------ */
563 public String getQuery()
564 {
565 return _query;
566 }
567
568 /* ------------------------------------------------------------ */
569 public boolean hasQuery()
570 {
571 return _query!=null && _query.length()>0;
572 }
573
574 /* ------------------------------------------------------------ */
575 public String getFragment()
576 {
577 return _fragment;
578 }
579
580 /* ------------------------------------------------------------ */
581 public void decodeQueryTo(MultiMap<String> parameters)
582 {
583 if (_query==_fragment)
584 return;
585 UrlEncoded.decodeUtf8To(_query,parameters);
586 }
587
588 /* ------------------------------------------------------------ */
589 public void decodeQueryTo(MultiMap<String> parameters, String encoding) throws UnsupportedEncodingException
590 {
591 decodeQueryTo(parameters,Charset.forName(encoding));
592 }
593
594 /* ------------------------------------------------------------ */
595 public void decodeQueryTo(MultiMap<String> parameters, Charset encoding) throws UnsupportedEncodingException
596 {
597 if (_query==_fragment)
598 return;
599
600 if (encoding==null || StandardCharsets.UTF_8.equals(encoding))
601 UrlEncoded.decodeUtf8To(_query,parameters);
602 else
603 UrlEncoded.decodeTo(_query,parameters,encoding);
604 }
605
606 /* ------------------------------------------------------------ */
607 public void clear()
608 {
609 _uri=null;
610
611 _scheme=null;
612 _host=null;
613 _port=-1;
614 _path=null;
615 _param=null;
616 _query=null;
617 _fragment=null;
618
619 _decodedPath=null;
620 }
621
622 /* ------------------------------------------------------------ */
623 public boolean isAbsolute()
624 {
625 return _scheme!=null && _scheme.length()>0;
626 }
627
628 /* ------------------------------------------------------------ */
629 @Override
630 public String toString()
631 {
632 if (_uri==null)
633 {
634 StringBuilder out = new StringBuilder();
635
636 if (_scheme!=null)
637 out.append(_scheme).append(':');
638
639 if (_host != null)
640 {
641 out.append("//");
642 if (_user != null)
643 out.append(_user).append('@');
644 out.append(_host);
645 }
646
647 if (_port>0)
648 out.append(':').append(_port);
649
650 if (_path!=null)
651 out.append(_path);
652
653 if (_query!=null)
654 out.append('?').append(_query);
655
656 if (_fragment!=null)
657 out.append('#').append(_fragment);
658
659 if (out.length()>0)
660 _uri=out.toString();
661 else
662 _uri="";
663 }
664 return _uri;
665 }
666
667 /* ------------------------------------------------------------ */
668 public boolean equals(Object o)
669 {
670 if (o==this)
671 return true;
672 if (!(o instanceof HttpURI))
673 return false;
674 return toString().equals(o.toString());
675 }
676
677 /* ------------------------------------------------------------ */
678 public void setScheme(String scheme)
679 {
680 _scheme=scheme;
681 _uri=null;
682 }
683
684 /* ------------------------------------------------------------ */
685 /**
686 * @param host the host
687 * @param port the port
688 */
689 public void setAuthority(String host, int port)
690 {
691 _host=host;
692 _port=port;
693 _uri=null;
694 }
695
696 /* ------------------------------------------------------------ */
697 /**
698 * @param path the path
699 */
700 public void setPath(String path)
701 {
702 _uri=null;
703 _path=path;
704 _decodedPath=null;
705 }
706
707 /* ------------------------------------------------------------ */
708 public void setPathQuery(String path)
709 {
710 _uri=null;
711 _path=null;
712 _decodedPath=null;
713 _param=null;
714 _fragment=null;
715 if (path!=null)
716 parse(State.PATH,path,0,path.length());
717 }
718
719 /* ------------------------------------------------------------ */
720 public void setQuery(String query)
721 {
722 _query=query;
723 _uri=null;
724 }
725
726 /* ------------------------------------------------------------ */
727 public URI toURI() throws URISyntaxException
728 {
729 return new URI(_scheme,null,_host,_port,_path,_query==null?null:UrlEncoded.decodeString(_query),_fragment);
730 }
731
732 /* ------------------------------------------------------------ */
733 public String getPathQuery()
734 {
735 if (_query==null)
736 return _path;
737 return _path+"?"+_query;
738 }
739
740 /* ------------------------------------------------------------ */
741 public String getAuthority()
742 {
743 if (_port>0)
744 return _host+":"+_port;
745 return _host;
746 }
747
748
749 }