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