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