RetroZilla/netwerk/protocol/gopher/src/nsGopherChannel.cpp

753 lines
22 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Gopher protocol code.
*
* The Initial Developer of the Original Code is
* Bradley Baetz.
* Portions created by the Initial Developer are Copyright (C) 2000
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Bradley Baetz <bbaetz@student.usyd.edu.au>
* Darin Fisher <darin@netscape.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
// gopher implementation - based on datetime and finger implementations
// and documentation
#include "nsGopherChannel.h"
#include "nsMimeTypes.h"
#include "nsEscape.h"
#include "nsNetUtil.h"
#include "netCore.h"
#include "nsCRT.h"
#include "prlog.h"
#include "nsIServiceManager.h"
#include "nsILoadGroup.h"
#include "nsIInterfaceRequestor.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsISocketTransportService.h"
#include "nsIStringStream.h"
#include "nsIStreamConverterService.h"
#include "nsITXTToHTMLConv.h"
#include "nsIEventQueue.h"
#include "nsEventQueueUtils.h"
#include "nsIPrefService.h"
#include "nsIPrefBranch.h"
static NS_DEFINE_CID(kSocketTransportServiceCID, NS_SOCKETTRANSPORTSERVICE_CID);
static NS_DEFINE_CID(kStreamConverterServiceCID, NS_STREAMCONVERTERSERVICE_CID);
#ifdef PR_LOGGING
extern PRLogModuleInfo* gGopherLog;
#endif
#define BUFFER_SEG_SIZE (4*1024)
#define BUFFER_MAX_SIZE (64*1024)
// nsGopherChannel methods
nsGopherChannel::nsGopherChannel()
: mContentLength(-1),
mType(-1),
mStatus(NS_OK),
mIsPending(PR_FALSE)
{
}
nsGopherChannel::~nsGopherChannel()
{
#ifdef PR_LOGGING
nsCAutoString spec;
mUrl->GetAsciiSpec(spec);
PR_LOG(gGopherLog, PR_LOG_ALWAYS, ("~nsGopherChannel() for %s", spec.get()));
#endif
}
NS_IMPL_THREADSAFE_ISUPPORTS5(nsGopherChannel,
nsIChannel,
nsIRequest,
nsIStreamListener,
nsIRequestObserver,
nsITransportEventSink)
nsresult
nsGopherChannel::Init(nsIURI* uri, nsIProxyInfo* proxyInfo)
{
nsresult rv;
nsCOMPtr<nsIURL> url = do_QueryInterface(uri, &rv);
if (NS_FAILED(rv))
return NS_ERROR_MALFORMED_URI;
mUrl = uri;
mProxyInfo = proxyInfo;
nsCAutoString buffer;
rv = url->GetPath(buffer); // unescaped down below
if (NS_FAILED(rv))
return rv;
rv = url->GetAsciiHost(mHost);
if (NS_FAILED(rv))
return rv;
rv = url->GetPort(&mPort);
if (NS_FAILED(rv))
return rv;
PRBool restrictedPort = PR_TRUE;
nsCOMPtr<nsIPrefBranch> branch;
nsCOMPtr<nsIPrefService> prefs = do_GetService("@mozilla.org/preferences-service;1", &rv);
if (!NS_FAILED(rv)) {
branch = do_QueryInterface(prefs);
branch->GetBoolPref("network.gopher.port-restricted" , &restrictedPort);
}
// For security reasons, don't allow anything expect the default
// gopher port (70). See bug 71916 - bbaetz@cs.mcgill.ca
if(!restrictedPort) {
if (mPort==-1)
mPort=GOPHER_PORT;
} else
mPort=GOPHER_PORT;
// No path given
if (buffer[0]=='\0' || (buffer[0]=='/' && buffer[1]=='\0')) {
mType = '1';
mSelector.Truncate();
} else {
mType = buffer[1]; // Ignore leading '/'
// Do it this way in case selector contains embedded nulls after
// unescaping
char* selector = nsCRT::strdup(buffer.get()+2);
PRInt32 count = nsUnescapeCount(selector);
mSelector.Assign(selector,count);
nsCRT::free(selector);
if (mSelector.FindCharInSet(nsCString("\t\n\r\0",4)) != -1) {
// gopher selectors cannot containt tab, cr, lf, or \0
return NS_ERROR_MALFORMED_URI;
}
}
PR_LOG(gGopherLog,
PR_LOG_DEBUG,
("Host: mHost = %s, mPort = %d\n", mHost.get(), mPort));
PR_LOG(gGopherLog,
PR_LOG_DEBUG,
("Status: mType = %c, mSelector = %s\n", mType, mSelector.get()));
return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
// nsIRequest methods:
NS_IMETHODIMP
nsGopherChannel::GetName(nsACString &result)
{
return mUrl->GetHostPort(result);
}
NS_IMETHODIMP
nsGopherChannel::IsPending(PRBool *result)
{
*result = mIsPending;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetStatus(nsresult *status)
{
if (mPump && NS_SUCCEEDED(mStatus))
mPump->GetStatus(status);
else
*status = mStatus;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::Cancel(nsresult status)
{
PR_LOG(gGopherLog,
PR_LOG_DEBUG,
("nsGopherChannel::Cancel() called [this=%x, status=%x]\n",
this,status));
NS_ASSERTION(NS_FAILED(status), "shouldn't cancel with a success code");
mStatus = status;
if (mPump)
return mPump->Cancel(status);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::Suspend()
{
if (mPump)
return mPump->Suspend();
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::Resume()
{
if (mPump)
return mPump->Resume();
return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
// nsIChannel methods:
NS_IMETHODIMP
nsGopherChannel::GetOriginalURI(nsIURI* *aURI)
{
*aURI = mOriginalURI ? mOriginalURI : mUrl;
NS_ADDREF(*aURI);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetOriginalURI(nsIURI* aURI)
{
mOriginalURI = aURI;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetURI(nsIURI* *aURI)
{
*aURI = mUrl;
NS_IF_ADDREF(*aURI);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::Open(nsIInputStream **_retval)
{
return NS_ImplementChannelOpen(this, _retval);
}
NS_IMETHODIMP
nsGopherChannel::AsyncOpen(nsIStreamListener *aListener, nsISupports *ctxt)
{
PR_LOG(gGopherLog, PR_LOG_DEBUG, ("nsGopherChannel::AsyncOpen() called [this=%x]\n",
this));
// get callback interfaces...
nsresult rv;
PRInt32 port;
rv = mUrl->GetPort(&port);
if (NS_FAILED(rv))
return rv;
rv = NS_CheckPortSafety(port, "gopher");
if (NS_FAILED(rv))
return rv;
// push stream converters in front of the consumer...
nsCOMPtr<nsIStreamListener> converter;
rv = PushStreamConverters(aListener, getter_AddRefs(converter));
if (NS_FAILED(rv)) return rv;
// create socket transport
nsCOMPtr<nsISocketTransportService> socketService =
do_GetService(kSocketTransportServiceCID, &rv);
if (NS_FAILED(rv)) return rv;
rv = socketService->CreateTransport(nsnull, 0,
mHost,
mPort,
mProxyInfo,
getter_AddRefs(mTransport));
if (NS_FAILED(rv)) return rv;
// setup notification callbacks...
if (!(mLoadFlags & LOAD_BACKGROUND)) {
nsCOMPtr<nsIEventQueue> eventQ;
NS_GetCurrentEventQ(getter_AddRefs(eventQ));
if (eventQ)
mTransport->SetEventSink(this, eventQ);
}
// open buffered, asynchronous socket input stream, and use a input stream
// pump to read from it.
nsCOMPtr<nsIInputStream> input;
rv = mTransport->OpenInputStream(0, 0, 0, getter_AddRefs(input));
if (NS_FAILED(rv)) return rv;
rv = SendRequest();
if (NS_FAILED(rv)) return rv;
rv = NS_NewInputStreamPump(getter_AddRefs(mPump), input);
if (NS_FAILED(rv)) return rv;
rv = mPump->AsyncRead(this, nsnull);
if (NS_FAILED(rv)) return rv;
if (mLoadGroup)
mLoadGroup->AddRequest(this, nsnull);
mIsPending = PR_TRUE;
if (converter)
mListener = converter;
else
mListener = aListener;
mListenerContext = ctxt;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetLoadFlags(PRUint32 *aLoadFlags)
{
*aLoadFlags = mLoadFlags;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetLoadFlags(PRUint32 aLoadFlags)
{
mLoadFlags = aLoadFlags;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetContentType(nsACString &aContentType)
{
if (!mContentType.IsEmpty()) {
aContentType = mContentType;
return NS_OK;
}
switch(mType) {
case '0':
case 'h':
aContentType.AssignLiteral(TEXT_HTML);
break;
case '1':
aContentType.AssignLiteral(APPLICATION_HTTP_INDEX_FORMAT);
break;
case '2': // CSO search - unhandled, should not be selectable
aContentType.AssignLiteral(TEXT_HTML);
break;
case '3': // "Error" - should not be selectable
aContentType.AssignLiteral(TEXT_HTML);
break;
case '4': // "BinHexed Macintosh file"
aContentType.AssignLiteral(APPLICATION_BINHEX);
break;
case '5':
// "DOS binary archive of some sort" - is the mime-type correct?
aContentType.AssignLiteral(APPLICATION_OCTET_STREAM);
break;
case '6':
aContentType.AssignLiteral(APPLICATION_UUENCODE);
break;
case '7': // search - returns a directory listing
aContentType.AssignLiteral(APPLICATION_HTTP_INDEX_FORMAT);
break;
case '8': // telnet - type doesn't make sense
aContentType.AssignLiteral(TEXT_PLAIN);
break;
case '9': // "Binary file!"
aContentType.AssignLiteral(APPLICATION_OCTET_STREAM);
break;
case 'g':
aContentType.AssignLiteral(IMAGE_GIF);
break;
case 'i': // info line- should not be selectable
aContentType.AssignLiteral(TEXT_HTML);
break;
case 'I':
aContentType.AssignLiteral(IMAGE_GIF);
break;
case 'T': // tn3270 - type doesn't make sense
aContentType.AssignLiteral(TEXT_PLAIN);
break;
default:
if (!mContentTypeHint.IsEmpty()) {
aContentType = mContentTypeHint;
} else {
NS_WARNING("Unknown gopher type");
aContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE);
}
break;
}
PR_LOG(gGopherLog,PR_LOG_DEBUG,
("GetContentType returning %s\n", PromiseFlatCString(aContentType).get()));
// XXX do we want to cache this result?
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetContentType(const nsACString &aContentType)
{
// If AsyncOpen has been called, then treat this value as a content-type
// override. Otherwise, treat it as a content-type hint.
//
// In the case in which we are being given a content-type hint, we have no
// ways of determining a charset on our own, so just set mContentCharset
// from the charset part of this.
nsCString *contentType = mIsPending ? &mContentType : &mContentTypeHint;
// Not in libnecko, so can't use net_ParseContentType
NS_ParseContentType(aContentType, *contentType, mContentCharset);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetContentCharset(nsACString &aContentCharset)
{
aContentCharset = mContentCharset;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetContentCharset(const nsACString &aContentCharset)
{
// If someone gives us a charset hint we should just use that charset.
// So we don't care when this is being called.
mContentCharset = aContentCharset;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetContentLength(PRInt32 *aContentLength)
{
*aContentLength = mContentLength;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetContentLength(PRInt32 aContentLength)
{
NS_NOTREACHED("nsGopherChannel::SetContentLength");
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
nsGopherChannel::GetLoadGroup(nsILoadGroup* *aLoadGroup)
{
*aLoadGroup = mLoadGroup;
NS_IF_ADDREF(*aLoadGroup);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetLoadGroup(nsILoadGroup* aLoadGroup)
{
mLoadGroup = aLoadGroup;
mProgressSink = nsnull;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetOwner(nsISupports* *aOwner)
{
*aOwner = mOwner.get();
NS_IF_ADDREF(*aOwner);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetOwner(nsISupports* aOwner)
{
mOwner = aOwner;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetNotificationCallbacks(nsIInterfaceRequestor* *aCallbacks)
{
NS_IF_ADDREF(*aCallbacks = mCallbacks);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks)
{
mCallbacks = aCallbacks;
mProgressSink = nsnull;
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::GetSecurityInfo(nsISupports * *aSecurityInfo)
{
if (mTransport)
return mTransport->GetSecurityInfo(aSecurityInfo);
return NS_ERROR_NOT_AVAILABLE;
}
// nsIRequestObserver methods
NS_IMETHODIMP
nsGopherChannel::OnStartRequest(nsIRequest *aRequest, nsISupports *aContext)
{
PR_LOG(gGopherLog,
PR_LOG_DEBUG,
("nsGopherChannel::OnStartRequest called [this=%x, aRequest=%x]\n",
this, aRequest));
return mListener->OnStartRequest(this, mListenerContext);
}
NS_IMETHODIMP
nsGopherChannel::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext,
nsresult aStatus)
{
PR_LOG(gGopherLog,
PR_LOG_DEBUG,
("nsGopherChannel::OnStopRequest called [this=%x, aRequest=%x, aStatus=%x]\n",
this,aRequest,aStatus));
if (NS_SUCCEEDED(mStatus))
mStatus = aStatus;
if (mListener) {
mListener->OnStopRequest(this, mListenerContext, mStatus);
mListener = 0;
mListenerContext = 0;
}
if (mLoadGroup)
mLoadGroup->RemoveRequest(this, nsnull, mStatus);
mTransport->Close(mStatus);
mTransport = 0;
mPump = 0;
// Drop notification callbacks to prevent cycles.
mCallbacks = 0;
mProgressSink = 0;
return NS_OK;
}
// nsIStreamListener method
NS_IMETHODIMP
nsGopherChannel::OnDataAvailable(nsIRequest *req, nsISupports *ctx,
nsIInputStream *stream,
PRUint32 offset, PRUint32 count)
{
PR_LOG(gGopherLog, PR_LOG_DEBUG,
("OnDataAvailable called - [this=%x, aLength=%d]\n",this,count));
return mListener->OnDataAvailable(this, mListenerContext, stream,
offset, count);
}
nsresult
nsGopherChannel::SendRequest()
{
nsresult rv = NS_OK;
// Note - you have to keep this as a class member, because the char input
// stream doesn't copy its buffer
mRequest.Assign(mSelector);
// So, we use the selector as is unless it is a search url
if (mType=='7') {
// Note that we don't use the "standard" nsIURL parsing stuff here
// because the only special character is ?, and its possible to search
// for a string containing a #, and so on
// XXX - should this find the last or first entry?
// '?' is valid in both the search string and the url
// so no matter what this does, it may be incorrect
// This only affects people codeing the query directly into the URL
PRInt32 pos = mRequest.RFindChar('?');
if (pos == -1) {
// We require a query string here - if we don't have one,
// then we need to ask the user
nsCOMPtr<nsIPrompt> prompter;
NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, prompter);
if (!prompter) {
NS_ERROR("We need a prompter!");
return NS_ERROR_FAILURE;
}
if (!mStringBundle) {
nsCOMPtr<nsIStringBundleService> bundleSvc =
do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
if (NS_FAILED(rv)) return rv;
rv = bundleSvc->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(mStringBundle));
if (NS_FAILED(rv)) return rv;
}
nsXPIDLString promptTitle;
nsXPIDLString promptText;
if (mStringBundle)
rv = mStringBundle->GetStringFromName(NS_LITERAL_STRING("GopherPromptTitle").get(),
getter_Copies(promptTitle));
if (NS_FAILED(rv) || !mStringBundle)
promptTitle.AssignLiteral("Search");
if (mStringBundle)
rv = mStringBundle->GetStringFromName(NS_LITERAL_STRING("GopherPromptText").get(),
getter_Copies(promptText));
if (NS_FAILED(rv) || !mStringBundle)
promptText.AssignLiteral("Enter a search term:");
nsXPIDLString search;
PRBool res;
prompter->Prompt(promptTitle.get(),
promptText.get(),
getter_Copies(search),
NULL,
NULL,
&res);
if (!res || !(*search.get()))
return NS_ERROR_FAILURE;
mRequest.Append('\t');
AppendUTF16toUTF8(search, mRequest); // XXX Is UTF-8 the right thing?
// and update our uri
nsCAutoString spec;
rv = mUrl->GetAsciiSpec(spec);
if (NS_FAILED(rv))
return rv;
spec.Append('?');
AppendUTF16toUTF8(search, spec);
rv = mUrl->SetSpec(spec);
if (NS_FAILED(rv))
return rv;
} else {
// Just replace it with a tab
mRequest.SetCharAt('\t',pos);
}
}
mRequest.Append(CRLF);
PR_LOG(gGopherLog,PR_LOG_DEBUG,
("Sending: %s\n", mRequest.get()));
// open a buffered, blocking output stream. (it should never block because
// the buffer is big enough for our entire request.)
nsCOMPtr<nsIOutputStream> output;
rv = mTransport->OpenOutputStream(nsITransport::OPEN_BLOCKING,
mRequest.Length(), 1,
getter_AddRefs(output));
if (NS_FAILED(rv)) return rv;
PRUint32 n;
rv = output->Write(mRequest.get(), mRequest.Length(), &n);
if (NS_FAILED(rv)) return rv;
if (n != mRequest.Length())
return NS_ERROR_UNEXPECTED;
return NS_OK;
}
nsresult
nsGopherChannel::PushStreamConverters(nsIStreamListener *listener, nsIStreamListener **result)
{
nsresult rv;
nsCOMPtr<nsIStreamListener> converterListener;
nsCOMPtr<nsIStreamConverterService> StreamConvService =
do_GetService(kStreamConverterServiceCID, &rv);
if (NS_FAILED(rv)) return rv;
// What we now do depends on what type of file we have
if (mType=='1' || mType=='7') {
// Send the directory format back for a directory
rv = StreamConvService->AsyncConvertData("text/gopher-dir",
APPLICATION_HTTP_INDEX_FORMAT,
listener,
mUrl,
getter_AddRefs(converterListener));
if (NS_FAILED(rv)) return rv;
} else if (mType=='0') {
// Convert general file
rv = StreamConvService->AsyncConvertData("text/plain",
"text/html",
listener,
mListenerContext,
getter_AddRefs(converterListener));
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsITXTToHTMLConv> converter(do_QueryInterface(converterListener));
if (converter) {
nsCAutoString spec;
rv = mUrl->GetSpec(spec);
converter->SetTitle(NS_ConvertUTF8toUCS2(spec).get());
converter->PreFormatHTML(PR_TRUE);
}
}
NS_IF_ADDREF(*result = converterListener);
return NS_OK;
}
NS_IMETHODIMP
nsGopherChannel::OnTransportStatus(nsITransport *trans, nsresult status,
PRUint64 progress, PRUint64 progressMax)
{
if (!mProgressSink)
NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, mProgressSink);
// suppress status notification if channel is no longer pending!
if (mProgressSink && NS_SUCCEEDED(mStatus) && mPump && !(mLoadFlags & LOAD_BACKGROUND)) {
NS_ConvertUTF8toUTF16 host(mHost);
mProgressSink->OnStatus(this, nsnull, status, host.get());
if (status == nsISocketTransport::STATUS_RECEIVING_FROM ||
status == nsISocketTransport::STATUS_SENDING_TO) {
mProgressSink->OnProgress(this, nsnull, progress, progressMax);
}
}
return NS_OK;
}