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