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;
42         if ( i[1].length ) {
43             _scheme = i[0].toLower;
44             rest = i[2];
45         } else {
46             return false;
47         }
48         // separate Authority from path and query
49         i = rest.findSplit("/");
50         auto authority = i[0];
51         auto path_and_query = i[2];
52         
53         // find user/password/host:port in authority
54         i = authority.findSplit("@");
55         string up;
56         string hp;
57         if ( i[1].length ) {
58             up = i[0];
59             hp = i[2];
60         } else {
61             hp = i[0];
62         }
63 
64         i = hp.findSplit(":");
65         _original_host = i[0];
66         _host = i[0];
67         _port = i[2].length ? to!ushort(i[2]) : standard_ports[_scheme];
68 
69         if ( up.length ) {
70             i = up.findSplit(":");
71             _username = i[0];
72             _password = i[2];
73         }
74         // finished with authority
75         // handle path and query
76         if ( path_and_query.length ) {
77             i = path_and_query.findSplit("?");
78             _path = "/" ~ i[0];
79             if ( i[2].length) {
80                 _query = "?" ~ i[2];
81             }
82         }
83         //
84         return true;
85     }
86     
87     string recalc_uri(Flag!"params" params = Yes.params) const pure @safe {
88         string userinfo;
89         if ( _username ) {
90             userinfo = "%s".format(_username);
91             if ( _password ) {
92                 userinfo ~= ":" ~ _password;
93             }
94             userinfo ~= "@";
95         }
96         string r = "%s://%s%s".format(_scheme, userinfo, _host);
97         if ( _scheme !in standard_ports || standard_ports[_scheme] != _port ) {
98             r ~= ":%d".format(_port);
99         }
100         r ~= _path;
101         if ( params == Flag!"params".yes && _query ) {
102             r ~= _query;
103         }
104         return r;
105     }
106     mixin(Getter_Setter!string("scheme"));
107     mixin(Getter_Setter!string("host"));
108     mixin(Getter_Setter!string("username"));
109     mixin(Getter_Setter!string("password"));
110     mixin(Getter_Setter!ushort("port"));
111     mixin(Getter_Setter!string("path"));
112     mixin(Getter("query"));
113     mixin(Getter("original_host"));
114     @property void query(string s) {
115         if ( s[0]=='?' ) {
116             _query = s;
117         }
118         else {
119             _query = "?" ~ s;
120         }
121     }
122 //    mixin(setter("scheme"));
123 //    mixin(setter("host"));
124 //    mixin(setter("username"));
125 //    mixin(setter("password"));
126 //    mixin(setter("port"));
127 //    mixin(setter("path"));
128 //    mixin(setter("query"));
129     @property auto uri(Flag!"params" params = Yes.params) pure @safe const {
130         return recalc_uri(params);
131     }
132     @property void uri(string s) @trusted {
133         _uri = s;
134         //        auto parsed = uri_grammar(s);
135         //        if ( !parsed.successful || parsed.matches.joiner.count != __uri.length) {
136         //            throw new UriException("Can't parse uri '" ~ __uri ~ "'");
137         //        }
138         //        traverseTree(parsed);
139     }
140     void idn_encode() @safe {
141         _host = requests.idna.idn_encode(_original_host);
142     }
143 }
144 unittest {
145     import std.exception;
146     import std.experimental.logger;
147     
148     globalLogLevel(LogLevel.info);
149     auto a = URI("http://example.com/");
150     assert(a.scheme == "http");
151     assert(a.host == "example.com");
152     assert(a.path == "/");
153     a = URI("svn+ssh://igor@example.com:1234");
154     assert(a.scheme == "svn+ssh");
155     assert(a.host == "example.com");
156     assert(a.username == "igor");
157     assert(a.path == "/");
158     a = URI("http://igor:pass;word@example.com:1234/abc?q=x");
159     assert(a.password == "pass;word");
160     assert(a.port == 1234);
161     assert(a.path == "/abc");
162     assert(a.query == "?q=x");
163     assert(a.uri(No.params) == "http://igor:pass;word@example.com:1234/abc",
164         "Expected http://igor:pass;word@example.com:1234/abc, got %s".format(a.uri(No.params)));
165     a.scheme = "https";
166     a.query = "x=y";
167     a.port = 345;
168     auto expected = "https://igor:pass;word@example.com:345/abc?x=y";
169     assert(a.uri == expected, "Expected '%s', got '%s'".format(expected, a.uri));
170     assertThrown!UriException(URI("@unparsable"));
171     a = URI("http://registrera-domän.se");
172     a.idn_encode();
173     assert(a.host == "xn--registrera-domn-elb.se");
174 }
175