1 /* 2 * Copyright 2013-2014 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 */ 8 9 10 #include <assert.h> 11 #include <ctype.h> 12 #include <stdlib.h> 13 #include <stdio.h> 14 15 #include <Directory.h> 16 #include <DynamicBuffer.h> 17 #include <File.h> 18 #include <GopherRequest.h> 19 #include <NodeInfo.h> 20 #include <Path.h> 21 #include <Socket.h> 22 #include <StackOrHeapArray.h> 23 #include <String.h> 24 #include <StringList.h> 25 26 27 #ifndef LIBNETAPI_DEPRECATED 28 using namespace BPrivate::Network; 29 #endif 30 31 /* 32 * TODO: fix '+' in selectors, cf. gopher://gophernicus.org/1/doc/gopher/ 33 * TODO: add proper favicon 34 * TODO: add proper dir and document icons 35 * TODO: correctly eat the extraneous .\r\n at end of text files 36 * TODO: move parsing stuff to a translator? 37 * 38 * docs: 39 * gopher://gopher.floodgap.com/1/gopher/tech 40 * gopher://gopher.floodgap.com/0/overbite/dbrowse?pluginm%201 41 * 42 * tests: 43 * gopher://sdf.org/1/sdf/historical images 44 * gopher://gopher.r-36.net/1/ large photos 45 * gopher://sdf.org/1/sdf/classes binaries 46 * gopher://sdf.org/1/users/ long page 47 * gopher://jgw.mdns.org/1/ search items 48 * gopher://jgw.mdns.org/1/MISC/ 's' item (sound) 49 * gopher://gopher.floodgap.com/1/gopher broken link 50 * gopher://sdf.org/1/maps/m missing lines 51 * gopher://sdf.org/1/foo gophernicus reports errors incorrectly 52 * gopher://gopher.floodgap.com/1/foo correct error report 53 */ 54 55 /** Type of Gopher items */ 56 typedef enum { 57 GOPHER_TYPE_NONE = 0, /**< none set */ 58 GOPHER_TYPE_ENDOFPAGE = '.', /**< a dot alone on a line */ 59 /* these come from http://tools.ietf.org/html/rfc1436 */ 60 GOPHER_TYPE_TEXTPLAIN = '0', /**< text/plain */ 61 GOPHER_TYPE_DIRECTORY = '1', /**< gopher directory */ 62 GOPHER_TYPE_CSO_SEARCH = '2', /**< CSO search */ 63 GOPHER_TYPE_ERROR = '3', /**< error message */ 64 GOPHER_TYPE_BINHEX = '4', /**< binhex encoded text */ 65 GOPHER_TYPE_BINARCHIVE = '5', /**< binary archive file */ 66 GOPHER_TYPE_UUENCODED = '6', /**< uuencoded text */ 67 GOPHER_TYPE_QUERY = '7', /**< gopher search query */ 68 GOPHER_TYPE_TELNET = '8', /**< telnet link */ 69 GOPHER_TYPE_BINARY = '9', /**< generic binary */ 70 GOPHER_TYPE_DUPSERV = '+', /**< duplicated server */ 71 GOPHER_TYPE_GIF = 'g', /**< GIF image */ 72 GOPHER_TYPE_IMAGE = 'I', /**< image (depends, usually jpeg) */ 73 GOPHER_TYPE_TN3270 = 'T', /**< tn3270 session */ 74 /* not standardized but widely used, 75 * cf. http://en.wikipedia.org/wiki/Gopher_%28protocol%29#Gopher_item_types 76 */ 77 GOPHER_TYPE_HTML = 'h', /**< HTML file or URL */ 78 GOPHER_TYPE_INFO = 'i', /**< information text */ 79 GOPHER_TYPE_AUDIO = 's', /**< audio (wav?) */ 80 /* not standardized, some servers use them */ 81 GOPHER_TYPE_DOC = 'd', /**< gophernicus uses it for PS and PDF */ 82 GOPHER_TYPE_PNG = 'p', /**< PNG image */ 83 /* cf. gopher://namcub.accelera-labs.com/1/pics */ 84 GOPHER_TYPE_MIME = 'M', /**< multipart/mixed MIME data */ 85 /* cf. http://www.pms.ifi.lmu.de/mitarbeiter/ohlbach/multimedia/IT/IBMtutorial/3376c61.html */ 86 /* cf. http://nofixedpoint.motd.org/2011/02/22/an-introduction-to-the-gopher-protocol/ */ 87 GOPHER_TYPE_PDF = 'P', /**< PDF file */ 88 GOPHER_TYPE_BITMAP = ':', /**< Bitmap image (Gopher+) */ 89 GOPHER_TYPE_MOVIE = ';', /**< Movie (Gopher+) */ 90 GOPHER_TYPE_SOUND = '<', /**< Sound (Gopher+) */ 91 GOPHER_TYPE_CALENDAR = 'c', /**< Calendar */ 92 GOPHER_TYPE_EVENT = 'e', /**< Event */ 93 GOPHER_TYPE_MBOX = 'm', /**< mbox file */ 94 } gopher_item_type; 95 96 /** Types of fields in a line */ 97 typedef enum { 98 FIELD_NAME, 99 FIELD_SELECTOR, 100 FIELD_HOST, 101 FIELD_PORT, 102 FIELD_GPFLAG, 103 FIELD_EOL, 104 FIELD_COUNT = FIELD_EOL 105 } gopher_field; 106 107 /** Map of gopher types to MIME types */ 108 static struct { 109 gopher_item_type type; 110 const char *mime; 111 } gopher_type_map[] = { 112 /* these come from http://tools.ietf.org/html/rfc1436 */ 113 { GOPHER_TYPE_TEXTPLAIN, "text/plain" }, 114 { GOPHER_TYPE_DIRECTORY, "text/html;charset=UTF-8" }, 115 { GOPHER_TYPE_QUERY, "text/html;charset=UTF-8" }, 116 { GOPHER_TYPE_GIF, "image/gif" }, 117 { GOPHER_TYPE_HTML, "text/html" }, 118 /* those are not standardized */ 119 { GOPHER_TYPE_PDF, "application/pdf" }, 120 { GOPHER_TYPE_PNG, "image/png"}, 121 { GOPHER_TYPE_NONE, NULL } 122 }; 123 124 static const char *kStyleSheet = "\n" 125 "/*\n" 126 " * gopher listing style\n" 127 " */\n" 128 "\n" 129 "body#gopher {\n" 130 " /* margin: 10px;*/\n" 131 " background-color: Window;\n" 132 " color: WindowText;\n" 133 " font-size: 100%;\n" 134 " padding-bottom: 2em; }\n" 135 "\n" 136 "body#gopher div.uplink {\n" 137 " padding: 0;\n" 138 " margin: 0;\n" 139 " position: fixed;\n" 140 " top: 5px;\n" 141 " right: 5px; }\n" 142 "\n" 143 "body#gopher h1 {\n" 144 " padding: 5mm;\n" 145 " margin: 0;\n" 146 " border-bottom: 2px solid #777; }\n" 147 "\n" 148 "body#gopher span {\n" 149 " margin-left: 1em;\n" 150 " padding-left: 2em;\n" 151 " font-family: 'Noto Sans Mono', Courier, monospace;\n" 152 " word-wrap: break-word;\n" 153 " white-space: pre-wrap; }\n" 154 "\n" 155 "body#gopher span.error {\n" 156 " color: #f00; }\n" 157 "\n" 158 "body#gopher span.unknown {\n" 159 " color: #800; }\n" 160 "\n" 161 "body#gopher span.dir {\n" 162 " background-image: url('resource:icons/directory.png');\n" 163 " background-repeat: no-repeat;\n" 164 " background-position: bottom left; }\n" 165 "\n" 166 "body#gopher span.text {\n" 167 " background-image: url('resource:icons/content.png');\n" 168 " background-repeat: no-repeat;\n" 169 " background-position: bottom left; }\n" 170 "\n" 171 "body#gopher span.query {\n" 172 " background-image: url('resource:icons/search.png');\n" 173 " background-repeat: no-repeat;\n" 174 " background-position: bottom left; }\n" 175 "\n" 176 "body#gopher span.img img {\n" 177 " display: block;\n" 178 " margin-left:auto;\n" 179 " margin-right:auto; }\n"; 180 181 static const int32 kGopherBufferSize = 4096; 182 183 static const bool kInlineImages = true; 184 185 186 #ifdef LIBNETAPI_DEPRECATED 187 BGopherRequest::BGopherRequest(const BUrl& url, BUrlProtocolListener* listener, 188 BUrlContext* context) 189 : 190 BNetworkRequest(url, listener, context, "BUrlProtocol.Gopher", "gopher"), 191 fItemType(GOPHER_TYPE_NONE), 192 fPosition(0), 193 fResult() 194 { 195 fSocket = new(std::nothrow) BSocket(); 196 197 fUrl.UrlDecode(); 198 // the first part of the path is actually the document type 199 200 fPath = Url().Path(); 201 if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") { 202 // default entry 203 fItemType = GOPHER_TYPE_DIRECTORY; 204 fPath = ""; 205 } else if (fPath.Length() > 1 && fPath[0] == '/') { 206 fItemType = fPath[1]; 207 fPath.Remove(0, 2); 208 } 209 } 210 211 #else 212 213 BGopherRequest::BGopherRequest(const BUrl& url, BDataIO* output, 214 BUrlProtocolListener* listener, BUrlContext* context) 215 : 216 BNetworkRequest(url, output, listener, context, "BUrlProtocol.Gopher", 217 "gopher"), 218 fItemType(GOPHER_TYPE_NONE), 219 fPosition(0), 220 fResult() 221 { 222 fSocket = new(std::nothrow) BSocket(); 223 224 fUrl.UrlDecode(); 225 // the first part of the path is actually the document type 226 227 fPath = Url().Path(); 228 if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") { 229 // default entry 230 fItemType = GOPHER_TYPE_DIRECTORY; 231 fPath = ""; 232 } else if (fPath.Length() > 1 && fPath[0] == '/') { 233 fItemType = fPath[1]; 234 fPath.Remove(0, 2); 235 } 236 } 237 #endif // LIBNETAPI_DEPRECATED 238 239 240 BGopherRequest::~BGopherRequest() 241 { 242 Stop(); 243 244 delete fSocket; 245 } 246 247 248 status_t 249 BGopherRequest::Stop() 250 { 251 if (fSocket != NULL) { 252 fSocket->Disconnect(); 253 // Unlock any pending connect, read or write operation. 254 } 255 return BNetworkRequest::Stop(); 256 } 257 258 259 const BUrlResult& 260 BGopherRequest::Result() const 261 { 262 return fResult; 263 } 264 265 266 #ifdef LIBNETAPI_DEPRECATED 267 status_t 268 BGopherRequest::_ProtocolLoop() 269 { 270 if (fSocket == NULL) 271 return B_NO_MEMORY; 272 273 if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) { 274 _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, 275 "Unable to resolve hostname (%s), aborting.", 276 fUrl.Host().String()); 277 return B_SERVER_NOT_FOUND; 278 } 279 280 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.", 281 fUrl.Authority().String(), fRemoteAddr.Port()); 282 status_t connectError = fSocket->Connect(fRemoteAddr); 283 284 if (connectError != B_OK) { 285 _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s", 286 strerror(connectError)); 287 return connectError; 288 } 289 290 //! ProtocolHook:ConnectionOpened 291 if (fListener != NULL) 292 fListener->ConnectionOpened(this); 293 294 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, 295 "Connection opened, sending request."); 296 297 _SendRequest(); 298 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent."); 299 300 // Receive loop 301 bool receiveEnd = false; 302 status_t readError = B_OK; 303 ssize_t bytesRead = 0; 304 //ssize_t bytesReceived = 0; 305 //ssize_t bytesTotal = 0; 306 bool dataValidated = false; 307 BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize); 308 309 while (!fQuit && !receiveEnd) { 310 bytesRead = fSocket->Read(chunk, kGopherBufferSize); 311 312 if (bytesRead < 0) { 313 readError = bytesRead; 314 break; 315 } else if (bytesRead == 0) 316 receiveEnd = true; 317 318 fInputBuffer.AppendData(chunk, bytesRead); 319 320 if (!dataValidated) { 321 size_t i; 322 // on error (file doesn't exist, ...) the server sends 323 // a faked directory entry with an error message 324 if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') { 325 int tabs = 0; 326 bool crlf = false; 327 328 // make sure the buffer only contains printable characters 329 // and has at least 3 tabs before a CRLF 330 for (i = 0; i < fInputBuffer.Size(); i++) { 331 char c = fInputBuffer.Data()[i]; 332 if (c == '\t') { 333 if (!crlf) 334 tabs++; 335 } else if (c == '\r' || c == '\n') { 336 if (tabs < 3) 337 break; 338 crlf = true; 339 } else if (!isprint(fInputBuffer.Data()[i])) { 340 crlf = false; 341 break; 342 } 343 } 344 if (crlf && tabs > 2 && tabs < 5) { 345 // TODO: 346 //if enough data 347 // else continue 348 fItemType = GOPHER_TYPE_DIRECTORY; 349 readError = B_RESOURCE_NOT_FOUND; 350 // continue parsing the error text anyway 351 } 352 } 353 // special case for buggy(?) Gophernicus/1.5 354 static const char *buggy = "Error: File or directory not found!"; 355 if (fInputBuffer.Size() > strlen(buggy) 356 && !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) { 357 fItemType = GOPHER_TYPE_DIRECTORY; 358 readError = B_RESOURCE_NOT_FOUND; 359 // continue parsing the error text anyway 360 // but it won't look good 361 } 362 363 // now we probably have correct data 364 dataValidated = true; 365 366 //! ProtocolHook:ResponseStarted 367 if (fListener != NULL) 368 fListener->ResponseStarted(this); 369 370 // now we can assign MIME type if we know it 371 const char *mime = "application/octet-stream"; 372 for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) { 373 if (gopher_type_map[i].type == fItemType) { 374 mime = gopher_type_map[i].mime; 375 break; 376 } 377 } 378 fResult.SetContentType(mime); 379 380 // we don't really have headers but well... 381 //! ProtocolHook:HeadersReceived 382 if (fListener != NULL) 383 fListener->HeadersReceived(this, fResult); 384 } 385 386 if (_NeedsParsing()) 387 _ParseInput(receiveEnd); 388 else if (fInputBuffer.Size()) { 389 // send input directly 390 if (fListener != NULL) { 391 fListener->DataReceived(this, (const char *)fInputBuffer.Data(), 392 fPosition, fInputBuffer.Size()); 393 } 394 395 fPosition += fInputBuffer.Size(); 396 397 if (fListener != NULL) 398 fListener->DownloadProgress(this, fPosition, 0); 399 400 // XXX: this is plain stupid, we already copied the data 401 // and just want to drop it... 402 char *inputTempBuffer = new(std::nothrow) char[bytesRead]; 403 if (inputTempBuffer == NULL) { 404 readError = B_NO_MEMORY; 405 break; 406 } 407 fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size()); 408 delete[] inputTempBuffer; 409 } 410 } 411 412 if (fPosition > 0) 413 fResult.SetLength(fPosition); 414 415 fSocket->Disconnect(); 416 417 if (readError != B_OK) 418 return readError; 419 420 return fQuit ? B_INTERRUPTED : B_OK; 421 } 422 423 # else 424 425 status_t 426 BGopherRequest::_ProtocolLoop() 427 { 428 if (fSocket == NULL) 429 return B_NO_MEMORY; 430 431 if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) { 432 _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, 433 "Unable to resolve hostname (%s), aborting.", 434 fUrl.Host().String()); 435 return B_SERVER_NOT_FOUND; 436 } 437 438 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.", 439 fUrl.Authority().String(), fRemoteAddr.Port()); 440 status_t connectError = fSocket->Connect(fRemoteAddr); 441 442 if (connectError != B_OK) { 443 _EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s", 444 strerror(connectError)); 445 return connectError; 446 } 447 448 //! ProtocolHook:ConnectionOpened 449 if (fListener != NULL) 450 fListener->ConnectionOpened(this); 451 452 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, 453 "Connection opened, sending request."); 454 455 _SendRequest(); 456 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent."); 457 458 // Receive loop 459 bool receiveEnd = false; 460 status_t readError = B_OK; 461 ssize_t bytesRead = 0; 462 //ssize_t bytesReceived = 0; 463 //ssize_t bytesTotal = 0; 464 bool dataValidated = false; 465 BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize); 466 467 while (!fQuit && !receiveEnd) { 468 bytesRead = fSocket->Read(chunk, kGopherBufferSize); 469 470 if (bytesRead < 0) { 471 readError = bytesRead; 472 break; 473 } else if (bytesRead == 0) 474 receiveEnd = true; 475 476 fInputBuffer.AppendData(chunk, bytesRead); 477 478 if (!dataValidated) { 479 size_t i; 480 // on error (file doesn't exist, ...) the server sends 481 // a faked directory entry with an error message 482 if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') { 483 int tabs = 0; 484 bool crlf = false; 485 486 // make sure the buffer only contains printable characters 487 // and has at least 3 tabs before a CRLF 488 for (i = 0; i < fInputBuffer.Size(); i++) { 489 char c = fInputBuffer.Data()[i]; 490 if (c == '\t') { 491 if (!crlf) 492 tabs++; 493 } else if (c == '\r' || c == '\n') { 494 if (tabs < 3) 495 break; 496 crlf = true; 497 } else if (!isprint(fInputBuffer.Data()[i])) { 498 crlf = false; 499 break; 500 } 501 } 502 if (crlf && tabs > 2 && tabs < 5) { 503 // TODO: 504 //if enough data 505 // else continue 506 fItemType = GOPHER_TYPE_DIRECTORY; 507 readError = B_RESOURCE_NOT_FOUND; 508 // continue parsing the error text anyway 509 } 510 } 511 // special case for buggy(?) Gophernicus/1.5 512 static const char *buggy = "Error: File or directory not found!"; 513 if (fInputBuffer.Size() > strlen(buggy) 514 && !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) { 515 fItemType = GOPHER_TYPE_DIRECTORY; 516 readError = B_RESOURCE_NOT_FOUND; 517 // continue parsing the error text anyway 518 // but it won't look good 519 } 520 521 // now we probably have correct data 522 dataValidated = true; 523 524 //! ProtocolHook:ResponseStarted 525 if (fListener != NULL) 526 fListener->ResponseStarted(this); 527 528 // now we can assign MIME type if we know it 529 const char *mime = "application/octet-stream"; 530 for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) { 531 if (gopher_type_map[i].type == fItemType) { 532 mime = gopher_type_map[i].mime; 533 break; 534 } 535 } 536 fResult.SetContentType(mime); 537 538 // we don't really have headers but well... 539 //! ProtocolHook:HeadersReceived 540 if (fListener != NULL) 541 fListener->HeadersReceived(this); 542 } 543 544 if (_NeedsParsing()) 545 readError = _ParseInput(receiveEnd); 546 else if (fInputBuffer.Size()) { 547 // send input directly 548 if (fOutput != NULL) { 549 size_t written = 0; 550 readError = fOutput->WriteExactly( 551 (const char*)fInputBuffer.Data(), fInputBuffer.Size(), 552 &written); 553 if (fListener != NULL && written > 0) 554 fListener->BytesWritten(this, written); 555 if (readError != B_OK) 556 break; 557 } 558 559 fPosition += fInputBuffer.Size(); 560 561 if (fListener != NULL) 562 fListener->DownloadProgress(this, fPosition, 0); 563 564 // XXX: this is plain stupid, we already copied the data 565 // and just want to drop it... 566 char *inputTempBuffer = new(std::nothrow) char[bytesRead]; 567 if (inputTempBuffer == NULL) { 568 readError = B_NO_MEMORY; 569 break; 570 } 571 fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size()); 572 delete[] inputTempBuffer; 573 } 574 } 575 576 if (fPosition > 0) 577 fResult.SetLength(fPosition); 578 579 fSocket->Disconnect(); 580 581 if (readError != B_OK) 582 return readError; 583 584 return fQuit ? B_INTERRUPTED : B_OK; 585 } 586 #endif // LIBNETAPI_DEPRECATED 587 588 589 void 590 BGopherRequest::_SendRequest() 591 { 592 BString request; 593 594 request << fPath; 595 596 if (Url().HasRequest()) 597 request << '\t' << Url().Request(); 598 599 request << "\r\n"; 600 601 fSocket->Write(request.String(), request.Length()); 602 } 603 604 605 bool 606 BGopherRequest::_NeedsParsing() 607 { 608 if (fItemType == GOPHER_TYPE_DIRECTORY 609 || fItemType == GOPHER_TYPE_QUERY) 610 return true; 611 return false; 612 } 613 614 615 bool 616 BGopherRequest::_NeedsLastDotStrip() 617 { 618 if (fItemType == GOPHER_TYPE_DIRECTORY 619 || fItemType == GOPHER_TYPE_QUERY 620 || fItemType == GOPHER_TYPE_TEXTPLAIN) 621 return true; 622 return false; 623 } 624 625 626 #ifdef LIBNETAPI_DEPRECATED 627 void 628 BGopherRequest::_ParseInput(bool last) 629 { 630 BString line; 631 632 while (_GetLine(line) == B_OK) { 633 char type = GOPHER_TYPE_NONE; 634 BStringList fields; 635 636 line.MoveInto(&type, 0, 1); 637 638 line.Split("\t", false, fields); 639 640 if (type != GOPHER_TYPE_ENDOFPAGE 641 && fields.CountStrings() < FIELD_GPFLAG) 642 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, 643 "Unterminated gopher item (type '%c')", type); 644 645 BString pageTitle; 646 BString item; 647 BString title = fields.StringAt(FIELD_NAME); 648 BString link("gopher://"); 649 BString user; 650 if (fields.CountStrings() > 3) { 651 link << fields.StringAt(FIELD_HOST); 652 if (fields.StringAt(FIELD_PORT).Length()) 653 link << ":" << fields.StringAt(FIELD_PORT); 654 link << "/" << type; 655 //if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/') 656 // link << "/"; 657 link << fields.StringAt(FIELD_SELECTOR); 658 } 659 _HTMLEscapeString(title); 660 _HTMLEscapeString(link); 661 662 switch (type) { 663 case GOPHER_TYPE_ENDOFPAGE: 664 /* end of the page */ 665 break; 666 case GOPHER_TYPE_TEXTPLAIN: 667 item << "<a href=\"" << link << "\">" 668 "<span class=\"text\">" << title << "</span></a>" 669 "<br/>\n"; 670 break; 671 case GOPHER_TYPE_BINARY: 672 case GOPHER_TYPE_BINHEX: 673 case GOPHER_TYPE_BINARCHIVE: 674 case GOPHER_TYPE_UUENCODED: 675 item << "<a href=\"" << link << "\">" 676 "<span class=\"binary\">" << title << "</span></a>" 677 "<br/>\n"; 678 break; 679 case GOPHER_TYPE_DIRECTORY: 680 /* 681 * directory link 682 */ 683 item << "<a href=\"" << link << "\">" 684 "<span class=\"dir\">" << title << "</span></a>" 685 "<br/>\n"; 686 break; 687 case GOPHER_TYPE_ERROR: 688 item << "<span class=\"error\">" << title << "</span>" 689 "<br/>\n"; 690 if (fPosition == 0 && pageTitle.Length() == 0) 691 pageTitle << "Error: " << title; 692 break; 693 case GOPHER_TYPE_QUERY: 694 /* TODO: handle search better. 695 * For now we use an unnamed input field and accept sending ?=foo 696 * as it seems at least Veronica-2 ignores the = but it's unclean. 697 */ 698 item << "<form method=\"get\" action=\"" << link << "\" " 699 "onsubmit=\"window.location = this.action + '?' + " 700 "this.elements['q'].value; return false;\">" 701 "<span class=\"query\">" 702 "<label>" << title << " " 703 "<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />" 704 "</label>" 705 "</span></form>" 706 "<br/>\n"; 707 break; 708 case GOPHER_TYPE_TELNET: 709 /* telnet: links 710 * cf. gopher://78.80.30.202/1/ps3 711 * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202 712 */ 713 link = "telnet://"; 714 user = fields.StringAt(FIELD_SELECTOR); 715 if (user.FindLast('/') > -1) { 716 user.Remove(0, user.FindLast('/')); 717 link << user << "@"; 718 } 719 link << fields.StringAt(FIELD_HOST); 720 if (fields.StringAt(FIELD_PORT) != "23") 721 link << ":" << fields.StringAt(FIELD_PORT); 722 723 item << "<a href=\"" << link << "\">" 724 "<span class=\"telnet\">" << title << "</span></a>" 725 "<br/>\n"; 726 break; 727 case GOPHER_TYPE_TN3270: 728 /* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */ 729 link = "tn3270://"; 730 user = fields.StringAt(FIELD_SELECTOR); 731 if (user.FindLast('/') > -1) { 732 user.Remove(0, user.FindLast('/')); 733 link << user << "@"; 734 } 735 link << fields.StringAt(FIELD_HOST); 736 if (fields.StringAt(FIELD_PORT) != "23") 737 link << ":" << fields.StringAt(FIELD_PORT); 738 739 item << "<a href=\"" << link << "\">" 740 "<span class=\"telnet\">" << title << "</span></a>" 741 "<br/>\n"; 742 break; 743 case GOPHER_TYPE_CSO_SEARCH: 744 /* CSO search. 745 * At least Lynx supports a cso:// URI scheme: 746 * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html 747 */ 748 link = "cso://"; 749 user = fields.StringAt(FIELD_SELECTOR); 750 if (user.FindLast('/') > -1) { 751 user.Remove(0, user.FindLast('/')); 752 link << user << "@"; 753 } 754 link << fields.StringAt(FIELD_HOST); 755 if (fields.StringAt(FIELD_PORT) != "105") 756 link << ":" << fields.StringAt(FIELD_PORT); 757 758 item << "<a href=\"" << link << "\">" 759 "<span class=\"cso\">" << title << "</span></a>" 760 "<br/>\n"; 761 break; 762 case GOPHER_TYPE_GIF: 763 case GOPHER_TYPE_IMAGE: 764 case GOPHER_TYPE_PNG: 765 case GOPHER_TYPE_BITMAP: 766 /* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */ 767 if (kInlineImages) { 768 item << "<a href=\"" << link << "\">" 769 "<span class=\"img\">" << title << " " 770 "<img src=\"" << link << "\" " 771 "alt=\"" << title << "\"/>" 772 "</span></a>" 773 "<br/>\n"; 774 break; 775 } 776 /* fallback to default, link them */ 777 item << "<a href=\"" << link << "\">" 778 "<span class=\"img\">" << title << "</span></a>" 779 "<br/>\n"; 780 break; 781 case GOPHER_TYPE_HTML: 782 /* cf. gopher://pineapple.vg/1 */ 783 if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) { 784 link = fields.StringAt(FIELD_SELECTOR); 785 link.Remove(0, 4); 786 } 787 /* cf. gopher://sdf.org/1/sdf/classes/ */ 788 789 item << "<a href=\"" << link << "\">" 790 "<span class=\"html\">" << title << "</span></a>" 791 "<br/>\n"; 792 break; 793 case GOPHER_TYPE_INFO: 794 // TITLE resource, cf. 795 // gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt 796 if (fPosition == 0 && pageTitle.Length() == 0 797 && fields.StringAt(FIELD_SELECTOR) == "TITLE") { 798 pageTitle = title; 799 break; 800 } 801 item << "<span class=\"info\">" << title << "</span>" 802 "<br/>\n"; 803 break; 804 case GOPHER_TYPE_AUDIO: 805 case GOPHER_TYPE_SOUND: 806 item << "<a href=\"" << link << "\">" 807 "<span class=\"audio\">" << title << "</span></a>" 808 "<audio src=\"" << link << "\" " 809 //TODO:Fix crash in WebPositive with these 810 //"controls=\"controls\" " 811 //"width=\"300\" height=\"50\" " 812 "alt=\"" << title << "\"/>" 813 "<span>[player]</span></audio>" 814 "<br/>\n"; 815 break; 816 case GOPHER_TYPE_PDF: 817 case GOPHER_TYPE_DOC: 818 /* generic case for known-to-work items */ 819 item << "<a href=\"" << link << "\">" 820 "<span class=\"document\">" << title << "</span></a>" 821 "<br/>\n"; 822 break; 823 case GOPHER_TYPE_MOVIE: 824 item << "<a href=\"" << link << "\">" 825 "<span class=\"video\">" << title << "</span></a>" 826 "<video src=\"" << link << "\" " 827 //TODO:Fix crash in WebPositive with these 828 //"controls=\"controls\" " 829 //"width=\"300\" height=\"300\" " 830 "alt=\"" << title << "\"/>" 831 "<span>[player]</span></audio>" 832 "<br/>\n"; 833 break; 834 default: 835 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, 836 "Unknown gopher item (type 0x%02x '%c')", type, type); 837 item << "<a href=\"" << link << "\">" 838 "<span class=\"unknown\">" << title << "</span></a>" 839 "<br/>\n"; 840 break; 841 } 842 843 if (fPosition == 0) { 844 if (pageTitle.Length() == 0) 845 pageTitle << "Index of " << Url(); 846 847 const char *uplink = "."; 848 if (fPath.EndsWith("/")) 849 uplink = ".."; 850 851 // emit header 852 BString header; 853 header << 854 "<html>\n" 855 "<head>\n" 856 "<meta http-equiv=\"Content-Type\"" 857 " content=\"text/html; charset=UTF-8\" />\n" 858 //FIXME: fix links 859 //"<link rel=\"icon\" type=\"image/png\"" 860 // " href=\"resource:icons/directory.png\">\n" 861 "<style type=\"text/css\">\n" << kStyleSheet << "</style>\n" 862 "<title>" << pageTitle << "</title>\n" 863 "</head>\n" 864 "<body id=\"gopher\">\n" 865 "<div class=\"uplink dontprint\">\n" 866 "<a href=" << uplink << ">[up]</a>\n" 867 "<a href=\"/\">[top]</a>\n" 868 "</div>\n" 869 "<h1>" << pageTitle << "</h1>\n"; 870 871 if (fListener != NULL) { 872 fListener->DataReceived(this, header.String(), fPosition, 873 header.Length()); 874 } 875 876 fPosition += header.Length(); 877 878 if (fListener != NULL) 879 fListener->DownloadProgress(this, fPosition, 0); 880 } 881 882 if (item.Length()) { 883 if (fListener != NULL) { 884 fListener->DataReceived(this, item.String(), fPosition, 885 item.Length()); 886 } 887 888 fPosition += item.Length(); 889 890 if (fListener != NULL) 891 fListener->DownloadProgress(this, fPosition, 0); 892 } 893 } 894 895 if (last) { 896 // emit footer 897 BString footer = 898 "</div>\n" 899 "</body>\n" 900 "</html>\n"; 901 902 if (fListener != NULL) { 903 fListener->DataReceived(this, footer.String(), fPosition, 904 footer.Length()); 905 } 906 907 fPosition += footer.Length(); 908 909 if (fListener != NULL) 910 fListener->DownloadProgress(this, fPosition, 0); 911 } 912 } 913 914 #else 915 916 status_t 917 BGopherRequest::_ParseInput(bool last) 918 { 919 BString line; 920 921 while (_GetLine(line) == B_OK) { 922 char type = GOPHER_TYPE_NONE; 923 BStringList fields; 924 925 line.MoveInto(&type, 0, 1); 926 927 line.Split("\t", false, fields); 928 929 if (type != GOPHER_TYPE_ENDOFPAGE 930 && fields.CountStrings() < FIELD_GPFLAG) 931 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, 932 "Unterminated gopher item (type '%c')", type); 933 934 BString pageTitle; 935 BString item; 936 BString title = fields.StringAt(FIELD_NAME); 937 BString link("gopher://"); 938 BString user; 939 if (fields.CountStrings() > 3) { 940 link << fields.StringAt(FIELD_HOST); 941 if (fields.StringAt(FIELD_PORT).Length()) 942 link << ":" << fields.StringAt(FIELD_PORT); 943 link << "/" << type; 944 //if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/') 945 // link << "/"; 946 link << fields.StringAt(FIELD_SELECTOR); 947 } 948 _HTMLEscapeString(title); 949 _HTMLEscapeString(link); 950 951 switch (type) { 952 case GOPHER_TYPE_ENDOFPAGE: 953 /* end of the page */ 954 break; 955 case GOPHER_TYPE_TEXTPLAIN: 956 item << "<a href=\"" << link << "\">" 957 "<span class=\"text\">" << title << "</span></a>" 958 "<br/>\n"; 959 break; 960 case GOPHER_TYPE_BINARY: 961 case GOPHER_TYPE_BINHEX: 962 case GOPHER_TYPE_BINARCHIVE: 963 case GOPHER_TYPE_UUENCODED: 964 item << "<a href=\"" << link << "\">" 965 "<span class=\"binary\">" << title << "</span></a>" 966 "<br/>\n"; 967 break; 968 case GOPHER_TYPE_DIRECTORY: 969 /* 970 * directory link 971 */ 972 item << "<a href=\"" << link << "\">" 973 "<span class=\"dir\">" << title << "</span></a>" 974 "<br/>\n"; 975 break; 976 case GOPHER_TYPE_ERROR: 977 item << "<span class=\"error\">" << title << "</span>" 978 "<br/>\n"; 979 if (fPosition == 0 && pageTitle.Length() == 0) 980 pageTitle << "Error: " << title; 981 break; 982 case GOPHER_TYPE_QUERY: 983 /* TODO: handle search better. 984 * For now we use an unnamed input field and accept sending ?=foo 985 * as it seems at least Veronica-2 ignores the = but it's unclean. 986 */ 987 item << "<form method=\"get\" action=\"" << link << "\" " 988 "onsubmit=\"window.location = this.action + '?' + " 989 "this.elements['q'].value; return false;\">" 990 "<span class=\"query\">" 991 "<label>" << title << " " 992 "<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />" 993 "</label>" 994 "</span></form>" 995 "<br/>\n"; 996 break; 997 case GOPHER_TYPE_TELNET: 998 /* telnet: links 999 * cf. gopher://78.80.30.202/1/ps3 1000 * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202 1001 */ 1002 link = "telnet://"; 1003 user = fields.StringAt(FIELD_SELECTOR); 1004 if (user.FindLast('/') > -1) { 1005 user.Remove(0, user.FindLast('/')); 1006 link << user << "@"; 1007 } 1008 link << fields.StringAt(FIELD_HOST); 1009 if (fields.StringAt(FIELD_PORT) != "23") 1010 link << ":" << fields.StringAt(FIELD_PORT); 1011 1012 item << "<a href=\"" << link << "\">" 1013 "<span class=\"telnet\">" << title << "</span></a>" 1014 "<br/>\n"; 1015 break; 1016 case GOPHER_TYPE_TN3270: 1017 /* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */ 1018 link = "tn3270://"; 1019 user = fields.StringAt(FIELD_SELECTOR); 1020 if (user.FindLast('/') > -1) { 1021 user.Remove(0, user.FindLast('/')); 1022 link << user << "@"; 1023 } 1024 link << fields.StringAt(FIELD_HOST); 1025 if (fields.StringAt(FIELD_PORT) != "23") 1026 link << ":" << fields.StringAt(FIELD_PORT); 1027 1028 item << "<a href=\"" << link << "\">" 1029 "<span class=\"telnet\">" << title << "</span></a>" 1030 "<br/>\n"; 1031 break; 1032 case GOPHER_TYPE_CSO_SEARCH: 1033 /* CSO search. 1034 * At least Lynx supports a cso:// URI scheme: 1035 * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html 1036 */ 1037 link = "cso://"; 1038 user = fields.StringAt(FIELD_SELECTOR); 1039 if (user.FindLast('/') > -1) { 1040 user.Remove(0, user.FindLast('/')); 1041 link << user << "@"; 1042 } 1043 link << fields.StringAt(FIELD_HOST); 1044 if (fields.StringAt(FIELD_PORT) != "105") 1045 link << ":" << fields.StringAt(FIELD_PORT); 1046 1047 item << "<a href=\"" << link << "\">" 1048 "<span class=\"cso\">" << title << "</span></a>" 1049 "<br/>\n"; 1050 break; 1051 case GOPHER_TYPE_GIF: 1052 case GOPHER_TYPE_IMAGE: 1053 case GOPHER_TYPE_PNG: 1054 case GOPHER_TYPE_BITMAP: 1055 /* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */ 1056 if (kInlineImages) { 1057 item << "<a href=\"" << link << "\">" 1058 "<span class=\"img\">" << title << " " 1059 "<img src=\"" << link << "\" " 1060 "alt=\"" << title << "\"/>" 1061 "</span></a>" 1062 "<br/>\n"; 1063 break; 1064 } 1065 /* fallback to default, link them */ 1066 item << "<a href=\"" << link << "\">" 1067 "<span class=\"img\">" << title << "</span></a>" 1068 "<br/>\n"; 1069 break; 1070 case GOPHER_TYPE_HTML: 1071 /* cf. gopher://pineapple.vg/1 */ 1072 if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) { 1073 link = fields.StringAt(FIELD_SELECTOR); 1074 link.Remove(0, 4); 1075 } 1076 /* cf. gopher://sdf.org/1/sdf/classes/ */ 1077 1078 item << "<a href=\"" << link << "\">" 1079 "<span class=\"html\">" << title << "</span></a>" 1080 "<br/>\n"; 1081 break; 1082 case GOPHER_TYPE_INFO: 1083 // TITLE resource, cf. 1084 // gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt 1085 if (fPosition == 0 && pageTitle.Length() == 0 1086 && fields.StringAt(FIELD_SELECTOR) == "TITLE") { 1087 pageTitle = title; 1088 break; 1089 } 1090 item << "<span class=\"info\">" << title << "</span>" 1091 "<br/>\n"; 1092 break; 1093 case GOPHER_TYPE_AUDIO: 1094 case GOPHER_TYPE_SOUND: 1095 item << "<a href=\"" << link << "\">" 1096 "<span class=\"audio\">" << title << "</span></a>" 1097 "<audio src=\"" << link << "\" " 1098 //TODO:Fix crash in WebPositive with these 1099 //"controls=\"controls\" " 1100 //"width=\"300\" height=\"50\" " 1101 "alt=\"" << title << "\"/>" 1102 "<span>[player]</span></audio>" 1103 "<br/>\n"; 1104 break; 1105 case GOPHER_TYPE_PDF: 1106 case GOPHER_TYPE_DOC: 1107 /* generic case for known-to-work items */ 1108 item << "<a href=\"" << link << "\">" 1109 "<span class=\"document\">" << title << "</span></a>" 1110 "<br/>\n"; 1111 break; 1112 case GOPHER_TYPE_MOVIE: 1113 item << "<a href=\"" << link << "\">" 1114 "<span class=\"video\">" << title << "</span></a>" 1115 "<video src=\"" << link << "\" " 1116 //TODO:Fix crash in WebPositive with these 1117 //"controls=\"controls\" " 1118 //"width=\"300\" height=\"300\" " 1119 "alt=\"" << title << "\"/>" 1120 "<span>[player]</span></audio>" 1121 "<br/>\n"; 1122 break; 1123 default: 1124 _EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, 1125 "Unknown gopher item (type 0x%02x '%c')", type, type); 1126 item << "<a href=\"" << link << "\">" 1127 "<span class=\"unknown\">" << title << "</span></a>" 1128 "<br/>\n"; 1129 break; 1130 } 1131 1132 if (fPosition == 0) { 1133 if (pageTitle.Length() == 0) 1134 pageTitle << "Index of " << Url(); 1135 1136 const char *uplink = "."; 1137 if (fPath.EndsWith("/")) 1138 uplink = ".."; 1139 1140 // emit header 1141 BString header; 1142 header << 1143 "<html>\n" 1144 "<head>\n" 1145 "<meta http-equiv=\"Content-Type\"" 1146 " content=\"text/html; charset=UTF-8\" />\n" 1147 //FIXME: fix links 1148 //"<link rel=\"icon\" type=\"image/png\"" 1149 // " href=\"resource:icons/directory.png\">\n" 1150 "<style type=\"text/css\">\n" << kStyleSheet << "</style>\n" 1151 "<title>" << pageTitle << "</title>\n" 1152 "</head>\n" 1153 "<body id=\"gopher\">\n" 1154 "<div class=\"uplink dontprint\">\n" 1155 "<a href=" << uplink << ">[up]</a>\n" 1156 "<a href=\"/\">[top]</a>\n" 1157 "</div>\n" 1158 "<h1>" << pageTitle << "</h1>\n"; 1159 1160 if (fOutput != NULL) { 1161 size_t written = 0; 1162 status_t error = fOutput->WriteExactly(header.String(), 1163 header.Length(), &written); 1164 if (fListener != NULL && written > 0) 1165 fListener->BytesWritten(this, written); 1166 if (error != B_OK) 1167 return error; 1168 } 1169 1170 fPosition += header.Length(); 1171 1172 if (fListener != NULL) 1173 fListener->DownloadProgress(this, fPosition, 0); 1174 } 1175 1176 if (item.Length()) { 1177 if (fOutput != NULL) { 1178 size_t written = 0; 1179 status_t error = fOutput->WriteExactly(item.String(), 1180 item.Length(), &written); 1181 if (fListener != NULL && written > 0) 1182 fListener->BytesWritten(this, written); 1183 if (error != B_OK) 1184 return error; 1185 } 1186 1187 fPosition += item.Length(); 1188 1189 if (fListener != NULL) 1190 fListener->DownloadProgress(this, fPosition, 0); 1191 } 1192 } 1193 1194 if (last) { 1195 // emit footer 1196 BString footer = 1197 "</div>\n" 1198 "</body>\n" 1199 "</html>\n"; 1200 1201 if (fListener != NULL) { 1202 size_t written = 0; 1203 status_t error = fOutput->WriteExactly(footer.String(), 1204 footer.Length(), &written); 1205 if (fListener != NULL && written > 0) 1206 fListener->BytesWritten(this, written); 1207 if (error != B_OK) 1208 return error; 1209 } 1210 1211 fPosition += footer.Length(); 1212 1213 if (fListener != NULL) 1214 fListener->DownloadProgress(this, fPosition, 0); 1215 } 1216 1217 return B_OK; 1218 } 1219 #endif 1220 1221 1222 BString& 1223 BGopherRequest::_HTMLEscapeString(BString &str) 1224 { 1225 str.ReplaceAll("&", "&"); 1226 str.ReplaceAll("<", "<"); 1227 str.ReplaceAll(">", ">"); 1228 return str; 1229 } 1230