HTTP_C: Implement SetClientCertContext, GetSSLError, BeginRequest, BeginRequestAsync (#4753)
* HTTP_C: Implement SetClientCertContext and GetSSLError; Stubbed BeginRequest and BeginRequestAsync * HTTP_C: Move logs to beginning of function calls
This commit is contained in:
parent
2706a4d658
commit
624271696e
2 changed files with 253 additions and 31 deletions
|
@ -43,6 +43,10 @@ const ResultCode ERROR_TOO_MANY_CLIENT_CERTS = // 0xD8A0A0CB
|
|||
ErrorLevel::Permanent);
|
||||
const ResultCode ERROR_WRONG_CERT_ID = // 0xD8E0B839
|
||||
ResultCode(57, ErrorModule::SSL, ErrorSummary::InvalidArgument, ErrorLevel::Permanent);
|
||||
const ResultCode ERROR_WRONG_CERT_HANDLE = // 0xD8A0A0C9
|
||||
ResultCode(201, ErrorModule::HTTP, ErrorSummary::InvalidState, ErrorLevel::Permanent);
|
||||
const ResultCode ERROR_CERT_ALREADY_SET = // 0xD8A0A03D
|
||||
ResultCode(61, ErrorModule::HTTP, ErrorSummary::InvalidState, ErrorLevel::Permanent);
|
||||
|
||||
void HTTP_C::Initialize(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx, 0x1, 1, 4);
|
||||
|
@ -79,6 +83,8 @@ void HTTP_C::InitializeConnectionSession(Kernel::HLERequestContext& ctx) {
|
|||
const Context::Handle context_handle = rp.Pop<u32>();
|
||||
u32 pid = rp.PopPID();
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, context_id={} pid={}", context_handle, pid);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
|
@ -105,7 +111,96 @@ void HTTP_C::InitializeConnectionSession(Kernel::HLERequestContext& ctx) {
|
|||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
LOG_DEBUG(Service_HTTP, "called, context_id={} pid={}", context_handle, pid);
|
||||
}
|
||||
|
||||
void HTTP_C::BeginRequest(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx, 0x9, 1, 0);
|
||||
const Context::Handle context_handle = rp.Pop<u32>();
|
||||
|
||||
LOG_WARNING(Service_HTTP, "(STUBBED) called, context_id={}", context_handle);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
if (!session_data->initialized) {
|
||||
LOG_ERROR(Service_HTTP, "Tried to make a request on an uninitialized session");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// This command can only be called with a bound context
|
||||
if (!session_data->current_http_context) {
|
||||
LOG_ERROR(Service_HTTP, "Tried to make a request without a bound context");
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ResultCode(ErrorDescription::NotImplemented, ErrorModule::HTTP,
|
||||
ErrorSummary::Internal, ErrorLevel::Permanent));
|
||||
return;
|
||||
}
|
||||
|
||||
if (session_data->current_http_context != context_handle) {
|
||||
LOG_ERROR(
|
||||
Service_HTTP,
|
||||
"Tried to make a request on a mismatched session input context={} session context={}",
|
||||
context_handle, *session_data->current_http_context);
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
auto itr = contexts.find(context_handle);
|
||||
ASSERT(itr != contexts.end());
|
||||
|
||||
// TODO(B3N30): Make the request
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
void HTTP_C::BeginRequestAsync(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx, 0xA, 1, 0);
|
||||
const Context::Handle context_handle = rp.Pop<u32>();
|
||||
|
||||
LOG_WARNING(Service_HTTP, "(STUBBED) called, context_id={}", context_handle);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
if (!session_data->initialized) {
|
||||
LOG_ERROR(Service_HTTP, "Tried to make a request on an uninitialized session");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// This command can only be called with a bound context
|
||||
if (!session_data->current_http_context) {
|
||||
LOG_ERROR(Service_HTTP, "Tried to make a request without a bound context");
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ResultCode(ErrorDescription::NotImplemented, ErrorModule::HTTP,
|
||||
ErrorSummary::Internal, ErrorLevel::Permanent));
|
||||
return;
|
||||
}
|
||||
|
||||
if (session_data->current_http_context != context_handle) {
|
||||
LOG_ERROR(
|
||||
Service_HTTP,
|
||||
"Tried to make a request on a mismatched session input context={} session context={}",
|
||||
context_handle, *session_data->current_http_context);
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
auto itr = contexts.find(context_handle);
|
||||
ASSERT(itr != contexts.end());
|
||||
|
||||
// TODO(B3N30): Make the request
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
void HTTP_C::CreateContext(Kernel::HLERequestContext& ctx) {
|
||||
|
@ -238,6 +333,9 @@ void HTTP_C::AddRequestHeader(Kernel::HLERequestContext& ctx) {
|
|||
std::string value(value_size - 1, '\0');
|
||||
value_buffer.Read(&value[0], 0, value_size - 1);
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}", name, value,
|
||||
context_handle);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
|
@ -294,9 +392,6 @@ void HTTP_C::AddRequestHeader(Kernel::HLERequestContext& ctx) {
|
|||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.PushMappedBuffer(value_buffer);
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}", name, value,
|
||||
context_handle);
|
||||
}
|
||||
|
||||
void HTTP_C::AddPostDataAscii(Kernel::HLERequestContext& ctx) {
|
||||
|
@ -314,6 +409,9 @@ void HTTP_C::AddPostDataAscii(Kernel::HLERequestContext& ctx) {
|
|||
std::string value(value_size - 1, '\0');
|
||||
value_buffer.Read(&value[0], 0, value_size - 1);
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}", name, value,
|
||||
context_handle);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
|
@ -369,9 +467,93 @@ void HTTP_C::AddPostDataAscii(Kernel::HLERequestContext& ctx) {
|
|||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 2);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.PushMappedBuffer(value_buffer);
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, name={}, value={}, context_handle={}", name, value,
|
||||
context_handle);
|
||||
void HTTP_C::SetClientCertContext(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx, 0x29, 2, 0);
|
||||
const u32 context_handle = rp.Pop<u32>();
|
||||
const u32 client_cert_handle = rp.Pop<u32>();
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called with context_handle={} client_cert_handle={}", context_handle,
|
||||
client_cert_handle);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
if (!session_data->initialized) {
|
||||
LOG_ERROR(Service_HTTP, "Tried to set client cert on an uninitialized session");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
// This command can only be called with a bound context
|
||||
if (!session_data->current_http_context) {
|
||||
LOG_ERROR(Service_HTTP, "Tried to set client cert without a bound context");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ResultCode(ErrorDescription::NotImplemented, ErrorModule::HTTP,
|
||||
ErrorSummary::Internal, ErrorLevel::Permanent));
|
||||
return;
|
||||
}
|
||||
|
||||
if (session_data->current_http_context != context_handle) {
|
||||
LOG_ERROR(Service_HTTP,
|
||||
"Tried to add set client cert on a mismatched session input context={} session "
|
||||
"context={}",
|
||||
context_handle, *session_data->current_http_context);
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_STATE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
auto http_context_itr = contexts.find(context_handle);
|
||||
ASSERT(http_context_itr != contexts.end());
|
||||
|
||||
auto cert_context_itr = client_certs.find(client_cert_handle);
|
||||
if (cert_context_itr == client_certs.end()) {
|
||||
LOG_ERROR(Service_HTTP, "called with wrong client_cert_handle {}", client_cert_handle);
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_WRONG_CERT_HANDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (http_context_itr->second.ssl_config.client_cert_ctx.lock()) {
|
||||
LOG_ERROR(Service_HTTP,
|
||||
"Tried to set a client cert to a context that already has a client cert");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ERROR_CERT_ALREADY_SET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (http_context_itr->second.state != RequestState::NotStarted) {
|
||||
LOG_ERROR(Service_HTTP,
|
||||
"Tried to set a client cert on a context that has already been started.");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(ResultCode(ErrCodes::InvalidRequestState, ErrorModule::HTTP,
|
||||
ErrorSummary::InvalidState, ErrorLevel::Permanent));
|
||||
return;
|
||||
}
|
||||
|
||||
http_context_itr->second.ssl_config.client_cert_ctx = cert_context_itr->second;
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
void HTTP_C::GetSSLError(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx, 0x2a, 2, 0);
|
||||
const u32 context_handle = rp.Pop<u32>();
|
||||
const u32 unk = rp.Pop<u32>();
|
||||
|
||||
LOG_WARNING(Service_HTTP, "(STUBBED) called, context_handle={}, unk={}", context_handle, unk);
|
||||
|
||||
auto http_context_itr = contexts.find(context_handle);
|
||||
ASSERT(http_context_itr != contexts.end());
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
// Since we create the actual http/ssl context only when the request is submitted we can't check
|
||||
// for SSL Errors here. Just submit no error.
|
||||
rb.Push<u32>(0);
|
||||
}
|
||||
|
||||
void HTTP_C::OpenClientCertContext(Kernel::HLERequestContext& ctx) {
|
||||
|
@ -381,6 +563,8 @@ void HTTP_C::OpenClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
Kernel::MappedBuffer& cert_buffer = rp.PopMappedBuffer();
|
||||
Kernel::MappedBuffer& key_buffer = rp.PopMappedBuffer();
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, cert_size {}, key_size {}", cert_size, key_size);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
|
@ -396,18 +580,17 @@ void HTTP_C::OpenClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
LOG_ERROR(Service_HTTP, "tried to load more then 2 client certs");
|
||||
result = ERROR_TOO_MANY_CLIENT_CERTS;
|
||||
} else {
|
||||
++client_certs_counter;
|
||||
client_certs[client_certs_counter].handle = client_certs_counter;
|
||||
client_certs[client_certs_counter].certificate.resize(cert_size);
|
||||
cert_buffer.Read(&client_certs[client_certs_counter].certificate[0], 0, cert_size);
|
||||
client_certs[client_certs_counter].private_key.resize(key_size);
|
||||
cert_buffer.Read(&client_certs[client_certs_counter].private_key[0], 0, key_size);
|
||||
client_certs[client_certs_counter].session_id = session_data->session_id;
|
||||
client_certs[++client_certs_counter] = std::make_shared<ClientCertContext>();
|
||||
client_certs[client_certs_counter]->handle = client_certs_counter;
|
||||
client_certs[client_certs_counter]->certificate.resize(cert_size);
|
||||
cert_buffer.Read(&client_certs[client_certs_counter]->certificate[0], 0, cert_size);
|
||||
client_certs[client_certs_counter]->private_key.resize(key_size);
|
||||
cert_buffer.Read(&client_certs[client_certs_counter]->private_key[0], 0, key_size);
|
||||
client_certs[client_certs_counter]->session_id = session_data->session_id;
|
||||
|
||||
++session_data->num_client_certs;
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, cert_size {}, key_size {}", cert_size, key_size);
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 4);
|
||||
rb.Push(result);
|
||||
rb.PushMappedBuffer(cert_buffer);
|
||||
|
@ -418,6 +601,8 @@ void HTTP_C::OpenDefaultClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
IPC::RequestParser rp(ctx, 0x33, 1, 0);
|
||||
u8 cert_id = rp.Pop<u8>();
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, cert_id={} cert_handle={}", cert_id, client_certs_counter);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
|
@ -459,8 +644,8 @@ void HTTP_C::OpenDefaultClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
|
||||
const auto& it = std::find_if(client_certs.begin(), client_certs.end(),
|
||||
[default_cert_id, &session_data](const auto& i) {
|
||||
return default_cert_id == i.second.cert_id &&
|
||||
session_data->session_id == i.second.session_id;
|
||||
return default_cert_id == i.second->cert_id &&
|
||||
session_data->session_id == i.second->session_id;
|
||||
});
|
||||
|
||||
if (it != client_certs.end()) {
|
||||
|
@ -472,24 +657,24 @@ void HTTP_C::OpenDefaultClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
return;
|
||||
}
|
||||
|
||||
++client_certs_counter;
|
||||
client_certs[client_certs_counter].handle = client_certs_counter;
|
||||
client_certs[client_certs_counter].certificate = ClCertA.certificate;
|
||||
client_certs[client_certs_counter].private_key = ClCertA.private_key;
|
||||
client_certs[client_certs_counter].session_id = session_data->session_id;
|
||||
client_certs[++client_certs_counter] = std::make_shared<ClientCertContext>();
|
||||
client_certs[client_certs_counter]->handle = client_certs_counter;
|
||||
client_certs[client_certs_counter]->certificate = ClCertA.certificate;
|
||||
client_certs[client_certs_counter]->private_key = ClCertA.private_key;
|
||||
client_certs[client_certs_counter]->session_id = session_data->session_id;
|
||||
++session_data->num_client_certs;
|
||||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(2, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
rb.Push<u32>(client_certs_counter);
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, cert_id={}", cert_id);
|
||||
}
|
||||
|
||||
void HTTP_C::CloseClientCertContext(Kernel::HLERequestContext& ctx) {
|
||||
IPC::RequestParser rp(ctx, 0x34, 1, 0);
|
||||
ClientCertContext::Handle cert_handle = rp.Pop<u32>();
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, cert_handle={}", cert_handle);
|
||||
|
||||
auto* session_data = GetSessionData(ctx.Session());
|
||||
ASSERT(session_data);
|
||||
|
||||
|
@ -501,7 +686,7 @@ void HTTP_C::CloseClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (client_certs[cert_handle].session_id != session_data->session_id) {
|
||||
if (client_certs[cert_handle]->session_id != session_data->session_id) {
|
||||
LOG_ERROR(Service_HTTP, "called from another main session");
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
// This just return success without doing anything
|
||||
|
@ -514,8 +699,6 @@ void HTTP_C::CloseClientCertContext(Kernel::HLERequestContext& ctx) {
|
|||
|
||||
IPC::RequestBuilder rb = rp.MakeBuilder(1, 0);
|
||||
rb.Push(RESULT_SUCCESS);
|
||||
|
||||
LOG_DEBUG(Service_HTTP, "called, cert_handle={}", cert_handle);
|
||||
}
|
||||
|
||||
void HTTP_C::Finalize(Kernel::HLERequestContext& ctx) {
|
||||
|
@ -614,8 +797,8 @@ HTTP_C::HTTP_C() : ServiceFramework("http:C", 32) {
|
|||
{0x00060040, nullptr, "GetDownloadSizeState"},
|
||||
{0x00070040, nullptr, "GetRequestError"},
|
||||
{0x00080042, &HTTP_C::InitializeConnectionSession, "InitializeConnectionSession"},
|
||||
{0x00090040, nullptr, "BeginRequest"},
|
||||
{0x000A0040, nullptr, "BeginRequestAsync"},
|
||||
{0x00090040, &HTTP_C::BeginRequest, "BeginRequest"},
|
||||
{0x000A0040, &HTTP_C::BeginRequestAsync, "BeginRequestAsync"},
|
||||
{0x000B0082, nullptr, "ReceiveData"},
|
||||
{0x000C0102, nullptr, "ReceiveDataTimeout"},
|
||||
{0x000D0146, nullptr, "SetProxy"},
|
||||
|
@ -645,8 +828,8 @@ HTTP_C::HTTP_C() : ServiceFramework("http:C", 32) {
|
|||
{0x00250080, nullptr, "AddDefaultCert"},
|
||||
{0x00260080, nullptr, "SelectRootCertChain"},
|
||||
{0x002700C4, nullptr, "SetClientCert"},
|
||||
{0x00290080, nullptr, "SetClientCertContext"},
|
||||
{0x002A0040, nullptr, "GetSSLError"},
|
||||
{0x00290080, &HTTP_C::SetClientCertContext, "SetClientCertContext"},
|
||||
{0x002A0040, &HTTP_C::GetSSLError, "GetSSLError"},
|
||||
{0x002B0080, nullptr, "SetSSLOpt"},
|
||||
{0x002C0080, nullptr, "SetSSLClearOpt"},
|
||||
{0x002D0000, nullptr, "CreateRootCertChain"},
|
||||
|
|
|
@ -193,6 +193,24 @@ private:
|
|||
*/
|
||||
void InitializeConnectionSession(Kernel::HLERequestContext& ctx);
|
||||
|
||||
/**
|
||||
* HTTP_C::BeginRequest service function
|
||||
* Inputs:
|
||||
* 1 : Context handle
|
||||
* Outputs:
|
||||
* 1 : Result of function, 0 on success, otherwise error code
|
||||
*/
|
||||
void BeginRequest(Kernel::HLERequestContext& ctx);
|
||||
|
||||
/**
|
||||
* HTTP_C::BeginRequestAsync service function
|
||||
* Inputs:
|
||||
* 1 : Context handle
|
||||
* Outputs:
|
||||
* 1 : Result of function, 0 on success, otherwise error code
|
||||
*/
|
||||
void BeginRequestAsync(Kernel::HLERequestContext& ctx);
|
||||
|
||||
/**
|
||||
* HTTP_C::AddRequestHeader service function
|
||||
* Inputs:
|
||||
|
@ -223,6 +241,27 @@ private:
|
|||
*/
|
||||
void AddPostDataAscii(Kernel::HLERequestContext& ctx);
|
||||
|
||||
/**
|
||||
* HTTP_C::SetClientCertContext service function
|
||||
* Inputs:
|
||||
* 1 : Context handle
|
||||
* 2 : Cert context handle
|
||||
* Outputs:
|
||||
* 1 : Result of function, 0 on success, otherwise error code
|
||||
*/
|
||||
void SetClientCertContext(Kernel::HLERequestContext& ctx);
|
||||
|
||||
/**
|
||||
* HTTP_C::GetSSLError service function
|
||||
* Inputs:
|
||||
* 1 : Context handle
|
||||
* 2 : Unknown
|
||||
* Outputs:
|
||||
* 1 : Result of function, 0 on success, otherwise error code
|
||||
* 2 : SSL Error code
|
||||
*/
|
||||
void GetSSLError(Kernel::HLERequestContext& ctx);
|
||||
|
||||
/**
|
||||
* HTTP_C::OpenClientCertContext service function
|
||||
* Inputs:
|
||||
|
@ -280,7 +319,7 @@ private:
|
|||
std::unordered_map<Context::Handle, Context> contexts;
|
||||
|
||||
/// Global list of ClientCert contexts currently opened.
|
||||
std::unordered_map<ClientCertContext::Handle, ClientCertContext> client_certs;
|
||||
std::unordered_map<ClientCertContext::Handle, std::shared_ptr<ClientCertContext>> client_certs;
|
||||
|
||||
struct {
|
||||
std::vector<u8> certificate;
|
||||
|
|
Loading…
Reference in a new issue