xref: /haiku/src/bin/urlwrapper.cpp (revision 1b80286772b529a3d6de3bbeb0720c62e6a32fed)
1 /*
2  * Copyright 2007, Haiku. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  *		François Revol, revol@free.fr
7  */
8 
9 /*
10  * urlwrapper: wraps URL mime types around command line apps
11  * or other apps that don't handle them directly.
12  */
13 #define DEBUG 1
14 
15 #include <Alert.h>
16 #include <Application.h>
17 #include <AppFileInfo.h>
18 #include <Debug.h>
19 #include <Mime.h>
20 #include <Message.h>
21 #include <TypeConstants.h>
22 #include <Roster.h>
23 #include <String.h>
24 
25 #include <ctype.h>
26 #include <stdio.h>
27 #include <unistd.h>
28 
29 /* compile-time configuration */
30 #include "urlwrapper.h"
31 
32 const char *kAppSig = APP_SIGNATURE;
33 const char *kTrackerSig = "application/x-vnd.Be-TRAK";
34 
35 #ifdef __HAIKU__
36 const char *kTerminalSig = "application/x-vnd.Haiku-Terminal";
37 #else
38 const char *kTerminalSig = "application/x-vnd.Be-SHEL";
39 #endif
40 
41 #ifdef HANDLE_BESHARE
42 const char *kBeShareSig = "application/x-vnd.Sugoi-BeShare";
43 #endif
44 
45 #ifdef HANDLE_IM
46 const char *kIMSig = "application/x-vnd.m_eiman.sample_im_client";
47 #endif
48 
49 #ifdef HANDLE_VLC
50 const char *kVLCSig = "application/x-vnd.videolan-vlc";
51 #endif
52 
53 // TODO: make a public BUrl class for use by apps ?
54 class Url : public BString {
55 public:
56 		Url(const char *url) : BString(url) { fStatus = ParseAndSplit(); };
57 		~Url() {};
58 status_t	InitCheck() const { return fStatus; };
59 status_t	ParseAndSplit();
60 
61 bool		HasHost() const { return host.Length(); };
62 bool		HasPort() const { return port.Length(); };
63 bool		HasUser() const { return user.Length(); };
64 bool		HasPass() const { return pass.Length(); };
65 bool		HasPath() const { return path.Length(); };
66 BString		Proto() const { return BString(proto); };
67 BString		Full() const { return BString(full); }; // RFC1738's "sheme-part"
68 BString		Host() const { return BString(host); };
69 BString		Port() const { return BString(port); };
70 BString		User() const { return BString(user); };
71 BString		Pass() const { return BString(pass); };
72 
73 BString		proto;
74 BString		full;
75 BString		host;
76 BString		port;
77 BString		user;
78 BString		pass;
79 BString		path;
80 private:
81 status_t	fStatus;
82 };
83 
84 class UrlWrapperApp : public BApplication
85 {
86 public:
87 	UrlWrapperApp();
88 	~UrlWrapperApp();
89 status_t	SplitUrl(const char *url, BString &host, BString &port, BString &user, BString &pass, BString &path);
90 status_t	UnurlString(BString &s);
91 status_t	Warn(const char *url);
92 virtual void	RefsReceived(BMessage *msg);
93 virtual void	ArgvReceived(int32 argc, char **argv);
94 virtual void	ReadyToRun(void);
95 private:
96 
97 };
98 
99 // proto:[//]user:pass@host:port/path
100 status_t Url::ParseAndSplit()
101 {
102 	int32 v;
103 	BString left;
104 
105 	v = FindFirst(":");
106 	if (v < 0)
107 		return B_BAD_VALUE;
108 
109 	// TODO: proto and host should be lowercased.
110 	// see http://en.wikipedia.org/wiki/URL_normalization
111 
112 	CopyInto(proto, 0, v);
113 	CopyInto(left, v + 1, Length() - v);
114 	// TODO: RFC1738 says the // part should indicate the uri follows the u:p@h:p/path convention, so it should be used to check for special cases.
115 	if (left.FindFirst("//") == 0)
116 		left.RemoveFirst("//");
117 	full = left;
118 
119 	// path part
120 	// actually some apps handle file://[host]/path
121 	// but I have no idea what proto it implies...
122 	// or maybe it's just to emphasize on "localhost".
123 	v = left.FindFirst("/");
124 	if (v == 0 || proto == "file") {
125 		path = left;
126 		return 0;
127 	}
128 	// some protos actually implies path if it's the only component
129 	if ((v < 0) && (proto == "beshare" || proto == "irc")) {
130 		path = left;
131 		return 0;
132 	}
133 
134 	if (v > -1) {
135 		left.MoveInto(path, v+1, left.Length()-v);
136 		left.Remove(v, 1);
137 	}
138 
139 	// user:pass@host
140 	v = left.FindFirst("@");
141 	if (v > -1) {
142 		left.MoveInto(user, 0, v);
143 		left.Remove(0, 1);
144 		v = user.FindFirst(":");
145 		if (v > -1) {
146 			user.MoveInto(pass, v, user.Length() - v);
147 			pass.Remove(0, 1);
148 		}
149 	} else if (proto == "finger") {
150 		// single component implies user
151 		// see also: http://www.subir.com/lynx/lynx_help/lynx_url_support.html
152 		user = left;
153 		return 0;
154 	}
155 
156 	// host:port
157 	v = left.FindFirst(":");
158 	if (v > -1) {
159 		left.MoveInto(port, v + 1, left.Length() - v);
160 		left.Remove(v, 1);
161 	}
162 
163 	// not much left...
164 	host = left;
165 
166 	return 0;
167 }
168 
169 status_t UrlWrapperApp::SplitUrl(const char *url, BString &host, BString &port, BString &user, BString &pass, BString &path)
170 {
171 	Url u(url);
172 	if (u.InitCheck() < 0)
173 		return u.InitCheck();
174 	host = u.host;
175 	port = u.port;
176 	user = u.user;
177 	pass = u.pass;
178 	path = u.path;
179 	return 0;
180 }
181 
182 UrlWrapperApp::UrlWrapperApp() : BApplication(kAppSig)
183 {
184 #if 0
185 	BMimeType mt(B_URL_TELNET);
186 	if (mt.InitCheck())
187 		return;
188 	if (!mt.IsInstalled()) {
189 		mt.Install();
190 	}
191 #endif
192 #if 0
193 	BAppFileInfo afi;
194 	if (!afi.Supports(&mt)) {
195 		//printf("adding support for telnet url\n");
196 		BMessage typemsg;
197 		typemsg.AddString("types", url_mime);
198 		afi.SetSupportedTypes(&typemsg, true);
199 	}
200 #endif
201 }
202 
203 UrlWrapperApp::~UrlWrapperApp()
204 {
205 }
206 
207 
208 
209 status_t UrlWrapperApp::UnurlString(BString &s)
210 {
211 	// TODO: check for %00 and bail out!
212 	int32 length = s.Length();
213 	int i;
214 	for (i = 0; s[i] && i < length - 2; i++) {
215 		if (s[i] == '%' && isxdigit(s[i+1]) && isxdigit(s[i+2])) {
216 			int c;
217 			sscanf(s.String() + i + 1, "%02x", &c);
218 			s.Remove(i, 3);
219 			s.Insert((char)c, 1, i);
220 			length -= 2;
221 		}
222 	}
223 
224 	return B_OK;
225 }
226 
227 status_t UrlWrapperApp::Warn(const char *url)
228 {
229 	BString message("An application has requested the system to open the following url: \n");
230 	message << "\n" << url << "\n\n";
231 	message << "This type of urls has a potential security risk.\n";
232 	message << "Proceed anyway ?";
233 	BAlert *alert = new BAlert("Warning", message.String(), "Ok", "No", NULL, B_WIDTH_AS_USUAL, B_WARNING_ALERT);
234 	int32 v;
235 	v = alert->Go();
236 	if (v == 0)
237 		return B_OK;
238 	return B_ERROR;
239 }
240 
241 void UrlWrapperApp::RefsReceived(BMessage *msg)
242 {
243 	char buff[B_PATH_NAME_LENGTH];
244 	int32 index = 0;
245 	entry_ref ref;
246 	char *args[] = { "urlwrapper", buff, NULL };
247 	status_t err;
248 
249 	while (msg->FindRef("refs", index++, &ref) == B_OK) {
250 		BFile f(&ref, B_READ_ONLY);
251 		BNodeInfo ni(&f);
252 		BString mimetype;
253 		if (f.InitCheck() == B_OK && ni.InitCheck() == B_OK) {
254 			ni.GetType(mimetype.LockBuffer(B_MIME_TYPE_LENGTH));
255 			mimetype.UnlockBuffer();
256 #ifdef HANDLE_URL_FILES
257 			// http://filext.com/file-extension/URL
258 			if (mimetype == "wwwserver/redirection"
259 			 || mimetype == "application/internet-shortcut"
260 			 || mimetype == "application/x-url"
261 			 || mimetype == "message/external-body"
262 			 || mimetype == "text/url"
263 			 || mimetype == "text/x-url") {
264 				// http://www.cyanwerks.com/file-format-url.html
265 				off_t size;
266 				if (f.GetSize(&size) < B_OK)
267 					continue;
268 				BString contents, url;
269 				if (f.ReadAt(0LL, contents.LockBuffer(size), size) < B_OK)
270 					continue;
271 				while (contents.Length()) {
272 					BString line;
273 					int32 cr = contents.FindFirst('\n');
274 					if (cr < 0)
275 						cr = contents.Length();
276 					//contents.MoveInto(line, 0, cr);
277 					contents.CopyInto(line, 0, cr);
278 					contents.Remove(0, cr+1);
279 					line.RemoveAll("\r");
280 					if (!line.Length())
281 						continue;
282 					if (!line.ICompare("URL=", 4)) {
283 						line.MoveInto(url, 4, line.Length());
284 						break;
285 					}
286 				}
287 				if (url.Length()) {
288 					args[1] = (char *)url.String();
289 					//ArgvReceived(2, args);
290 					err = be_roster->Launch("application/x-vnd.Be.URL.http", 1, args+1);
291 					continue;
292 				}
293 			}
294 #endif
295 			/* eat everything as bookmark files */
296 #ifdef HANDLE_BOOKMARK_FILES
297 			if (f.ReadAttr("META:url", B_STRING_TYPE, 0LL, buff, B_PATH_NAME_LENGTH) > 0) {
298 				//ArgvReceived(2, args);
299 				err = be_roster->Launch("application/x-vnd.Be.URL.http", 1, args+1);
300 				continue;
301 			}
302 #endif
303 		}
304 	}
305 }
306 
307 void UrlWrapperApp::ArgvReceived(int32 argc, char **argv)
308 {
309 #if 0
310 	for (int i = 1; i < argc; i++) {
311 		//printf("argv[%d]=%s\n", i, argv[i]);
312 	}
313 #endif
314 	if (argc <= 1)
315 		return;
316 
317 	const char *failc = " || read -p 'Press any key'";
318 	const char *pausec = " ; read -p 'Press any key'";
319 	char *args[] = { "/bin/sh", "-c", NULL, NULL};
320 
321 	Url u(argv[1]);
322 	BString url = u.Full();
323 	if (u.InitCheck() < 0) {
324 		fprintf(stderr, "malformed url: '%s'\n", u.String());
325 		return;
326 	}
327 
328 	// XXX: debug
329 	PRINT(("PROTO='%s'\n", u.proto.String()));
330 	PRINT(("HOST='%s'\n", u.host.String()));
331 	PRINT(("PORT='%s'\n", u.port.String()));
332 	PRINT(("USER='%s'\n", u.user.String()));
333 	PRINT(("PASS='%s'\n", u.pass.String()));
334 	PRINT(("PATH='%s'\n", u.path.String()));
335 
336 	if (u.proto == "telnet") {
337 		BString cmd("telnet ");
338 		if (u.HasUser())
339 			cmd << "-l " << u.user << " ";
340 		cmd << u.host;
341 		if (u.HasPort())
342 			cmd << " " << u.port;
343 		PRINT(("CMD='%s'\n", cmd.String()));
344 		cmd << failc;
345 		args[2] = (char *)cmd.String();
346 		be_roster->Launch(kTerminalSig, 3, args);
347 		return;
348 	}
349 
350 	// see draft: http://tools.ietf.org/wg/secsh/draft-ietf-secsh-scp-sftp-ssh-uri/
351 	if (u.proto == "ssh") {
352 		BString cmd("ssh ");
353 
354 		if (u.HasUser())
355 			cmd << "-l " << u.user << " ";
356 		if (u.HasPort())
357 			cmd << "-oPort=" << u.port << " ";
358 		cmd << u.host;
359 		PRINT(("CMD='%s'\n", cmd.String()));
360 		cmd << failc;
361 		args[2] = (char *)cmd.String();
362 		be_roster->Launch(kTerminalSig, 3, args);
363 		// TODO: handle errors
364 		return;
365 	}
366 
367 	if (u.proto == "ftp") {
368 		BString cmd("ftp ");
369 
370 		/*
371 		if (user.Length())
372 			cmd << "-l " << user << " ";
373 		cmd << host;
374 		*/
375 		cmd << u.full;
376 		PRINT(("CMD='%s'\n", cmd.String()));
377 		cmd << failc;
378 		args[2] = (char *)cmd.String();
379 		be_roster->Launch(kTerminalSig, 3, args);
380 		// TODO: handle errors
381 		return;
382 	}
383 
384 	if (u.proto == "sftp") {
385 		BString cmd("sftp ");
386 
387 		//cmd << url;
388 		if (u.HasPort())
389 			cmd << "-oPort=" << u.port << " ";
390 		if (u.HasUser())
391 			cmd << u.user << "@";
392 		cmd << u.host;
393 		if (u.HasPath())
394 			cmd << ":" << u.path;
395 		PRINT(("CMD='%s'\n", cmd.String()));
396 		cmd << failc;
397 		args[2] = (char *)cmd.String();
398 		be_roster->Launch(kTerminalSig, 3, args);
399 		// TODO: handle errors
400 		return;
401 	}
402 
403 	if (u.proto == "finger") {
404 		BString cmd("/bin/finger ");
405 
406 		if (u.HasUser())
407 			cmd << u.user;
408 		if (u.HasHost() == 0)
409 			u.host = "127.0.0.1";
410 		cmd << "@" << u.host;
411 		PRINT(("CMD='%s'\n", cmd.String()));
412 		cmd << pausec;
413 		args[2] = (char *)cmd.String();
414 		be_roster->Launch(kTerminalSig, 3, args);
415 		// TODO: handle errors
416 		return;
417 	}
418 
419 #ifdef HANDLE_HTTP_WGET
420 	if (u.proto == "http") {
421 		BString cmd("/bin/wget ");
422 
423 		//cmd << url;
424 		if (u.HasUser())
425 			cmd << u.user << "@";
426 		cmd << u.full;
427 		PRINT(("CMD='%s'\n", cmd.String()));
428 		cmd << pausec;
429 		args[2] = (char *)cmd.String();
430 		be_roster->Launch(kTerminalSig, 3, args);
431 		// TODO: handle errors
432 		return;
433 	}
434 #endif
435 
436 #ifdef HANDLE_FILE
437 	if (u.proto == "file") {
438 		BMessage m(B_REFS_RECEIVED);
439 		entry_ref ref;
440 		UnurlString(u.path);
441 		if (get_ref_for_path(u.path.String(), &ref) < B_OK)
442 			return;
443 		m.AddRef("refs", &ref);
444 		be_roster->Launch(kTrackerSig, &m);
445 		return;
446 	}
447 #endif
448 
449 #ifdef HANDLE_QUERY
450 	// XXX:TODO: split options
451 	if (u.proto == "query") {
452 		// mktemp ?
453 		BString qname("/tmp/query-url-temp-");
454 		qname << getpid() << "-" << system_time();
455 		BFile query(qname.String(), O_CREAT|O_EXCL);
456 		// XXX: should check for failure
457 
458 		BString s;
459 		int32 v;
460 
461 		UnurlString(u.full);
462 		// TODO: handle options (list of attrs in the column, ...)
463 
464 		v = 'qybF'; // QuerY By Formula XXX: any #define for that ?
465 		query.WriteAttr("_trk/qryinitmode", B_INT32_TYPE, 0LL, &v, sizeof(v));
466 		s = "TextControl";
467 		query.WriteAttr("_trk/focusedView", B_STRING_TYPE, 0LL, s.String(), s.Length()+1);
468 		s = u.full;
469 		PRINT(("QUERY='%s'\n", s.String()));
470 		query.WriteAttr("_trk/qryinitstr", B_STRING_TYPE, 0LL, s.String(), s.Length()+1);
471 		query.WriteAttr("_trk/qrystr", B_STRING_TYPE, 0LL, s.String(), s.Length()+1);
472 		s = "application/x-vnd.Be-query";
473 		query.WriteAttr("BEOS:TYPE", 'MIMS', 0LL, s.String(), s.Length()+1);
474 
475 
476 		BEntry e(qname.String());
477 		entry_ref er;
478 		if (e.GetRef(&er) >= B_OK)
479 			be_roster->Launch(&er);
480 		return;
481 	}
482 #endif
483 
484 #ifdef HANDLE_SH
485 	if (u.proto == "sh") {
486 		BString cmd(u.Full());
487 		if (Warn(u.String()) != B_OK)
488 			return;
489 		PRINT(("CMD='%s'\n", cmd.String()));
490 		cmd << pausec;
491 		args[2] = (char *)cmd.String();
492 		be_roster->Launch(kTerminalSig, 3, args);
493 		// TODO: handle errors
494 		return;
495 	}
496 #endif
497 
498 #ifdef HANDLE_BESHARE
499 	if (u.proto == "beshare") {
500 		team_id team;
501 		BMessenger msgr(kBeShareSig);
502 		// if no instance is running, or we want a specific server, start it.
503 		if (!msgr.IsValid() || u.HasHost()) {
504 			be_roster->Launch(kBeShareSig, (BMessage *)NULL, &team);
505 			msgr = BMessenger(NULL, team);
506 		}
507 		if (u.HasHost()) {
508 			BMessage mserver('serv');
509 			mserver.AddString("server", u.host);
510 			msgr.SendMessage(&mserver);
511 
512 		}
513 		if (u.HasPath()) {
514 			BMessage mquery('quer');
515 			mquery.AddString("query", u.path);
516 			msgr.SendMessage(&mquery);
517 		}
518 		// TODO: handle errors
519 		return;
520 	}
521 #endif
522 
523 #ifdef HANDLE_IM
524 	if (u.proto == "icq" || u.proto == "msn") {
525 		// TODO
526 		team_id team;
527 		be_roster->Launch(kIMSig, (BMessage *)NULL, &team);
528 		BMessenger msgr(NULL, team);
529 		if (u.HasHost()) {
530 			BMessage mserver(B_REFS_RECEIVED);
531 			mserver.AddString("server", u.host);
532 			msgr.SendMessage(&httpmserver);
533 
534 		}
535 		// TODO: handle errors
536 		return;
537 	}
538 #endif
539 
540 #ifdef HANDLE_VLC
541 	if (u.proto == "mms" || u.proto == "rtp" || u.proto == "rtsp") {
542 		args[0] = "vlc";
543 		args[1] = (char *)u.String();
544 		be_roster->Launch(kVLCSig, 2, args);
545 		return;
546 	}
547 #endif
548 
549 #ifdef HANDLE_AUDIO
550 	// TODO
551 #endif
552 
553 	// vnc: ?
554 	// irc: ?
555 	//
556 	// svn: ?
557 	// cvs: ?
558 	// smb: cifsmount ?
559 	// nfs: mount_nfs ?
560 	//
561 	// mailto: ? but BeMail & Beam both handle it already (not fully though).
562 	//
563 	// itps: pcast: podcast: s//http/ + parse xml to get url to mp3 stream...
564 	// audio: s//http:/ + default MediaPlayer -- see http://forums.winamp.com/showthread.php?threadid=233130
565 	//
566 	// gps: ? I should submit an RFC for that one :)
567 
568 }
569 
570 void UrlWrapperApp::ReadyToRun(void)
571 {
572 	Quit();
573 }
574 
575 int main(int argc, char **argv)
576 {
577 	UrlWrapperApp app;
578 	if (be_app)
579 		app.Run();
580 	return 0;
581 }
582