/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set sw=2 ts=8 et ft=cpp : */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "MediaParent.h"

#include "mozilla/Base64.h"
#include <mozilla/StaticMutex.h>

#include "MediaUtils.h"
#include "MediaEngine.h"
#include "VideoUtils.h"
#include "nsClassHashtable.h"
#include "nsThreadUtils.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsIInputStream.h"
#include "nsILineInputStream.h"
#include "nsIOutputStream.h"
#include "nsISafeOutputStream.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsIFile.h"
#include "nsISupportsImpl.h"
#include "mozilla/Logging.h"

#undef LOG
mozilla::LazyLogModule gMediaParentLog("MediaParent");
#define LOG(args) MOZ_LOG(gMediaParentLog, mozilla::LogLevel::Debug, args)

// A file in the profile dir is used to persist mOriginKeys used to anonymize
// deviceIds to be unique per origin, to avoid them being supercookies.

#define ORIGINKEYS_FILE u"enumerate_devices.txt"
#define ORIGINKEYS_VERSION "1"

namespace mozilla::media {

StaticMutex sOriginKeyStoreStsMutex;

class OriginKeyStore {
  NS_INLINE_DECL_REFCOUNTING(OriginKeyStore);
  class OriginKey {
   public:
    static const size_t DecodedLength = 18;
    static const size_t EncodedLength = DecodedLength * 4 / 3;

    explicit OriginKey(const nsACString& aKey,
                       int64_t aSecondsStamp = 0)  // 0 = temporal
        : mKey(aKey), mSecondsStamp(aSecondsStamp) {}

    nsCString mKey;  // Base64 encoded.
    int64_t mSecondsStamp;
  };

  class OriginKeysTable {
   public:
    OriginKeysTable() : mPersistCount(0) {}

    nsresult GetPrincipalKey(const ipc::PrincipalInfo& aPrincipalInfo,
                             nsCString& aResult, bool aPersist = false) {
      nsAutoCString principalString;
      PrincipalInfoToString(aPrincipalInfo, principalString);

      OriginKey* key;
      if (!mKeys.Get(principalString, &key)) {
        nsCString salt;  // Make a new one
        nsresult rv = GenerateRandomName(salt, OriginKey::EncodedLength);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
        key = mKeys.InsertOrUpdate(principalString, MakeUnique<OriginKey>(salt))
                  .get();
      }
      if (aPersist && !key->mSecondsStamp) {
        key->mSecondsStamp = PR_Now() / PR_USEC_PER_SEC;
        mPersistCount++;
      }
      aResult = key->mKey;
      return NS_OK;
    }

    void Clear(int64_t aSinceWhen) {
      // Avoid int64_t* <-> void* casting offset
      OriginKey since(nsCString(), aSinceWhen / PR_USEC_PER_SEC);
      for (auto iter = mKeys.Iter(); !iter.Done(); iter.Next()) {
        auto originKey = iter.UserData();
        LOG((((originKey->mSecondsStamp >= since.mSecondsStamp)
                  ? "%s: REMOVE %" PRId64 " >= %" PRId64
                  : "%s: KEEP   %" PRId64 " < %" PRId64),
             __FUNCTION__, originKey->mSecondsStamp, since.mSecondsStamp));

        if (originKey->mSecondsStamp >= since.mSecondsStamp) {
          iter.Remove();
        }
      }
      mPersistCount = 0;
    }

   private:
    void PrincipalInfoToString(const ipc::PrincipalInfo& aPrincipalInfo,
                               nsACString& aString) {
      switch (aPrincipalInfo.type()) {
        case ipc::PrincipalInfo::TSystemPrincipalInfo:
          aString.AssignLiteral("[System Principal]");
          return;

        case ipc::PrincipalInfo::TNullPrincipalInfo: {
          const ipc::NullPrincipalInfo& info =
              aPrincipalInfo.get_NullPrincipalInfo();
          aString.Assign(info.spec());
          return;
        }

        case ipc::PrincipalInfo::TContentPrincipalInfo: {
          const ipc::ContentPrincipalInfo& info =
              aPrincipalInfo.get_ContentPrincipalInfo();
          aString.Assign(info.originNoSuffix());

          nsAutoCString suffix;
          info.attrs().CreateSuffix(suffix);
          aString.Append(suffix);
          return;
        }

        case ipc::PrincipalInfo::TExpandedPrincipalInfo: {
          const ipc::ExpandedPrincipalInfo& info =
              aPrincipalInfo.get_ExpandedPrincipalInfo();

          aString.AssignLiteral("[Expanded Principal [");

          for (uint32_t i = 0; i < info.allowlist().Length(); i++) {
            nsAutoCString str;
            PrincipalInfoToString(info.allowlist()[i], str);

            if (i != 0) {
              aString.AppendLiteral(", ");
            }

            aString.Append(str);
          }

          aString.AppendLiteral("]]");
          return;
        }

        default:
          MOZ_CRASH("Unknown PrincipalInfo type!");
      }
    }

   protected:
    nsClassHashtable<nsCStringHashKey, OriginKey> mKeys;
    size_t mPersistCount;
  };

  class OriginKeysLoader : public OriginKeysTable {
   public:
    OriginKeysLoader() = default;

    nsresult GetPrincipalKey(const ipc::PrincipalInfo& aPrincipalInfo,
                             nsCString& aResult, bool aPersist = false) {
      auto before = mPersistCount;
      nsresult rv =
          OriginKeysTable::GetPrincipalKey(aPrincipalInfo, aResult, aPersist);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }

      if (mPersistCount != before) {
        Save();
      }
      return NS_OK;
    }

    already_AddRefed<nsIFile> GetFile() {
      MOZ_ASSERT(mProfileDir);
      nsCOMPtr<nsIFile> file;
      nsresult rv = mProfileDir->Clone(getter_AddRefs(file));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return nullptr;
      }
      file->Append(nsLiteralString(ORIGINKEYS_FILE));
      return file.forget();
    }

    // Format of file is key secondsstamp origin (first line is version #):
    //
    // 1
    // rOMAAbFujNwKyIpj4RJ3Wt5Q 1424733961 http://fiddle.jshell.net
    // rOMAAbFujNwKyIpj4RJ3Wt5Q 1424734841 http://mozilla.github.io
    // etc.

    nsresult Read() {
      nsCOMPtr<nsIFile> file = GetFile();
      if (NS_WARN_IF(!file)) {
        return NS_ERROR_UNEXPECTED;
      }
      bool exists;
      nsresult rv = file->Exists(&exists);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (!exists) {
        return NS_OK;
      }

      nsCOMPtr<nsIInputStream> stream;
      rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      nsCOMPtr<nsILineInputStream> i = do_QueryInterface(stream);
      MOZ_ASSERT(i);
      MOZ_ASSERT(!mPersistCount);

      nsCString line;
      bool hasMoreLines;
      rv = i->ReadLine(line, &hasMoreLines);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (!line.EqualsLiteral(ORIGINKEYS_VERSION)) {
        // If version on disk is newer than we can understand then ignore it.
        return NS_OK;
      }

      while (hasMoreLines) {
        rv = i->ReadLine(line, &hasMoreLines);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return rv;
        }
        // Read key secondsstamp origin.
        // Ignore any lines that don't fit format in the comment above exactly.
        int32_t f = line.FindChar(' ');
        if (f < 0) {
          continue;
        }
        const nsACString& key = Substring(line, 0, f);
        const nsACString& s = Substring(line, f + 1);
        f = s.FindChar(' ');
        if (f < 0) {
          continue;
        }
        int64_t secondsstamp = Substring(s, 0, f).ToInteger64(&rv);
        if (NS_FAILED(rv)) {
          continue;
        }
        const nsACString& origin = Substring(s, f + 1);

        // Validate key
        if (key.Length() != OriginKey::EncodedLength) {
          continue;
        }
        nsCString dummy;
        rv = Base64Decode(key, dummy);
        if (NS_FAILED(rv)) {
          continue;
        }
        mKeys.InsertOrUpdate(origin, MakeUnique<OriginKey>(key, secondsstamp));
      }
      mPersistCount = mKeys.Count();
      return NS_OK;
    }

    nsresult Write() {
      nsCOMPtr<nsIFile> file = GetFile();
      if (NS_WARN_IF(!file)) {
        return NS_ERROR_UNEXPECTED;
      }

      nsCOMPtr<nsIOutputStream> stream;
      nsresult rv =
          NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }

      nsAutoCString versionBuffer;
      versionBuffer.AppendLiteral(ORIGINKEYS_VERSION);
      versionBuffer.Append('\n');

      uint32_t count;
      rv = stream->Write(versionBuffer.Data(), versionBuffer.Length(), &count);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      if (count != versionBuffer.Length()) {
        return NS_ERROR_UNEXPECTED;
      }
      for (const auto& entry : mKeys) {
        const nsACString& origin = entry.GetKey();
        OriginKey* originKey = entry.GetWeak();

        if (!originKey->mSecondsStamp) {
          continue;  // don't write temporal ones
        }

        nsCString originBuffer;
        originBuffer.Append(originKey->mKey);
        originBuffer.Append(' ');
        originBuffer.AppendInt(originKey->mSecondsStamp);
        originBuffer.Append(' ');
        originBuffer.Append(origin);
        originBuffer.Append('\n');

        rv = stream->Write(originBuffer.Data(), originBuffer.Length(), &count);
        if (NS_WARN_IF(NS_FAILED(rv)) || count != originBuffer.Length()) {
          break;
        }
      }

      nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream);
      MOZ_ASSERT(safeStream);

      rv = safeStream->Finish();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      return NS_OK;
    }

    nsresult Load() {
      nsresult rv = Read();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        Delete();
      }
      return rv;
    }

    nsresult Save() {
      nsresult rv = Write();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        NS_WARNING("Failed to write data for EnumerateDevices id-persistence.");
        Delete();
      }
      return rv;
    }

    void Clear(int64_t aSinceWhen) {
      OriginKeysTable::Clear(aSinceWhen);
      Delete();
      Save();
    }

    nsresult Delete() {
      nsCOMPtr<nsIFile> file = GetFile();
      if (NS_WARN_IF(!file)) {
        return NS_ERROR_UNEXPECTED;
      }
      nsresult rv = file->Remove(false);
      if (rv == NS_ERROR_FILE_NOT_FOUND) {
        return NS_OK;
      }
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      return NS_OK;
    }

    void SetProfileDir(nsIFile* aProfileDir) {
      MOZ_ASSERT(!NS_IsMainThread());
      bool first = !mProfileDir;
      mProfileDir = aProfileDir;
      // Load from disk when we first get a profileDir, but not subsequently.
      if (first) {
        Load();
      }
    }

   private:
    nsCOMPtr<nsIFile> mProfileDir;
  };

 private:
  static OriginKeyStore* sOriginKeyStore;

  virtual ~OriginKeyStore() {
    MOZ_ASSERT(NS_IsMainThread());
    sOriginKeyStore = nullptr;
    LOG(("%s", __FUNCTION__));
  }

 public:
  static RefPtr<OriginKeyStore> Get() {
    MOZ_ASSERT(NS_IsMainThread());
    if (!sOriginKeyStore) {
      sOriginKeyStore = new OriginKeyStore();
    }
    return RefPtr(sOriginKeyStore);
  }

  // Only accessed on StreamTS threads
  OriginKeysLoader mOriginKeys MOZ_GUARDED_BY(sOriginKeyStoreStsMutex);
  OriginKeysTable mPrivateBrowsingOriginKeys
      MOZ_GUARDED_BY(sOriginKeyStoreStsMutex);
};
OriginKeyStore* OriginKeyStore::sOriginKeyStore = nullptr;

template <class Super>
mozilla::ipc::IPCResult Parent<Super>::RecvGetPrincipalKey(
    const ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist,
    PMediaParent::GetPrincipalKeyResolver&& aResolve) {
  MOZ_ASSERT(NS_IsMainThread());

  // First, get profile dir.

  nsCOMPtr<nsIFile> profileDir;
  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                                       getter_AddRefs(profileDir));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return IPCResult(this, false);
  }

  // Resolver has to be called in MainThread but the key is discovered
  // in a different thread. We wrap the resolver around a MozPromise to make
  // it more flexible and pass it to the new task. When this is done the
  // resolver is resolved in MainThread.

  // Then over to stream-transport thread (a thread pool) to do the actual
  // file io. Stash a promise to hold the answer and get an id for this request.

  nsCOMPtr<nsIEventTarget> sts =
      do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
  MOZ_ASSERT(sts);
  auto taskQueue = TaskQueue::Create(sts.forget(), "RecvGetPrincipalKey");
  RefPtr<Parent<Super>> that(this);

  InvokeAsync(
      taskQueue, __func__,
      [this, that, profileDir, aPrincipalInfo, aPersist]() {
        MOZ_ASSERT(!NS_IsMainThread());

        StaticMutexAutoLock lock(sOriginKeyStoreStsMutex);
        mOriginKeyStore->mOriginKeys.SetProfileDir(profileDir);

        nsresult rv;
        nsAutoCString result;
        if (IsPrincipalInfoPrivate(aPrincipalInfo)) {
          rv = mOriginKeyStore->mPrivateBrowsingOriginKeys.GetPrincipalKey(
              aPrincipalInfo, result);
        } else {
          rv = mOriginKeyStore->mOriginKeys.GetPrincipalKey(aPrincipalInfo,
                                                            result, aPersist);
        }

        if (NS_WARN_IF(NS_FAILED(rv))) {
          return PrincipalKeyPromise::CreateAndReject(rv, __func__);
        }
        return PrincipalKeyPromise::CreateAndResolve(result, __func__);
      })
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [aResolve](const PrincipalKeyPromise::ResolveOrRejectValue& aValue) {
            if (aValue.IsReject()) {
              aResolve(""_ns);
            } else {
              aResolve(aValue.ResolveValue());
            }
          });

  return IPC_OK();
}

template <class Super>
mozilla::ipc::IPCResult Parent<Super>::RecvSanitizeOriginKeys(
    const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIFile> profileDir;
  nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
                                       getter_AddRefs(profileDir));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return IPCResult(this, false);
  }
  // Over to stream-transport thread (a thread pool) to do the file io.

  nsCOMPtr<nsIEventTarget> sts =
      do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
  MOZ_ASSERT(sts);
  RefPtr<Parent<Super>> that(this);

  rv = sts->Dispatch(
      NewRunnableFrom(
          [this, that, profileDir, aSinceWhen, aOnlyPrivateBrowsing]() {
            MOZ_ASSERT(!NS_IsMainThread());
            StaticMutexAutoLock lock(sOriginKeyStoreStsMutex);
            mOriginKeyStore->mPrivateBrowsingOriginKeys.Clear(aSinceWhen);
            if (!aOnlyPrivateBrowsing) {
              mOriginKeyStore->mOriginKeys.SetProfileDir(profileDir);
              mOriginKeyStore->mOriginKeys.Clear(aSinceWhen);
            }
            return NS_OK;
          }),
      NS_DISPATCH_NORMAL);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return IPCResult(this, false);
  }
  return IPC_OK();
}

template <class Super>
void Parent<Super>::ActorDestroy(ActorDestroyReason aWhy) {
  // No more IPC from here
  mDestroyed = true;
  LOG(("%s", __FUNCTION__));
}

template <class Super>
Parent<Super>::Parent()
    : mOriginKeyStore(OriginKeyStore::Get()), mDestroyed(false) {
  LOG(("media::Parent: %p", this));
}

template <class Super>
Parent<Super>::~Parent() {
  NS_ReleaseOnMainThread("Parent<Super>::mOriginKeyStore",
                         mOriginKeyStore.forget());
  LOG(("~media::Parent: %p", this));
}

PMediaParent* AllocPMediaParent() {
  Parent<PMediaParent>* obj = new Parent<PMediaParent>();
  obj->AddRef();
  return obj;
}

bool DeallocPMediaParent(media::PMediaParent* aActor) {
  static_cast<Parent<PMediaParent>*>(aActor)->Release();
  return true;
}

}  // namespace mozilla::media

// Instantiate templates to satisfy linker
template class mozilla::media::Parent<mozilla::media::NonE10s>;
