/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* ***** 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 mozilla.org code.
 *
 * The Initial Developer of the Original Code is Google Inc.
 * Portions created by the Initial Developer are Copyright (C) 2005
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Darin Fisher <darin@meer.net>
 *
 * 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 "nsPACMan.h"
#include "nsIDNSService.h"
#include "nsIDNSListener.h"
#include "nsICancelable.h"
#include "nsIAuthPrompt.h"
#include "nsIHttpChannel.h"
#include "nsIPrefService.h"
#include "nsIPrefBranch.h"
#include "nsEventQueueUtils.h"
#include "nsNetUtil.h"
#include "nsAutoLock.h"
#include "nsAutoPtr.h"
#include "nsCRT.h"
#include "prmon.h"

//-----------------------------------------------------------------------------

// Check to see if the underlying request was not an error page in the case of
// a HTTP request.  For other types of channels, just return true.
static PRBool
HttpRequestSucceeded(nsIStreamLoader *loader)
{
  nsCOMPtr<nsIRequest> request;
  loader->GetRequest(getter_AddRefs(request));

  PRBool result = PR_TRUE;  // default to assuming success

  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request);
  if (httpChannel)
    httpChannel->GetRequestSucceeded(&result);

  return result;
}

//-----------------------------------------------------------------------------

// These objects are stored in nsPACMan::mPendingQ

class PendingPACQuery : public PRCList, public nsIDNSListener
{
public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIDNSLISTENER

  PendingPACQuery(nsPACMan *pacMan, nsIURI *uri, nsPACManCallback *callback)
    : mPACMan(pacMan)
    , mURI(uri)
    , mCallback(callback)
  {
    PR_INIT_CLIST(this);
  }

  nsresult Start();
  void     Complete(nsresult status, const nsCString &pacString);

private:
  nsPACMan                  *mPACMan;  // weak reference
  nsCOMPtr<nsIURI>           mURI;
  nsRefPtr<nsPACManCallback> mCallback;
  nsCOMPtr<nsICancelable>    mDNSRequest;
};

// This is threadsafe because we implement nsIDNSListener
NS_IMPL_THREADSAFE_ISUPPORTS1(PendingPACQuery, nsIDNSListener)

nsresult
PendingPACQuery::Start()
{
  if (mDNSRequest)
    return NS_OK;  // already started

  nsresult rv;
  nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID, &rv);
  if (NS_FAILED(rv)) {
    NS_WARNING("unable to get the DNS service");
    return rv;
  }

  nsCAutoString host;
  rv = mURI->GetAsciiHost(host);
  if (NS_FAILED(rv))
    return rv;

  nsCOMPtr<nsIEventQueue> eventQ;
  rv = NS_GetCurrentEventQ(getter_AddRefs(eventQ));
  if (NS_FAILED(rv))
    return rv;

  rv = dns->AsyncResolve(host, nsIDNSService::RESOLVE_EXTENDED_ERRS, this,
                         eventQ, getter_AddRefs(mDNSRequest));
  if (NS_FAILED(rv))
    NS_WARNING("DNS AsyncResolve failed");

  return rv;
}

// This may be called before or after OnLookupComplete
void
PendingPACQuery::Complete(nsresult status, const nsCString &pacString)
{
  if (!mCallback)
    return;

  mCallback->OnQueryComplete(status, pacString);
  mCallback = nsnull;

  if (mDNSRequest) {
    mDNSRequest->Cancel(NS_ERROR_ABORT);
    mDNSRequest = nsnull;
  }
}

NS_IMETHODIMP
PendingPACQuery::OnLookupComplete(nsICancelable *request,
                                  nsIDNSRecord *record,
                                  nsresult status)
{
  // NOTE: we don't care about the results of this DNS query.  We issued
  //       this DNS query just to pre-populate our DNS cache.
 
  mDNSRequest = nsnull;  // break reference cycle

  // If we've already completed this query then do nothing.
  if (!mCallback)
    return NS_OK;

  // We're no longer pending, so we can remove ourselves.
  PR_REMOVE_LINK(this);
  NS_RELEASE_THIS();

  nsCAutoString pacString;
  status = mPACMan->GetProxyForURI(mURI, pacString);
  Complete(status, pacString);
  return NS_OK;
}

//-----------------------------------------------------------------------------

nsPACMan::nsPACMan()
  : mLoadEvent(nsnull)
  , mShutdown(PR_FALSE)
  , mScheduledReload(LL_MAXINT)
  , mLoadFailureCount(0)
{
  PR_INIT_CLIST(&mPendingQ);
}

nsPACMan::~nsPACMan()
{
  NS_ASSERTION(mLoader == nsnull, "pac man not shutdown properly");
  NS_ASSERTION(mPAC == nsnull, "pac man not shutdown properly");
  NS_ASSERTION(PR_CLIST_IS_EMPTY(&mPendingQ), "pac man not shutdown properly");
}

void
nsPACMan::Shutdown()
{
  CancelExistingLoad();
  ProcessPendingQ(NS_ERROR_ABORT);

  mPAC = nsnull;
  mShutdown = PR_TRUE;
}

nsresult
nsPACMan::GetProxyForURI(nsIURI *uri, nsACString &result)
{
  NS_ENSURE_STATE(!mShutdown);

  if (IsPACURI(uri)) {
    result.Truncate();
    return NS_OK;
  }

  MaybeReloadPAC();

  if (IsLoading())
    return NS_ERROR_IN_PROGRESS;
  if (!mPAC)
    return NS_ERROR_NOT_AVAILABLE;

  nsCAutoString spec, host;
  uri->GetAsciiSpec(spec);
  uri->GetAsciiHost(host);

  return mPAC->GetProxyForURI(spec, host, result);
}

nsresult
nsPACMan::AsyncGetProxyForURI(nsIURI *uri, nsPACManCallback *callback)
{
  NS_ENSURE_STATE(!mShutdown);

  MaybeReloadPAC();

  PendingPACQuery *query = new PendingPACQuery(this, uri, callback);
  if (!query)
    return NS_ERROR_OUT_OF_MEMORY;
  NS_ADDREF(query);
  PR_APPEND_LINK(query, &mPendingQ);

  // If we're waiting for the PAC file to load, then delay starting the query.
  // See OnStreamComplete.  However, if this is the PAC URI then query right
  // away since we know the result will be DIRECT.  We could shortcut some code
  // in this case by issuing the callback directly from here, but that would
  // require extra code, so we just go through the usual async code path.
  if (IsLoading() && !IsPACURI(uri))
    return NS_OK;

  nsresult rv = query->Start();
  if (NS_FAILED(rv)) {
    NS_WARNING("failed to start PAC query");
    PR_REMOVE_LINK(query);
    NS_RELEASE(query);
  }

  return rv;
}

void *PR_CALLBACK
nsPACMan::LoadEvent_Handle(PLEvent *ev)
{
  NS_REINTERPRET_CAST(nsPACMan *, PL_GetEventOwner(ev))->StartLoading();
  return nsnull;
}

void PR_CALLBACK
nsPACMan::LoadEvent_Destroy(PLEvent *ev)
{
  nsPACMan *self = NS_REINTERPRET_CAST(nsPACMan *, PL_GetEventOwner(ev));
  self->mLoadEvent = nsnull;
  self->Release();
  delete ev;
}

nsresult
nsPACMan::LoadPACFromURI(nsIURI *pacURI)
{
  NS_ENSURE_STATE(!mShutdown);

  nsCOMPtr<nsIStreamLoader> loader =
      do_CreateInstance(NS_STREAMLOADER_CONTRACTID);
  NS_ENSURE_STATE(loader);

  // Since we might get called from nsProtocolProxyService::Init, we need to
  // post an event back to the main thread before we try to use the IO service.
  //
  // But, we need to flag ourselves as loading, so that we queue up any PAC
  // queries the enter between now and when we actually load the PAC file.

  if (!mLoadEvent) {
    mLoadEvent = new PLEvent;
    if (!mLoadEvent)
      return NS_ERROR_OUT_OF_MEMORY;

    NS_ADDREF_THIS();
    PL_InitEvent(mLoadEvent, this, LoadEvent_Handle, LoadEvent_Destroy);

    nsCOMPtr<nsIEventQueue> eventQ;
    nsresult rv = NS_GetCurrentEventQ(getter_AddRefs(eventQ));
    if (NS_FAILED(rv) || NS_FAILED(rv = eventQ->PostEvent(mLoadEvent))) {
      PL_DestroyEvent(mLoadEvent);
      return rv;
    }
  }

  CancelExistingLoad();

  mLoader = loader;
  mPACURI = pacURI;
  mPAC = nsnull;
  return NS_OK;
}

nsresult
nsPACMan::StartLoading()
{
  // CancelExistingLoad was called...
  if (!mLoader) {
    ProcessPendingQ(NS_ERROR_ABORT);
    return NS_OK;
  }

  // Always hit the origin server when loading PAC.
  nsCOMPtr<nsIIOService> ios = do_GetIOService();
  if (ios) {
    nsCOMPtr<nsIChannel> channel;

    // NOTE: This results in GetProxyForURI being called
    ios->NewChannelFromURI(mPACURI, getter_AddRefs(channel));

    if (channel) {
      channel->SetLoadFlags(nsIRequest::LOAD_BYPASS_CACHE);
      channel->SetNotificationCallbacks(this);
      if (NS_SUCCEEDED(mLoader->Init(channel, this, nsnull)))
        return NS_OK;
    }
  }

  CancelExistingLoad();
  ProcessPendingQ(NS_ERROR_UNEXPECTED);
  return NS_OK;
}

void
nsPACMan::MaybeReloadPAC()
{
  if (!mPACURI)
    return;

  if (PR_Now() > mScheduledReload) {
    mScheduledReload = LL_MAXINT;
    LoadPACFromURI(mPACURI);
  }
}

void
nsPACMan::OnLoadFailure()
{
  PRInt32 minInterval = 5;    // 5 seconds
  PRInt32 maxInterval = 300;  // 5 minutes

  nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
  if (prefs) {
    prefs->GetIntPref("network.proxy.autoconfig_retry_interval_min",
                      &minInterval);
    prefs->GetIntPref("network.proxy.autoconfig_retry_interval_max",
                      &maxInterval);
  }

  PRInt32 interval = minInterval << mLoadFailureCount++;  // seconds
  if (!interval || interval > maxInterval)
    interval = maxInterval;

#ifdef DEBUG
  printf("PAC load failure: will retry in %d seconds\n", interval);
#endif

  mScheduledReload = PR_Now() + PRInt64(interval) * PR_USEC_PER_SEC;
}

void
nsPACMan::CancelExistingLoad()
{
  if (mLoader) {
    nsCOMPtr<nsIRequest> request;
    mLoader->GetRequest(getter_AddRefs(request));
    if (request)
      request->Cancel(NS_ERROR_ABORT);
    mLoader = nsnull;
  }
}

void
nsPACMan::ProcessPendingQ(nsresult status)
{
  // Now, start any pending queries
  PRCList *node = PR_LIST_HEAD(&mPendingQ);
  while (node != &mPendingQ) {
    PendingPACQuery *query = NS_STATIC_CAST(PendingPACQuery *, node);
    node = PR_NEXT_LINK(node);
    if (NS_SUCCEEDED(status)) {
      // keep the query in the list (so we can complete it from Shutdown if
      // necessary).
      status = query->Start();
    }
    if (NS_FAILED(status)) {
      // remove the query from the list
      PR_REMOVE_LINK(query);
      query->Complete(status, EmptyCString());
      NS_RELEASE(query);
    }
  }
}

NS_IMPL_ISUPPORTS3(nsPACMan, nsIStreamLoaderObserver, nsIInterfaceRequestor,
                   nsIChannelEventSink)

NS_IMETHODIMP
nsPACMan::OnStreamComplete(nsIStreamLoader *loader,
                           nsISupports *context,
                           nsresult status,
                           PRUint32 dataLen,
                           const PRUint8 *data)
{
  if (mLoader != loader) {
    // If this happens, then it means that LoadPACFromURI was called more
    // than once before the initial call completed.  In this case, status
    // should be NS_ERROR_ABORT, and if so, then we know that we can and
    // should delay any processing.
    if (status == NS_ERROR_ABORT)
      return NS_OK;
  }

  mLoader = nsnull;

  if (NS_SUCCEEDED(status) && HttpRequestSucceeded(loader)) {
    // Get the URI spec used to load this PAC script.
    nsCAutoString pacURI;
    {
      nsCOMPtr<nsIRequest> request;
      loader->GetRequest(getter_AddRefs(request));
      nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
      if (channel) {
        nsCOMPtr<nsIURI> uri;
        channel->GetURI(getter_AddRefs(uri));
        if (uri)
          uri->GetAsciiSpec(pacURI);
      }
    }

    if (!mPAC) {
      mPAC = do_CreateInstance(NS_PROXYAUTOCONFIG_CONTRACTID, &status);
      if (!mPAC)
        NS_WARNING("failed to instantiate PAC component");
    }
    if (NS_SUCCEEDED(status)) {
      // We assume that the PAC text is ASCII (or ISO-Latin-1).  We've had this
      // assumption forever, and some real-world PAC scripts actually have some
      // non-ASCII text in comment blocks (see bug 296163).
      const char *text = (const char *) data;
      status = mPAC->Init(pacURI, NS_ConvertASCIItoUTF16(text, dataLen));
    }

    // Even if the PAC file could not be parsed, we did succeed in loading the
    // data for it.
    mLoadFailureCount = 0;
  } else {
    // We were unable to load the PAC file (presumably because of a network
    // failure).  Try again a little later.
    OnLoadFailure();
  }

  // Reset mPAC if necessary
  if (mPAC && NS_FAILED(status))
    mPAC = nsnull;

  ProcessPendingQ(status);
  return NS_OK;
}

NS_IMETHODIMP
nsPACMan::GetInterface(const nsIID &iid, void **result)
{
  // In case loading the PAC file requires authentication.
  if (iid.Equals(NS_GET_IID(nsIAuthPrompt)))
    return CallCreateInstance(NS_DEFAULTAUTHPROMPT_CONTRACTID,
                              nsnull, iid, result);

  // In case loading the PAC file results in a redirect.
  if (iid.Equals(NS_GET_IID(nsIChannelEventSink))) {
    NS_ADDREF_THIS();
    *result = NS_STATIC_CAST(nsIChannelEventSink *, this);
    return NS_OK;
  }

  return NS_ERROR_NO_INTERFACE;
}

NS_IMETHODIMP
nsPACMan::OnChannelRedirect(nsIChannel *oldChannel, nsIChannel *newChannel,
                            PRUint32 flags)
{
  return newChannel->GetURI(getter_AddRefs(mPACURI));
}
