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