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