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 <Alert.h> 22 #include <Debug.h> 23 #include <NodeInfo.h> 24 #include <Roster.h> 25 #include <String.h> 26 #include <Url.h> 27 28 #include "urlwrapper.h" 29 30 31 const char* kAppSig = "application/x-vnd.Haiku-urlwrapper"; 32 const char* kTrackerSig = "application/x-vnd.Be-TRAK"; 33 const char* kTerminalSig = "application/x-vnd.Haiku-Terminal"; 34 const char* kBeShareSig = "application/x-vnd.Sugoi-BeShare"; 35 const char* kIMSig = "application/x-vnd.m_eiman.sample_im_client"; 36 const char* kVLCSig = "application/x-vnd.videolan-vlc"; 37 38 const char* kURLHandlerSigBase = "application/x-vnd.Be.URL."; 39 40 41 UrlWrapper::UrlWrapper() : BApplication(kAppSig) 42 { 43 } 44 45 46 UrlWrapper::~UrlWrapper() 47 { 48 } 49 50 51 status_t 52 UrlWrapper::_Warn(const char* url) 53 { 54 BString message("An application has requested the system to open the " 55 "following url: \n"); 56 message << "\n" << url << "\n\n"; 57 message << "This type of URL has a potential security risk.\n"; 58 message << "Proceed anyway?"; 59 BAlert* alert = new BAlert("Warning", message.String(), "Proceed", "Stop", NULL, 60 B_WIDTH_AS_USUAL, B_WARNING_ALERT); 61 int32 button; 62 button = alert->Go(); 63 if (button == 0) 64 return B_OK; 65 66 return B_ERROR; 67 } 68 69 70 void 71 UrlWrapper::RefsReceived(BMessage* msg) 72 { 73 char buff[B_PATH_NAME_LENGTH]; 74 int32 index = 0; 75 entry_ref ref; 76 char* args[] = { const_cast<char*>("urlwrapper"), buff, NULL }; 77 status_t err; 78 79 while (msg->FindRef("refs", index++, &ref) == B_OK) { 80 BFile f(&ref, B_READ_ONLY); 81 BNodeInfo ni(&f); 82 BString mimetype; 83 BString extension(ref.name); 84 extension.Remove(0, extension.FindLast('.') + 1); 85 if (f.InitCheck() == B_OK && ni.InitCheck() == B_OK) { 86 ni.GetType(mimetype.LockBuffer(B_MIME_TYPE_LENGTH)); 87 mimetype.UnlockBuffer(); 88 89 // Internet Explorer Shortcut 90 if (mimetype == "text/x-url" || extension == "url") { 91 // http://filext.com/file-extension/URL 92 // http://www.cyanwerks.com/file-format-url.html 93 off_t size; 94 if (f.GetSize(&size) < B_OK) 95 continue; 96 BString contents; 97 BString url; 98 if (f.ReadAt(0LL, contents.LockBuffer(size), size) < B_OK) 99 continue; 100 contents.UnlockBuffer(); 101 while (contents.Length()) { 102 BString line; 103 int32 cr = contents.FindFirst('\n'); 104 if (cr < 0) 105 cr = contents.Length(); 106 //contents.MoveInto(line, 0, cr); 107 contents.CopyInto(line, 0, cr); 108 contents.Remove(0, cr+1); 109 line.RemoveAll("\r"); 110 if (!line.Length()) 111 continue; 112 if (!line.ICompare("URL=", 4)) { 113 line.MoveInto(url, 4, line.Length()); 114 break; 115 } 116 } 117 if (url.Length()) { 118 BPrivate::Support::BUrl u(url.String()); 119 args[1] = (char*)u.String(); 120 mimetype = kURLHandlerSigBase; 121 mimetype += u.Proto(); 122 err = be_roster->Launch(mimetype.String(), 1, args + 1); 123 if (err != B_OK && err != B_ALREADY_RUNNING) 124 err = be_roster->Launch(kAppSig, 1, args + 1); 125 continue; 126 } 127 } 128 if (mimetype == "text/x-webloc" || extension == "webloc") { 129 // OSX url shortcuts 130 // XML file + resource fork 131 off_t size; 132 if (f.GetSize(&size) < B_OK) 133 continue; 134 BString contents; 135 BString url; 136 if (f.ReadAt(0LL, contents.LockBuffer(size), size) < B_OK) 137 continue; 138 contents.UnlockBuffer(); 139 int state = 0; 140 while (contents.Length()) { 141 BString line; 142 int32 cr = contents.FindFirst('\n'); 143 if (cr < 0) 144 cr = contents.Length(); 145 contents.CopyInto(line, 0, cr); 146 contents.Remove(0, cr+1); 147 line.RemoveAll("\r"); 148 if (!line.Length()) 149 continue; 150 int32 s, e; 151 switch (state) { 152 case 0: 153 if (!line.ICompare("<?xml", 5)) 154 state = 1; 155 break; 156 case 1: 157 if (!line.ICompare("<plist", 6)) 158 state = 2; 159 break; 160 case 2: 161 if (!line.ICompare("<dict>", 6)) 162 state = 3; 163 break; 164 case 3: 165 if (line.IFindFirst("<key>URL</key>") > -1) 166 state = 4; 167 break; 168 case 4: 169 if ((s = line.IFindFirst("<string>")) > -1 170 && (e = line.IFindFirst("</string>")) > s) { 171 state = 5; 172 s += 8; 173 line.MoveInto(url, s, e - s); 174 break; 175 } else 176 state = 3; 177 break; 178 default: 179 break; 180 } 181 if (state == 5) { 182 break; 183 } 184 } 185 if (url.Length()) { 186 BPrivate::Support::BUrl u(url.String()); 187 args[1] = (char*)u.String(); 188 mimetype = kURLHandlerSigBase; 189 mimetype += u.Proto(); 190 err = be_roster->Launch(mimetype.String(), 1, args + 1); 191 if (err != B_OK && err != B_ALREADY_RUNNING) 192 err = be_roster->Launch(kAppSig, 1, args + 1); 193 continue; 194 } 195 } 196 197 // NetPositive Bookmark or any file with a META:url attribute 198 if (f.ReadAttr("META:url", B_STRING_TYPE, 0LL, buff, 199 B_PATH_NAME_LENGTH) > 0) { 200 BPrivate::Support::BUrl u(buff); 201 args[1] = (char*)u.String(); 202 mimetype = kURLHandlerSigBase; 203 mimetype += u.Proto(); 204 err = be_roster->Launch(mimetype.String(), 1, args + 1); 205 if (err != B_OK && err != B_ALREADY_RUNNING) 206 err = be_roster->Launch(kAppSig, 1, args + 1); 207 continue; 208 } 209 } 210 } 211 } 212 213 214 void 215 UrlWrapper::ArgvReceived(int32 argc, char** argv) 216 { 217 if (argc <= 1) 218 return; 219 220 const char* failc = " || read -p 'Press any key'"; 221 const char* pausec = " ; read -p 'Press any key'"; 222 char* args[] = { (char *)"/bin/sh", (char *)"-c", NULL, NULL}; 223 224 BPrivate::Support::BUrl url(argv[1]); 225 226 BString full = url.Full(); 227 BString proto = url.Proto(); 228 BString host = url.Host(); 229 BString port = url.Port(); 230 BString user = url.User(); 231 BString pass = url.Pass(); 232 BString path = url.Path(); 233 234 if (url.InitCheck() < 0) { 235 fprintf(stderr, "malformed url: '%s'\n", url.String()); 236 return; 237 } 238 239 // XXX: debug 240 PRINT(("PROTO='%s'\n", proto.String())); 241 PRINT(("HOST='%s'\n", host.String())); 242 PRINT(("PORT='%s'\n", port.String())); 243 PRINT(("USER='%s'\n", user.String())); 244 PRINT(("PASS='%s'\n", pass.String())); 245 PRINT(("PATH='%s'\n", path.String())); 246 247 if (proto == "about") { 248 app_info info; 249 BString sig; 250 // BUrl could get an accessor for the full - proto part... 251 sig = host << "/" << path; 252 BMessage msg(B_ABOUT_REQUESTED); 253 if (be_roster->GetAppInfo(sig.String(), &info) == B_OK) { 254 BMessenger msgr(sig.String()); 255 msgr.SendMessage(&msg); 256 return; 257 } 258 if (be_roster->Launch(sig.String(), &msg) == B_OK) 259 return; 260 be_roster->Launch("application/x-vnd.Haiku-About"); 261 return; 262 } 263 264 if (proto == "telnet") { 265 BString cmd("telnet "); 266 if (url.HasUser()) 267 cmd << "-l " << user << " "; 268 cmd << host; 269 if (url.HasPort()) 270 cmd << " " << port; 271 PRINT(("CMD='%s'\n", cmd.String())); 272 cmd << failc; 273 args[2] = (char*)cmd.String(); 274 be_roster->Launch(kTerminalSig, 3, args); 275 return; 276 } 277 278 // see draft: 279 // http://tools.ietf.org/wg/secsh/draft-ietf-secsh-scp-sftp-ssh-uri/ 280 if (proto == "ssh") { 281 BString cmd("ssh "); 282 283 if (url.HasUser()) 284 cmd << "-l " << user << " "; 285 if (url.HasPort()) 286 cmd << "-oPort=" << port << " "; 287 cmd << host; 288 PRINT(("CMD='%s'\n", cmd.String())); 289 cmd << failc; 290 args[2] = (char*)cmd.String(); 291 be_roster->Launch(kTerminalSig, 3, args); 292 // TODO: handle errors 293 return; 294 } 295 296 if (proto == "ftp") { 297 BString cmd("ftp "); 298 299 cmd << proto << "://"; 300 /* 301 if (user.Length()) 302 cmd << "-l " << user << " "; 303 cmd << host; 304 */ 305 cmd << full; 306 PRINT(("CMD='%s'\n", cmd.String())); 307 cmd << failc; 308 args[2] = (char*)cmd.String(); 309 be_roster->Launch(kTerminalSig, 3, args); 310 // TODO: handle errors 311 return; 312 } 313 314 if (proto == "sftp") { 315 BString cmd("sftp "); 316 317 //cmd << url; 318 if (url.HasPort()) 319 cmd << "-oPort=" << port << " "; 320 if (url.HasUser()) 321 cmd << user << "@"; 322 cmd << host; 323 if (url.HasPath()) 324 cmd << ":" << path; 325 PRINT(("CMD='%s'\n", cmd.String())); 326 cmd << failc; 327 args[2] = (char*)cmd.String(); 328 be_roster->Launch(kTerminalSig, 3, args); 329 // TODO: handle errors 330 return; 331 } 332 333 if (proto == "finger") { 334 BString cmd("/bin/finger "); 335 336 if (url.HasUser()) 337 cmd << user; 338 if (url.HasHost() == 0) 339 host = "127.0.0.1"; 340 cmd << "@" << host; 341 PRINT(("CMD='%s'\n", cmd.String())); 342 cmd << pausec; 343 args[2] = (char*)cmd.String(); 344 be_roster->Launch(kTerminalSig, 3, args); 345 // TODO: handle errors 346 return; 347 } 348 349 if (proto == "http" || proto == "https" /*|| proto == "ftp"*/) { 350 BString cmd("/bin/wget "); 351 352 //cmd << url; 353 cmd << proto << "://"; 354 if (url.HasUser()) 355 cmd << user << "@"; 356 cmd << full; 357 PRINT(("CMD='%s'\n", cmd.String())); 358 cmd << pausec; 359 args[2] = (char*)cmd.String(); 360 be_roster->Launch(kTerminalSig, 3, args); 361 // TODO: handle errors 362 return; 363 } 364 365 if (proto == "file") { 366 BMessage m(B_REFS_RECEIVED); 367 entry_ref ref; 368 _DecodeUrlString(path); 369 if (get_ref_for_path(path.String(), &ref) < B_OK) 370 return; 371 m.AddRef("refs", &ref); 372 be_roster->Launch(kTrackerSig, &m); 373 return; 374 } 375 376 // XXX:TODO: split options 377 if (proto == "query") { 378 // mktemp ? 379 BString qname("/tmp/query-url-temp-"); 380 qname << getpid() << "-" << system_time(); 381 BFile query(qname.String(), O_CREAT|O_EXCL); 382 // XXX: should check for failure 383 384 BString s; 385 int32 v; 386 387 _DecodeUrlString(full); 388 // TODO: handle options (list of attrs in the column, ...) 389 390 v = 'qybF'; // QuerY By Formula XXX: any #define for that ? 391 query.WriteAttr("_trk/qryinitmode", B_INT32_TYPE, 0LL, &v, sizeof(v)); 392 s = "TextControl"; 393 query.WriteAttr("_trk/focusedView", B_STRING_TYPE, 0LL, s.String(), 394 s.Length()+1); 395 s = full; 396 PRINT(("QUERY='%s'\n", s.String())); 397 query.WriteAttr("_trk/qryinitstr", B_STRING_TYPE, 0LL, s.String(), 398 s.Length()+1); 399 query.WriteAttr("_trk/qrystr", B_STRING_TYPE, 0LL, s.String(), 400 s.Length()+1); 401 s = "application/x-vnd.Be-query"; 402 query.WriteAttr("BEOS:TYPE", 'MIMS', 0LL, s.String(), s.Length()+1); 403 404 405 BEntry e(qname.String()); 406 entry_ref er; 407 if (e.GetRef(&er) >= B_OK) 408 be_roster->Launch(&er); 409 return; 410 } 411 412 if (proto == "sh") { 413 BString cmd(full); 414 if (_Warn(url.String()) != B_OK) 415 return; 416 PRINT(("CMD='%s'\n", cmd.String())); 417 cmd << pausec; 418 args[2] = (char*)cmd.String(); 419 be_roster->Launch(kTerminalSig, 3, args); 420 // TODO: handle errors 421 return; 422 } 423 424 if (proto == "beshare") { 425 team_id team; 426 BMessenger msgr(kBeShareSig); 427 // if no instance is running, or we want a specific server, start it. 428 if (!msgr.IsValid() || url.HasHost()) { 429 be_roster->Launch(kBeShareSig, (BMessage*)NULL, &team); 430 msgr = BMessenger(NULL, team); 431 } 432 if (url.HasHost()) { 433 BMessage mserver('serv'); 434 mserver.AddString("server", host); 435 msgr.SendMessage(&mserver); 436 437 } 438 if (url.HasPath()) { 439 BMessage mquery('quer'); 440 mquery.AddString("query", path); 441 msgr.SendMessage(&mquery); 442 } 443 // TODO: handle errors 444 return; 445 } 446 447 if (proto == "icq" || proto == "msn") { 448 // TODO 449 team_id team; 450 be_roster->Launch(kIMSig, (BMessage*)NULL, &team); 451 BMessenger msgr(NULL, team); 452 if (url.HasHost()) { 453 BMessage mserver(B_REFS_RECEIVED); 454 mserver.AddString("server", host); 455 msgr.SendMessage(&mserver); 456 457 } 458 // TODO: handle errors 459 return; 460 } 461 462 if (proto == "mms" || proto == "rtp" || proto == "rtsp") { 463 args[0] = (char*)url.String(); 464 be_roster->Launch(kVLCSig, 1, args); 465 return; 466 } 467 468 469 /* 470 471 More ? 472 cf. http://en.wikipedia.org/wiki/URI_scheme 473 cf. http://www.iana.org/assignments/uri-schemes.html 474 475 Audio: (SoundPlay specific, identical to http:// to a shoutcast server) 476 477 vnc: ? 478 irc: ? 479 im: http://tools.ietf.org/html/rfc3860 480 481 svn: handled by checkitout 482 cvs: handled by checkitout 483 git: handled by checkitout 484 rsync: handled by checkitout - http://tools.ietf.org/html/rfc5781 485 486 smb: cifsmount ? 487 nfs: mount_nfs ? http://tools.ietf.org/html/rfc2224 488 ipp: http://tools.ietf.org/html/rfc3510 489 490 mailto: ? Mail & Beam both handle it already (not fully though). 491 imap: to describe mail accounts ? http://tools.ietf.org/html/rfc5092 492 pop: http://tools.ietf.org/html/rfc2384 493 mid: cid: as per RFC 2392 494 http://www.rfc-editor.org/rfc/rfc2392.txt query MAIL:cid 495 message:<MID> http://daringfireball.net/2007/12/message_urls_leopard_mail 496 497 itps: pcast: podcast: s//http/ + parse xml to get url to mp3 stream... 498 audio: s//http:/ + default MediaPlayer 499 -- see http://forums.winamp.com/showthread.php?threadid=233130 500 501 gps: ? I should submit an RFC for that one :) 502 503 webcal: (is http: to .ics file) 504 505 data: (but it's dangerous) 506 507 */ 508 509 510 } 511 512 513 status_t 514 UrlWrapper::_DecodeUrlString(BString& string) 515 { 516 // TODO: check for %00 and bail out! 517 int32 length = string.Length(); 518 int i; 519 for (i = 0; string[i] && i < length - 2; i++) { 520 if (string[i] == '%' && isxdigit(string[i+1]) 521 && isxdigit(string[i+2])) { 522 int c; 523 sscanf(string.String() + i + 1, "%02x", &c); 524 string.Remove(i, 3); 525 string.Insert((char)c, 1, i); 526 length -= 2; 527 } 528 } 529 530 return B_OK; 531 } 532 533 534 void 535 UrlWrapper::ReadyToRun(void) 536 { 537 Quit(); 538 } 539 540 541 // #pragma mark 542 543 544 int main(int argc, char** argv) 545 { 546 UrlWrapper app; 547 if (be_app) 548 app.Run(); 549 return 0; 550 } 551 552