1 module requests.uri;
2
3 import std.experimental.logger;
4 import std.array;
5 import std.format;
6 import std.algorithm;
7 import std.conv;
8 import std.typecons;
9 import requests.utils;
10 static import requests.idna;
11
12 class UriException: Exception {
13 this(string msg, string file = __FILE__, size_t line = __LINE__) @safe pure {
14 super(msg, file, line);
15 }
16 }
17
18 struct URI {
19 import std.string;
20 private {
21 string _uri;
22 string _scheme;
23 string _username;
24 string _password;
25 ushort _port=80;
26 string _host;
27 string _path="/";
28 string _query;
29 string _original_host; // can differ from _host if host is unicode
30 }
31 this(string uri) @safe pure {
32 _uri = uri;
33 auto parsed = uri_parse(uri);
34 if ( !parsed ) {
35 throw new UriException("Can't parse uri '" ~ _uri ~ "'");
36 }
37 }
38
39 bool uri_parse(string uri) @safe pure {
40 auto i = uri.findSplit("://");
41 string rest, authority, path_and_query;
42 if ( i[1].length ) {
43 _scheme = i[0].toLower;
44 rest = i[2];
45 } else {
46 return false;
47 }
48 if ( _scheme !in standard_ports ) {
49 return false;
50 }
51 // separate Authority from path and query
52 auto query = rest.indexOf("?");
53 auto path = rest.indexOf("/");
54 // auth/p?q p>0 q>p
55 // auth/p p>0 q=-1
56 // auth?q/p p>0 q<p
57 // auth?q p=-1 q>0
58 // auth p=-1 q=-1
59 if ( path >= 0 ) {
60 if ( query > path || query == -1 ) {
61 // a/p?q or a/p
62 authority = rest[0..path];
63 path_and_query = rest[path+1..$];
64 } else if ( query < path ) {
65 // a?q/p
66 authority = rest[0..query];
67 path_and_query = rest[query..$];
68 }
69 } else {
70 if ( query >= 0) {
71 // auth?q p=-1 q>0
72 authority = rest[0..query];
73 path_and_query = rest[query..$];
74 } else {
75 // auth p=-1 q=-1
76 authority = rest;
77 path_and_query = "";
78 }
79 }
80
81 // find user/password/host:port in authority
82 i = authority.findSplit("@");
83 string up;
84 string hp;
85 if ( i[1].length ) {
86 up = i[0];
87 hp = i[2];
88 } else {
89 hp = i[0];
90 }
91
92 i = hp.findSplit(":");
93 _original_host = i[0];
94 _host = i[0];
95 _port = i[2].length ? to!ushort(i[2]) : standard_ports[_scheme];
96
97 if ( up.length ) {
98 i = up.findSplit(":");
99 _username = i[0];
100 _password = i[2];
101 }
102 // finished with authority
103 // handle path and query
104 if ( path_and_query.length ) {
105 i = path_and_query.findSplit("?");
106 _path = "/" ~ i[0];
107 if ( i[2].length) {
108 _query = "?" ~ i[2];
109 }
110 }
111 //
112 return true;
113 }
114
115 string recalc_uri(Flag!"params" params = Yes.params) const pure @safe {
116 string userinfo;
117 if ( _username ) {
118 userinfo = "%s".format(_username);
119 if ( _password ) {
120 userinfo ~= ":" ~ _password;
121 }
122 userinfo ~= "@";
123 }
124 string r = "%s://%s%s".format(_scheme, userinfo, _host);
125 if ( _scheme !in standard_ports || standard_ports[_scheme] != _port ) {
126 r ~= ":%d".format(_port);
127 }
128 r ~= _path;
129 if ( params == Flag!"params".yes && _query ) {
130 r ~= _query;
131 }
132 return r;
133 }
134 mixin(Getter_Setter!string("scheme"));
135 mixin(Getter_Setter!string("host"));
136 mixin(Getter_Setter!string("username"));
137 mixin(Getter_Setter!string("password"));
138 mixin(Getter_Setter!ushort("port"));
139 mixin(Getter_Setter!string("path"));
140 mixin(Getter("query"));
141 mixin(Getter("original_host"));
142 @property void query(string s) {
143 if ( s[0]=='?' ) {
144 _query = s;
145 }
146 else {
147 _query = "?" ~ s;
148 }
149 }
150 // mixin(setter("scheme"));
151 // mixin(setter("host"));
152 // mixin(setter("username"));
153 // mixin(setter("password"));
154 // mixin(setter("port"));
155 // mixin(setter("path"));
156 // mixin(setter("query"));
157 @property auto uri(Flag!"params" params = Yes.params) pure @safe const {
158 return recalc_uri(params);
159 }
160 @property void uri(string s) @trusted {
161 _uri = s;
162 // auto parsed = uri_grammar(s);
163 // if ( !parsed.successful || parsed.matches.joiner.count != __uri.length) {
164 // throw new UriException("Can't parse uri '" ~ __uri ~ "'");
165 // }
166 // traverseTree(parsed);
167 }
168 void idn_encode() @safe {
169 _host = requests.idna.idn_encode(_original_host);
170 }
171 }
172 unittest {
173 import std.exception;
174 import std.experimental.logger;
175
176 globalLogLevel(LogLevel.info);
177 auto a = URI("http://example.com/");
178 assert(a.scheme == "http");
179 assert(a.host == "example.com");
180 assert(a.path == "/");
181 a = URI("https://igor@example.com:1234");
182 assert(a.scheme == "https");
183 assert(a.host == "example.com");
184 assert(a.username == "igor");
185 assert(a.path == "/");
186 a = URI("https://example.com?a");
187 assert(a.scheme == "https");
188 assert(a.host == "example.com");
189 assert(a.path == "/");
190 assert(a.query == "?a");
191 a = URI("https://example.com?a=/test");
192 assert(a.scheme == "https");
193 assert(a.host == "example.com");
194 assert(a.path == "/");
195 assert(a.query == "?a=/test");
196 a = URI("http://igor:pass;word@example.com:1234/abc?q=x");
197 assert(a.password == "pass;word");
198 assert(a.port == 1234);
199 assert(a.path == "/abc");
200 assert(a.query == "?q=x");
201 assert(a.uri(No.params) == "http://igor:pass;word@example.com:1234/abc",
202 "Expected http://igor:pass;word@example.com:1234/abc, got %s".format(a.uri(No.params)));
203 a.scheme = "https";
204 a.query = "x=y";
205 a.port = 345;
206 auto expected = "https://igor:pass;word@example.com:345/abc?x=y";
207 assert(a.uri == expected, "Expected '%s', got '%s'".format(expected, a.uri));
208 assertThrown!UriException(URI("@unparsable"));
209 a = URI("http://registrera-domän.se");
210 a.idn_encode();
211 assert(a.host == "xn--registrera-domn-elb.se");
212 assertThrown!UriException(URI("cnn://deeplink?section=livetv&subsection=sliver&stream=CNN1"));
213 }
214