xref: /haiku/src/bin/urlwrapper.cpp (revision 865a0be1c022cdd7d155ecb3a44ca2769bce4f60)
1 /*
2  * Copyright 2007-2010 Haiku Inc. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  *		François Revol, revol@free.fr
7  *		Jonas Sundström, jonas@kirilla.com
8  *		Stephan Aßmus <superstippi@gmx.de>
9  */
10 
11 /*
12  * urlwrapper: wraps URL mime types around command line apps
13  * or other apps that don't handle them directly.
14  */
15 #define DEBUG 0
16 
17 #include <ctype.h>
18 #include <stdio.h>
19 #include <unistd.h>
20 
21 #include <Alert.h>
22 #include <Debug.h>
23 #include <NodeInfo.h>
24 #include <Roster.h>
25 #include <String.h>
26 #include <Url.h>
27 
28 #include "urlwrapper.h"
29 
30 
31 const char* kAppSig = "application/x-vnd.Haiku-urlwrapper";
32 const char* kTrackerSig = "application/x-vnd.Be-TRAK";
33 const char* kTerminalSig = "application/x-vnd.Haiku-Terminal";
34 const char* kBeShareSig = "application/x-vnd.Sugoi-BeShare";
35 const char* kIMSig = "application/x-vnd.m_eiman.sample_im_client";
36 const char* kVLCSig = "application/x-vnd.videolan-vlc";
37 
38 const char* kURLHandlerSigBase = "application/x-vnd.Be.URL.";
39 
40 
41 UrlWrapper::UrlWrapper() : BApplication(kAppSig)
42 {
43 }
44 
45 
46 UrlWrapper::~UrlWrapper()
47 {
48 }
49 
50 
51 status_t
52 UrlWrapper::_Warn(const char* url)
53 {
54 	BString message("An application has requested the system to open the "
55 		"following url: \n");
56 	message << "\n" << url << "\n\n";
57 	message << "This type of URL has a potential security risk.\n";
58 	message << "Proceed anyway?";
59 	BAlert* alert = new BAlert("Warning", message.String(), "Proceed", "Stop", NULL,
60 		B_WIDTH_AS_USUAL, B_WARNING_ALERT);
61 	int32 button;
62 	button = alert->Go();
63 	if (button == 0)
64 		return B_OK;
65 
66 	return B_ERROR;
67 }
68 
69 
70 void
71 UrlWrapper::RefsReceived(BMessage* msg)
72 {
73 	char buff[B_PATH_NAME_LENGTH];
74 	int32 index = 0;
75 	entry_ref ref;
76 	char* args[] = { const_cast<char*>("urlwrapper"), buff, NULL };
77 	status_t err;
78 
79 	while (msg->FindRef("refs", index++, &ref) == B_OK) {
80 		BFile f(&ref, B_READ_ONLY);
81 		BNodeInfo ni(&f);
82 		BString mimetype;
83 		BString extension(ref.name);
84 		extension.Remove(0, extension.FindLast('.') + 1);
85 		if (f.InitCheck() == B_OK && ni.InitCheck() == B_OK) {
86 			ni.GetType(mimetype.LockBuffer(B_MIME_TYPE_LENGTH));
87 			mimetype.UnlockBuffer();
88 
89 			// Internet Explorer Shortcut
90 			if (mimetype == "text/x-url" || extension == "url") {
91 				// http://filext.com/file-extension/URL
92 				// http://www.cyanwerks.com/file-format-url.html
93 				off_t size;
94 				if (f.GetSize(&size) < B_OK)
95 					continue;
96 				BString contents;
97 				BString url;
98 				if (f.ReadAt(0LL, contents.LockBuffer(size), size) < B_OK)
99 					continue;
100 				contents.UnlockBuffer();
101 				while (contents.Length()) {
102 					BString line;
103 					int32 cr = contents.FindFirst('\n');
104 					if (cr < 0)
105 						cr = contents.Length();
106 					//contents.MoveInto(line, 0, cr);
107 					contents.CopyInto(line, 0, cr);
108 					contents.Remove(0, cr+1);
109 					line.RemoveAll("\r");
110 					if (!line.Length())
111 						continue;
112 					if (!line.ICompare("URL=", 4)) {
113 						line.MoveInto(url, 4, line.Length());
114 						break;
115 					}
116 				}
117 				if (url.Length()) {
118 					BPrivate::Support::BUrl u(url.String());
119 					args[1] = (char*)u.String();
120 					mimetype = kURLHandlerSigBase;
121 					mimetype += u.Proto();
122 					err = be_roster->Launch(mimetype.String(), 1, args + 1);
123 					if (err != B_OK && err != B_ALREADY_RUNNING)
124 						err = be_roster->Launch(kAppSig, 1, args + 1);
125 					continue;
126 				}
127 			}
128 			if (mimetype == "text/x-webloc" || extension == "webloc") {
129 				// OSX url shortcuts
130 				// XML file + resource fork
131 				off_t size;
132 				if (f.GetSize(&size) < B_OK)
133 					continue;
134 				BString contents;
135 				BString url;
136 				if (f.ReadAt(0LL, contents.LockBuffer(size), size) < B_OK)
137 					continue;
138 				contents.UnlockBuffer();
139 				int state = 0;
140 				while (contents.Length()) {
141 					BString line;
142 					int32 cr = contents.FindFirst('\n');
143 					if (cr < 0)
144 						cr = contents.Length();
145 					contents.CopyInto(line, 0, cr);
146 					contents.Remove(0, cr+1);
147 					line.RemoveAll("\r");
148 					if (!line.Length())
149 						continue;
150 					int32 s, e;
151 					switch (state) {
152 						case 0:
153 							if (!line.ICompare("<?xml", 5))
154 								state = 1;
155 							break;
156 						case 1:
157 							if (!line.ICompare("<plist", 6))
158 								state = 2;
159 							break;
160 						case 2:
161 							if (!line.ICompare("<dict>", 6))
162 								state = 3;
163 							break;
164 						case 3:
165 							if (line.IFindFirst("<key>URL</key>") > -1)
166 								state = 4;
167 							break;
168 						case 4:
169 							if ((s = line.IFindFirst("<string>")) > -1
170 								&& (e = line.IFindFirst("</string>")) > s) {
171 								state = 5;
172 								s += 8;
173 								line.MoveInto(url, s, e - s);
174 								break;
175 							} else
176 								state = 3;
177 							break;
178 						default:
179 							break;
180 					}
181 					if (state == 5) {
182 						break;
183 					}
184 				}
185 				if (url.Length()) {
186 					BPrivate::Support::BUrl u(url.String());
187 					args[1] = (char*)u.String();
188 					mimetype = kURLHandlerSigBase;
189 					mimetype += u.Proto();
190 					err = be_roster->Launch(mimetype.String(), 1, args + 1);
191 					if (err != B_OK && err != B_ALREADY_RUNNING)
192 						err = be_roster->Launch(kAppSig, 1, args + 1);
193 					continue;
194 				}
195 			}
196 
197 			// NetPositive Bookmark or any file with a META:url attribute
198 			if (f.ReadAttr("META:url", B_STRING_TYPE, 0LL, buff,
199 				B_PATH_NAME_LENGTH) > 0) {
200 				BPrivate::Support::BUrl u(buff);
201 				args[1] = (char*)u.String();
202 				mimetype = kURLHandlerSigBase;
203 				mimetype += u.Proto();
204 				err = be_roster->Launch(mimetype.String(), 1, args + 1);
205 				if (err != B_OK && err != B_ALREADY_RUNNING)
206 					err = be_roster->Launch(kAppSig, 1, args + 1);
207 				continue;
208 			}
209 		}
210 	}
211 }
212 
213 
214 void
215 UrlWrapper::ArgvReceived(int32 argc, char** argv)
216 {
217 	if (argc <= 1)
218 		return;
219 
220 	const char* failc = " || read -p 'Press any key'";
221 	const char* pausec = " ; read -p 'Press any key'";
222 	char* args[] = { (char *)"/bin/sh", (char *)"-c", NULL, NULL};
223 
224 	BPrivate::Support::BUrl url(argv[1]);
225 
226 	BString full = url.Full();
227 	BString proto = url.Proto();
228 	BString host = url.Host();
229 	BString port = url.Port();
230 	BString user = url.User();
231 	BString pass = url.Pass();
232 	BString path = url.Path();
233 
234 	if (url.InitCheck() < 0) {
235 		fprintf(stderr, "malformed url: '%s'\n", url.String());
236 		return;
237 	}
238 
239 	// XXX: debug
240 	PRINT(("PROTO='%s'\n", proto.String()));
241 	PRINT(("HOST='%s'\n", host.String()));
242 	PRINT(("PORT='%s'\n", port.String()));
243 	PRINT(("USER='%s'\n", user.String()));
244 	PRINT(("PASS='%s'\n", pass.String()));
245 	PRINT(("PATH='%s'\n", path.String()));
246 
247 	if (proto == "about") {
248 		app_info info;
249 		BString sig;
250 		// BUrl could get an accessor for the full - proto part...
251 		sig = host << "/" << path;
252 		BMessage msg(B_ABOUT_REQUESTED);
253 		if (be_roster->GetAppInfo(sig.String(), &info) == B_OK) {
254 			BMessenger msgr(sig.String());
255 			msgr.SendMessage(&msg);
256 			return;
257 		}
258 		if (be_roster->Launch(sig.String(), &msg) == B_OK)
259 			return;
260 		be_roster->Launch("application/x-vnd.Haiku-About");
261 		return;
262 	}
263 
264 	if (proto == "telnet") {
265 		BString cmd("telnet ");
266 		if (url.HasUser())
267 			cmd << "-l " << user << " ";
268 		cmd << host;
269 		if (url.HasPort())
270 			cmd << " " << port;
271 		PRINT(("CMD='%s'\n", cmd.String()));
272 		cmd << failc;
273 		args[2] = (char*)cmd.String();
274 		be_roster->Launch(kTerminalSig, 3, args);
275 		return;
276 	}
277 
278 	// see draft:
279 	// http://tools.ietf.org/wg/secsh/draft-ietf-secsh-scp-sftp-ssh-uri/
280 	if (proto == "ssh") {
281 		BString cmd("ssh ");
282 
283 		if (url.HasUser())
284 			cmd << "-l " << user << " ";
285 		if (url.HasPort())
286 			cmd << "-oPort=" << port << " ";
287 		cmd << host;
288 		PRINT(("CMD='%s'\n", cmd.String()));
289 		cmd << failc;
290 		args[2] = (char*)cmd.String();
291 		be_roster->Launch(kTerminalSig, 3, args);
292 		// TODO: handle errors
293 		return;
294 	}
295 
296 	if (proto == "ftp") {
297 		BString cmd("ftp ");
298 
299 		cmd << proto << "://";
300 		/*
301 		if (user.Length())
302 			cmd << "-l " << user << " ";
303 		cmd << host;
304 		*/
305 		cmd << full;
306 		PRINT(("CMD='%s'\n", cmd.String()));
307 		cmd << failc;
308 		args[2] = (char*)cmd.String();
309 		be_roster->Launch(kTerminalSig, 3, args);
310 		// TODO: handle errors
311 		return;
312 	}
313 
314 	if (proto == "sftp") {
315 		BString cmd("sftp ");
316 
317 		//cmd << url;
318 		if (url.HasPort())
319 			cmd << "-oPort=" << port << " ";
320 		if (url.HasUser())
321 			cmd << user << "@";
322 		cmd << host;
323 		if (url.HasPath())
324 			cmd << ":" << path;
325 		PRINT(("CMD='%s'\n", cmd.String()));
326 		cmd << failc;
327 		args[2] = (char*)cmd.String();
328 		be_roster->Launch(kTerminalSig, 3, args);
329 		// TODO: handle errors
330 		return;
331 	}
332 
333 	if (proto == "finger") {
334 		BString cmd("/bin/finger ");
335 
336 		if (url.HasUser())
337 			cmd << user;
338 		if (url.HasHost() == 0)
339 			host = "127.0.0.1";
340 		cmd << "@" << host;
341 		PRINT(("CMD='%s'\n", cmd.String()));
342 		cmd << pausec;
343 		args[2] = (char*)cmd.String();
344 		be_roster->Launch(kTerminalSig, 3, args);
345 		// TODO: handle errors
346 		return;
347 	}
348 
349 	if (proto == "http" || proto == "https" /*|| proto == "ftp"*/) {
350 		BString cmd("/bin/wget ");
351 
352 		//cmd << url;
353 		cmd << proto << "://";
354 		if (url.HasUser())
355 			cmd << user << "@";
356 		cmd << full;
357 		PRINT(("CMD='%s'\n", cmd.String()));
358 		cmd << pausec;
359 		args[2] = (char*)cmd.String();
360 		be_roster->Launch(kTerminalSig, 3, args);
361 		// TODO: handle errors
362 		return;
363 	}
364 
365 	if (proto == "file") {
366 		BMessage m(B_REFS_RECEIVED);
367 		entry_ref ref;
368 		_DecodeUrlString(path);
369 		if (get_ref_for_path(path.String(), &ref) < B_OK)
370 			return;
371 		m.AddRef("refs", &ref);
372 		be_roster->Launch(kTrackerSig, &m);
373 		return;
374 	}
375 
376 	// XXX:TODO: split options
377 	if (proto == "query") {
378 		// mktemp ?
379 		BString qname("/tmp/query-url-temp-");
380 		qname << getpid() << "-" << system_time();
381 		BFile query(qname.String(), O_CREAT|O_EXCL);
382 		// XXX: should check for failure
383 
384 		BString s;
385 		int32 v;
386 
387 		_DecodeUrlString(full);
388 		// TODO: handle options (list of attrs in the column, ...)
389 
390 		v = 'qybF'; // QuerY By Formula XXX: any #define for that ?
391 		query.WriteAttr("_trk/qryinitmode", B_INT32_TYPE, 0LL, &v, sizeof(v));
392 		s = "TextControl";
393 		query.WriteAttr("_trk/focusedView", B_STRING_TYPE, 0LL, s.String(),
394 			s.Length()+1);
395 		s = full;
396 		PRINT(("QUERY='%s'\n", s.String()));
397 		query.WriteAttr("_trk/qryinitstr", B_STRING_TYPE, 0LL, s.String(),
398 			s.Length()+1);
399 		query.WriteAttr("_trk/qrystr", B_STRING_TYPE, 0LL, s.String(),
400 			s.Length()+1);
401 		s = "application/x-vnd.Be-query";
402 		query.WriteAttr("BEOS:TYPE", 'MIMS', 0LL, s.String(), s.Length()+1);
403 
404 
405 		BEntry e(qname.String());
406 		entry_ref er;
407 		if (e.GetRef(&er) >= B_OK)
408 			be_roster->Launch(&er);
409 		return;
410 	}
411 
412 	if (proto == "sh") {
413 		BString cmd(full);
414 		if (_Warn(url.String()) != B_OK)
415 			return;
416 		PRINT(("CMD='%s'\n", cmd.String()));
417 		cmd << pausec;
418 		args[2] = (char*)cmd.String();
419 		be_roster->Launch(kTerminalSig, 3, args);
420 		// TODO: handle errors
421 		return;
422 	}
423 
424 	if (proto == "beshare") {
425 		team_id team;
426 		BMessenger msgr(kBeShareSig);
427 		// if no instance is running, or we want a specific server, start it.
428 		if (!msgr.IsValid() || url.HasHost()) {
429 			be_roster->Launch(kBeShareSig, (BMessage*)NULL, &team);
430 			msgr = BMessenger(NULL, team);
431 		}
432 		if (url.HasHost()) {
433 			BMessage mserver('serv');
434 			mserver.AddString("server", host);
435 			msgr.SendMessage(&mserver);
436 
437 		}
438 		if (url.HasPath()) {
439 			BMessage mquery('quer');
440 			mquery.AddString("query", path);
441 			msgr.SendMessage(&mquery);
442 		}
443 		// TODO: handle errors
444 		return;
445 	}
446 
447 	if (proto == "icq" || proto == "msn") {
448 		// TODO
449 		team_id team;
450 		be_roster->Launch(kIMSig, (BMessage*)NULL, &team);
451 		BMessenger msgr(NULL, team);
452 		if (url.HasHost()) {
453 			BMessage mserver(B_REFS_RECEIVED);
454 			mserver.AddString("server", host);
455 			msgr.SendMessage(&mserver);
456 
457 		}
458 		// TODO: handle errors
459 		return;
460 	}
461 
462 	if (proto == "mms" || proto == "rtp" || proto == "rtsp") {
463 		args[0] = (char*)url.String();
464 		be_roster->Launch(kVLCSig, 1, args);
465 		return;
466 	}
467 
468 
469 	/*
470 
471 	More ?
472 	cf. http://en.wikipedia.org/wiki/URI_scheme
473 	cf. http://www.iana.org/assignments/uri-schemes.html
474 
475 	Audio: (SoundPlay specific, identical to http:// to a shoutcast server)
476 
477 	vnc: ?
478 	irc: ?
479 	im: http://tools.ietf.org/html/rfc3860
480 
481 	svn: handled by checkitout
482 	cvs: handled by checkitout
483 	git: handled by checkitout
484 	rsync: handled by checkitout - http://tools.ietf.org/html/rfc5781
485 
486 	smb: cifsmount ?
487 	nfs: mount_nfs ? http://tools.ietf.org/html/rfc2224
488 	ipp: http://tools.ietf.org/html/rfc3510
489 
490 	mailto: ? Mail & Beam both handle it already (not fully though).
491 	imap: to describe mail accounts ? http://tools.ietf.org/html/rfc5092
492 	pop: http://tools.ietf.org/html/rfc2384
493 	mid: cid: as per RFC 2392
494 	http://www.rfc-editor.org/rfc/rfc2392.txt query MAIL:cid
495 	message:<MID> http://daringfireball.net/2007/12/message_urls_leopard_mail
496 
497 	itps: pcast: podcast: s//http/ + parse xml to get url to mp3 stream...
498 	audio: s//http:/ + default MediaPlayer
499 	-- see http://forums.winamp.com/showthread.php?threadid=233130
500 
501 	gps: ? I should submit an RFC for that one :)
502 
503 	webcal: (is http: to .ics file)
504 
505 	data: (but it's dangerous)
506 
507 	*/
508 
509 
510 }
511 
512 
513 status_t
514 UrlWrapper::_DecodeUrlString(BString& string)
515 {
516 	// TODO: check for %00 and bail out!
517 	int32 length = string.Length();
518 	int i;
519 	for (i = 0; string[i] && i < length - 2; i++) {
520 		if (string[i] == '%' && isxdigit(string[i+1])
521 			&& isxdigit(string[i+2])) {
522 			int c;
523 			sscanf(string.String() + i + 1, "%02x", &c);
524 			string.Remove(i, 3);
525 			string.Insert((char)c, 1, i);
526 			length -= 2;
527 		}
528 	}
529 
530 	return B_OK;
531 }
532 
533 
534 void
535 UrlWrapper::ReadyToRun(void)
536 {
537 	Quit();
538 }
539 
540 
541 // #pragma mark
542 
543 
544 int main(int argc, char** argv)
545 {
546 	UrlWrapper app;
547 	if (be_app)
548 		app.Run();
549 	return 0;
550 }
551 
552