Skip to content

Commit

Permalink
cool#10630 doc electronic sign: send the hash on the server
Browse files Browse the repository at this point in the history
I missed this so far, but
<https://docs.eideasy.com/guide/api-credentials.html> suggests that the
the API calls the require a secret (and not just the client ID) has to
be performed on the server, so the secret can be kept there.

Add a new .uno:PrepareSignature UNO command that performs the API call
on the server and generates a command result; so the new setup is quite
similar to the old fetch() API which did a HTTP request in the browser
and also had a callback.

This requires adapting the cypress test that used to intercept network
calls via cy.intercept(). Now we want to intercept traffic on the
websocket instead. This seems to be possible by using
<https://docs.cypress.io/api/commands/fixture> to load test data, then
<https://docs.cypress.io/api/commands/stub> to intercept sending a
specific UNO command, finally using the underlying
<https://sinonjs.org/releases/latest/stubs/#stubcallthrough> to leave
the rest of the UNO commands unchanged.

The other problematic "download signature" step is not yet changed here.

Signed-off-by: Miklos Vajna <[email protected]>
Change-Id: I84e7eee60fe9dc6e620cd71f8b85a086a0d08c4c
  • Loading branch information
vmiklos committed Dec 6, 2024
1 parent 4a18324 commit 735a0d1
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 44 deletions.
56 changes: 20 additions & 36 deletions browser/src/control/Control.ESignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ namespace cool {
commandValues: SignatureResponse;
}

export interface CommandResultResponse {
commandName: string;
success: boolean;
// Depends on the value of commandName
result: any;
}

export interface HashSendResponse {
doc_id: string;
available_methods: Array<string>;
Expand Down Expand Up @@ -100,13 +107,22 @@ namespace cool {
this.clientId = clientId;

app.map.on('commandvalues', this.onCommandValues.bind(this));
app.map.on('commandresult', this.onCommandResult.bind(this));
}

insert(): void {
// Step 1: extract the document hash.
app.socket.sendMessage('commandvalues command=.uno:Signature');
}

// Handles the result of dispatched UNO commands
onCommandResult(event: CommandResultResponse): void {
if (event.commandName == '.uno:PrepareSignature') {
const response = <HashSendResponse>event.result;
this.handleSendHashResponse(event.success, response);
}
}

// Handles the command values response for .uno:Signature
onCommandValues(event: CommandValuesResponse): void {
if (event.commandName != '.uno:Signature') {
Expand All @@ -121,14 +137,12 @@ namespace cool {
const digest = signatureResponse.digest;

// Step 2: send the hash, get a document ID.
const url = this.url + '/api/signatures/prepare-files-for-signing';
const redirectUrl = window.makeHttpUrl('/cool/signature');
const documentName = <HTMLInputElement>(
document.querySelector('#document-name-input')
);
const fileName = documentName.value;
const body = {
secret: this.secret,
client_id: this.clientId,
// Create a PKCS#7 binary signature
container_type: 'cades',
Expand All @@ -146,44 +160,14 @@ namespace cool {
// Automatic file download will not happen after signing
nodownload: true,
};
const headers = {
'Content-Type': 'application/json',
const args = {
body: body,
};
const request = new Request(url, {
method: 'POST',
body: JSON.stringify(body),
headers: headers,
});
window.fetch(request).then(
(response) => {
this.handleSendHashBytes(response);
},
(error) => {
app.console.log(
'failed to fetch /api/signatures/prepare-files-for-signing: ' +
error.message,
);
},
);
}

// Handles the 'send hash' response bytes
handleSendHashBytes(response: Response): void {
response.json().then(
(json) => {
this.handleSendHashJson(response.ok, json);
},
(error) => {
app.console.log(
'failed to parse response from /api/signatures/prepare-files-for-signing as JSON: ' +
error.message,
);
},
);
app.map.sendUnoCommand('.uno:PrepareSignature', args);
}

// Handles the 'send hash' response JSON
handleSendHashJson(ok: boolean, response: HashSendResponse): void {
handleSendHashResponse(ok: boolean, response: HashSendResponse): void {
if (!ok) {
app.console.log(
'/api/signatures/prepare-files-for-signing failed: ' +
Expand Down
2 changes: 1 addition & 1 deletion browser/src/control/Toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ L.Map.include({

var isAllowedInReadOnly = false;
var allowedCommands = ['.uno:Save', '.uno:WordCountDialog',
'.uno:Signature', '.uno:ShowResolvedAnnotations',
'.uno:Signature', '.uno:PrepareSignature', '.uno:ShowResolvedAnnotations',
'.uno:ToolbarMode?Mode:string=notebookbar_online.ui', '.uno:ToolbarMode?Mode:string=Default',
'.uno:ExportToEPUB', '.uno:ExportToPDF', '.uno:ExportDirectToPDF', '.uno:MoveKeepInsertMode', '.uno:ShowRuler'];
if (app.isCommentEditingAllowed()) {
Expand Down
23 changes: 16 additions & 7 deletions cypress_test/integration_tests/desktop/draw/esign_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@ describe(['tagdesktop'], 'Electronic sign operations.', function() {
// Given a document that can be signed:
helper.setupAndLoadDocument('draw/esign.pdf', /*isMultiUser=*/false, /*copyCertificates=*/true);

cy.intercept('POST', 'https://test.eideasy.com/api/signatures/prepare-files-for-signing',
{fixture : 'fixtures/eideasy-send-hash.json'}).as('sendHash');
let sendHashResult;
cy.fixture('fixtures/eideasy-send-hash.json').then((result) => {
sendHashResult = result;
});
cy.getFrameWindow().then(function(win) {
const sendUnoCommand = cy.stub(win.app.map, 'sendUnoCommand');
sendUnoCommand.withArgs('.uno:PrepareSignature').as('sendHash').callsFake((commandName, args) => {
expect(args.body.signature_redirect).to.satisfy(url => url.endsWith('/cool/signature'));
// File name is like esign-Create-an-electronic-signature--0wvs9.pdf
expect(args.body.files[0].fileName).to.match(/^esign.*pdf$/i);
win.app.map.fire('commandresult', {commandName: '.uno:PrepareSignature', success: true, result: sendHashResult});
});
// Call the original sendUnoCommand() for other commands
sendUnoCommand.callThrough();
});
cy.getFrameWindow()
.then(function(win) {
cy.stub(win, 'open').as('windowOpen');
Expand All @@ -20,11 +33,7 @@ describe(['tagdesktop'], 'Electronic sign operations.', function() {
// When signing that document:
cy.cGet('#menu-insert').click();
cy.cGet('#menu-insert-esignature').click();
cy.wait(['@sendHash']).then(interception => {
expect(interception.request.body.signature_redirect).to.satisfy(url => url.endsWith('/cool/signature'));
// File name is like esign-Create-an-electronic-signature--0wvs9.pdf
expect(interception.request.body.files[0].fileName).to.match(/^esign.*pdf$/i);
});
cy.get('@sendHash').should('be.called');
cy.cGet('#ESignatureDialog button#ok').click();
cy.get('@windowOpen').should('be.called');
const response = {
Expand Down
91 changes: 91 additions & 0 deletions wsd/ClientSession.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,90 @@ void ClientSession::onTileProcessed(TileWireId wireId)
LOG_INF("Tileprocessed message with an unknown wire-id '" << wireId << "' from session " << getId());
}

#if !MOBILEAPP
namespace
{
std::shared_ptr<http::Session> makePrepareSignatureSession(const std::shared_ptr<ClientSession> clientSession, const std::string& requestUrl)
{
// Create the session and set a finished callback
std::shared_ptr<http::Session> httpSession = http::Session::create(requestUrl);
if (!httpSession)
{
LOG_WRN("PrepareSignature: failed to create HTTP session");
return nullptr;
}

http::Session::FinishedCallback finishedCallback = [clientSession](const std::shared_ptr<http::Session>& session)
{
const std::shared_ptr<const http::Response> httpResponse = session->response();
Poco::JSON::Object::Ptr resultArguments = new Poco::JSON::Object();
resultArguments->set("commandName", ".uno:PrepareSignature");

bool ok = httpResponse->statusLine().statusCode() == http::StatusCode::OK;
resultArguments->set("success", ok);

const std::string& responseBody = httpResponse->getBody();
Poco::JSON::Object::Ptr responseBodyObject = new Poco::JSON::Object();
if (!JsonUtil::parseJSON(responseBody, responseBodyObject))
{
LOG_WRN("PrepareSignature: failed to parse response body as JSON");
return;
}
resultArguments->set("result", responseBodyObject);

std::ostringstream oss;
resultArguments->stringify(oss);
std::string result = "unocommandresult: " + oss.str();
clientSession->sendTextFrame(result);
};
httpSession->setFinishedHandler(std::move(finishedCallback));
return httpSession;
}
}

bool ClientSession::handlePrepareSignature(const StringVector& tokens)
{
// Make the HTTP session: this requires an URL
Poco::JSON::Object::Ptr userPrivateInfoObject = new Poco::JSON::Object();
if (!JsonUtil::parseJSON(getUserPrivateInfo(), userPrivateInfoObject))
{
LOG_WRN("PrepareSignature: failed to parse user private info as JSON");
return false;
}
std::string requestUrl;
JsonUtil::findJSONValue(userPrivateInfoObject, "ESignatureBaseUrl", requestUrl);
requestUrl += "/api/signatures/prepare-files-for-signing";
std::shared_ptr<http::Session> httpSession = makePrepareSignatureSession(client_from_this(), requestUrl);
if (!httpSession)
{
return false;
}

// Make the request: this requires a JSON body, where we set the secret
std::string commandArguments = tokens.cat(' ', 2);
Poco::JSON::Object::Ptr commandArgumentsObject;
if (!JsonUtil::parseJSON(commandArguments, commandArgumentsObject))
{
LOG_WRN("PrepareSignature: failed to parse arguments as JSON");
return false;
}
auto requestBodyObject = commandArgumentsObject->get("body").extract<Poco::JSON::Object::Ptr>();
std::string secret;
JsonUtil::findJSONValue(userPrivateInfoObject, "ESignatureSecret", secret);
requestBodyObject->set("secret", secret);
std::string requestBody;
std::stringstream oss;
requestBodyObject->stringify(oss);
requestBody = oss.str();
http::Request httpRequest(Poco::URI(requestUrl).getPathAndQuery());
httpRequest.setVerb(http::Request::VERB_POST);
httpRequest.setBody(requestBody, "application/json");
std::shared_ptr<DocumentBroker> docBroker = getDocumentBroker();
httpSession->asyncRequest(httpRequest, docBroker->getPoll());
return true;
}
#endif

bool ClientSession::_handleInput(const char *buffer, int length)
{
LOG_TRC("handling incoming [" << getAbbreviatedMessage(buffer, length) << ']');
Expand Down Expand Up @@ -1200,6 +1284,13 @@ bool ClientSession::_handleInput(const char *buffer, int length)
tokens.equals(0, "geta11ycaretposition") ||
tokens.equals(0, "getpresentationinfo"))
{
#if !MOBILEAPP
if (tokens.equals(0, "uno") && tokens.equals(1, ".uno:PrepareSignature"))
{
return handlePrepareSignature(tokens);
}
#endif

if (tokens.equals(0, "key"))
_keyEvents++;

Expand Down
2 changes: 2 additions & 0 deletions wsd/ClientSession.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ class ClientSession final : public Session

virtual bool _handleInput(const char* buffer, int length) override;

bool handlePrepareSignature(const StringVector& tokens);

bool loadDocument(const char* buffer, int length, const StringVector& tokens,
const std::shared_ptr<DocumentBroker>& docBroker);
bool getStatus(const char* buffer, int length,
Expand Down

0 comments on commit 735a0d1

Please sign in to comment.