/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=8 et tw=80 : */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// HttpLog.h should generally be included first #include"HttpLog.h"
// Log on level :5, instead of default :4. #undef LOG #define LOG(args) LOG5(args) #undef LOG_ENABLED #define LOG_ENABLED() LOG5_ENABLED()
static_assert(nsISupportsPriority::PRIORITY_LOWEST <= kNormalPriority, "Lowest Priority should be less than kNormalPriority");
// values of priority closer to 0 are higher priority for the priority // argument. This value is used as a group, which maps to a // weight that is related to the nsISupportsPriority that we are given.
int32_t httpPriority; if (priority >= nsISupportsPriority::PRIORITY_LOWEST) {
httpPriority = kWorstPriority;
} elseif (priority <= nsISupportsPriority::PRIORITY_HIGHEST) {
httpPriority = kBestPriority;
} else {
httpPriority = kNormalPriority + priority;
}
MOZ_ASSERT(httpPriority >= 0);
SetPriority(static_cast<uint32_t>(httpPriority));
}
// ReadSegments() is used to write data down the socket. Generally, HTTP // request data is pulled from the approriate transaction and // converted to HTTP/2 data. Sometimes control data like a window-update is // generated instead.
nsresult Http2StreamBase::ReadSegments(nsAHttpSegmentReader* reader,
uint32_t count, uint32_t* countRead) {
LOG3(("Http2StreamBase %p ReadSegments reader=%p count=%d state=%x", this,
reader, count, mUpstreamState));
RefPtr<Http2Session> session = Session(); // Reader is nullptr when this is a push stream.
MOZ_DIAGNOSTIC_ASSERT(!reader || (reader == session) ||
(IsTunnel() && NS_FAILED(Condition())));
if (NS_FAILED(Condition())) { return Condition();
}
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
if (mRecvdFin || mRecvdReset) { // Don't transmit any request frames if the peer cannot respond
LOG3(
("Http2StreamBase %p ReadSegments request stream aborted due to" " response side closure\n", this)); return NS_ERROR_ABORT;
}
// avoid runt chunks if possible by anticipating // full data frames if (count > (mChunkSize + 8)) {
uint32_t numchunks = count / (mChunkSize + 8);
count = numchunks * (mChunkSize + 8);
}
switch (mUpstreamState) { case GENERATING_HEADERS: case GENERATING_BODY: case SENDING_BODY: // Call into the HTTP Transaction to generate the HTTP request // stream. That stream will show up in OnReadSegment().
mSegmentReader = reader;
rv = CallToReadData(count, countRead);
mSegmentReader = nullptr;
// Check to see if the transaction's request could be written out now. // If not, mark the stream for callback when writing can proceed. if (NS_SUCCEEDED(rv) && mUpstreamState == GENERATING_HEADERS &&
!mRequestHeadersDone) {
session->TransactionHasDataToWrite(this);
}
// mTxinlineFrameUsed represents any queued un-sent frame. It might // be 0 if there is no such frame, which is not a gurantee that we // don't have more request body to send - just that any data that was // sent comprised a complete HTTP/2 frame. Likewise, a non 0 value is // a queued, but complete, http/2 frame length.
// Mark that we are blocked on read if the http transaction needs to // provide more of the request message body and there is nothing queued // for writing if (rv == NS_BASE_STREAM_WOULD_BLOCK && !mTxInlineFrameUsed) {
LOG(("Http2StreamBase %p mRequestBlockedOnRead = 1", this));
mRequestBlockedOnRead = 1;
}
// A transaction that had already generated its headers before it was // queued at the session level (due to concurrency concerns) may not call // onReadSegment off the ReadSegments() stack above.
// When mTransaction->ReadSegments returns NS_BASE_STREAM_WOULD_BLOCK it // means it may have already finished providing all the request data // necessary to generate open, calling OnReadSegment will drive sending // the request; this may happen after dequeue of the stream.
LOG3((" OnReadSegment returned 0x%08" PRIx32, static_cast<uint32_t>(rv2))); if (NS_SUCCEEDED(rv2)) {
mRequestBlockedOnRead = 0;
}
}
// If the sending flow control window is open (!mBlockedOnRwin) then // continue sending the request if (!mBlockedOnRwin && mOpenGenerated && !mTxInlineFrameUsed &&
NS_SUCCEEDED(rv) && (!*countRead) && CloseSendStreamWhenDone()) {
MOZ_ASSERT(!mQueued);
MOZ_ASSERT(mRequestHeadersDone);
LOG3(
("Http2StreamBase::ReadSegments %p 0x%X: Sending request data " "complete, " "mUpstreamState=%x\n", this, mStreamID, mUpstreamState)); if (mSentFin) {
ChangeState(UPSTREAM_COMPLETE);
} else {
GenerateDataFrameHeader(0, true);
ChangeState(SENDING_FIN_STREAM);
session->TransactionHasDataToWrite(this);
rv = NS_BASE_STREAM_WOULD_BLOCK;
}
} break;
case SENDING_FIN_STREAM: // We were trying to send the FIN-STREAM but were blocked from // sending it out - try again. if (!mSentFin) {
mSegmentReader = reader;
rv = TransmitFrame(nullptr, nullptr, false);
mSegmentReader = nullptr;
MOZ_ASSERT(NS_FAILED(rv) || !mTxInlineFrameUsed, "Transmit Frame should be all or nothing"); if (NS_SUCCEEDED(rv)) ChangeState(UPSTREAM_COMPLETE);
} else {
rv = NS_OK;
mTxInlineFrameUsed = 0; // cancel fin data packet
ChangeState(UPSTREAM_COMPLETE);
}
*countRead = 0;
// don't change OK to WOULD BLOCK. we are really done sending if OK break;
case UPSTREAM_COMPLETE:
*countRead = 0;
rv = NS_OK; break;
uint64_t Http2StreamBase::LocalUnAcked() { // reduce unacked by the amount of undelivered data // to help assert flow control
uint64_t undelivered = mSimpleBuffer.Available();
if (NS_SUCCEEDED(rv)) {
rv = mSimpleBuffer.Write(buf, *countWritten); if (NS_FAILED(rv)) {
MOZ_ASSERT(rv == NS_ERROR_OUT_OF_MEMORY); return NS_ERROR_OUT_OF_MEMORY;
}
} return rv;
}
bool Http2StreamBase::DeferCleanup(nsresult status) { // do not cleanup a stream that has data buffered for the transaction return (NS_SUCCEEDED(status) && mSimpleBuffer.Available());
}
// WriteSegments() is used to read data off the socket. Generally this is // just a call through to the associated nsHttpTransaction for this stream // for the remaining data bytes indicated by the current DATA frame.
nsresult Http2StreamBase::WriteSegments(nsAHttpSegmentWriter* writer,
uint32_t count,
uint32_t* countWritten) {
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
MOZ_ASSERT(!mSegmentWriter, "segment writer in progress");
if (rv == NS_BASE_STREAM_WOULD_BLOCK) { // consuming transaction won't take data. but we need to read it into a // buffer so that it won't block other streams. but we should not advance // the flow control window so that we'll eventually push back on the sender.
rv = BufferInput(count, countWritten);
LOG3(("Http2StreamBase::WriteSegments %p Buffered %" PRIX32 " %d\n", this, static_cast<uint32_t>(rv), *countWritten));
}
nsresult Http2StreamBase::ParseHttpRequestHeaders(constchar* buf,
uint32_t avail,
uint32_t* countUsed) { // Returns NS_OK even if the headers are incomplete // set mRequestHeadersDone flag if they are complete
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
MOZ_ASSERT(mUpstreamState == GENERATING_HEADERS);
MOZ_ASSERT(!mRequestHeadersDone);
// We can use the simple double crlf because firefox is the // only client we are parsing
int32_t endHeader = mFlatHttpRequestHeaders.Find("\r\n\r\n");
if (endHeader == kNotFound) { // We don't have all the headers yet
LOG3(
("Http2StreamBase::ParseHttpRequestHeaders %p " "Need more header bytes. Len = %zd", this, mFlatHttpRequestHeaders.Length()));
*countUsed = avail; return NS_OK;
}
// We have recvd all the headers, trim the local // buffer of the final empty line, and set countUsed to reflect // the whole header has been consumed.
uint32_t oldLen = mFlatHttpRequestHeaders.Length();
mFlatHttpRequestHeaders.SetLength(endHeader + 2);
*countUsed = avail - (oldLen - endHeader) + 4;
mRequestHeadersDone = 1;
Http2Stream* selfRegularStream = this->GetHttp2Stream(); if (selfRegularStream) { return selfRegularStream->CheckPushCache();
}
return NS_OK;
}
// This is really a headers frame, but open is pretty clear from a workflow pov
nsresult Http2StreamBase::GenerateOpen() { // It is now OK to assign a streamID that we are assured will // be monotonically increasing amongst new streams on this // session
RefPtr<Http2Session> session = Session();
mStreamID = session->RegisterStreamID(this);
MOZ_ASSERT(mStreamID & 1, "Http2 Stream Channel ID must be odd");
MOZ_ASSERT(!mOpenGenerated);
mOpenGenerated = 1;
LOG3(("Http2StreamBase %p Stream ID 0x%X [session=%p]\n", this, mStreamID,
session.get()));
if (mStreamID >= 0x80000000) { // streamID must fit in 31 bits. Evading This is theoretically possible // because stream ID assignment is asynchronous to stream creation // because of the protocol requirement that the new stream ID // be monotonically increasing. In reality this is really not possible // because new streams stop being added to a session with millions of // IDs still available and no race condition is going to bridge that gap; // so we can be comfortable on just erroring out for correctness in that // case.
LOG3(("Stream assigned out of range ID: 0x%X", mStreamID)); return NS_ERROR_UNEXPECTED;
}
// Now we need to convert the flat http headers into a set // of HTTP/2 headers by writing to mTxInlineFrame{sz}
if (firstFrameFlags & Http2Session::kFlag_END_STREAM) {
SetSentFin(true);
}
// split this one HEADERS frame up into N HEADERS + CONTINUATION frames if it // exceeds the 2^14-1 limit for 1 frame. Do it by inserting header size gaps // in the existing frame for the new headers and for the first one a priority // field. There is no question this is ugly, but a 16KB HEADERS frame should // be a long tail event, so this is really just for correctness and a nop in // the base case. //
void Http2StreamBase::AdjustInitialWindow() { // The default initial_window is sized for pushed streams. When we // generate a client pulled stream we want to disable flow control for // the stream with a window update. Do the same for pushed streams // when they connect to a pull.
// right now mClientReceiveWindow is the lower push limit // bump it up to the pull limit set by the channel or session // don't allow windows less than push
uint32_t bump = 0;
RefPtr<Http2Session> session = Session();
nsHttpTransaction* trans = HttpTransaction(); if (trans && trans->InitialRwin()) {
bump = (trans->InitialRwin() > mClientReceiveWindow)
? (trans->InitialRwin() - mClientReceiveWindow)
: 0;
} else {
MOZ_ASSERT(session->InitialRwin() >= mClientReceiveWindow);
bump = session->InitialRwin() - mClientReceiveWindow;
}
LOG3(("AdjustInitialwindow increased flow control window %p 0x%X %u\n", this,
wireStreamId, bump)); if (!bump) { // nothing to do return;
}
// Setting the TCP send buffer, introduced in // https://bugzilla.mozilla.org/show_bug.cgi?id=790184, which the following // comment refers to, is being removed once we verify no increases in error // rate. // // normally on non-windows platform we use TCP autotuning for // the socket buffers, and this works well (managing enough // buffers for BDP while conserving memory) for HTTP even when // it creates really deep queues. However this 'buffer bloat' is // a problem for http/2 because it ruins the low latency properties // necessary for PING and cancel to work meaningfully.
// If this stream represents a large upload, disable autotuning for // the session and cap the send buffers by default at 128KB. // (10Mbit/sec @ 100ms) //
uint32_t bufferSize = gHttpHandler->SpdySendBufferSize(); if (StaticPrefs::network_http_http2_send_buffer_size() > 0 &&
(mTotalSent > bufferSize) && !mSetTCPSocketBuffer) {
mSetTCPSocketBuffer = 1;
mSocketTransport->SetSendBufferSize(bufferSize);
}
if (!mSentWaitingFor && !mRequestBodyLenRemaining) {
mSentWaitingFor = 1; if (Transaction()) {
Transaction()->OnTransportStatus(mSocketTransport,
NS_NET_STATUS_WAITING_FOR, 0);
}
}
}
nsresult Http2StreamBase::TransmitFrame(constchar* buf, uint32_t* countUsed, bool forceCommitment) { // If TransmitFrame returns SUCCESS than all the data is sent (or at least // buffered at the session level), if it returns WOULD_BLOCK then none of // the data is sent.
// You can call this function with no data and no out parameter in order to // flush internal buffers that were previously blocked on writing. You can // of course feed new data to it as well.
// In the (relatively common) event that we have a small amount of data // split between the inlineframe and the streamframe, then move the stream // data into the inlineframe via copy in order to coalesce into one write. // Given the interaction with ssl this is worth the small copy cost. if (mTxStreamFrameSize && mTxInlineFrameUsed &&
mTxStreamFrameSize < Http2Session::kDefaultBufferSize &&
mTxInlineFrameUsed + mTxStreamFrameSize < mTxInlineFrameSize) {
LOG3(("Coalesce Transmit"));
memcpy(&mTxInlineFrame[mTxInlineFrameUsed], buf, mTxStreamFrameSize); if (countUsed) *countUsed += mTxStreamFrameSize;
mTxInlineFrameUsed += mTxStreamFrameSize;
mTxStreamFrameSize = 0;
}
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
MOZ_ASSERT(!forceCommitment, "forceCommitment with WOULD_BLOCK");
session->TransactionHasDataToWrite(this);
} if (NS_FAILED(rv)) { // this will include WOULD_BLOCK return rv;
}
// This function calls mSegmentReader->OnReadSegment to report the actual // http/2 bytes through to the session object and then the HttpConnection // which calls the socket write function. It will accept all of the inline and // stream data because of the above 'commitment' even if it has to buffer
Http2Session::LogIO(session, this, "Writing from Inline Buffer", reinterpret_cast<char*>(mTxInlineFrame.get()),
transmittedCount);
if (mTxStreamFrameSize) { if (!buf) { // this cannot happen
MOZ_ASSERT(false, "Stream transmit with null buf argument to " "TransmitFrame()");
LOG3(("Stream transmit with null buf argument to TransmitFrame()\n")); return NS_ERROR_UNEXPECTED;
}
// If there is already data buffered, just add to that to form // a single TLS Application Data Record - otherwise skip the memcpy if (session->AmountOfOutputBuffered()) {
rv = session->BufferOutput(buf, mTxStreamFrameSize, &transmittedCount);
} else {
rv = session->OnReadSegment(buf, mTxStreamFrameSize, &transmittedCount);
}
LOG3(
("Http2StreamBase::TransmitFrame for regular session=%p " "stream=%p result %" PRIx32 " len=%d",
session.get(), this, static_cast<uint32_t>(rv), transmittedCount));
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
MOZ_ASSERT(!mTxInlineFrameUsed, "inline frame not empty");
MOZ_ASSERT(!mTxStreamFrameSize, "stream frame not empty");
uint8_t frameFlags = 0; if (lastFrame) {
frameFlags |= Http2Session::kFlag_END_STREAM; if (dataLength) SetSentFin(true);
}
// Ensure the :status is just an HTTP status code // https://tools.ietf.org/html/rfc7540#section-8.1.2.4 // https://bugzilla.mozilla.org/show_bug.cgi?id=1352146
nsAutoCString parsedStatusString;
parsedStatusString.AppendInt(httpResponseCode); if (!parsedStatusString.Equals(statusString)) {
LOG3(
("Http2StreamBase::ConvertResposeHeaders %p status %s is not just a " "code", this, statusString.BeginReading())); // Results in stream reset with PROTOCOL_ERROR return NS_ERROR_ILLEGAL_VALUE;
}
if (httpResponseCode == 421) { // Origin Frame requires 421 to remove this origin from the origin set
RefPtr<Http2Session> session = Session();
session->Received421(ConnectionInfo());
}
if (aHeadersIn.Length() && aHeadersOut.Length()) {
Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_SIZE, aHeadersIn.Length());
uint32_t ratio = aHeadersIn.Length() * 100 / aHeadersOut.Length();
Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_RATIO, ratio);
}
// The decoding went ok. Now we can customize and clean up.
nsHttpTransaction* trans = HttpTransaction(); if (trans) {
trans->SetHttpTrailers(flatTrailers);
} else {
LOG3(("Http2StreamBase::ConvertResponseTrailers %p no trans", this));
}
return NS_OK;
}
void Http2StreamBase::SetResponseIsComplete() {
nsHttpTransaction* trans = HttpTransaction(); if (trans) {
trans->SetResponseIsComplete();
}
}
void Http2StreamBase::SetAllHeadersReceived() { if (mAllHeadersReceived) { return;
}
if (mState == RESERVED_BY_REMOTE) { // pushed streams needs to wait until headers have // arrived to open up their window
LOG3(
("Http2StreamBase::SetAllHeadersReceived %p state OPEN from reserved\n", this));
mState = OPEN;
AdjustInitialWindow();
}
nsHttpTransaction* trans = HttpTransaction(); if (!trans) { return;
}
// we create 6 fake dependency streams per session, // these streams are never opened with HEADERS. our first opened stream is 0xd // 3 depends 0, weight 200, leader class (kLeaderGroupID) // 5 depends 0, weight 100, other (kOtherGroupID) // 7 depends 0, weight 0, background (kBackgroundGroupID) // 9 depends 7, weight 0, speculative (kSpeculativeGroupID) // b depends 3, weight 0, follower class (kFollowerGroupID) // d depends 0, weight 240, urgent-start class (kUrgentStartGroupID) // // streams for leaders (html, js, css) depend on 3 // streams for folowers (images) depend on b // default streams (xhr, async js) depend on 5 // explicit bg streams (beacon, etc..) depend on 7 // spculative bg streams depend on 9 // urgent-start streams depend on d
void Http2StreamBase::CurrentBrowserIdChanged(uint64_t id) { if (!mStreamID) { // For pushed streams, we ignore the direct call from the session and // instead let it come to the internal function from the pushed stream, so // we don't accidentally send two PRIORITY frames for the same stream. return;
}
// If the tab is in the background // we can probably lower the priority of this request by 1. // (to get lower priority we increase urgency). if (isInBackground && urgency < 6) {
urgency++;
}
if (streamID) {
session->SendPriorityUpdateFrame(streamID, urgency, incremental);
}
}
switch (mUpstreamState) { case GENERATING_HEADERS: // The buffer is the HTTP request stream, including at least part of the // HTTP request header. This state's job is to build a HEADERS frame // from the header information. count is the number of http bytes // available (which may include more than the header), and in countRead we // return the number of those bytes that we consume (i.e. the portion that // are header bytes)
if (!mRequestHeadersDone) { if (NS_FAILED(rv = ParseHttpRequestHeaders(buf, count, countRead))) { return rv;
}
}
LOG3(
("ParseHttpRequestHeaders %p used %d of %d. " "requestheadersdone = %d mOpenGenerated = %d\n", this, *countRead, count, mRequestHeadersDone, mOpenGenerated)); if (mOpenGenerated) {
SetHTTPState(OPEN);
AdjustInitialWindow(); // This version of TransmitFrame cannot block
rv = TransmitFrame(nullptr, nullptr, true);
ChangeState(GENERATING_BODY); break;
}
MOZ_ASSERT(*countRead == count, "Header parsing not complete but unused data"); break;
case GENERATING_BODY: // if there is session flow control and either the stream window is active // and exhaused or the session window is exhausted then suspend if (!AllowFlowControlledWrite()) {
*countRead = 0;
LOG3(
("Http2StreamBase this=%p, id 0x%X request body suspended because " "remote window is stream=%" PRId64 " session=%" PRId64 ".\n", this, mStreamID, mServerReceiveWindow,
session->ServerSessionWindow()));
mBlockedOnRwin = true; return NS_BASE_STREAM_WOULD_BLOCK;
}
mBlockedOnRwin = false;
// The chunk is the smallest of: availableData, configured chunkSize, // stream window, session window, or 14 bit framing limit. // Its amazing we send anything at all.
dataLength = std::min(count, mChunkSize);
if (dataLength > Http2Session::kMaxFrameData) {
dataLength = Http2Session::kMaxFrameData;
}
if (dataLength > session->ServerSessionWindow()) {
dataLength = static_cast<uint32_t>(session->ServerSessionWindow());
}
if (dataLength > mServerReceiveWindow) {
dataLength = static_cast<uint32_t>(mServerReceiveWindow);
}
LOG3(("Http2StreamBase %p id 0x%x request len remaining %" PRId64 ", " "count avail %u, chunk used %u", this, mStreamID, mRequestBodyLenRemaining, count, dataLength)); if (!dataLength && mRequestBodyLenRemaining) { return NS_BASE_STREAM_WOULD_BLOCK;
} if (dataLength > mRequestBodyLenRemaining) { return NS_ERROR_UNEXPECTED;
}
mRequestBodyLenRemaining -= dataLength;
GenerateDataFrameHeader(dataLength, !mRequestBodyLenRemaining);
ChangeState(SENDING_BODY);
[[fallthrough]];
case SENDING_BODY:
MOZ_ASSERT(mTxInlineFrameUsed, "OnReadSegment Send Data Header 0b");
rv = TransmitFrame(buf, countRead, false);
MOZ_ASSERT(NS_FAILED(rv) || !mTxInlineFrameUsed, "Transmit Frame should be all or nothing");
LOG3(("TransmitFrame() rv=%" PRIx32 " returning %d data bytes. " "Header is %d Body is %d.", static_cast<uint32_t>(rv), *countRead, mTxInlineFrameUsed,
mTxStreamFrameSize));
// normalize a partial write with a WOULD_BLOCK into just a partial write // as some code will take WOULD_BLOCK to mean an error with nothing // written (e.g. nsHttpTransaction::ReadRequestSegment() if (rv == NS_BASE_STREAM_WOULD_BLOCK && *countRead) rv = NS_OK;
// If that frame was all sent, look for another one if (!mTxInlineFrameUsed) ChangeState(GENERATING_BODY); break;
case SENDING_FIN_STREAM:
MOZ_ASSERT(false, "resuming partial fin stream out of OnReadSegment"); break;
MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (!mSegmentWriter) { return NS_BASE_STREAM_WOULD_BLOCK;
}
// sometimes we have read data from the network and stored it in a pipe // so that other streams can proceed when the gecko caller is not processing // data events fast enough and flow control hasn't caught up yet. This // gets the stored data out of that pipe if (!mBypassInputBuffer && mSimpleBuffer.Available()) {
*countWritten = mSimpleBuffer.Read(buf, count);
MOZ_ASSERT(*countWritten);
LOG3(
("Http2StreamBase::OnWriteSegment read from flow control buffer %p %x " "%d\n", this, mStreamID, *countWritten)); return NS_OK;
}
// read from the network return mSegmentWriter->OnWriteSegment(buf, count, countWritten);
}
nsresult Http2StreamBase::Finish0RTT(bool aRestart, bool aAlpnChanged) {
MOZ_ASSERT(Transaction());
mAttempting0RTT = false; // Instead of passing (aRestart, aAlpnChanged) here, we use aAlpnChanged for // both arguments because as long as the alpn token stayed the same, we can // just reuse what we have in our buffer to send instead of having to have // the transaction rewind and read it all over again. We only need to rewind // the transaction if we're switching to a new protocol, because our buffer // won't get used in that case. // .. // however, we send in the aRestart value to indicate that early data failed // for devtools purposes
nsresult rv = NS_OK;
nsAHttpTransaction* trans = Transaction(); if (trans) {
rv = trans->Finish0RTT(aAlpnChanged, aAlpnChanged); if (aRestart) {
nsHttpTransaction* hTrans = trans->QueryHttpTransaction(); if (hTrans) {
hTrans->Refused0RTT();
}
}
} return rv;
}
nsresult Http2StreamBase::GetOriginAttributes(mozilla::OriginAttributes* oa) { if (!mSocketTransport) { return NS_ERROR_UNEXPECTED;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.