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", 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 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, 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 227 void 228 UrlWrapper::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 581 status_t 582 UrlWrapper::_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 602 void 603 UrlWrapper::ReadyToRun(void) 604 { 605 Quit(); 606 } 607 608 609 // #pragma mark 610 611 612 int main(int argc, char** argv) 613 { 614 UrlWrapper app; 615 if (be_app) 616 app.Run(); 617 return 0; 618 } 619 620