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