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