Commit 61a0f661 authored by Kunihiko Sakamoto's avatar Kunihiko Sakamoto Committed by Commit Bot

Extract signed exchange contents from CBOR

This patch teaches SignedExchangeHandler to decode signed exchange
body encoded with the application/http-exchange+cbor format [1].

The test file origin-signed-response-iframe.htxg was created using
gen-signedexchange of [2] with the following command:

gen-signedexchange -uri https://example.com/test.html \
    -content test.html -o origin-signed-response-iframe.htxg \
    -miRecordSize=100

where test.html is the html part of origin-signed-response-iframe.php
before this patch.

[1] https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#rfc.section.5
[2] https://github.com/WICG/webpackage/pull/119

Bug: 803774
Change-Id: I4d7bf5215e825cfa084c431db6d2cc87219d0a34
Reviewed-on: https://chromium-review.googlesource.com/897172
Commit-Queue: Kunihiko Sakamoto <ksakamoto@chromium.org>
Reviewed-by: default avatarKouhei Ueno <kouhei@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Cr-Commit-Position: refs/heads/master@{#534643}
parent 5fd5ad14
...@@ -5,126 +5,253 @@ ...@@ -5,126 +5,253 @@
#include "content/browser/loader/signed_exchange_handler.h" #include "content/browser/loader/signed_exchange_handler.h"
#include "base/feature_list.h" #include "base/feature_list.h"
#include "components/cbor/cbor_reader.h"
#include "content/browser/loader/merkle_integrity_source_stream.h"
#include "content/public/common/content_features.h" #include "content/public/common/content_features.h"
#include "mojo/public/cpp/system/string_data_pipe_producer.h" #include "mojo/public/cpp/system/string_data_pipe_producer.h"
#include "net/base/io_buffer.h" #include "net/base/io_buffer.h"
#include "net/filter/source_stream.h"
#include "net/http/http_response_headers.h" #include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "services/network/public/cpp/resource_response.h" #include "services/network/public/cpp/resource_response.h"
#include "services/network/public/cpp/url_loader_completion_status.h" #include "services/network/public/cpp/url_loader_completion_status.h"
namespace content { namespace content {
namespace {
constexpr size_t kBufferSizeForRead = 65536;
// Field names defined in the application/http-exchange+cbor content type:
// https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#rfc.section.5
constexpr char kHtxg[] = "htxg";
constexpr char kRequest[] = "request";
constexpr char kResponse[] = "response";
constexpr char kPayload[] = "payload";
constexpr char kUrlKey[] = ":url";
constexpr char kMethodKey[] = ":method";
constexpr char kStatusKey[] = ":status";
constexpr char kMiHeader[] = "MI";
cbor::CBORValue BytestringFromString(base::StringPiece in_string) {
return cbor::CBORValue(
std::vector<uint8_t>(in_string.begin(), in_string.end()));
}
bool IsStringEqualTo(const cbor::CBORValue& value, const char* str) {
return value.is_string() && value.GetString() == str;
}
// TODO(https://crbug.com/803774): Just for now, remove once we have streaming
// CBOR parser.
class BufferSourceStream : public net::SourceStream {
public:
BufferSourceStream(const std::vector<uint8_t>& bytes)
: net::SourceStream(SourceStream::TYPE_NONE), buf_(bytes), ptr_(0u) {}
int Read(net::IOBuffer* dest_buffer,
int buffer_size,
const net::CompletionCallback& callback) override {
int bytes = std::min(static_cast<int>(buf_.size() - ptr_), buffer_size);
memcpy(dest_buffer->data(), &buf_[ptr_], bytes);
ptr_ += bytes;
return bytes;
}
std::string Description() const override { return "buffer"; }
private:
std::vector<uint8_t> buf_;
size_t ptr_;
};
} // namespace
SignedExchangeHandler::SignedExchangeHandler( SignedExchangeHandler::SignedExchangeHandler(
std::unique_ptr<net::SourceStream> upstream, std::unique_ptr<net::SourceStream> body,
ExchangeHeadersCallback headers_callback) ExchangeHeadersCallback headers_callback)
: net::FilterSourceStream(net::SourceStream::TYPE_NONE, : headers_callback_(std::move(headers_callback)),
std::move(upstream)), source_(std::move(body)),
headers_callback_(std::move(headers_callback)),
weak_factory_(this) { weak_factory_(this) {
DCHECK(base::FeatureList::IsEnabled(features::kSignedHTTPExchange)); DCHECK(base::FeatureList::IsEnabled(features::kSignedHTTPExchange));
// Triggering the first read (asynchronously) for header parsing. // Triggering the first read (asynchronously) for CBOR parsing.
header_out_buf_ = base::MakeRefCounted<net::IOBufferWithSize>(1); read_buf_ = base::MakeRefCounted<net::IOBufferWithSize>(kBufferSizeForRead);
base::SequencedTaskRunnerHandle::Get()->PostTask( base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&SignedExchangeHandler::DoHeaderLoop, FROM_HERE, base::BindOnce(&SignedExchangeHandler::ReadLoop,
weak_factory_.GetWeakPtr())); weak_factory_.GetWeakPtr()));
} }
SignedExchangeHandler::~SignedExchangeHandler() = default; SignedExchangeHandler::~SignedExchangeHandler() = default;
int SignedExchangeHandler::FilterData(net::IOBuffer* output_buffer, void SignedExchangeHandler::ReadLoop() {
int output_buffer_size,
net::IOBuffer* input_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_eof_reached) {
*consumed_bytes = 0;
original_body_string_.append(input_buffer->data(), input_buffer_size);
*consumed_bytes += input_buffer_size;
// We shouldn't write any data into the out buffer while we're
// parsing the header.
if (headers_callback_)
return 0;
if (upstream_eof_reached) {
// Run the parser code if this part is run for the first time.
// Now original_body_string_ has the entire body.
// (Note that we may come here multiple times if output_buffer_size
// is not big enough.
// TODO(https://crbug.com/803774): Do the streaming instead.
size_t size_to_copy =
std::min(original_body_string_.size() - body_string_offset_,
base::checked_cast<size_t>(output_buffer_size));
memcpy(output_buffer->data(),
original_body_string_.data() + body_string_offset_, size_to_copy);
body_string_offset_ += size_to_copy;
return base::checked_cast<int>(size_to_copy);
}
return 0;
}
std::string SignedExchangeHandler::GetTypeAsString() const {
return "HTXG"; // Tentative.
}
void SignedExchangeHandler::DoHeaderLoop() {
// Run the internal read loop by ourselves until we finish
// parsing the headers. (After that the caller should pumb
// the Read() calls).
DCHECK(headers_callback_); DCHECK(headers_callback_);
DCHECK(header_out_buf_); DCHECK(read_buf_);
int rv = Read(header_out_buf_.get(), header_out_buf_->size(), int rv = source_->Read(
base::BindRepeating(&SignedExchangeHandler::DidReadForHeaders, read_buf_.get(), read_buf_->size(),
base::Unretained(this), false /* sync */)); base::BindRepeating(&SignedExchangeHandler::DidRead,
base::Unretained(this), false /* sync */));
if (rv != net::ERR_IO_PENDING) if (rv != net::ERR_IO_PENDING)
DidReadForHeaders(true /* sync */, rv); DidRead(true /* sync */, rv);
} }
void SignedExchangeHandler::DidReadForHeaders(bool completed_syncly, void SignedExchangeHandler::DidRead(bool completed_syncly, int result) {
int result) { if (result < 0) {
if (MaybeRunHeadersCallback() || result < 0) DVLOG(1) << "Error reading body stream: " << result;
RunErrorCallback(static_cast<net::Error>(result));
return;
}
if (result == 0) {
if (!RunHeadersCallback())
RunErrorCallback(net::ERR_FAILED);
return; return;
DCHECK_EQ(0, result); }
original_body_string_.append(read_buf_->data(), result);
if (completed_syncly) { if (completed_syncly) {
base::SequencedTaskRunnerHandle::Get()->PostTask( base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&SignedExchangeHandler::DoHeaderLoop, FROM_HERE, base::BindOnce(&SignedExchangeHandler::ReadLoop,
weak_factory_.GetWeakPtr())); weak_factory_.GetWeakPtr()));
} else { } else {
DoHeaderLoop(); ReadLoop();
} }
} }
bool SignedExchangeHandler::MaybeRunHeadersCallback() { bool SignedExchangeHandler::RunHeadersCallback() {
if (!headers_callback_) DCHECK(headers_callback_);
cbor::CBORReader::DecoderError error;
base::Optional<cbor::CBORValue> root = cbor::CBORReader::Read(
base::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(original_body_string_.data()),
original_body_string_.size()),
&error);
if (!root) {
DVLOG(1) << "CBOR parsing failed: "
<< cbor::CBORReader::ErrorCodeToString(error);
return false; return false;
}
original_body_string_.clear();
// If this was the first read, fire the headers callback now. if (!root->is_array()) {
// TODO(https://crbug.com/803774): This is just for testing, we should DVLOG(1) << "CBOR root is not an array";
// implement the CBOR parsing here. return false;
FillMockExchangeHeaders(); }
std::move(headers_callback_) const auto& root_array = root->GetArray();
.Run(request_url_, request_method_, response_head_, ssl_info_); if (!IsStringEqualTo(root_array[0], kHtxg)) {
DVLOG(1) << "CBOR has no htxg signature";
return false;
}
if (!IsStringEqualTo(root_array[1], kRequest)) {
DVLOG(1) << "request field not found";
return false;
}
if (!root_array[2].is_map()) {
DVLOG(1) << "request field is not a map";
return false;
}
const auto& request_map = root_array[2].GetMap();
// TODO(https://crbug.com/803774): request payload may come here.
if (!IsStringEqualTo(root_array[3], kResponse)) {
DVLOG(1) << "response field not found";
return false;
}
if (!root_array[4].is_map()) {
DVLOG(1) << "response field is not a map";
return false;
}
const auto& response_map = root_array[4].GetMap();
if (!IsStringEqualTo(root_array[5], kPayload)) {
DVLOG(1) << "payload field not found";
return false;
}
if (!root_array[6].is_bytestring()) {
DVLOG(1) << "payload field is not a bytestring";
return false;
}
const auto& payload_bytes = root_array[6].GetBytestring();
auto url_iter = request_map.find(BytestringFromString(kUrlKey));
if (url_iter == request_map.end() || !url_iter->second.is_bytestring()) {
DVLOG(1) << ":url is not found or not a bytestring";
return false;
}
request_url_ = GURL(url_iter->second.GetBytestringAsString());
// TODO(https://crbug.com/803774) Consume the bytes size that were auto method_iter = request_map.find(BytestringFromString(kMethodKey));
// necessary to read out the headers. if (method_iter == request_map.end() ||
!method_iter->second.is_bytestring()) {
DVLOG(1) << ":method is not found or not a bytestring";
return false;
}
request_method_ = std::string(method_iter->second.GetBytestringAsString());
auto status_iter = response_map.find(BytestringFromString(kStatusKey));
if (status_iter == response_map.end() ||
!status_iter->second.is_bytestring()) {
DVLOG(1) << ":status is not found or not a bytestring";
return false;
}
base::StringPiece status_code_str =
status_iter->second.GetBytestringAsString();
std::string fake_header_str("HTTP/1.1 ");
status_code_str.AppendToString(&fake_header_str);
fake_header_str.append(" OK\r\n");
for (const auto& it : response_map) {
if (!it.first.is_bytestring() || !it.second.is_bytestring()) {
DVLOG(1) << "Non-bytestring value in the response map";
return false;
}
base::StringPiece name = it.first.GetBytestringAsString();
base::StringPiece value = it.second.GetBytestringAsString();
if (name == kMethodKey)
continue;
name.AppendToString(&fake_header_str);
fake_header_str.append(": ");
value.AppendToString(&fake_header_str);
fake_header_str.append("\r\n");
}
fake_header_str.append("\r\n");
response_head_.headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(fake_header_str.c_str(),
fake_header_str.size()));
// TODO(https://crbug.com/803774): |mime_type| should be derived from
// "Content-Type" header.
response_head_.mime_type = "text/html";
// TODO(https://crbug.com/803774): Check that the Signature header entry has
// integrity="mi".
std::string mi_header_value;
if (!response_head_.headers->EnumerateHeader(nullptr, kMiHeader,
&mi_header_value)) {
DVLOG(1) << "Signed exchange has no MI: header";
return false;
}
auto payload_stream = std::make_unique<BufferSourceStream>(payload_bytes);
auto mi_stream = std::make_unique<MerkleIntegritySourceStream>(
mi_header_value, std::move(payload_stream));
std::move(headers_callback_)
.Run(net::OK, request_url_, request_method_, response_head_,
std::move(mi_stream), ssl_info_);
return true; return true;
} }
void SignedExchangeHandler::FillMockExchangeHeaders() { void SignedExchangeHandler::RunErrorCallback(net::Error error) {
// TODO(https://crbug.com/803774): Get the request url by parsing CBOR format. DCHECK(headers_callback_);
request_url_ = GURL("https://example.com/test.html"); std::move(headers_callback_)
// TODO(https://crbug.com/803774): Get the request method by parsing CBOR .Run(error, GURL(), std::string(), network::ResourceResponseHead(),
// format. nullptr, base::nullopt);
request_method_ = "GET";
// TODO(https://crbug.com/803774): Get more headers by parsing CBOR.
scoped_refptr<net::HttpResponseHeaders> headers(
new net::HttpResponseHeaders("HTTP/1.1 200 OK"));
response_head_.headers = headers;
response_head_.mime_type = "text/html";
} }
} // namespace content } // namespace content
...@@ -11,51 +11,42 @@ ...@@ -11,51 +11,42 @@
#include "base/optional.h" #include "base/optional.h"
#include "mojo/public/cpp/system/data_pipe.h" #include "mojo/public/cpp/system/data_pipe.h"
#include "net/base/completion_callback.h" #include "net/base/completion_callback.h"
#include "net/filter/filter_source_stream.h"
#include "net/ssl/ssl_info.h" #include "net/ssl/ssl_info.h"
#include "services/network/public/cpp/resource_response.h" #include "services/network/public/cpp/resource_response.h"
#include "url/gurl.h" #include "url/gurl.h"
namespace net {
class SourceStream;
}
namespace content { namespace content {
// IMPORTANT: Currenly SignedExchangeHandler doesn't implement any CBOR parsing // IMPORTANT: Currenly SignedExchangeHandler doesn't implement any verifying
// logic nor verifying logic. It just behaves as if the passed body is a signed
// HTTP exchange which contains a request to "https://example.com/test.html" and
// a response with a payload which is equal to the original body.
// TODO(https://crbug.com/803774): Implement CBOR parsing logic and verifying
// logic. // logic.
class SignedExchangeHandler final : public net::FilterSourceStream { // TODO(https://crbug.com/803774): Implement verifying logic.
class SignedExchangeHandler final {
public: public:
// TODO(https://crbug.com/803774): Add verification status here. // TODO(https://crbug.com/803774): Add verification status here.
using ExchangeHeadersCallback = using ExchangeHeadersCallback =
base::OnceCallback<void(const GURL& request_url, base::OnceCallback<void(net::Error error,
const GURL& request_url,
const std::string& request_method, const std::string& request_method,
const network::ResourceResponseHead&, const network::ResourceResponseHead&,
std::unique_ptr<net::SourceStream> payload_stream,
base::Optional<net::SSLInfo>)>; base::Optional<net::SSLInfo>)>;
// Once constructed |this| starts reading the |body| and parses the response // Once constructed |this| starts reading the |body| and parses the response
// as a signed HTTP exchange. The response body of the exchange can be read // as a signed HTTP exchange. The response body of the exchange can be read
// from |this| as a net::SourceStream after |headers_callback| is called. // from |payload_stream| passed to |headers_callback|.
SignedExchangeHandler(std::unique_ptr<net::SourceStream> body, SignedExchangeHandler(std::unique_ptr<net::SourceStream> body,
ExchangeHeadersCallback headers_callback); ExchangeHeadersCallback headers_callback);
~SignedExchangeHandler() override; ~SignedExchangeHandler();
// net::FilterSourceStream:
int FilterData(net::IOBuffer* output_buffer,
int output_buffer_size,
net::IOBuffer* input_buffer,
int input_buffer_size,
int* consumed_bytes,
bool upstream_eof_reached) override;
std::string GetTypeAsString() const override;
private: private:
void DoHeaderLoop(); void ReadLoop();
void DidReadForHeaders(bool completed_syncly, int result); void DidRead(bool completed_syncly, int result);
bool MaybeRunHeadersCallback(); bool RunHeadersCallback();
void RunErrorCallback(net::Error);
// TODO(https://crbug.com/803774): Remove this.
void FillMockExchangeHeaders();
// Signed exchange contents. // Signed exchange contents.
GURL request_url_; GURL request_url_;
...@@ -64,16 +55,12 @@ class SignedExchangeHandler final : public net::FilterSourceStream { ...@@ -64,16 +55,12 @@ class SignedExchangeHandler final : public net::FilterSourceStream {
base::Optional<net::SSLInfo> ssl_info_; base::Optional<net::SSLInfo> ssl_info_;
ExchangeHeadersCallback headers_callback_; ExchangeHeadersCallback headers_callback_;
std::unique_ptr<net::SourceStream> source_;
// Internal IOBuffer used during reading the header. Note that during parsing
// the header we don't really need the output buffer, but we still need to
// give some > 0 buffer.
scoped_refptr<net::IOBufferWithSize> header_out_buf_;
// TODO(https://crbug.cxom/803774): Just for now. Implement the streaming // TODO(https://crbug.cxom/803774): Just for now. Implement the streaming
// parser. // parser.
scoped_refptr<net::IOBufferWithSize> read_buf_;
std::string original_body_string_; std::string original_body_string_;
size_t body_string_offset_ = 0;
base::WeakPtrFactory<SignedExchangeHandler> weak_factory_; base::WeakPtrFactory<SignedExchangeHandler> weak_factory_;
......
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
namespace content { namespace content {
SourceStreamToDataPipe::SourceStreamToDataPipe( SourceStreamToDataPipe::SourceStreamToDataPipe(
net::SourceStream* source, std::unique_ptr<net::SourceStream> source,
mojo::ScopedDataPipeProducerHandle dest, mojo::ScopedDataPipeProducerHandle dest,
base::OnceCallback<void(int)> completion_callback) base::OnceCallback<void(int)> completion_callback)
: source_(source), : source_(std::move(source)),
dest_(std::move(dest)), dest_(std::move(dest)),
completion_callback_(std::move(completion_callback)), completion_callback_(std::move(completion_callback)),
writable_handle_watcher_(FROM_HERE, writable_handle_watcher_(FROM_HERE,
......
...@@ -22,9 +22,7 @@ namespace content { ...@@ -22,9 +22,7 @@ namespace content {
class SourceStreamToDataPipe { class SourceStreamToDataPipe {
public: public:
// Reads out the data from |source| and write into |dest|. // Reads out the data from |source| and write into |dest|.
// Note that this does not take the ownership of |source|, the caller SourceStreamToDataPipe(std::unique_ptr<net::SourceStream> source,
// needs to take care that it is kept alive.
SourceStreamToDataPipe(net::SourceStream* source,
mojo::ScopedDataPipeProducerHandle dest, mojo::ScopedDataPipeProducerHandle dest,
base::OnceCallback<void(int)> completion_callback); base::OnceCallback<void(int)> completion_callback);
~SourceStreamToDataPipe(); ~SourceStreamToDataPipe();
...@@ -40,7 +38,7 @@ class SourceStreamToDataPipe { ...@@ -40,7 +38,7 @@ class SourceStreamToDataPipe {
void OnDataPipeClosed(MojoResult result); void OnDataPipeClosed(MojoResult result);
void OnComplete(int result); void OnComplete(int result);
net::SourceStream* source_; // Not owned. std::unique_ptr<net::SourceStream> source_;
mojo::ScopedDataPipeProducerHandle dest_; mojo::ScopedDataPipeProducerHandle dest_;
base::OnceCallback<void(int)> completion_callback_; base::OnceCallback<void(int)> completion_callback_;
......
...@@ -190,10 +190,18 @@ void WebPackageLoader::ConnectToClient( ...@@ -190,10 +190,18 @@ void WebPackageLoader::ConnectToClient(
} }
void WebPackageLoader::OnHTTPExchangeFound( void WebPackageLoader::OnHTTPExchangeFound(
net::Error error,
const GURL& request_url, const GURL& request_url,
const std::string& request_method, const std::string& request_method,
const network::ResourceResponseHead& resource_response, const network::ResourceResponseHead& resource_response,
std::unique_ptr<net::SourceStream> payload_stream,
base::Optional<net::SSLInfo> ssl_info) { base::Optional<net::SSLInfo> ssl_info) {
if (error) {
// This will eventually delete |this|.
client_->OnComplete(network::URLLoaderCompletionStatus(error));
return;
}
// TODO(https://crbug.com/803774): Handle no-GET request_method as a error. // TODO(https://crbug.com/803774): Handle no-GET request_method as a error.
DCHECK(original_response_timing_info_); DCHECK(original_response_timing_info_);
forwarding_client_->OnReceiveRedirect( forwarding_client_->OnReceiveRedirect(
...@@ -213,7 +221,7 @@ void WebPackageLoader::OnHTTPExchangeFound( ...@@ -213,7 +221,7 @@ void WebPackageLoader::OnHTTPExchangeFound(
pending_body_consumer_ = std::move(data_pipe.consumer_handle); pending_body_consumer_ = std::move(data_pipe.consumer_handle);
body_data_pipe_adapter_ = std::make_unique<SourceStreamToDataPipe>( body_data_pipe_adapter_ = std::make_unique<SourceStreamToDataPipe>(
signed_exchange_handler_.get(), std::move(data_pipe.producer_handle), std::move(payload_stream), std::move(data_pipe.producer_handle),
base::BindOnce(&WebPackageLoader::FinishReadingBody, base::BindOnce(&WebPackageLoader::FinishReadingBody,
base::Unretained(this))); base::Unretained(this)));
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
#include "services/network/public/cpp/net_adapters.h" #include "services/network/public/cpp/net_adapters.h"
#include "services/network/public/interfaces/url_loader.mojom.h" #include "services/network/public/interfaces/url_loader.mojom.h"
namespace net {
class SourceStream;
} // namespace net
namespace content { namespace content {
class SignedExchangeHandler; class SignedExchangeHandler;
...@@ -66,9 +70,11 @@ class WebPackageLoader final : public network::mojom::URLLoaderClient, ...@@ -66,9 +70,11 @@ class WebPackageLoader final : public network::mojom::URLLoaderClient,
// Called from |signed_exchange_handler_| when it finds an origin-signed HTTP // Called from |signed_exchange_handler_| when it finds an origin-signed HTTP
// exchange. // exchange.
void OnHTTPExchangeFound( void OnHTTPExchangeFound(
net::Error error,
const GURL& request_url, const GURL& request_url,
const std::string& request_method, const std::string& request_method,
const network::ResourceResponseHead& resource_response, const network::ResourceResponseHead& resource_response,
std::unique_ptr<net::SourceStream> payload_stream,
base::Optional<net::SSLInfo> ssl_info); base::Optional<net::SSLInfo> ssl_info);
void FinishReadingBody(int result); void FinishReadingBody(int result);
......
...@@ -26,8 +26,6 @@ promise_test(function(t) { ...@@ -26,8 +26,6 @@ promise_test(function(t) {
return promise; return promise;
}) })
.then((event) => { .then((event) => {
// TODO(https://crbug.com/80374): Currently this URL is hard coded in
// SignedExchangeHandler.
assert_equals(event.data.location, 'https://example.com/test.html'); assert_equals(event.data.location, 'https://example.com/test.html');
}); });
}, 'Location of origin-signed HTTP response'); }, 'Location of origin-signed HTTP response');
......
<?php <?php
$fp = fopen("origin-signed-response-iframe.htxg", "rb");
header('Content-Type: application/http-exchange+cbor'); header('Content-Type: application/http-exchange+cbor');
header("HTTP/1.0 200 OK");
fpassthru($fp);
?> ?>
<!DOCTYPE html>
<body>
<script>
window.addEventListener('message', (event) => {
event.data.port.postMessage({location: document.location.href});
}, false);
</script>
hello<br>
world
</body>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment