зеркало из https://github.com/mozilla/gecko-dev.git
499 строки
16 KiB
C++
499 строки
16 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/* vim:set ts=4 sw=4 sts=4 cin et:
|
|
*
|
|
* ***** 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 ***** */
|
|
|
|
#include "nsGopherChannel.h"
|
|
#include "nsGopherHandler.h"
|
|
#include "nsBaseContentStream.h"
|
|
#include "nsIAsyncInputStream.h"
|
|
#include "nsIAsyncOutputStream.h"
|
|
#include "nsISocketTransportService.h"
|
|
#include "nsISocketTransport.h"
|
|
#include "nsIStringBundle.h"
|
|
#include "nsITXTToHTMLConv.h"
|
|
#include "nsIPrompt.h"
|
|
#include "nsServiceManagerUtils.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "nsStreamUtils.h"
|
|
#include "nsMimeTypes.h"
|
|
#include "nsNetCID.h"
|
|
#include "nsCOMPtr.h"
|
|
#include "nsAutoPtr.h"
|
|
#include "nsEscape.h"
|
|
#include "nsCRT.h"
|
|
#include "netCore.h"
|
|
|
|
// Specifies the maximum number of output stream buffer segments that we can
|
|
// allocate before giving up. At 4k per segment, this corresponds to a max
|
|
// gopher request of 400k, which should be plenty.
|
|
#define GOPHER_MAX_WRITE_SEGMENT_COUNT 100
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
class nsGopherContentStream : public nsBaseContentStream
|
|
, public nsIInputStreamCallback
|
|
, public nsIOutputStreamCallback
|
|
{
|
|
public:
|
|
NS_DECL_ISUPPORTS_INHERITED
|
|
NS_DECL_NSIINPUTSTREAMCALLBACK
|
|
NS_DECL_NSIOUTPUTSTREAMCALLBACK
|
|
|
|
// stream methods that we override:
|
|
NS_IMETHOD Available(PRUint32 *result);
|
|
NS_IMETHOD ReadSegments(nsWriteSegmentFun writer, void *closure,
|
|
PRUint32 count, PRUint32 *result);
|
|
NS_IMETHOD CloseWithStatus(nsresult status);
|
|
|
|
nsGopherContentStream(nsGopherChannel *channel)
|
|
: nsBaseContentStream(PR_TRUE) // non-blocking
|
|
, mChannel(channel) {
|
|
}
|
|
|
|
nsresult OpenSocket(nsIEventTarget *target);
|
|
nsresult OnSocketWritable();
|
|
nsresult ParseTypeAndSelector(char &type, nsCString &selector);
|
|
nsresult PromptForQueryString(nsCString &result);
|
|
void UpdateContentType(char type);
|
|
nsresult SendRequest();
|
|
|
|
protected:
|
|
virtual void OnCallbackPending();
|
|
|
|
private:
|
|
nsRefPtr<nsGopherChannel> mChannel;
|
|
nsCOMPtr<nsISocketTransport> mSocket;
|
|
nsCOMPtr<nsIAsyncOutputStream> mSocketOutput;
|
|
nsCOMPtr<nsIAsyncInputStream> mSocketInput;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS_INHERITED2(nsGopherContentStream,
|
|
nsBaseContentStream,
|
|
nsIInputStreamCallback,
|
|
nsIOutputStreamCallback)
|
|
|
|
NS_IMETHODIMP
|
|
nsGopherContentStream::Available(PRUint32 *result)
|
|
{
|
|
if (mSocketInput)
|
|
return mSocketInput->Available(result);
|
|
|
|
return nsBaseContentStream::Available(result);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsGopherContentStream::ReadSegments(nsWriteSegmentFun writer, void *closure,
|
|
PRUint32 count, PRUint32 *result)
|
|
{
|
|
// Insert a thunk here so that the input stream passed to the writer is
|
|
// this input stream instead of mSocketInput.
|
|
if (mSocketInput) {
|
|
nsWriteSegmentThunk thunk = { this, writer, closure };
|
|
return mSocketInput->ReadSegments(NS_WriteSegmentThunk, &thunk, count,
|
|
result);
|
|
}
|
|
|
|
return nsBaseContentStream::ReadSegments(writer, closure, count, result);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsGopherContentStream::CloseWithStatus(nsresult status)
|
|
{
|
|
if (mSocket) {
|
|
mSocket->Close(status);
|
|
mSocket = nsnull;
|
|
mSocketInput = nsnull;
|
|
mSocketOutput = nsnull;
|
|
}
|
|
return nsBaseContentStream::CloseWithStatus(status);
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsGopherContentStream::OnInputStreamReady(nsIAsyncInputStream *stream)
|
|
{
|
|
// Forward this notification
|
|
DispatchCallbackSync();
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
nsGopherContentStream::OnOutputStreamReady(nsIAsyncOutputStream *stream)
|
|
{
|
|
// If we're already closed, mSocketOutput is going to be null and we'll
|
|
// just be getting notified that it got closed (by outselves). In that
|
|
// case, nothing to do here.
|
|
if (!mSocketOutput) {
|
|
NS_ASSERTION(NS_FAILED(Status()), "How did that happen?");
|
|
return NS_OK;
|
|
}
|
|
|
|
// We have to close ourselves if we hit an error here in order to propagate
|
|
// the error to our consumer. Otherwise, just forward the notification so
|
|
// that the consumer will know to start reading.
|
|
|
|
nsresult rv = OnSocketWritable();
|
|
if (NS_FAILED(rv))
|
|
CloseWithStatus(rv);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsGopherContentStream::OnCallbackPending()
|
|
{
|
|
nsresult rv;
|
|
|
|
// We have a callback, so failure means we should close the stream.
|
|
if (!mSocket) {
|
|
rv = OpenSocket(CallbackTarget());
|
|
} else if (mSocketInput) {
|
|
rv = mSocketInput->AsyncWait(this, 0, 0, CallbackTarget());
|
|
}
|
|
|
|
if (NS_FAILED(rv))
|
|
CloseWithStatus(rv);
|
|
}
|
|
|
|
nsresult
|
|
nsGopherContentStream::OpenSocket(nsIEventTarget *target)
|
|
{
|
|
// This function is called to get things started.
|
|
|
|
// We begin by opening a socket to the specified host and wait for the
|
|
// socket to become writable.
|
|
|
|
nsCAutoString host;
|
|
nsresult rv = mChannel->URI()->GetAsciiHost(host);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
if (host.IsEmpty())
|
|
return NS_ERROR_MALFORMED_URI;
|
|
|
|
// For security reasons, don't allow anything expect the default
|
|
// gopher port (70). See bug 71916 - bbaetz@cs.mcgill.ca
|
|
PRInt32 port = GOPHER_PORT;
|
|
|
|
// Create socket tranport
|
|
nsCOMPtr<nsISocketTransportService> sts =
|
|
do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
rv = sts->CreateTransport(nsnull, 0, host, port, mChannel->ProxyInfo(),
|
|
getter_AddRefs(mSocket));
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
mSocket->SetQoSBits(gGopherHandler->GetQoSBits());
|
|
|
|
// Setup progress and status notifications
|
|
rv = mSocket->SetEventSink(mChannel, target);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
nsCOMPtr<nsIOutputStream> output;
|
|
rv = mSocket->OpenOutputStream(0, 0, GOPHER_MAX_WRITE_SEGMENT_COUNT,
|
|
getter_AddRefs(output));
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
mSocketOutput = do_QueryInterface(output);
|
|
NS_ENSURE_STATE(mSocketOutput);
|
|
|
|
return mSocketOutput->AsyncWait(this, 0, 0, target);
|
|
}
|
|
|
|
nsresult
|
|
nsGopherContentStream::OnSocketWritable()
|
|
{
|
|
// Write to output stream (we can do this in one big chunk)
|
|
nsresult rv = SendRequest();
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
// Open input stream
|
|
nsCOMPtr<nsIInputStream> input;
|
|
rv = mSocket->OpenInputStream(0, 0, 0, getter_AddRefs(input));
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
mSocketInput = do_QueryInterface(input, &rv);
|
|
|
|
NS_ASSERTION(CallbackTarget(), "where is my pending callback?");
|
|
rv = mSocketInput->AsyncWait(this, 0, 0, CallbackTarget());
|
|
|
|
return rv;
|
|
}
|
|
|
|
nsresult
|
|
nsGopherContentStream::ParseTypeAndSelector(char &type, nsCString &selector)
|
|
{
|
|
nsCAutoString buffer;
|
|
nsresult rv = mChannel->URI()->GetPath(buffer); // unescaped down below
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
// No path given
|
|
if (buffer[0] == '\0' || (buffer[0] == '/' && buffer[1] == '\0')) {
|
|
type = '1';
|
|
selector.Truncate();
|
|
} else {
|
|
NS_ENSURE_STATE(buffer[1] != '\0');
|
|
|
|
type = buffer[1]; // Ignore leading '/'
|
|
|
|
// Do it this way in case selector contains embedded nulls after
|
|
// unescaping.
|
|
char *sel = buffer.BeginWriting() + 2;
|
|
PRInt32 count = nsUnescapeCount(sel);
|
|
selector.Assign(sel, count);
|
|
|
|
// NOTE: FindCharInSet cannot be used to search for a null byte.
|
|
if (selector.FindCharInSet("\t\n\r") != kNotFound ||
|
|
selector.FindChar('\0') != kNotFound) {
|
|
// gopher selectors cannot containt tab, cr, lf, or \0
|
|
return NS_ERROR_MALFORMED_URI;
|
|
}
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
nsGopherContentStream::PromptForQueryString(nsCString &result)
|
|
{
|
|
nsCOMPtr<nsIPrompt> prompter;
|
|
mChannel->GetCallback(prompter);
|
|
if (!prompter) {
|
|
NS_ERROR("We need a prompter!");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsCOMPtr<nsIStringBundle> bundle;
|
|
nsCOMPtr<nsIStringBundleService> bundleSvc =
|
|
do_GetService(NS_STRINGBUNDLE_CONTRACTID);
|
|
if (bundleSvc)
|
|
bundleSvc->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(bundle));
|
|
|
|
nsXPIDLString promptTitle, promptText;
|
|
if (bundle) {
|
|
bundle->GetStringFromName(NS_LITERAL_STRING("GopherPromptTitle").get(),
|
|
getter_Copies(promptTitle));
|
|
bundle->GetStringFromName(NS_LITERAL_STRING("GopherPromptText").get(),
|
|
getter_Copies(promptText));
|
|
}
|
|
if (promptTitle.IsEmpty())
|
|
promptTitle.AssignLiteral("Search");
|
|
if (promptText.IsEmpty())
|
|
promptText.AssignLiteral("Enter a search term:");
|
|
|
|
nsXPIDLString value;
|
|
PRBool res = PR_FALSE;
|
|
PRBool checkState;
|
|
prompter->Prompt(promptTitle.get(), promptText.get(),
|
|
getter_Copies(value), NULL, &checkState, &res);
|
|
if (!res || value.IsEmpty())
|
|
return NS_ERROR_FAILURE;
|
|
|
|
CopyUTF16toUTF8(value, result); // XXX Is UTF-8 the right thing?
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsGopherContentStream::UpdateContentType(char type)
|
|
{
|
|
const char *contentType = nsnull;
|
|
|
|
switch(type) {
|
|
case '0':
|
|
case 'h':
|
|
case '2': // CSO search - unhandled, should not be selectable
|
|
case '3': // "Error" - should not be selectable
|
|
case 'i': // info line- should not be selectable
|
|
contentType = TEXT_HTML;
|
|
break;
|
|
case '1':
|
|
case '7': // search - returns a directory listing
|
|
contentType = APPLICATION_HTTP_INDEX_FORMAT;
|
|
break;
|
|
case 'g':
|
|
case 'I':
|
|
contentType = IMAGE_GIF;
|
|
break;
|
|
case 'T': // tn3270 - type doesn't make sense
|
|
case '8': // telnet - type doesn't make sense
|
|
contentType = TEXT_PLAIN;
|
|
break;
|
|
case '5': // "DOS binary archive of some sort" - is the mime-type correct?
|
|
case '9': // "Binary file!"
|
|
contentType = APPLICATION_OCTET_STREAM;
|
|
break;
|
|
case '4': // "BinHexed Macintosh file"
|
|
contentType = APPLICATION_BINHEX;
|
|
break;
|
|
case '6':
|
|
contentType = APPLICATION_UUENCODE;
|
|
break;
|
|
}
|
|
|
|
if (contentType)
|
|
mChannel->SetContentType(nsDependentCString(contentType));
|
|
}
|
|
|
|
nsresult
|
|
nsGopherContentStream::SendRequest()
|
|
{
|
|
char type;
|
|
nsCAutoString request; // used to build request data
|
|
|
|
nsresult rv = ParseTypeAndSelector(type, request);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
// So, we use the selector as is unless it is a search url
|
|
if (type == '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 = request.RFindChar('?');
|
|
if (pos != kNotFound) {
|
|
// Just replace it with a tab
|
|
request.SetCharAt('\t', pos);
|
|
} else {
|
|
// We require a query string here - if we don't have one,
|
|
// then we need to ask the user
|
|
nsCAutoString search;
|
|
rv = PromptForQueryString(search);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
request.Append('\t');
|
|
request.Append(search);
|
|
|
|
// and update our uri (XXX should probably redirect instead to avoid
|
|
// confusing consumers of the channel)
|
|
nsCAutoString spec;
|
|
rv = mChannel->URI()->GetAsciiSpec(spec);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
|
|
spec.Append('?');
|
|
spec.Append(search);
|
|
rv = mChannel->URI()->SetSpec(spec);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
request.Append(CRLF);
|
|
|
|
PRUint32 n;
|
|
rv = mSocketOutput->Write(request.get(), request.Length(), &n);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
NS_ENSURE_STATE(n == request.Length());
|
|
|
|
// Now, push stream converters appropriately based on our 'type'
|
|
if (type == '1' || type == '7') {
|
|
rv = mChannel->PushStreamConverter("text/gopher-dir",
|
|
APPLICATION_HTTP_INDEX_FORMAT);
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
} else if (type == '0') {
|
|
nsCOMPtr<nsIStreamListener> converter;
|
|
rv = mChannel->PushStreamConverter(TEXT_PLAIN, TEXT_HTML, PR_TRUE,
|
|
getter_AddRefs(converter));
|
|
if (NS_FAILED(rv))
|
|
return rv;
|
|
nsCOMPtr<nsITXTToHTMLConv> config = do_QueryInterface(converter);
|
|
if (config) {
|
|
nsCAutoString spec;
|
|
mChannel->URI()->GetSpec(spec);
|
|
config->SetTitle(NS_ConvertUTF8toUTF16(spec).get());
|
|
config->PreFormatHTML(PR_TRUE);
|
|
}
|
|
}
|
|
|
|
UpdateContentType(type);
|
|
return NS_OK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
NS_IMPL_ISUPPORTS_INHERITED1(nsGopherChannel,
|
|
nsBaseChannel,
|
|
nsIProxiedChannel)
|
|
|
|
NS_IMETHODIMP
|
|
nsGopherChannel::GetProxyInfo(nsIProxyInfo** aProxyInfo)
|
|
{
|
|
*aProxyInfo = ProxyInfo();
|
|
NS_IF_ADDREF(*aProxyInfo);
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
nsGopherChannel::OpenContentStream(PRBool async, nsIInputStream **result,
|
|
nsIChannel** channel)
|
|
{
|
|
// Implement nsIChannel::Open in terms of nsIChannel::AsyncOpen
|
|
if (!async)
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
|
|
nsRefPtr<nsIInputStream> stream = new nsGopherContentStream(this);
|
|
if (!stream)
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
|
|
*result = nsnull;
|
|
stream.swap(*result);
|
|
return NS_OK;
|
|
}
|
|
|
|
PRBool
|
|
nsGopherChannel::GetStatusArg(nsresult status, nsString &statusArg)
|
|
{
|
|
nsCAutoString host;
|
|
URI()->GetHost(host);
|
|
CopyUTF8toUTF16(host, statusArg);
|
|
return PR_TRUE;
|
|
}
|