1 /**
2 	URL parsing routines.
3 
4 	Copyright: © 2012 rejectedsoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dub.internal.vibecompat.inet.url;
9 
10 public import dub.internal.vibecompat.inet.path;
11 
12 version (Have_vibe_d) public import vibe.inet.url;
13 else:
14 
15 import std.algorithm;
16 import std.array;
17 import std.conv;
18 import std.exception;
19 import std..string;
20 import std.uri;
21 
22 
23 /**
24 	Represents a URL decomposed into its components.
25 */
26 struct URL {
27 	private {
28 		string m_schema;
29 		string m_pathString;
30 		Path m_path;
31 		string m_host;
32 		ushort m_port;
33 		string m_username;
34 		string m_password;
35 		string m_queryString;
36 		string m_anchor;
37 	}
38 
39 	/// Constructs a new URL object from its components.
40 	this(string schema, string host, ushort port, Path path)
41 	{
42 		m_schema = schema;
43 		m_host = host;
44 		m_port = port;
45 		m_path = path;
46 		m_pathString = path.toString();
47 	}
48 	/// ditto
49 	this(string schema, Path path)
50 	{
51 		this(schema, null, 0, path);
52 	}
53 
54 	/** Constructs a URL from its string representation.
55 
56 		TODO: additional validation required (e.g. valid host and user names and port)
57 	*/
58 	this(string url_string)
59 	{
60 		auto str = url_string;
61 		enforce(str.length > 0, "Empty URL.");
62 		if( str[0] != '/' ){
63 			auto idx = str.countUntil(':');
64 			enforce(idx > 0, "No schema in URL:"~str);
65 			m_schema = str[0 .. idx];
66 			str = str[idx+1 .. $];
67 			bool requires_host = false;
68 
69 			switch(m_schema){
70 				case "http":
71 				case "https":
72 				case "ftp":
73 				case "spdy":
74 				case "sftp":
75 				case "file":
76 					// proto://server/path style
77 					enforce(str.startsWith("//"), "URL must start with proto://...");
78 					requires_host = true;
79 					str = str[2 .. $];
80 					goto default;
81 				default:
82 					auto si = str.countUntil('/');
83 					if( si < 0 ) si = str.length;
84 					auto ai = str[0 .. si].countUntil('@');
85 					sizediff_t hs = 0;
86 					if( ai >= 0 ){
87 						hs = ai+1;
88 						auto ci = str[0 .. ai].countUntil(':');
89 						if( ci >= 0 ){
90 							m_username = str[0 .. ci];
91 							m_password = str[ci+1 .. ai];
92 						} else m_username = str[0 .. ai];
93 						enforce(m_username.length > 0, "Empty user name in URL.");
94 					}
95 
96 					m_host = str[hs .. si];
97 					auto pi = m_host.countUntil(':');
98 					if(pi > 0) {
99 						enforce(pi < m_host.length-1, "Empty port in URL.");
100 						m_port = to!ushort(m_host[pi+1..$]);
101 						m_host = m_host[0 .. pi];
102 					}
103 
104 					enforce(!requires_host || m_schema == "file" || m_host.length > 0,
105 							"Empty server name in URL.");
106 					str = str[si .. $];
107 			}
108 		}
109 
110 		this.localURI = str;
111 	}
112 	/// ditto
113 	static URL parse(string url_string)
114 	{
115 		return URL(url_string);
116 	}
117 
118 	/// The schema/protocol part of the URL
119 	@property string schema() const { return m_schema; }
120 	/// ditto
121 	@property void schema(string v) { m_schema = v; }
122 
123 	/// The path part of the URL in the original string form
124 	@property string pathString() const { return m_pathString; }
125 
126 	/// The path part of the URL
127 	@property Path path() const { return m_path; }
128 	/// ditto
129 	@property void path(Path p)
130 	{
131 		m_path = p;
132 		auto pstr = p.toString();
133 		m_pathString = pstr;
134 	}
135 
136 	/// The host part of the URL (depends on the schema)
137 	@property string host() const { return m_host; }
138 	/// ditto
139 	@property void host(string v) { m_host = v; }
140 
141 	/// The port part of the URL (optional)
142 	@property ushort port() const { return m_port; }
143 	/// ditto
144 	@property port(ushort v) { m_port = v; }
145 
146 	/// The user name part of the URL (optional)
147 	@property string username() const { return m_username; }
148 	/// ditto
149 	@property void username(string v) { m_username = v; }
150 
151 	/// The password part of the URL (optional)
152 	@property string password() const { return m_password; }
153 	/// ditto
154 	@property void password(string v) { m_password = v; }
155 
156 	/// The query string part of the URL (optional)
157 	@property string queryString() const { return m_queryString; }
158 	/// ditto
159 	@property void queryString(string v) { m_queryString = v; }
160 
161 	/// The anchor part of the URL (optional)
162 	@property string anchor() const { return m_anchor; }
163 
164 	/// The path part plus query string and anchor
165 	@property string localURI()
166 	const {
167 		auto str = appender!string();
168 		str.reserve(m_pathString.length + 2 + queryString.length + anchor.length);
169 		str.put(encode(path.toString()));
170 		if( queryString.length ) {
171 			str.put("?");
172 			str.put(queryString);
173 		}
174 		if( anchor.length ) {
175 			str.put("#");
176 			str.put(anchor);
177 		}
178 		return str.data;
179 	}
180 	/// ditto
181 	@property void localURI(string str)
182 	{
183 		auto ai = str.countUntil('#');
184 		if( ai >= 0 ){
185 			m_anchor = str[ai+1 .. $];
186 			str = str[0 .. ai];
187 		}
188 
189 		auto qi = str.countUntil('?');
190 		if( qi >= 0 ){
191 			m_queryString = str[qi+1 .. $];
192 			str = str[0 .. qi];
193 		}
194 
195 		m_pathString = str;
196 		m_path = Path(decode(str));
197 	}
198 
199 	/// The URL to the parent path with query string and anchor stripped.
200 	@property URL parentURL() const {
201 		URL ret;
202 		ret.schema = schema;
203 		ret.host = host;
204 		ret.port = port;
205 		ret.username = username;
206 		ret.password = password;
207 		ret.path = path.parentPath;
208 		return ret;
209 	}
210 
211 	/// Converts this URL object to its string representation.
212 	string toString()
213 	const {
214 		import std.format;
215 		auto dst = appender!string();
216 		dst.put(schema);
217 		dst.put(":");
218 		switch(schema){
219 			default: break;
220 			case "file":
221 			case "http":
222 			case "https":
223 			case "ftp":
224 			case "spdy":
225 			case "sftp":
226 				dst.put("//");
227 				break;
228 		}
229 		dst.put(host);
230 		if( m_port > 0 ) formattedWrite(dst, ":%d", m_port);
231 		dst.put(localURI);
232 		return dst.data;
233 	}
234 
235 	bool startsWith(const URL rhs) const {
236 		if( m_schema != rhs.m_schema ) return false;
237 		if( m_host != rhs.m_host ) return false;
238 		// FIXME: also consider user, port, querystring, anchor etc
239 		return path.startsWith(rhs.m_path);
240 	}
241 
242 	URL opBinary(string OP)(Path rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); }
243 	URL opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); }
244 	void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { m_path ~= rhs; }
245 	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; }
246 
247 	/// Tests two URLs for equality using '=='.
248 	bool opEquals(ref const URL rhs) const {
249 		if( m_schema != rhs.m_schema ) return false;
250 		if( m_host != rhs.m_host ) return false;
251 		if( m_path != rhs.m_path ) return false;
252 		return true;
253 	}
254 	/// ditto
255 	bool opEquals(const URL other) const { return opEquals(other); }
256 
257 	int opCmp(ref const URL rhs) const {
258 		if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema);
259 		if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host);
260 		if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path);
261 		return true;
262 	}
263 }
264 
265 unittest {
266 	auto url = URL.parse("https://www.example.net/index.html");
267 	assert(url.schema == "https", url.schema);
268 	assert(url.host == "www.example.net", url.host);
269 	assert(url.path == Path("/index.html"), url.path.toString());
270 
271 	url = URL.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor");
272 	assert(url.schema == "http", url.schema);
273 	assert(url.username == "jo.doe", url.username);
274 	assert(url.password == "password", url.password);
275 	assert(url.port == 4711, to!string(url.port));
276 	assert(url.host == "sub.www.example.net", url.host);
277 	assert(url.path.toString() == "/sub2/index.html", url.path.toString());
278 	assert(url.queryString == "query", url.queryString);
279 	assert(url.anchor == "anchor", url.anchor);
280 }