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