diff --git a/atom/browser/api/atom_api_net.cc b/atom/browser/api/atom_api_net.cc new file mode 100644 index 0000000000..24008ed7ae --- /dev/null +++ b/atom/browser/api/atom_api_net.cc @@ -0,0 +1,61 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/atom_api_net.h" +#include "atom/browser/api/atom_api_url_request.h" +#include "atom/common/node_includes.h" +#include "native_mate/dictionary.h" + +namespace atom { + +namespace api { + +Net::Net(v8::Isolate* isolate) { + Init(isolate); +} + +Net::~Net() {} + +// static +v8::Local Net::Create(v8::Isolate* isolate) { + return mate::CreateHandle(isolate, new Net(isolate)).ToV8(); +} + +// static +void Net::BuildPrototype(v8::Isolate* isolate, + v8::Local prototype) { + prototype->SetClassName(mate::StringToV8(isolate, "Net")); + mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) + .SetProperty("URLRequest", &Net::URLRequest); +} + +v8::Local Net::URLRequest(v8::Isolate* isolate) { + return URLRequest::GetConstructor(isolate)->GetFunction(); +} + +} // namespace api + +} // namespace atom + +namespace { + +using atom::api::Net; +using atom::api::URLRequest; + +void Initialize(v8::Local exports, + v8::Local unused, + v8::Local context, + void* priv) { + v8::Isolate* isolate = context->GetIsolate(); + + URLRequest::SetConstructor(isolate, base::Bind(URLRequest::New)); + + mate::Dictionary dict(isolate, exports); + dict.Set("net", Net::Create(isolate)); + dict.Set("Net", Net::GetConstructor(isolate)->GetFunction()); +} + +} // namespace + +NODE_MODULE_CONTEXT_AWARE_BUILTIN(atom_browser_net, Initialize) diff --git a/atom/browser/api/atom_api_net.h b/atom/browser/api/atom_api_net.h new file mode 100644 index 0000000000..2a0fa4140c --- /dev/null +++ b/atom/browser/api/atom_api_net.h @@ -0,0 +1,35 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_NET_H_ +#define ATOM_BROWSER_API_ATOM_API_NET_H_ + +#include "atom/browser/api/event_emitter.h" + +namespace atom { + +namespace api { + +class Net : public mate::EventEmitter { + public: + static v8::Local Create(v8::Isolate* isolate); + + static void BuildPrototype(v8::Isolate* isolate, + v8::Local prototype); + + v8::Local URLRequest(v8::Isolate* isolate); + + protected: + explicit Net(v8::Isolate* isolate); + ~Net() override; + + private: + DISALLOW_COPY_AND_ASSIGN(Net); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_NET_H_ diff --git a/atom/browser/api/atom_api_url_request.cc b/atom/browser/api/atom_api_url_request.cc new file mode 100644 index 0000000000..fe60bbb128 --- /dev/null +++ b/atom/browser/api/atom_api_url_request.cc @@ -0,0 +1,435 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/api/atom_api_url_request.h" +#include +#include "atom/browser/api/atom_api_session.h" +#include "atom/browser/net/atom_url_request.h" +#include "atom/common/api/event_emitter_caller.h" +#include "atom/common/native_mate_converters/callback.h" +#include "atom/common/native_mate_converters/net_converter.h" +#include "atom/common/native_mate_converters/string16_converter.h" +#include "atom/common/node_includes.h" +#include "native_mate/dictionary.h" + +namespace mate { + +template <> +struct Converter> { + static v8::Local ToV8( + v8::Isolate* isolate, + scoped_refptr buffer) { + return node::Buffer::Copy(isolate, buffer->data(), buffer->size()) + .ToLocalChecked(); + } + + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + scoped_refptr* out) { + auto size = node::Buffer::Length(val); + + if (size == 0) { + // Support conversion from empty buffer. A use case is + // a GET request without body. + // Since zero-sized IOBuffer(s) are not supported, we set the + // out pointer to null. + *out = nullptr; + return true; + } + auto data = node::Buffer::Data(val); + if (!data) { + // This is an error as size is positive but data is null. + return false; + } + + *out = new net::IOBufferWithSize(size); + // We do a deep copy. We could have used Buffer's internal memory + // but that is much more complicated to be properly handled. + memcpy((*out)->data(), data, size); + return true; + } +}; + +} // namespace mate + +namespace atom { +namespace api { + +template +URLRequest::StateBase::StateBase(Flags initialState) + : state_(initialState) {} + +template +void URLRequest::StateBase::SetFlag(Flags flag) { + state_ = + static_cast(static_cast(state_) | static_cast(flag)); +} + +template +bool URLRequest::StateBase::operator==(Flags flag) const { + return state_ == flag; +} + +template +bool URLRequest::StateBase::IsFlagSet(Flags flag) const { + return static_cast(state_) & static_cast(flag); +} + +URLRequest::RequestState::RequestState() + : StateBase(RequestStateFlags::kNotStarted) {} + +bool URLRequest::RequestState::NotStarted() const { + return *this == RequestStateFlags::kNotStarted; +} + +bool URLRequest::RequestState::Started() const { + return IsFlagSet(RequestStateFlags::kStarted); +} + +bool URLRequest::RequestState::Finished() const { + return IsFlagSet(RequestStateFlags::kFinished); +} + +bool URLRequest::RequestState::Canceled() const { + return IsFlagSet(RequestStateFlags::kCanceled); +} + +bool URLRequest::RequestState::Failed() const { + return IsFlagSet(RequestStateFlags::kFailed); +} + +bool URLRequest::RequestState::Closed() const { + return IsFlagSet(RequestStateFlags::kClosed); +} + +URLRequest::ResponseState::ResponseState() + : StateBase(ResponseStateFlags::kNotStarted) {} + +bool URLRequest::ResponseState::NotStarted() const { + return *this == ResponseStateFlags::kNotStarted; +} + +bool URLRequest::ResponseState::Started() const { + return IsFlagSet(ResponseStateFlags::kStarted); +} + +bool URLRequest::ResponseState::Ended() const { + return IsFlagSet(ResponseStateFlags::kEnded); +} + +bool URLRequest::ResponseState::Failed() const { + return IsFlagSet(ResponseStateFlags::kFailed); +} + +URLRequest::URLRequest(v8::Isolate* isolate, v8::Local wrapper) { + InitWith(isolate, wrapper); +} + +URLRequest::~URLRequest() { + // A request has been created in JS, it was not used and then + // it got collected, no close event to cleanup, only destructor + // is called. + if (atom_request_) { + atom_request_->Terminate(); + } +} + +// static +mate::WrappableBase* URLRequest::New(mate::Arguments* args) { + auto isolate = args->isolate(); + v8::Local options; + args->GetNext(&options); + mate::Dictionary dict(isolate, options); + std::string method; + dict.Get("method", &method); + std::string url; + dict.Get("url", &url); + std::string partition; + mate::Handle session; + if (dict.Get("session", &session)) { + } else if (dict.Get("partition", &partition)) { + session = Session::FromPartition(isolate, partition); + } else { + // Use the default session if not specified. + session = Session::FromPartition(isolate, ""); + } + auto browser_context = session->browser_context(); + auto api_url_request = new URLRequest(args->isolate(), args->GetThis()); + auto atom_url_request = + AtomURLRequest::Create(browser_context, method, url, api_url_request); + + api_url_request->atom_request_ = atom_url_request; + + return api_url_request; +} + +// static +void URLRequest::BuildPrototype(v8::Isolate* isolate, + v8::Local prototype) { + prototype->SetClassName(mate::StringToV8(isolate, "URLRequest")); + mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) + // Request API + .MakeDestroyable() + .SetMethod("write", &URLRequest::Write) + .SetMethod("cancel", &URLRequest::Cancel) + .SetMethod("setExtraHeader", &URLRequest::SetExtraHeader) + .SetMethod("removeExtraHeader", &URLRequest::RemoveExtraHeader) + .SetMethod("setChunkedUpload", &URLRequest::SetChunkedUpload) + .SetProperty("notStarted", &URLRequest::NotStarted) + .SetProperty("finished", &URLRequest::Finished) + // Response APi + .SetProperty("statusCode", &URLRequest::StatusCode) + .SetProperty("statusMessage", &URLRequest::StatusMessage) + .SetProperty("rawResponseHeaders", &URLRequest::RawResponseHeaders) + .SetProperty("httpVersionMajor", &URLRequest::ResponseHttpVersionMajor) + .SetProperty("httpVersionMinor", &URLRequest::ResponseHttpVersionMinor); +} + +bool URLRequest::NotStarted() const { + return request_state_.NotStarted(); +} + +bool URLRequest::Finished() const { + return request_state_.Finished(); +} + +bool URLRequest::Canceled() const { + return request_state_.Canceled(); +} + +bool URLRequest::Write(scoped_refptr buffer, + bool is_last) { + if (request_state_.Canceled() || request_state_.Failed() || + request_state_.Finished() || request_state_.Closed()) { + return false; + } + + if (request_state_.NotStarted()) { + request_state_.SetFlag(RequestStateFlags::kStarted); + // Pin on first write. + Pin(); + } + + if (is_last) { + request_state_.SetFlag(RequestStateFlags::kFinished); + EmitRequestEvent(true, "finish"); + } + + DCHECK(atom_request_); + if (atom_request_) { + return atom_request_->Write(buffer, is_last); + } + return false; +} + +void URLRequest::Cancel() { + if (request_state_.Canceled() || request_state_.Closed()) { + // Cancel only once. + return; + } + + // Mark as canceled. + request_state_.SetFlag(RequestStateFlags::kCanceled); + + DCHECK(atom_request_); + if (atom_request_ && request_state_.Started()) { + // Really cancel if it was started. + atom_request_->Cancel(); + } + EmitRequestEvent(true, "abort"); + + if (response_state_.Started() && !response_state_.Ended()) { + EmitResponseEvent(true, "aborted"); + } + Close(); +} + +bool URLRequest::SetExtraHeader(const std::string& name, + const std::string& value) { + // Request state must be in the initial non started state. + if (!request_state_.NotStarted()) { + // Cannot change headers after send. + return false; + } + + if (!net::HttpUtil::IsValidHeaderName(name)) { + return false; + } + + if (!net::HttpUtil::IsValidHeaderValue(value)) { + return false; + } + + DCHECK(atom_request_); + if (atom_request_) { + atom_request_->SetExtraHeader(name, value); + } + return true; +} + +void URLRequest::RemoveExtraHeader(const std::string& name) { + // State must be equal to not started. + if (!request_state_.NotStarted()) { + // Cannot change headers after send. + return; + } + DCHECK(atom_request_); + if (atom_request_) { + atom_request_->RemoveExtraHeader(name); + } +} + +void URLRequest::SetChunkedUpload(bool is_chunked_upload) { + // State must be equal to not started. + if (!request_state_.NotStarted()) { + // Cannot change headers after send. + return; + } + DCHECK(atom_request_); + if (atom_request_) { + atom_request_->SetChunkedUpload(is_chunked_upload); + } +} + +void URLRequest::OnAuthenticationRequired( + scoped_refptr auth_info) { + if (request_state_.Canceled() || request_state_.Closed()) { + return; + } + + DCHECK(atom_request_); + if (!atom_request_) { + return; + } + + Emit("login", auth_info.get(), + base::Bind(&AtomURLRequest::PassLoginInformation, atom_request_)); +} + +void URLRequest::OnResponseStarted( + scoped_refptr response_headers) { + if (request_state_.Canceled() || request_state_.Failed() || + request_state_.Closed()) { + // Don't emit any event after request cancel. + return; + } + response_headers_ = response_headers; + response_state_.SetFlag(ResponseStateFlags::kStarted); + Emit("response"); +} + +void URLRequest::OnResponseData( + scoped_refptr buffer) { + if (request_state_.Canceled() || request_state_.Closed() || + request_state_.Failed() || response_state_.Failed()) { + // In case we received an unexpected event from Chromium net, + // don't emit any data event after request cancel/error/close. + return; + } + if (!buffer || !buffer->data() || !buffer->size()) { + return; + } + Emit("data", buffer); +} + +void URLRequest::OnResponseCompleted() { + if (request_state_.Canceled() || request_state_.Closed() || + request_state_.Failed() || response_state_.Failed()) { + // In case we received an unexpected event from Chromium net, + // don't emit any data event after request cancel/error/close. + return; + } + response_state_.SetFlag(ResponseStateFlags::kEnded); + Emit("end"); + Close(); +} + +void URLRequest::OnError(const std::string& error, bool isRequestError) { + auto error_object = v8::Exception::Error(mate::StringToV8(isolate(), error)); + if (isRequestError) { + request_state_.SetFlag(RequestStateFlags::kFailed); + EmitRequestEvent(false, "error", error_object); + } else { + response_state_.SetFlag(ResponseStateFlags::kFailed); + EmitResponseEvent(false, "error", error_object); + } + Close(); +} + +int URLRequest::StatusCode() const { + if (response_headers_) { + return response_headers_->response_code(); + } + return -1; +} + +std::string URLRequest::StatusMessage() const { + std::string result; + if (response_headers_) { + result = response_headers_->GetStatusText(); + } + return result; +} + +net::HttpResponseHeaders* URLRequest::RawResponseHeaders() const { + return response_headers_.get(); +} + +uint32_t URLRequest::ResponseHttpVersionMajor() const { + if (response_headers_) { + return response_headers_->GetHttpVersion().major_value(); + } + return 0; +} + +uint32_t URLRequest::ResponseHttpVersionMinor() const { + if (response_headers_) { + return response_headers_->GetHttpVersion().minor_value(); + } + return 0; +} + +void URLRequest::Close() { + if (!request_state_.Closed()) { + request_state_.SetFlag(RequestStateFlags::kClosed); + if (response_state_.Started()) { + // Emit a close event if we really have a response object. + EmitResponseEvent(true, "close"); + } + EmitRequestEvent(true, "close"); + } + Unpin(); + if (atom_request_) { + // A request has been created in JS, used and then it ended. + // We release unneeded net resources. + atom_request_->Terminate(); + } + atom_request_ = nullptr; +} + +void URLRequest::Pin() { + if (wrapper_.IsEmpty()) { + wrapper_.Reset(isolate(), GetWrapper()); + } +} + +void URLRequest::Unpin() { + wrapper_.Reset(); +} + +template +void URLRequest::EmitRequestEvent(Args... args) { + v8::HandleScope handle_scope(isolate()); + mate::CustomEmit(isolate(), GetWrapper(), "_emitRequestEvent", args...); +} + +template +void URLRequest::EmitResponseEvent(Args... args) { + v8::HandleScope handle_scope(isolate()); + mate::CustomEmit(isolate(), GetWrapper(), "_emitResponseEvent", args...); +} + +} // namespace api + +} // namespace atom diff --git a/atom/browser/api/atom_api_url_request.h b/atom/browser/api/atom_api_url_request.h new file mode 100644 index 0000000000..3aae14bb19 --- /dev/null +++ b/atom/browser/api/atom_api_url_request.h @@ -0,0 +1,206 @@ +// Copyright (c) 2016 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_API_ATOM_API_URL_REQUEST_H_ +#define ATOM_BROWSER_API_ATOM_API_URL_REQUEST_H_ + +#include +#include +#include "atom/browser/api/event_emitter.h" +#include "atom/browser/api/trackable_object.h" +#include "base/memory/weak_ptr.h" +#include "native_mate/dictionary.h" +#include "native_mate/handle.h" +#include "native_mate/wrappable_base.h" +#include "net/base/auth.h" +#include "net/base/io_buffer.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request_context.h" + +namespace atom { + +class AtomURLRequest; + +namespace api { + +// +// The URLRequest class implements the V8 binding between the JavaScript API +// and Chromium native net library. It is responsible for handling HTTP/HTTPS +// requests. +// +// The current class provides only the binding layer. Two other JavaScript +// classes (ClientRequest and IncomingMessage) in the net module provide the +// final API, including some state management and arguments validation. +// +// URLRequest's methods fall into two main categories: command and event +// methods. They are always executed on the Browser's UI thread. +// Command methods are called directly from JavaScript code via the API defined +// in BuildPrototype. A command method is generally implemented by forwarding +// the call to a corresponding method on AtomURLRequest which does the +// synchronization on the Browser IO thread. The latter then calls into Chromium +// net library. On the other hand, net library events originate on the IO +// thread in AtomURLRequest and are synchronized back on the UI thread, then +// forwarded to a corresponding event method in URLRequest and then to +// JavaScript via the EmitRequestEvent/EmitResponseEvent helpers. +// +// URLRequest lifetime management: we followed the Wrapper/Wrappable pattern +// defined in native_mate. However, we augment that pattern with a pin/unpin +// mechanism. The main reason is that we want the JS API to provide a similar +// lifetime guarantees as the XMLHttpRequest. +// https://xhr.spec.whatwg.org/#garbage-collection +// +// The primary motivation is to not garbage collect a URLInstance as long as the +// object is emitting network events. For instance, in the following JS code +// +// (function() { +// let request = new URLRequest(...); +// request.on('response', (response)=>{ +// response.on('data', (data) = > { +// console.log(data.toString()); +// }); +// }); +// })(); +// +// we still want data to be logged even if the response/request objects are n +// more referenced in JavaScript. +// +// Binding by simply following the native_mate Wrapper/Wrappable pattern will +// delete the URLRequest object when the corresponding JS object is collected. +// The v8 handle is a private member in WrappableBase and it is always weak, +// there is no way to make it strong without changing native_mate. +// The solution we implement consists of maintaining some kind of state that +// prevents collection of JS wrappers as long as the request is emitting network +// events. At initialization, the object is unpinned. When the request starts, +// it is pinned. When no more events would be emitted, the object is unpinned +// and lifetime is again managed by the standard native mate Wrapper/Wrappable +// pattern. +// +// pin/unpin: are implemented by constructing/reseting a V8 strong persistent +// handle. +// +// The URLRequest/AtmURLRequest interaction could have been implemented in a +// single class. However, it implies that the resulting class lifetime will be +// managed by two conflicting mechanisms: JavaScript garbage collection and +// Chromium reference counting. Reasoning about lifetime issues become much +// more complex. +// +// We chose to split the implementation into two classes linked via a +// reference counted/raw pointers. A URLRequest instance is deleted if it is +// unpinned and the corresponding JS wrapper object is garbage collected. On the +// other hand, an AtmURLRequest instance lifetime is totally governed by +// reference counting. +// +class URLRequest : public mate::EventEmitter { + public: + static mate::WrappableBase* New(mate::Arguments* args); + + static void BuildPrototype(v8::Isolate* isolate, + v8::Local prototype); + + // Methods for reporting events into JavaScript. + void OnAuthenticationRequired( + scoped_refptr auth_info); + void OnResponseStarted( + scoped_refptr response_headers); + void OnResponseData(scoped_refptr data); + void OnResponseCompleted(); + void OnError(const std::string& error, bool isRequestError); + + protected: + explicit URLRequest(v8::Isolate* isolate, v8::Local wrapper); + ~URLRequest() override; + + private: + template + class StateBase { + public: + void SetFlag(Flags flag); + + protected: + explicit StateBase(Flags initialState); + bool operator==(Flags flag) const; + bool IsFlagSet(Flags flag) const; + + private: + Flags state_; + }; + + enum class RequestStateFlags { + kNotStarted = 0x0, + kStarted = 0x1, + kFinished = 0x2, + kCanceled = 0x4, + kFailed = 0x8, + kClosed = 0x10 + }; + + class RequestState : public StateBase { + public: + RequestState(); + bool NotStarted() const; + bool Started() const; + bool Finished() const; + bool Canceled() const; + bool Failed() const; + bool Closed() const; + }; + + enum class ResponseStateFlags { + kNotStarted = 0x0, + kStarted = 0x1, + kEnded = 0x2, + kFailed = 0x4 + }; + + class ResponseState : public StateBase { + public: + ResponseState(); + bool NotStarted() const; + bool Started() const; + bool Ended() const; + bool Canceled() const; + bool Failed() const; + bool Closed() const; + }; + + bool NotStarted() const; + bool Finished() const; + bool Canceled() const; + bool Failed() const; + bool Write(scoped_refptr buffer, bool is_last); + void Cancel(); + bool SetExtraHeader(const std::string& name, const std::string& value); + void RemoveExtraHeader(const std::string& name); + void SetChunkedUpload(bool is_chunked_upload); + + int StatusCode() const; + std::string StatusMessage() const; + net::HttpResponseHeaders* RawResponseHeaders() const; + uint32_t ResponseHttpVersionMajor() const; + uint32_t ResponseHttpVersionMinor() const; + + void Close(); + void Pin(); + void Unpin(); + template + void EmitRequestEvent(Args... args); + template + void EmitResponseEvent(Args... args); + + scoped_refptr atom_request_; + RequestState request_state_; + ResponseState response_state_; + + // Used to implement pin/unpin. + v8::Global wrapper_; + scoped_refptr response_headers_; + + DISALLOW_COPY_AND_ASSIGN(URLRequest); +}; + +} // namespace api + +} // namespace atom + +#endif // ATOM_BROWSER_API_ATOM_API_URL_REQUEST_H_ diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 124a0d3358..a1dc5203b6 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -35,10 +35,10 @@ #include "atom/common/native_mate_converters/gfx_converter.h" #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/image_converter.h" +#include "atom/common/native_mate_converters/net_converter.h" #include "atom/common/native_mate_converters/string16_converter.h" #include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/options_switches.h" -#include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "brightray/browser/inspectable_web_contents.h" #include "brightray/browser/inspectable_web_contents_view.h" @@ -66,7 +66,6 @@ #include "content/public/common/context_menu_params.h" #include "native_mate/dictionary.h" #include "native_mate/object_template_builder.h" -#include "net/http/http_response_headers.h" #include "net/url_request/url_request_context.h" #include "third_party/WebKit/public/web/WebFindOptions.h" #include "third_party/WebKit/public/web/WebInputEvent.h" @@ -141,32 +140,6 @@ struct Converter { } }; -template<> -struct Converter { - static v8::Local ToV8(v8::Isolate* isolate, - net::HttpResponseHeaders* headers) { - base::DictionaryValue response_headers; - if (headers) { - size_t iter = 0; - std::string key; - std::string value; - while (headers->EnumerateHeaderLines(&iter, &key, &value)) { - key = base::ToLowerASCII(key); - if (response_headers.HasKey(key)) { - base::ListValue* values = nullptr; - if (response_headers.GetList(key, &values)) - values->AppendString(value); - } else { - std::unique_ptr values(new base::ListValue()); - values->AppendString(value); - response_headers.Set(key, std::move(values)); - } - } - } - return ConvertToV8(isolate, response_headers); - } -}; - template<> struct Converter { static bool FromV8(v8::Isolate* isolate, v8::Local val, diff --git a/atom/browser/net/atom_url_request.cc b/atom/browser/net/atom_url_request.cc new file mode 100644 index 0000000000..628d316164 --- /dev/null +++ b/atom/browser/net/atom_url_request.cc @@ -0,0 +1,422 @@ +// Copyright (c) 2016 GitHub, Inc. +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/net/atom_url_request.h" +#include +#include "atom/browser/api/atom_api_url_request.h" +#include "atom/browser/atom_browser_context.h" +#include "base/callback.h" +#include "content/public/browser/browser_thread.h" +#include "net/base/elements_upload_data_stream.h" +#include "net/base/io_buffer.h" +#include "net/base/upload_bytes_element_reader.h" + +namespace { +const int kBufferSize = 4096; +} // namespace + +namespace atom { + +namespace internal { + +class UploadOwnedIOBufferElementReader : public net::UploadBytesElementReader { + public: + explicit UploadOwnedIOBufferElementReader( + scoped_refptr buffer) + : net::UploadBytesElementReader(buffer->data(), buffer->size()), + buffer_(buffer) {} + + ~UploadOwnedIOBufferElementReader() override {} + + static UploadOwnedIOBufferElementReader* CreateWithBuffer( + scoped_refptr buffer) { + return new UploadOwnedIOBufferElementReader(std::move(buffer)); + } + + private: + scoped_refptr buffer_; + + DISALLOW_COPY_AND_ASSIGN(UploadOwnedIOBufferElementReader); +}; + +} // namespace internal + +AtomURLRequest::AtomURLRequest(api::URLRequest* delegate) + : delegate_(delegate), + is_chunked_upload_(false), + response_read_buffer_(new net::IOBuffer(kBufferSize)) {} + +AtomURLRequest::~AtomURLRequest() { + DCHECK(!request_context_getter_); + DCHECK(!request_); +} + +scoped_refptr AtomURLRequest::Create( + AtomBrowserContext* browser_context, + const std::string& method, + const std::string& url, + api::URLRequest* delegate) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + DCHECK(browser_context); + DCHECK(!url.empty()); + DCHECK(delegate); + if (!browser_context || url.empty() || !delegate) { + return nullptr; + } + auto request_context_getter = browser_context->url_request_context_getter(); + DCHECK(request_context_getter); + if (!request_context_getter) { + return nullptr; + } + scoped_refptr atom_url_request(new AtomURLRequest(delegate)); + if (content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoInitialize, atom_url_request, + request_context_getter, method, url))) { + return atom_url_request; + } + return nullptr; +} + +void AtomURLRequest::Terminate() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + delegate_ = nullptr; + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoTerminate, this)); +} + +void AtomURLRequest::DoInitialize( + scoped_refptr request_context_getter, + const std::string& method, + const std::string& url) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + DCHECK(request_context_getter); + + request_context_getter_ = request_context_getter; + request_context_getter_->AddObserver(this); + auto context = request_context_getter_->GetURLRequestContext(); + if (!context) { + // Called after shutdown. + DoCancelWithError("Cannot start a request after shutdown.", true); + return; + } + + DCHECK(context); + request_ = context->CreateRequest( + GURL(url), net::RequestPriority::DEFAULT_PRIORITY, this); + if (!request_) { + DoCancelWithError("Failed to create a net::URLRequest.", true); + return; + } + request_->set_method(method); +} + +void AtomURLRequest::DoTerminate() { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + request_.reset(); + if (request_context_getter_) { + request_context_getter_->RemoveObserver(this); + request_context_getter_ = nullptr; + } +} + +bool AtomURLRequest::Write(scoped_refptr buffer, + bool is_last) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + return content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoWriteBuffer, this, buffer, is_last)); +} + +void AtomURLRequest::SetChunkedUpload(bool is_chunked_upload) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // The method can be called only before switching to multi-threaded mode, + // i.e. before the first call to write. + // So it is safe to change the object in the UI thread. + is_chunked_upload_ = is_chunked_upload; +} + +void AtomURLRequest::Cancel() { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + content::BrowserThread::PostTask(content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoCancel, this)); +} + +void AtomURLRequest::SetExtraHeader(const std::string& name, + const std::string& value) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoSetExtraHeader, this, name, value)); +} + +void AtomURLRequest::RemoveExtraHeader(const std::string& name) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoRemoveExtraHeader, this, name)); +} + +void AtomURLRequest::PassLoginInformation( + const base::string16& username, + const base::string16& password) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (username.empty() || password.empty()) { + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoCancelAuth, this)); + } else { + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&AtomURLRequest::DoSetAuth, this, username, password)); + } +} + +void AtomURLRequest::DoWriteBuffer( + scoped_refptr buffer, + bool is_last) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + + if (is_chunked_upload_) { + // Chunked encoding case. + + bool first_call = false; + if (!chunked_stream_writer_) { + std::unique_ptr chunked_stream( + new net::ChunkedUploadDataStream(0)); + chunked_stream_writer_ = chunked_stream->CreateWriter(); + request_->set_upload(std::move(chunked_stream)); + first_call = true; + } + + if (buffer) + // Non-empty buffer. + chunked_stream_writer_->AppendData(buffer->data(), buffer->size(), + is_last); + else if (is_last) + // Empty buffer and last chunk, i.e. request.end(). + chunked_stream_writer_->AppendData(nullptr, 0, true); + + if (first_call) { + request_->Start(); + } + } else { + if (buffer) { + // Handling potential empty buffers. + using internal::UploadOwnedIOBufferElementReader; + auto element_reader = + UploadOwnedIOBufferElementReader::CreateWithBuffer(std::move(buffer)); + upload_element_readers_.push_back( + std::unique_ptr(element_reader)); + } + + if (is_last) { + auto elements_upload_data_stream = new net::ElementsUploadDataStream( + std::move(upload_element_readers_), 0); + request_->set_upload( + std::unique_ptr(elements_upload_data_stream)); + request_->Start(); + } + } +} + +void AtomURLRequest::DoCancel() { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (request_) { + request_->Cancel(); + } + DoTerminate(); +} + +void AtomURLRequest::DoSetExtraHeader(const std::string& name, + const std::string& value) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + request_->SetExtraRequestHeaderByName(name, value, true); +} +void AtomURLRequest::DoRemoveExtraHeader(const std::string& name) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + request_->RemoveRequestHeaderByName(name); +} + +void AtomURLRequest::DoSetAuth(const base::string16& username, + const base::string16& password) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + request_->SetAuth(net::AuthCredentials(username, password)); +} + +void AtomURLRequest::DoCancelAuth() const { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + request_->CancelAuth(); +} + +void AtomURLRequest::DoCancelWithError(const std::string& error, + bool isRequestError) { + DoCancel(); + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind(&AtomURLRequest::InformDelegateErrorOccured, this, error, + isRequestError)); +} + +void AtomURLRequest::OnAuthRequired(net::URLRequest* request, + net::AuthChallengeInfo* auth_info) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind(&AtomURLRequest::InformDelegateAuthenticationRequired, this, + scoped_refptr(auth_info))); +} + +void AtomURLRequest::OnResponseStarted(net::URLRequest* request) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + DCHECK_EQ(request, request_.get()); + + scoped_refptr response_headers = + request->response_headers(); + const auto& status = request_->status(); + if (status.is_success()) { + // Success or pending trigger a Read. + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind(&AtomURLRequest::InformDelegateResponseStarted, this, + response_headers)); + ReadResponse(); + } else if (status.status() == net::URLRequestStatus::Status::FAILED) { + // Report error on Start. + DoCancelWithError(net::ErrorToString(status.ToNetError()), true); + } + // We don't report an error is the request is canceled. +} + +void AtomURLRequest::ReadResponse() { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + + int bytes_read = -1; + if (request_->Read(response_read_buffer_.get(), kBufferSize, &bytes_read)) { + OnReadCompleted(request_.get(), bytes_read); + } +} + +void AtomURLRequest::OnReadCompleted(net::URLRequest* request, int bytes_read) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + if (!request_) { + return; + } + DCHECK_EQ(request, request_.get()); + + const auto status = request_->status(); + + bool response_error = false; + bool data_ended = false; + bool data_transfer_error = false; + do { + if (!status.is_success()) { + response_error = true; + break; + } + if (bytes_read == 0) { + data_ended = true; + break; + } + if (bytes_read < 0 || !CopyAndPostBuffer(bytes_read)) { + data_transfer_error = true; + break; + } + } while ( + request_->Read(response_read_buffer_.get(), kBufferSize, &bytes_read)); + if (response_error) { + DoCancelWithError(net::ErrorToString(status.ToNetError()), false); + } else if (data_ended) { + content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind(&AtomURLRequest::InformDelegateResponseCompleted, this)); + DoTerminate(); + } else if (data_transfer_error) { + // We abort the request on corrupted data transfer. + DoCancelWithError("Failed to transfer data from IO to UI thread.", false); + } +} + +void AtomURLRequest::OnContextShuttingDown() { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + DoCancel(); +} + +bool AtomURLRequest::CopyAndPostBuffer(int bytes_read) { + DCHECK_CURRENTLY_ON(content::BrowserThread::IO); + + // data is only a wrapper for the asynchronous response_read_buffer_. + // Make a deep copy of payload and transfer ownership to the UI thread. + auto buffer_copy = new net::IOBufferWithSize(bytes_read); + memcpy(buffer_copy->data(), response_read_buffer_->data(), bytes_read); + + return content::BrowserThread::PostTask( + content::BrowserThread::UI, FROM_HERE, + base::Bind(&AtomURLRequest::InformDelegateResponseData, this, + buffer_copy)); +} + +void AtomURLRequest::InformDelegateAuthenticationRequired( + scoped_refptr auth_info) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (delegate_) + delegate_->OnAuthenticationRequired(auth_info); +} + +void AtomURLRequest::InformDelegateResponseStarted( + scoped_refptr response_headers) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + if (delegate_) + delegate_->OnResponseStarted(response_headers); +} + +void AtomURLRequest::InformDelegateResponseData( + scoped_refptr data) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // Transfer ownership of the data buffer, data will be released + // by the delegate's OnResponseData. + if (delegate_) + delegate_->OnResponseData(data); +} + +void AtomURLRequest::InformDelegateResponseCompleted() const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + if (delegate_) + delegate_->OnResponseCompleted(); +} + +void AtomURLRequest::InformDelegateErrorOccured(const std::string& error, + bool isRequestError) const { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + if (delegate_) + delegate_->OnError(error, isRequestError); +} + +} // namespace atom diff --git a/atom/browser/net/atom_url_request.h b/atom/browser/net/atom_url_request.h new file mode 100644 index 0000000000..d0b367e2d3 --- /dev/null +++ b/atom/browser/net/atom_url_request.h @@ -0,0 +1,104 @@ +// Copyright (c) 2016 GitHub, Inc. +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_NET_ATOM_URL_REQUEST_H_ +#define ATOM_BROWSER_NET_ATOM_URL_REQUEST_H_ + +#include +#include +#include "atom/browser/api/atom_api_url_request.h" +#include "atom/browser/atom_browser_context.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "net/base/auth.h" +#include "net/base/chunked_upload_data_stream.h" +#include "net/base/io_buffer.h" +#include "net/base/upload_element_reader.h" +#include "net/http/http_response_headers.h" +#include "net/url_request/url_request.h" +#include "net/url_request/url_request_context_getter_observer.h" + +namespace atom { + +class AtomURLRequest : public base::RefCountedThreadSafe, + public net::URLRequest::Delegate, + public net::URLRequestContextGetterObserver { + public: + static scoped_refptr Create( + AtomBrowserContext* browser_context, + const std::string& method, + const std::string& url, + api::URLRequest* delegate); + void Terminate(); + + bool Write(scoped_refptr buffer, bool is_last); + void SetChunkedUpload(bool is_chunked_upload); + void Cancel(); + void SetExtraHeader(const std::string& name, const std::string& value) const; + void RemoveExtraHeader(const std::string& name) const; + void PassLoginInformation(const base::string16& username, + const base::string16& password) const; + + protected: + // Overrides of net::URLRequest::Delegate + void OnAuthRequired(net::URLRequest* request, + net::AuthChallengeInfo* auth_info) override; + void OnResponseStarted(net::URLRequest* request) override; + void OnReadCompleted(net::URLRequest* request, int bytes_read) override; + + // Overrides of net::URLRequestContextGetterObserver + void OnContextShuttingDown() override; + + private: + friend class base::RefCountedThreadSafe; + + explicit AtomURLRequest(api::URLRequest* delegate); + ~AtomURLRequest() override; + + void DoInitialize(scoped_refptr, + const std::string& method, + const std::string& url); + void DoTerminate(); + void DoWriteBuffer(scoped_refptr buffer, + bool is_last); + void DoCancel(); + void DoSetExtraHeader(const std::string& name, + const std::string& value) const; + void DoRemoveExtraHeader(const std::string& name) const; + void DoSetAuth(const base::string16& username, + const base::string16& password) const; + void DoCancelAuth() const; + void DoCancelWithError(const std::string& error, bool isRequestError); + + void ReadResponse(); + bool CopyAndPostBuffer(int bytes_read); + + void InformDelegateAuthenticationRequired( + scoped_refptr auth_info) const; + void InformDelegateResponseStarted( + scoped_refptr) const; + void InformDelegateResponseData( + scoped_refptr data) const; + void InformDelegateResponseCompleted() const; + void InformDelegateErrorOccured(const std::string& error, + bool isRequestError) const; + + api::URLRequest* delegate_; + std::unique_ptr request_; + scoped_refptr request_context_getter_; + + bool is_chunked_upload_; + std::unique_ptr chunked_stream_; + std::unique_ptr chunked_stream_writer_; + std::vector> + upload_element_readers_; + scoped_refptr response_read_buffer_; + + DISALLOW_COPY_AND_ASSIGN(AtomURLRequest); +}; + +} // namespace atom + +#endif // ATOM_BROWSER_NET_ATOM_URL_REQUEST_H_ diff --git a/atom/common/api/atom_api_v8_util.cc b/atom/common/api/atom_api_v8_util.cc index ddacbb0808..a587cd772a 100644 --- a/atom/common/api/atom_api_v8_util.cc +++ b/atom/common/api/atom_api_v8_util.cc @@ -92,6 +92,11 @@ void TakeHeapSnapshot(v8::Isolate* isolate) { isolate->GetHeapProfiler()->TakeHeapSnapshot(); } +void RequestGarbageCollectionForTesting(v8::Isolate* isolate) { + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::GarbageCollectionType::kFullGarbageCollection); +} + void Initialize(v8::Local exports, v8::Local unused, v8::Local context, void* priv) { mate::Dictionary dict(context->GetIsolate(), exports); @@ -105,6 +110,8 @@ void Initialize(v8::Local exports, v8::Local unused, dict.SetMethod("createIDWeakMap", &atom::api::KeyWeakMap::Create); dict.SetMethod("createDoubleIDWeakMap", &atom::api::KeyWeakMap>::Create); + dict.SetMethod("requestGarbageCollectionForTesting", + &RequestGarbageCollectionForTesting); } } // namespace diff --git a/atom/common/api/event_emitter_caller.cc b/atom/common/api/event_emitter_caller.cc index 40448cad10..3fbb31c497 100644 --- a/atom/common/api/event_emitter_caller.cc +++ b/atom/common/api/event_emitter_caller.cc @@ -11,16 +11,16 @@ namespace mate { namespace internal { -v8::Local CallEmitWithArgs(v8::Isolate* isolate, - v8::Local obj, - ValueVector* args) { +v8::Local CallMethodWithArgs(v8::Isolate* isolate, + v8::Local obj, + const char* method, + ValueVector* args) { // Perform microtask checkpoint after running JavaScript. - v8::MicrotasksScope script_scope( - isolate, v8::MicrotasksScope::kRunMicrotasks); + v8::MicrotasksScope script_scope(isolate, + v8::MicrotasksScope::kRunMicrotasks); // Use node::MakeCallback to call the callback, and it will also run pending // tasks in Node.js. - return node::MakeCallback( - isolate, obj, "emit", args->size(), &args->front()); + return node::MakeCallback(isolate, obj, method, args->size(), &args->front()); } } // namespace internal diff --git a/atom/common/api/event_emitter_caller.h b/atom/common/api/event_emitter_caller.h index a2567da9d1..24917cbef6 100644 --- a/atom/common/api/event_emitter_caller.h +++ b/atom/common/api/event_emitter_caller.h @@ -15,37 +15,50 @@ namespace internal { using ValueVector = std::vector>; -v8::Local CallEmitWithArgs(v8::Isolate* isolate, - v8::Local obj, - ValueVector* args); +v8::Local CallMethodWithArgs(v8::Isolate* isolate, + v8::Local obj, + const char* method, + ValueVector* args); } // namespace internal // obj.emit.apply(obj, name, args...); // The caller is responsible of allocating a HandleScope. -template +template v8::Local EmitEvent(v8::Isolate* isolate, v8::Local obj, const StringType& name, const internal::ValueVector& args) { - internal::ValueVector concatenated_args = { StringToV8(isolate, name) }; + internal::ValueVector concatenated_args = {StringToV8(isolate, name)}; concatenated_args.reserve(1 + args.size()); concatenated_args.insert(concatenated_args.end(), args.begin(), args.end()); - return internal::CallEmitWithArgs(isolate, obj, &concatenated_args); + return internal::CallMethodWithArgs(isolate, obj, "emit", &concatenated_args); } // obj.emit(name, args...); // The caller is responsible of allocating a HandleScope. -template +template v8::Local EmitEvent(v8::Isolate* isolate, v8::Local obj, const StringType& name, const Args&... args) { internal::ValueVector converted_args = { - StringToV8(isolate, name), + StringToV8(isolate, name), ConvertToV8(isolate, args)..., + }; + return internal::CallMethodWithArgs(isolate, obj, "emit", &converted_args); +} + +// obj.custom_emit(args...) +template +v8::Local CustomEmit(v8::Isolate* isolate, + v8::Local object, + const char* custom_emit, + const Args&... args) { + internal::ValueVector converted_args = { ConvertToV8(isolate, args)..., }; - return internal::CallEmitWithArgs(isolate, obj, &converted_args); + return internal::CallMethodWithArgs(isolate, object, custom_emit, + &converted_args); } } // namespace mate diff --git a/atom/common/native_mate_converters/net_converter.cc b/atom/common/native_mate_converters/net_converter.cc index d74356e956..00a06566a2 100644 --- a/atom/common/native_mate_converters/net_converter.cc +++ b/atom/common/native_mate_converters/net_converter.cc @@ -10,6 +10,7 @@ #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/value_converter.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" #include "base/values.h" #include "native_mate/dictionary.h" #include "net/base/upload_bytes_element_reader.h" @@ -58,6 +59,31 @@ v8::Local Converter>::ToV8( return dict.GetHandle(); } +// static +v8::Local Converter::ToV8( + v8::Isolate* isolate, + net::HttpResponseHeaders* headers) { + base::DictionaryValue response_headers; + if (headers) { + size_t iter = 0; + std::string key; + std::string value; + while (headers->EnumerateHeaderLines(&iter, &key, &value)) { + key = base::ToLowerASCII(key); + if (response_headers.HasKey(key)) { + base::ListValue* values = nullptr; + if (response_headers.GetList(key, &values)) + values->AppendString(value); + } else { + std::unique_ptr values(new base::ListValue()); + values->AppendString(value); + response_headers.Set(key, std::move(values)); + } + } + } + return ConvertToV8(isolate, response_headers); +} + } // namespace mate namespace atom { diff --git a/atom/common/native_mate_converters/net_converter.h b/atom/common/native_mate_converters/net_converter.h index 37e4280695..16013e34f9 100644 --- a/atom/common/native_mate_converters/net_converter.h +++ b/atom/common/native_mate_converters/net_converter.h @@ -17,6 +17,7 @@ namespace net { class AuthChallengeInfo; class URLRequest; class X509Certificate; +class HttpResponseHeaders; } namespace mate { @@ -33,6 +34,12 @@ struct Converter> { const scoped_refptr& val); }; +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + net::HttpResponseHeaders* headers); +}; + } // namespace mate namespace atom { diff --git a/atom/common/node_bindings.cc b/atom/common/node_bindings.cc index a049be23a6..e869d2a703 100644 --- a/atom/common/node_bindings.cc +++ b/atom/common/node_bindings.cc @@ -39,6 +39,7 @@ REFERENCE_MODULE(atom_browser_debugger); REFERENCE_MODULE(atom_browser_desktop_capturer); REFERENCE_MODULE(atom_browser_download_item); REFERENCE_MODULE(atom_browser_menu); +REFERENCE_MODULE(atom_browser_net); REFERENCE_MODULE(atom_browser_power_monitor); REFERENCE_MODULE(atom_browser_power_save_blocker); REFERENCE_MODULE(atom_browser_protocol); diff --git a/docs/api/net.md b/docs/api/net.md new file mode 100644 index 0000000000..d14fe3dc02 --- /dev/null +++ b/docs/api/net.md @@ -0,0 +1,322 @@ +# net + +> Issue HTTP/HTTPS requests. + +The `net` module is a client-side API for issuing HTTP(S) requests. It is +similar to the [HTTP](https://nodejs.org/api/http.html) and +[HTTPS](https://nodejs.org/api/https.html) modules of Node.js but uses +Chromium native networking library instead of the Node.js implementation +offering therefore a much greater support regarding web proxies. + +Following is a non-exhaustive list of why you may consider using the `net` +module instead of the native Node.js modules: +* Automatic management of system proxy configuration, support of the wpad +protocol and proxy pac configuration files. +* Automatic tunneling of HTTPS requests. +* Support for authenticating proxies using basic, digest, NTLM, Kerberos or +negotiate authentication schemes. +* Support for traffic monitoring proxies: Fiddler-like proxies used for access +control and monitoring. + +The `net` module API has been specifically designed to mimic, as closely as +possible, the familiar Node.js API. The API components including classes, +methods, properties and event names are similar to those commonly used in +Node.js. + +For instance, the following example quickly shows how the `net` API might be +used: + +```javascript +const {app} = require('electron') +app.on('ready', () => { + const {net} = require('electron') + const request = net.request('https://github.com') + request.on('response', (response) => { + console.log(`STATUS: ${response.statusCode}`) + console.log(`HEADERS: ${JSON.stringify(response.headers)}`) + response.on('data', (chunk) => { + console.log(`BODY: ${chunk}`) + }) + response.on('end', () => { + console.log('No more data in response.') + }) + }) + request.end() +}) +``` + +By the way, it is almost identical to how you would normally use the +[HTTP](https://nodejs.org/api/http.html)/[HTTPS](https://nodejs.org/api/https.html) +modules of Node.js + +The `net` API can be used only after the application emits the `ready` event. +Trying to use the module before the `ready` event will throw an error. + +## Methods + +The `net` module has the following methods: + +### `net.request(options)` + +* `options`: Object or String - The `ClientRequest` constructor options. + +Returns `ClientRequest` + +Creates a `ClientRequest` instance using the provided `options` which are +directly forwarded to the `ClientRequest` constructor. The `net.request` method +would be used to issue both secure and insecure HTTP requests according to the +specified protocol scheme in the `options` object. + +## Class: ClientRequest + +`ClientRequest` implements the [Writable Stream](https://nodejs.org/api/stream.html#stream_writable_streams) +interface and it is therefore an [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). + +### `new ClientRequest(options)` + +* `options` Object or String - If `options` is a String, it is interpreted as +the request URL. +If it is an object, it is expected to fully specify an HTTP request via the +following properties: + * `method` String (optional) - The HTTP request method. Defaults to the GET +method. + * `url` String (optional) - The request URL. Must be provided in the absolute +form with the protocol scheme specified as http or https. + * `session` Object (optional) - The [`Session`](session.md) instance with +which the request is associated. + * `partition` String (optional) - The name of the [`partition`](session.md) + with which the request is associated. Defaults to the empty string. The +`session` option prevails on `partition`. Thus if a `session` is explicitly +specified, `partition` is ignored. + * `protocol` String (optional) - The protocol scheme in the form 'scheme:'. +Currently supported values are 'http:' or 'https:'. Defaults to 'http:'. + * `host` String (optional) - The server host provided as a concatenation of +the hostname and the port number 'hostname:port' + * `hostname` String (optional) - The server host name. + * `port` Integer (optional) - The server's listening port number. + * `path` String (optional) - The path part of the request URL. + +`options` properties such as `protocol`, `host`, `hostname`, `port` and `path` +strictly follow the Node.js model as described in the +[URL](https://nodejs.org/api/url.html) module. + +For instance, we could have created the same request to 'github.com' as follows: + +```JavaScript +const request = net.request({ + method: 'GET', + protocol: 'https:', + hostname: 'github.com', + port: 443, + path: '/' +}) +``` + +### Instance Events + +#### Event: 'response' + +Returns: + +* `response` IncomingMessage - An object representing the HTTP response message. + +#### Event: 'login' + +Returns: + +* `authInfo` Object + * `isProxy` Boolean + * `scheme` String + * `host` String + * `port` Integer + * `realm` String +* `callback` Function + +Emitted when an authenticating proxy is asking for user credentials. + +The `callback` function is expected to be called back with user credentials: + +* `usrename` String +* `password` String + +```JavaScript +request.on('login', (authInfo, callback) => { + callback('username', 'password') +}) +``` +Providing empty credentials will cancel the request and report an authentication +error on the response object: + +```JavaScript +request.on('response', (response) => { + console.log(`STATUS: ${response.statusCode}`); + response.on('error', (error) => { + console.log(`ERROR: ${JSON.stringify(error)}`) + }) +}) +request.on('login', (authInfo, callback) => { + callback() +}) +``` + +#### Event: 'finish' + +Emitted just after the last chunk of the `request`'s data has been written into +the `request` object. + +#### Event: 'abort' + +Emitted when the `request` is aborted. The `abort` event will not be fired if +the `request` is already closed. + +#### Event: 'error' + +Returns: + +* `error` Error - an error object providing some information about the failure. + +Emitted when the `net` module fails to issue a network request. Typically when +the `request` object emits an `error` event, a `close` event will subsequently +follow and no response object will be provided. + +#### Event: 'close' + +Emitted as the last event in the HTTP request-response transaction. The `close` +event indicates that no more events will be emitted on either the `request` or +`response` objects. + +### Instance Properties + +#### `request.chunkedEncoding` + +A Boolean specifying whether the request will use HTTP chunked transfer encoding +or not. Defaults to false. The property is readable and writable, however it can +be set only before the first write operation as the HTTP headers are not yet put +on the wire. Trying to set the `chunkedEncoding` property after the first write +will throw an error. + +Using chunked encoding is strongly recommended if you need to send a large +request body as data will be streamed in small chunks instead of being +internally buffered inside Electron process memory. + +### Instance Methods + +#### `request.setHeader(name, value)` + +* `name` String - An extra HTTP header name. +* `value` String - An extra HTTP header value. + +Adds an extra HTTP header. The header name will issued as it is without +lowercasing. It can be called only before first write. Calling this method after +the first write will throw an error. + +#### `request.getHeader(name)` + +* `name` String - Specify an extra header name. + +Returns String - The value of a previously set extra header name. + +#### `request.removeHeader(name)` + +* `name` String - Specify an extra header name. + +Removes a previously set extra header name. This method can be called only +before first write. Trying to call it after the first write will throw an error. + +#### `request.write(chunk[, encoding][, callback])` + +* `chunk` String or Buffer - A chunk of the request body's data. If it is a +string, it is converted into a Buffer using the specified encoding. +* `encoding` String (optional) - Used to convert string chunks into Buffer +objects. Defaults to 'utf-8'. +* `callback` Function (optional) - Called after the write operation ends. +`callback` is essentially a dummy function introduced in the purpose of keeping +similarity with the Node.js API. It is called asynchronously in the next tick +after `chunk` content have been delivered to the Chromium networking layer. +Contrary to the Node.js implementation, it is not guaranteed that `chunk` +content have been flushed on the wire before `callback` is called. + +Adds a chunk of data to the request body. The first write operation may cause +the request headers to be issued on the wire. After the first write operation, +it is not allowed to add or remove a custom header. + +#### `request.end([chunk][, encoding][, callback])` + +* `chunk` String or Buffer (optional) +* `encoding` String (optional) +* `callback` Function (optional) + +Sends the last chunk of the request data. Subsequent write or end operations +will not be allowed. The `finish` event is emitted just after the end operation. + +#### `request.abort()` + +Cancels an ongoing HTTP transaction. If the request has already emitted the +`close` event, the abort operation will have no effect. Otherwise an ongoing +event will emit `abort` and `close` events. Additionally, if there is an ongoing +response object,it will emit the `aborted` event. + + +## Class: IncomingMessage + +`IncomingMessage` represents an HTTP response message. +It is a [Readable Stream](https://nodejs.org/api/stream.html#stream_readable_streams) +and consequently an [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). + +### Instance Events + +#### Event 'data' + +Returns: + +* `chunk`: Buffer - A chunk of response body's data. + +The `data` event is the usual method of transferring response data into +applicative code. + +#### Event 'end' + +Indicates that response body has ended. + +#### Event 'aborted' + +Emitted when a request has been canceled during an ongoing HTTP transaction. + +#### Event 'error' + +Returns: + +`error` Error - Typically holds an error string identifying failure root cause. + +Emitted when an error was encountered while streaming response data events. For +instance, if the server closes the underlying while the response is still +streaming, an `error` event will be emitted on the response object and a `close` +event will subsequently follow on the request object. + +### Instance properties + +An `IncomingMessage` instance has the following readable properties: + +#### `response.statusCode` + +An Integer indicating the HTTP response status code. + +#### `response.statusMessage` + +A String representing the HTTP status message. + +#### `response.headers` + +An Object representing the response HTTP headers. The `headers` object is +formatted as follows: + +* All header names are lowercased. +* Each header name produces an array-valued property on the headers object. +* Each header value is pushed into the array associated with its header name. + +#### `response.httpVersion` + +A String indicating the HTTP protocol version number. Typical values are '1.0' +or '1.1'. Additionally `httpVersionMajor` and `httpVersionMinor` are two +Integer-valued readable properties that return respectively the HTTP major and +minor version numbers. diff --git a/filenames.gypi b/filenames.gypi index 1ea49106bd..68b58b04f7 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -23,6 +23,7 @@ 'lib/browser/api/menu-item.js', 'lib/browser/api/menu-item-roles.js', 'lib/browser/api/navigation-controller.js', + 'lib/browser/api/net.js', 'lib/browser/api/power-monitor.js', 'lib/browser/api/power-save-blocker.js', 'lib/browser/api/protocol.js', @@ -117,6 +118,8 @@ 'atom/browser/api/atom_api_menu_views.h', 'atom/browser/api/atom_api_menu_mac.h', 'atom/browser/api/atom_api_menu_mac.mm', + 'atom/browser/api/atom_api_net.cc', + 'atom/browser/api/atom_api_net.h', 'atom/browser/api/atom_api_power_monitor.cc', 'atom/browser/api/atom_api_power_monitor.h', 'atom/browser/api/atom_api_power_save_blocker.cc', @@ -135,6 +138,8 @@ 'atom/browser/api/atom_api_system_preferences_win.cc', 'atom/browser/api/atom_api_tray.cc', 'atom/browser/api/atom_api_tray.h', + 'atom/browser/api/atom_api_url_request.cc', + 'atom/browser/api/atom_api_url_request.h', 'atom/browser/api/atom_api_web_contents.cc', 'atom/browser/api/atom_api_web_contents.h', 'atom/browser/api/atom_api_web_contents_mac.mm', @@ -236,6 +241,8 @@ 'atom/browser/net/atom_network_delegate.h', 'atom/browser/net/atom_ssl_config_service.cc', 'atom/browser/net/atom_ssl_config_service.h', + 'atom/browser/net/atom_url_request.cc', + 'atom/browser/net/atom_url_request.h', 'atom/browser/net/atom_url_request_job_factory.cc', 'atom/browser/net/atom_url_request_job_factory.h', 'atom/browser/net/http_protocol_handler.cc', diff --git a/lib/browser/api/exports/electron.js b/lib/browser/api/exports/electron.js index c47c046906..9a486b575d 100644 --- a/lib/browser/api/exports/electron.js +++ b/lib/browser/api/exports/electron.js @@ -107,6 +107,12 @@ Object.defineProperties(exports, { return require('../web-contents') } }, + net: { + enumerable: true, + get: function () { + return require('../net') + } + }, // The internal modules, invisible unless you know their names. NavigationController: { diff --git a/lib/browser/api/net.js b/lib/browser/api/net.js new file mode 100644 index 0000000000..e8b6f2a473 --- /dev/null +++ b/lib/browser/api/net.js @@ -0,0 +1,359 @@ +'use strict' + +const url = require('url') +const {EventEmitter} = require('events') +const util = require('util') +const {Readable} = require('stream') +const {app} = require('electron') +const {Session} = process.atomBinding('session') +const {net, Net} = process.atomBinding('net') +const {URLRequest} = net + +Object.setPrototypeOf(Net.prototype, EventEmitter.prototype) +Object.setPrototypeOf(URLRequest.prototype, EventEmitter.prototype) + +const kSupportedProtocols = new Set(['http:', 'https:']) + +class IncomingMessage extends Readable { + constructor (urlRequest) { + super() + this.urlRequest = urlRequest + this.shouldPush = false + this.data = [] + this.urlRequest.on('data', (event, chunk) => { + this._storeInternalData(chunk) + this._pushInternalData() + }) + this.urlRequest.on('end', () => { + this._storeInternalData(null) + this._pushInternalData() + }) + } + + get statusCode () { + return this.urlRequest.statusCode + } + + get statusMessage () { + return this.urlRequest.statusMessage + } + + get headers () { + return this.urlRequest.rawResponseHeaders + } + + get httpVersion () { + return `${this.httpVersionMajor}.${this.httpVersionMinor}` + } + + get httpVersionMajor () { + return this.urlRequest.httpVersionMajor + } + + get httpVersionMinor () { + return this.urlRequest.httpVersionMinor + } + + get rawTrailers () { + throw new Error('HTTP trailers are not supported.') + } + + get trailers () { + throw new Error('HTTP trailers are not supported.') + } + + _storeInternalData (chunk) { + this.data.push(chunk) + } + + _pushInternalData () { + while (this.shouldPush && this.data.length > 0) { + const chunk = this.data.shift() + this.shouldPush = this.push(chunk) + } + } + + _read () { + this.shouldPush = true + this._pushInternalData() + } + +} + +URLRequest.prototype._emitRequestEvent = function (isAsync, ...rest) { + if (isAsync) { + process.nextTick(() => { + this.clientRequest.emit.apply(this.clientRequest, rest) + }) + } else { + this.clientRequest.emit.apply(this.clientRequest, rest) + } +} + +URLRequest.prototype._emitResponseEvent = function (isAsync, ...rest) { + if (isAsync) { + process.nextTick(() => { + this._response.emit.apply(this._response, rest) + }) + } else { + this._response.emit.apply(this._response, rest) + } +} + +class ClientRequest extends EventEmitter { + + constructor (options, callback) { + super() + + if (!app.isReady()) { + throw new Error('net module can only be used after app is ready') + } + + if (typeof options === 'string') { + options = url.parse(options) + } else { + options = util._extend({}, options) + } + + const method = (options.method || 'GET').toUpperCase() + let urlStr = options.url + + if (!urlStr) { + let urlObj = {} + const protocol = options.protocol || 'http:' + if (!kSupportedProtocols.has(protocol)) { + throw new Error('Protocol "' + protocol + '" not supported. ') + } + urlObj.protocol = protocol + + if (options.host) { + urlObj.host = options.host + } else { + if (options.hostname) { + urlObj.hostname = options.hostname + } else { + urlObj.hostname = 'localhost' + } + + if (options.port) { + urlObj.port = options.port + } + } + + if (options.path && / /.test(options.path)) { + // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ + // with an additional rule for ignoring percentage-escaped characters + // but that's a) hard to capture in a regular expression that performs + // well, and b) possibly too restrictive for real-world usage. That's + // why it only scans for spaces because those are guaranteed to create + // an invalid request. + throw new TypeError('Request path contains unescaped characters.') + } + let pathObj = url.parse(options.path || '/') + urlObj.pathname = pathObj.pathname + urlObj.search = pathObj.search + urlObj.hash = pathObj.hash + urlStr = url.format(urlObj) + } + + let urlRequestOptions = { + method: method, + url: urlStr + } + if (options.session) { + if (options.session instanceof Session) { + urlRequestOptions.session = options.session + } else { + throw new TypeError('`session` should be an instance of the Session class.') + } + } else if (options.partition) { + if (typeof options.partition === 'string') { + urlRequestOptions.partition = options.partition + } else { + throw new TypeError('`partition` should be an a string.') + } + } + + let urlRequest = new URLRequest(urlRequestOptions) + + // Set back and forward links. + this.urlRequest = urlRequest + urlRequest.clientRequest = this + + // This is a copy of the extra headers structure held by the native + // net::URLRequest. The main reason is to keep the getHeader API synchronous + // after the request starts. + this.extraHeaders = {} + + if (options.headers) { + for (let key in options.headers) { + this.setHeader(key, options.headers[key]) + } + } + + // Set when the request uses chunked encoding. Can be switched + // to true only once and never set back to false. + this.chunkedEncodingEnabled = false + + urlRequest.on('response', () => { + const response = new IncomingMessage(urlRequest) + urlRequest._response = response + this.emit('response', response) + }) + + urlRequest.on('login', (event, authInfo, callback) => { + this.emit('login', authInfo, (username, password) => { + // If null or undefined usrename/password, force to empty string. + if (username === null || username === undefined) { + username = '' + } + if (typeof username !== 'string') { + throw new Error('username must be a string') + } + if (password === null || password === undefined) { + password = '' + } + if (typeof password !== 'string') { + throw new Error('password must be a string') + } + callback(username, password) + }) + }) + + if (callback) { + this.once('response', callback) + } + } + + get chunkedEncoding () { + return this.chunkedEncodingEnabled + } + + set chunkedEncoding (value) { + if (!this.urlRequest.notStarted) { + throw new Error('Can\'t set the transfer encoding, headers have been sent.') + } + this.chunkedEncodingEnabled = value + } + + setHeader (name, value) { + if (typeof name !== 'string') { + throw new TypeError('`name` should be a string in setHeader(name, value).') + } + if (value === undefined) { + throw new Error('`value` required in setHeader("' + name + '", value).') + } + if (!this.urlRequest.notStarted) { + throw new Error('Can\'t set headers after they are sent.') + } + + const key = name.toLowerCase() + this.extraHeaders[key] = value + this.urlRequest.setExtraHeader(name, value) + } + + getHeader (name) { + if (arguments.length < 1) { + throw new Error('`name` is required for getHeader(name).') + } + + if (!this.extraHeaders) { + return + } + + const key = name.toLowerCase() + return this.extraHeaders[key] + } + + removeHeader (name) { + if (arguments.length < 1) { + throw new Error('`name` is required for removeHeader(name).') + } + + if (!this.urlRequest.notStarted) { + throw new Error('Can\'t remove headers after they are sent.') + } + + const key = name.toLowerCase() + delete this.extraHeaders[key] + this.urlRequest.removeExtraHeader(name) + } + + _write (chunk, encoding, callback, isLast) { + let chunkIsString = typeof chunk === 'string' + let chunkIsBuffer = chunk instanceof Buffer + if (!chunkIsString && !chunkIsBuffer) { + throw new TypeError('First argument must be a string or Buffer.') + } + + if (chunkIsString) { + // We convert all strings into binary buffers. + chunk = Buffer.from(chunk, encoding) + } + + // Since writing to the network is asynchronous, we conservatively + // assume that request headers are written after delivering the first + // buffer to the network IO thread. + if (this.urlRequest.notStarted) { + this.urlRequest.setChunkedUpload(this.chunkedEncoding) + } + + // Headers are assumed to be sent on first call to _writeBuffer, + // i.e. after the first call to write or end. + let result = this.urlRequest.write(chunk, isLast) + + // The write callback is fired asynchronously to mimic Node.js. + if (callback) { + process.nextTick(callback) + } + + return result + } + + write (data, encoding, callback) { + if (this.urlRequest.finished) { + let error = new Error('Write after end.') + process.nextTick(writeAfterEndNT, this, error, callback) + return true + } + + return this._write(data, encoding, callback, false) + } + + end (data, encoding, callback) { + if (this.urlRequest.finished) { + return false + } + + if (typeof data === 'function') { + callback = data + encoding = null + data = null + } else if (typeof encoding === 'function') { + callback = encoding + encoding = null + } + + data = data || '' + + return this._write(data, encoding, callback, true) + } + + abort () { + this.urlRequest.cancel() + } + +} + +function writeAfterEndNT (self, error, callback) { + self.emit('error', error) + if (callback) callback(error) +} + +Net.prototype.request = function (options, callback) { + return new ClientRequest(options, callback) +} + +net.ClientRequest = ClientRequest + +module.exports = net diff --git a/spec/api-net-spec.js b/spec/api-net-spec.js new file mode 100644 index 0000000000..9ddc11986c --- /dev/null +++ b/spec/api-net-spec.js @@ -0,0 +1,1352 @@ +const assert = require('assert') +const {remote} = require('electron') +const {ipcRenderer} = require('electron') +const http = require('http') +const url = require('url') +const {net} = remote +const {session} = remote + +function randomBuffer (size, start, end) { + start = start || 0 + end = end || 255 + let range = 1 + end - start + const buffer = Buffer.allocUnsafe(size) + for (let i = 0; i < size; ++i) { + buffer[i] = start + Math.floor(Math.random() * range) + } + return buffer +} + +function randomString (length) { + let buffer = randomBuffer(length, '0'.charCodeAt(0), 'z'.charCodeAt(0)) + return buffer.toString() +} + +const kOneKiloByte = 1024 +const kOneMegaByte = kOneKiloByte * kOneKiloByte + +describe('net module', function () { + // this.timeout(0) + describe('HTTP basics', function () { + let server + beforeEach(function (done) { + server = http.createServer() + server.listen(0, '127.0.0.1', function () { + server.url = 'http://127.0.0.1:' + server.address().port + done() + }) + }) + + afterEach(function () { + server.close(function () { + }) + server = null + }) + + it('should be able to issue a basic GET request', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert.equal(request.method, 'GET') + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request(`${server.url}${requestUrl}`) + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should be able to issue a basic POST request', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert.equal(request.method, 'POST') + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'POST', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should fetch correct data in a GET request', function (done) { + const requestUrl = '/requestUrl' + const bodyData = 'Hello World!' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert.equal(request.method, 'GET') + response.write(bodyData) + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request(`${server.url}${requestUrl}`) + urlRequest.on('response', function (response) { + let expectedBodyData = '' + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + expectedBodyData += chunk.toString() + }) + response.on('end', function () { + assert.equal(expectedBodyData, bodyData) + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should post the correct data in a POST request', function (done) { + const requestUrl = '/requestUrl' + const bodyData = 'Hello World!' + server.on('request', function (request, response) { + let postedBodyData = '' + switch (request.url) { + case requestUrl: + assert.equal(request.method, 'POST') + request.on('data', function (chunk) { + postedBodyData += chunk.toString() + }) + request.on('end', function () { + assert.equal(postedBodyData, bodyData) + response.end() + }) + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'POST', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.write(bodyData) + urlRequest.end() + }) + + it('should support chunked encoding', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.chunkedEncoding = true + assert.equal(request.method, 'POST') + assert.equal(request.headers['transfer-encoding'], 'chunked') + assert(!request.headers['content-length']) + request.on('data', function (chunk) { + response.write(chunk) + }) + request.on('end', function (chunk) { + response.end(chunk) + }) + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'POST', + url: `${server.url}${requestUrl}` + }) + + let chunkIndex = 0 + let chunkCount = 100 + let sentChunks = [] + let receivedChunks = [] + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + receivedChunks.push(chunk) + }) + response.on('end', function () { + let sentData = Buffer.concat(sentChunks) + let receivedData = Buffer.concat(receivedChunks) + assert.equal(sentData.toString(), receivedData.toString()) + assert.equal(chunkIndex, chunkCount) + done() + }) + response.resume() + }) + urlRequest.chunkedEncoding = true + while (chunkIndex < chunkCount) { + ++chunkIndex + let chunk = randomBuffer(kOneKiloByte) + sentChunks.push(chunk) + assert(urlRequest.write(chunk)) + } + urlRequest.end() + }) + }) + + describe('ClientRequest API', function () { + let server + beforeEach(function (done) { + server = http.createServer() + server.listen(0, '127.0.0.1', function () { + server.url = 'http://127.0.0.1:' + server.address().port + done() + }) + }) + + afterEach(function () { + server.close(function () { + }) + server = null + session.defaultSession.webRequest.onBeforeRequest(null) + }) + + it('request/response objects should emit expected events', function (done) { + const requestUrl = '/requestUrl' + let bodyData = randomString(kOneMegaByte) + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.write(bodyData) + response.end() + break + default: + assert(false) + } + }) + + let requestResponseEventEmitted = false + let requestFinishEventEmitted = false + let requestCloseEventEmitted = false + let responseDataEventEmitted = false + let responseEndEventEmitted = false + + function maybeDone (done) { + if (!requestCloseEventEmitted || !responseEndEventEmitted) { + return + } + + assert(requestResponseEventEmitted) + assert(requestFinishEventEmitted) + assert(requestCloseEventEmitted) + assert(responseDataEventEmitted) + assert(responseEndEventEmitted) + done() + } + + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + requestResponseEventEmitted = true + const statusCode = response.statusCode + assert.equal(statusCode, 200) + let buffers = [] + response.pause() + response.on('data', function (chunk) { + buffers.push(chunk) + responseDataEventEmitted = true + }) + response.on('end', function () { + let receivedBodyData = Buffer.concat(buffers) + assert(receivedBodyData.toString() === bodyData) + responseEndEventEmitted = true + maybeDone(done) + }) + response.resume() + response.on('error', function (error) { + assert.ifError(error) + }) + response.on('aborted', function () { + assert(false) + }) + }) + urlRequest.on('finish', function () { + requestFinishEventEmitted = true + }) + urlRequest.on('error', function (error) { + assert.ifError(error) + }) + urlRequest.on('abort', function () { + assert(false) + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + maybeDone(done) + }) + urlRequest.end() + }) + + it('should be able to set a custom HTTP request header before first write', function (done) { + const requestUrl = '/requestUrl' + const customHeaderName = 'Some-Custom-Header-Name' + const customHeaderValue = 'Some-Customer-Header-Value' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert.equal(request.headers[customHeaderName.toLowerCase()], + customHeaderValue) + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + const statusCode = response.statusCode + assert.equal(statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.setHeader(customHeaderName, customHeaderValue) + assert.equal(urlRequest.getHeader(customHeaderName), + customHeaderValue) + assert.equal(urlRequest.getHeader(customHeaderName.toLowerCase()), + customHeaderValue) + urlRequest.write('') + assert.equal(urlRequest.getHeader(customHeaderName), + customHeaderValue) + assert.equal(urlRequest.getHeader(customHeaderName.toLowerCase()), + customHeaderValue) + urlRequest.end() + }) + + it('should not be able to set a custom HTTP request header after first write', function (done) { + const requestUrl = '/requestUrl' + const customHeaderName = 'Some-Custom-Header-Name' + const customHeaderValue = 'Some-Customer-Header-Value' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert(!request.headers[customHeaderName.toLowerCase()]) + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + const statusCode = response.statusCode + assert.equal(statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.write('') + assert.throws(() => { + urlRequest.setHeader(customHeaderName, customHeaderValue) + }) + assert(!urlRequest.getHeader(customHeaderName)) + urlRequest.end() + }) + + it('should be able to remove a custom HTTP request header before first write', function (done) { + const requestUrl = '/requestUrl' + const customHeaderName = 'Some-Custom-Header-Name' + const customHeaderValue = 'Some-Customer-Header-Value' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert(!request.headers[customHeaderName.toLowerCase()]) + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + const statusCode = response.statusCode + assert.equal(statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.setHeader(customHeaderName, customHeaderValue) + assert.equal(urlRequest.getHeader(customHeaderName), + customHeaderValue) + urlRequest.removeHeader(customHeaderName) + assert(!urlRequest.getHeader(customHeaderName)) + urlRequest.write('') + urlRequest.end() + }) + + it('should not be able to remove a custom HTTP request header after first write', function (done) { + const requestUrl = '/requestUrl' + const customHeaderName = 'Some-Custom-Header-Name' + const customHeaderValue = 'Some-Customer-Header-Value' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert.equal(request.headers[customHeaderName.toLowerCase()], + customHeaderValue) + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + const statusCode = response.statusCode + assert.equal(statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.setHeader(customHeaderName, customHeaderValue) + assert.equal(urlRequest.getHeader(customHeaderName), + customHeaderValue) + urlRequest.write('') + assert.throws(function () { + urlRequest.removeHeader(customHeaderName) + }) + assert.equal(urlRequest.getHeader(customHeaderName), + customHeaderValue) + urlRequest.end() + }) + + it('should be able to abort an HTTP request before first write', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + assert(false) + }) + + let requestAbortEventEmitted = false + let requestCloseEventEmitted = false + + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert(false) + }) + urlRequest.on('finish', function () { + assert(false) + }) + urlRequest.on('error', function () { + assert(false) + }) + urlRequest.on('abort', function () { + requestAbortEventEmitted = true + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + assert(requestAbortEventEmitted) + assert(requestCloseEventEmitted) + done() + }) + urlRequest.abort() + assert(!urlRequest.write('')) + urlRequest.end() + }) + + it('it should be able to abort an HTTP request before request end', function (done) { + const requestUrl = '/requestUrl' + let requestReceivedByServer = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + requestReceivedByServer = true + cancelRequest() + break + default: + assert(false) + } + }) + + let requestAbortEventEmitted = false + let requestCloseEventEmitted = false + + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert(false) + }) + urlRequest.on('finish', function () { + assert(false) + }) + urlRequest.on('error', function () { + assert(false) + }) + urlRequest.on('abort', function () { + requestAbortEventEmitted = true + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + assert(requestReceivedByServer) + assert(requestAbortEventEmitted) + assert(requestCloseEventEmitted) + done() + }) + + urlRequest.chunkedEncoding = true + urlRequest.write(randomString(kOneKiloByte)) + function cancelRequest () { + urlRequest.abort() + } + }) + + it('it should be able to abort an HTTP request after request end and before response', function (done) { + const requestUrl = '/requestUrl' + let requestReceivedByServer = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + requestReceivedByServer = true + cancelRequest() + process.nextTick(() => { + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + }) + break + default: + assert(false) + } + }) + + let requestAbortEventEmitted = false + let requestFinishEventEmitted = false + let requestCloseEventEmitted = false + + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert(false) + }) + urlRequest.on('finish', function () { + requestFinishEventEmitted = true + }) + urlRequest.on('error', function () { + assert(false) + }) + urlRequest.on('abort', function () { + requestAbortEventEmitted = true + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + assert(requestFinishEventEmitted) + assert(requestReceivedByServer) + assert(requestAbortEventEmitted) + assert(requestCloseEventEmitted) + done() + }) + + urlRequest.end(randomString(kOneKiloByte)) + function cancelRequest () { + urlRequest.abort() + } + }) + + it('it should be able to abort an HTTP request after response start', function (done) { + const requestUrl = '/requestUrl' + let requestReceivedByServer = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + requestReceivedByServer = true + response.statusCode = 200 + response.statusMessage = 'OK' + response.write(randomString(kOneKiloByte)) + break + default: + assert(false) + } + }) + + let requestFinishEventEmitted = false + let requestResponseEventEmitted = false + let requestAbortEventEmitted = false + let requestCloseEventEmitted = false + let responseAbortedEventEmitted = false + + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + requestResponseEventEmitted = true + const statusCode = response.statusCode + assert.equal(statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + assert(false) + }) + response.resume() + response.on('error', function () { + assert(false) + }) + response.on('aborted', function () { + responseAbortedEventEmitted = true + }) + urlRequest.abort() + }) + urlRequest.on('finish', function () { + requestFinishEventEmitted = true + }) + urlRequest.on('error', function () { + assert(false) + }) + urlRequest.on('abort', function () { + requestAbortEventEmitted = true + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + assert(requestFinishEventEmitted, 'request should emit "finish" event') + assert(requestReceivedByServer, 'request should be received by the server') + assert(requestResponseEventEmitted, '"response" event should be emitted') + assert(requestAbortEventEmitted, 'request should emit "abort" event') + assert(responseAbortedEventEmitted, 'response should emit "aborted" event') + assert(requestCloseEventEmitted, 'request should emit "close" event') + done() + }) + urlRequest.end(randomString(kOneKiloByte)) + }) + + it('abort event should be emitted at most once', function (done) { + const requestUrl = '/requestUrl' + let requestReceivedByServer = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + requestReceivedByServer = true + cancelRequest() + break + default: + assert(false) + } + }) + + let requestFinishEventEmitted = false + let requestAbortEventCount = 0 + let requestCloseEventEmitted = false + + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert(false) + }) + urlRequest.on('finish', function () { + requestFinishEventEmitted = true + }) + urlRequest.on('error', function () { + assert(false) + }) + urlRequest.on('abort', function () { + ++requestAbortEventCount + urlRequest.abort() + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + // Let all pending async events to be emitted + setTimeout(function () { + assert(requestFinishEventEmitted) + assert(requestReceivedByServer) + assert.equal(requestAbortEventCount, 1) + assert(requestCloseEventEmitted) + done() + }, 500) + }) + + urlRequest.end(randomString(kOneKiloByte)) + function cancelRequest () { + urlRequest.abort() + urlRequest.abort() + } + }) + + it('Requests should be intercepted by webRequest module', function (done) { + const requestUrl = '/requestUrl' + const redirectUrl = '/redirectUrl' + let requestIsRedirected = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert(false) + break + case redirectUrl: + requestIsRedirected = true + response.end() + break + default: + assert(false) + } + }) + + let requestIsIntercepted = false + session.defaultSession.webRequest.onBeforeRequest( + function (details, callback) { + if (details.url === `${server.url}${requestUrl}`) { + requestIsIntercepted = true + callback({ + redirectURL: `${server.url}${redirectUrl}` + }) + } else { + callback({ + cancel: false + }) + } + }) + + const urlRequest = net.request(`${server.url}${requestUrl}`) + + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + assert(requestIsRedirected, 'The server should receive a request to the forward URL') + assert(requestIsIntercepted, 'The request should be intercepted by the webRequest module') + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should to able to create and intercept a request using a custom session object', function (done) { + const requestUrl = '/requestUrl' + const redirectUrl = '/redirectUrl' + const customPartitionName = 'custom-partition' + let requestIsRedirected = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert(false) + break + case redirectUrl: + requestIsRedirected = true + response.end() + break + default: + assert(false) + } + }) + + session.defaultSession.webRequest.onBeforeRequest( + function (details, callback) { + assert(false, 'Request should not be intercepted by the default session') + }) + + let customSession = session.fromPartition(customPartitionName, { + cache: false + }) + let requestIsIntercepted = false + customSession.webRequest.onBeforeRequest( + function (details, callback) { + if (details.url === `${server.url}${requestUrl}`) { + requestIsIntercepted = true + callback({ + redirectURL: `${server.url}${redirectUrl}` + }) + } else { + callback({ + cancel: false + }) + } + }) + + const urlRequest = net.request({ + url: `${server.url}${requestUrl}`, + session: customSession + }) + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + assert(requestIsRedirected, 'The server should receive a request to the forward URL') + assert(requestIsIntercepted, 'The request should be intercepted by the webRequest module') + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should throw if given an invalid session option', function (done) { + const requestUrl = '/requestUrl' + try { + const urlRequest = net.request({ + url: `${server.url}${requestUrl}`, + session: 1 + }) + urlRequest + } catch (exception) { + done() + } + }) + + it('should to able to create and intercept a request using a custom partition name', function (done) { + const requestUrl = '/requestUrl' + const redirectUrl = '/redirectUrl' + const customPartitionName = 'custom-partition' + let requestIsRedirected = false + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert(false) + break + case redirectUrl: + requestIsRedirected = true + response.end() + break + default: + assert(false) + } + }) + + session.defaultSession.webRequest.onBeforeRequest( + function (details, callback) { + assert(false, 'Request should not be intercepted by the default session') + }) + + let customSession = session.fromPartition(customPartitionName, { + cache: false + }) + let requestIsIntercepted = false + customSession.webRequest.onBeforeRequest( + function (details, callback) { + if (details.url === `${server.url}${requestUrl}`) { + requestIsIntercepted = true + callback({ + redirectURL: `${server.url}${redirectUrl}` + }) + } else { + callback({ + cancel: false + }) + } + }) + + const urlRequest = net.request({ + url: `${server.url}${requestUrl}`, + partition: customPartitionName + }) + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + assert(requestIsRedirected, 'The server should receive a request to the forward URL') + assert(requestIsIntercepted, 'The request should be intercepted by the webRequest module') + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should throw if given an invalid partition option', function (done) { + const requestUrl = '/requestUrl' + try { + const urlRequest = net.request({ + url: `${server.url}${requestUrl}`, + partition: 1 + }) + urlRequest + } catch (exception) { + done() + } + }) + + it('should be able to create a request with options', function (done) { + const requestUrl = '/' + const customHeaderName = 'Some-Custom-Header-Name' + const customHeaderValue = 'Some-Customer-Header-Value' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + assert.equal(request.method, 'GET') + assert.equal(request.headers[customHeaderName.toLowerCase()], + customHeaderValue) + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + break + default: + assert(false) + } + }) + + const serverUrl = url.parse(server.url) + let options = { + port: serverUrl.port, + hostname: '127.0.0.1', + headers: {} + } + options.headers[customHeaderName] = customHeaderValue + const urlRequest = net.request(options) + urlRequest.on('response', function (response) { + assert.equal(response.statusCode, 200) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should be able to pipe a readable stream into a net request', function (done) { + const nodeRequestUrl = '/nodeRequestUrl' + const netRequestUrl = '/netRequestUrl' + const bodyData = randomString(kOneMegaByte) + let netRequestReceived = false + let netRequestEnded = false + server.on('request', function (request, response) { + switch (request.url) { + case nodeRequestUrl: + response.write(bodyData) + response.end() + break + case netRequestUrl: + netRequestReceived = true + let receivedBodyData = '' + request.on('data', function (chunk) { + receivedBodyData += chunk.toString() + }) + request.on('end', function (chunk) { + netRequestEnded = true + if (chunk) { + receivedBodyData += chunk.toString() + } + assert.equal(receivedBodyData, bodyData) + response.end() + }) + break + default: + assert(false) + } + }) + + let nodeRequest = http.request(`${server.url}${nodeRequestUrl}`) + nodeRequest.on('response', function (nodeResponse) { + const netRequest = net.request(`${server.url}${netRequestUrl}`) + netRequest.on('response', function (netResponse) { + assert.equal(netResponse.statusCode, 200) + netResponse.pause() + netResponse.on('data', function (chunk) { + }) + netResponse.on('end', function () { + assert(netRequestReceived) + assert(netRequestEnded) + done() + }) + netResponse.resume() + }) + nodeResponse.pipe(netRequest) + }) + nodeRequest.end() + }) + + it('should emit error event on server socket close', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + request.socket.destroy() + break + default: + assert(false) + } + }) + let requestErrorEventEmitted = false + const urlRequest = net.request(`${server.url}${requestUrl}`) + urlRequest.on('error', function (error) { + assert(error) + requestErrorEventEmitted = true + }) + urlRequest.on('close', function () { + assert(requestErrorEventEmitted) + done() + }) + urlRequest.end() + }) + }) + describe('IncomingMessage API', function () { + let server + beforeEach(function (done) { + server = http.createServer() + server.listen(0, '127.0.0.1', function () { + server.url = 'http://127.0.0.1:' + server.address().port + done() + }) + }) + + afterEach(function () { + server.close() + server = null + }) + + it('response object should implement the IncomingMessage API', function (done) { + const requestUrl = '/requestUrl' + const customHeaderName = 'Some-Custom-Header-Name' + const customHeaderValue = 'Some-Customer-Header-Value' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.setHeader(customHeaderName, customHeaderValue) + response.end() + break + default: + assert(false) + } + }) + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + const statusCode = response.statusCode + assert(typeof statusCode === 'number') + assert.equal(statusCode, 200) + const statusMessage = response.statusMessage + assert(typeof statusMessage === 'string') + assert.equal(statusMessage, 'OK') + const headers = response.headers + assert(typeof headers === 'object') + assert.deepEqual(headers[customHeaderName.toLowerCase()], + [customHeaderValue]) + const httpVersion = response.httpVersion + assert(typeof httpVersion === 'string') + assert(httpVersion.length > 0) + const httpVersionMajor = response.httpVersionMajor + assert(typeof httpVersionMajor === 'number') + assert(httpVersionMajor >= 1) + const httpVersionMinor = response.httpVersionMinor + assert(typeof httpVersionMinor === 'number') + assert(httpVersionMinor >= 0) + response.pause() + response.on('data', function (chunk) { + }) + response.on('end', function () { + done() + }) + response.resume() + }) + urlRequest.end() + }) + + it('should be able to pipe a net response into a writable stream', function (done) { + const nodeRequestUrl = '/nodeRequestUrl' + const netRequestUrl = '/netRequestUrl' + const bodyData = randomString(kOneMegaByte) + server.on('request', function (request, response) { + switch (request.url) { + case netRequestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.write(bodyData) + response.end() + break + case nodeRequestUrl: + let receivedBodyData = '' + request.on('data', function (chunk) { + receivedBodyData += chunk.toString() + }) + request.on('end', function (chunk) { + if (chunk) { + receivedBodyData += chunk.toString() + } + assert.equal(receivedBodyData, bodyData) + response.end() + }) + break + default: + assert(false) + } + }) + ipcRenderer.once('api-net-spec-done', function () { + done() + }) + // Execute below code directly within the browser context without + // using the remote module. + ipcRenderer.send('eval', ` + const {net} = require('electron') + const http = require('http') + const netRequest = net.request('${server.url}${netRequestUrl}') + netRequest.on('response', function (netResponse) { + const serverUrl = url.parse('${server.url}') + const nodeOptions = { + method: 'POST', + path: '${nodeRequestUrl}', + port: serverUrl.port + } + let nodeRequest = http.request(nodeOptions) + nodeRequest.on('response', function (nodeResponse) { + nodeResponse.on('data', function (chunk) { + }) + nodeResponse.on('end', function (chunk) { + event.sender.send('api-net-spec-done') + }) + }) + netResponse.pipe(nodeRequest) + }) + netRequest.end() + `) + }) + + it('should not emit any event after close', function (done) { + const requestUrl = '/requestUrl' + let bodyData = randomString(kOneKiloByte) + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.write(bodyData) + response.end() + break + default: + assert(false) + } + }) + let requestCloseEventEmitted = false + const urlRequest = net.request({ + method: 'GET', + url: `${server.url}${requestUrl}` + }) + urlRequest.on('response', function (response) { + assert(!requestCloseEventEmitted) + const statusCode = response.statusCode + assert.equal(statusCode, 200) + response.pause() + response.on('data', function () { + }) + response.on('end', function () { + }) + response.resume() + response.on('error', function () { + assert(!requestCloseEventEmitted) + }) + response.on('aborted', function () { + assert(!requestCloseEventEmitted) + }) + }) + urlRequest.on('finish', function () { + assert(!requestCloseEventEmitted) + }) + urlRequest.on('error', function () { + assert(!requestCloseEventEmitted) + }) + urlRequest.on('abort', function () { + assert(!requestCloseEventEmitted) + }) + urlRequest.on('close', function () { + requestCloseEventEmitted = true + // Wait so that all async events get scheduled. + setTimeout(function () { + done() + }, 100) + }) + urlRequest.end() + }) + }) + describe('Stability and performance', function (done) { + let server + beforeEach(function (done) { + server = http.createServer() + server.listen(0, '127.0.0.1', function () { + server.url = 'http://127.0.0.1:' + server.address().port + done() + }) + }) + + afterEach(function () { + server.close() + server = null + }) + + it('should free unreferenced, never-started request objects without crash', function (done) { + const requestUrl = '/requestUrl' + ipcRenderer.once('api-net-spec-done', function () { + done() + }) + ipcRenderer.send('eval', ` + const {net} = require('electron') + const urlRequest = net.request('${server.url}${requestUrl}') + process.nextTick(function () { + const v8Util = process.atomBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() + event.sender.send('api-net-spec-done') + }) + `) + }) + it('should not collect on-going requests without crash', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.write(randomString(kOneKiloByte)) + ipcRenderer.once('api-net-spec-resume', function () { + response.write(randomString(kOneKiloByte)) + response.end() + }) + break + default: + assert(false) + } + }) + ipcRenderer.once('api-net-spec-done', function () { + done() + }) + // Execute below code directly within the browser context without + // using the remote module. + ipcRenderer.send('eval', ` + const {net} = require('electron') + const urlRequest = net.request('${server.url}${requestUrl}') + urlRequest.on('response', function (response) { + response.on('data', function () { + }) + response.on('end', function () { + event.sender.send('api-net-spec-done') + }) + process.nextTick(function () { + // Trigger a garbage collection. + const v8Util = process.atomBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() + event.sender.send('api-net-spec-resume') + }) + }) + urlRequest.end() + `) + }) + it('should collect unreferenced, ended requests without crash', function (done) { + const requestUrl = '/requestUrl' + server.on('request', function (request, response) { + switch (request.url) { + case requestUrl: + response.statusCode = 200 + response.statusMessage = 'OK' + response.end() + break + default: + assert(false) + } + }) + ipcRenderer.once('api-net-spec-done', function () { + done() + }) + ipcRenderer.send('eval', ` + const {net} = require('electron') + const urlRequest = net.request('${server.url}${requestUrl}') + urlRequest.on('response', function (response) { + response.on('data', function () { + }) + response.on('end', function () { + }) + }) + urlRequest.on('close', function () { + process.nextTick(function () { + const v8Util = process.atomBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() + event.sender.send('api-net-spec-done') + }) + }) + urlRequest.end() + `) + }) + }) +}) diff --git a/spec/static/main.js b/spec/static/main.js index e910e4de2c..58d8a1970a 100644 --- a/spec/static/main.js +++ b/spec/static/main.js @@ -8,6 +8,7 @@ const ipcMain = electron.ipcMain const dialog = electron.dialog const BrowserWindow = electron.BrowserWindow const protocol = electron.protocol +const v8 = require('v8') const Coverage = require('electabul').Coverage const fs = require('fs') @@ -24,6 +25,7 @@ var argv = require('yargs') var window = null process.port = 0 // will be used by crash-reporter spec. +v8.setFlagsFromString('--expose_gc') app.commandLine.appendSwitch('js-flags', '--expose_gc') app.commandLine.appendSwitch('ignore-certificate-errors') app.commandLine.appendSwitch('disable-renderer-backgrounding')