Skip to content

Commit 86a9090

Browse files
committed
cool#10630 doc electronic sign: send the hash on the server
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
1 parent 97f563f commit 86a9090

File tree

5 files changed

+126
-44
lines changed

5 files changed

+126
-44
lines changed

browser/src/control/Control.ESignature.ts

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ namespace cool {
1919
commandValues: SignatureResponse;
2020
}
2121

22+
export interface CommandResultResponse {
23+
commandName: string;
24+
success: boolean;
25+
// Depends on the value of commandName
26+
result: any;
27+
}
28+
2229
export interface HashSendResponse {
2330
doc_id: string;
2431
available_methods: Array<string>;
@@ -100,13 +107,22 @@ namespace cool {
100107
this.clientId = clientId;
101108

102109
app.map.on('commandvalues', this.onCommandValues.bind(this));
110+
app.map.on('commandresult', this.onCommandResult.bind(this));
103111
}
104112

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

118+
// Handles the result of dispatched UNO commands
119+
onCommandResult(event: CommandResultResponse): void {
120+
if (event.commandName == '.uno:PrepareSignature') {
121+
const response = <HashSendResponse>event.result;
122+
this.handleSendHashResponse(event.success, response);
123+
}
124+
}
125+
110126
// Handles the command values response for .uno:Signature
111127
onCommandValues(event: CommandValuesResponse): void {
112128
if (event.commandName != '.uno:Signature') {
@@ -121,14 +137,12 @@ namespace cool {
121137
const digest = signatureResponse.digest;
122138

123139
// Step 2: send the hash, get a document ID.
124-
const url = this.url + '/api/signatures/prepare-files-for-signing';
125140
const redirectUrl = window.makeHttpUrl('/cool/signature');
126141
const documentName = <HTMLInputElement>(
127142
document.querySelector('#document-name-input')
128143
);
129144
const fileName = documentName.value;
130145
const body = {
131-
secret: this.secret,
132146
client_id: this.clientId,
133147
// Create a PKCS#7 binary signature
134148
container_type: 'cades',
@@ -146,44 +160,14 @@ namespace cool {
146160
// Automatic file download will not happen after signing
147161
nodownload: true,
148162
};
149-
const headers = {
150-
'Content-Type': 'application/json',
163+
const args = {
164+
body: body,
151165
};
152-
const request = new Request(url, {
153-
method: 'POST',
154-
body: JSON.stringify(body),
155-
headers: headers,
156-
});
157-
window.fetch(request).then(
158-
(response) => {
159-
this.handleSendHashBytes(response);
160-
},
161-
(error) => {
162-
app.console.log(
163-
'failed to fetch /api/signatures/prepare-files-for-signing: ' +
164-
error.message,
165-
);
166-
},
167-
);
168-
}
169-
170-
// Handles the 'send hash' response bytes
171-
handleSendHashBytes(response: Response): void {
172-
response.json().then(
173-
(json) => {
174-
this.handleSendHashJson(response.ok, json);
175-
},
176-
(error) => {
177-
app.console.log(
178-
'failed to parse response from /api/signatures/prepare-files-for-signing as JSON: ' +
179-
error.message,
180-
);
181-
},
182-
);
166+
app.map.sendUnoCommand('.uno:PrepareSignature', args);
183167
}
184168

185169
// Handles the 'send hash' response JSON
186-
handleSendHashJson(ok: boolean, response: HashSendResponse): void {
170+
handleSendHashResponse(ok: boolean, response: HashSendResponse): void {
187171
if (!ok) {
188172
app.console.log(
189173
'/api/signatures/prepare-files-for-signing failed: ' +

browser/src/control/Toolbar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ L.Map.include({
378378

379379
var isAllowedInReadOnly = false;
380380
var allowedCommands = ['.uno:Save', '.uno:WordCountDialog',
381-
'.uno:Signature', '.uno:ShowResolvedAnnotations',
381+
'.uno:Signature', '.uno:PrepareSignature', '.uno:ShowResolvedAnnotations',
382382
'.uno:ToolbarMode?Mode:string=notebookbar_online.ui', '.uno:ToolbarMode?Mode:string=Default',
383383
'.uno:ExportToEPUB', '.uno:ExportToPDF', '.uno:ExportDirectToPDF', '.uno:MoveKeepInsertMode', '.uno:ShowRuler'];
384384
if (app.isCommentEditingAllowed()) {

cypress_test/integration_tests/desktop/draw/esign_spec.js

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,21 @@ describe(['tagdesktop'], 'Electronic sign operations.', function() {
88
// Given a document that can be signed:
99
helper.setupAndLoadDocument('draw/esign.pdf', /*isMultiUser=*/false, /*copyCertificates=*/true);
1010

11-
cy.intercept('POST', 'https://test.eideasy.com/api/signatures/prepare-files-for-signing',
12-
{fixture : 'fixtures/eideasy-send-hash.json'}).as('sendHash');
11+
let sendHashResult;
12+
cy.fixture('fixtures/eideasy-send-hash.json').then((result) => {
13+
sendHashResult = result;
14+
});
15+
cy.getFrameWindow().then(function(win) {
16+
const sendUnoCommand = cy.stub(win.app.map, 'sendUnoCommand');
17+
sendUnoCommand.withArgs('.uno:PrepareSignature').as('sendHash').callsFake((commandName, args) => {
18+
expect(args.body.signature_redirect).to.satisfy(url => url.endsWith('/cool/signature'));
19+
// File name is like esign-Create-an-electronic-signature--0wvs9.pdf
20+
expect(args.body.files[0].fileName).to.match(/^esign.*pdf$/i);
21+
win.app.map.fire('commandresult', {commandName: '.uno:PrepareSignature', success: true, result: sendHashResult});
22+
});
23+
// Call the original sendUnoCommand() for other commands
24+
sendUnoCommand.callThrough();
25+
});
1326
cy.getFrameWindow()
1427
.then(function(win) {
1528
cy.stub(win, 'open').as('windowOpen');
@@ -20,11 +33,7 @@ describe(['tagdesktop'], 'Electronic sign operations.', function() {
2033
// When signing that document:
2134
cy.cGet('#menu-insert').click();
2235
cy.cGet('#menu-insert-esignature').click();
23-
cy.wait(['@sendHash']).then(interception => {
24-
expect(interception.request.body.signature_redirect).to.satisfy(url => url.endsWith('/cool/signature'));
25-
// File name is like esign-Create-an-electronic-signature--0wvs9.pdf
26-
expect(interception.request.body.files[0].fileName).to.match(/^esign.*pdf$/i);
27-
});
36+
cy.get('@sendHash').should('be.called');
2837
cy.cGet('#ESignatureDialog button#ok').click();
2938
cy.get('@windowOpen').should('be.called');
3039
const response = {

wsd/ClientSession.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,88 @@ void ClientSession::onTileProcessed(TileWireId wireId)
428428
LOG_INF("Tileprocessed message with an unknown wire-id '" << wireId << "' from session " << getId());
429429
}
430430

431+
namespace
432+
{
433+
std::shared_ptr<http::Session> makePrepareSignatureSession(const std::shared_ptr<ClientSession> clientSession, const std::string& requestUrl)
434+
{
435+
// Create the session and set a finished callback
436+
std::shared_ptr<http::Session> httpSession = http::Session::create(requestUrl);
437+
if (!httpSession)
438+
{
439+
LOG_WRN("PrepareSignature: failed to create HTTP session");
440+
return nullptr;
441+
}
442+
443+
http::Session::FinishedCallback finishedCallback = [clientSession](const std::shared_ptr<http::Session>& session)
444+
{
445+
const std::shared_ptr<const http::Response> httpResponse = session->response();
446+
Poco::JSON::Object::Ptr resultArguments = new Poco::JSON::Object();
447+
resultArguments->set("commandName", ".uno:PrepareSignature");
448+
449+
bool ok = httpResponse->statusLine().statusCode() == http::StatusCode::OK;
450+
resultArguments->set("success", ok);
451+
452+
const std::string& responseBody = httpResponse->getBody();
453+
Poco::JSON::Object::Ptr responseBodyObject = new Poco::JSON::Object();
454+
if (!JsonUtil::parseJSON(responseBody, responseBodyObject))
455+
{
456+
LOG_WRN("PrepareSignature: failed to parse response body as JSON");
457+
return;
458+
}
459+
resultArguments->set("result", responseBodyObject);
460+
461+
std::ostringstream oss;
462+
resultArguments->stringify(oss);
463+
std::string result = "unocommandresult: " + oss.str();
464+
clientSession->sendTextFrame(result);
465+
};
466+
httpSession->setFinishedHandler(std::move(finishedCallback));
467+
return httpSession;
468+
}
469+
}
470+
471+
bool ClientSession::handlePrepareSignature(const StringVector& tokens)
472+
{
473+
// Make the HTTP session: this requires an URL
474+
Poco::JSON::Object::Ptr userPrivateInfoObject = new Poco::JSON::Object();
475+
if (!JsonUtil::parseJSON(getUserPrivateInfo(), userPrivateInfoObject))
476+
{
477+
LOG_WRN("PrepareSignature: failed to parse user private info as JSON");
478+
return false;
479+
}
480+
std::string requestUrl;
481+
JsonUtil::findJSONValue(userPrivateInfoObject, "ESignatureBaseUrl", requestUrl);
482+
requestUrl += "/api/signatures/prepare-files-for-signing";
483+
std::shared_ptr<http::Session> httpSession = makePrepareSignatureSession(client_from_this(), requestUrl);
484+
if (!httpSession)
485+
{
486+
return false;
487+
}
488+
489+
// Make the request: this requires a JSON body, where we set the secret
490+
std::string commandArguments = tokens.cat(' ', 2);
491+
Poco::JSON::Object::Ptr commandArgumentsObject;
492+
if (!JsonUtil::parseJSON(commandArguments, commandArgumentsObject))
493+
{
494+
LOG_WRN("PrepareSignature: failed to parse arguments as JSON");
495+
return false;
496+
}
497+
auto requestBodyObject = commandArgumentsObject->get("body").extract<Poco::JSON::Object::Ptr>();
498+
std::string secret;
499+
JsonUtil::findJSONValue(userPrivateInfoObject, "ESignatureSecret", secret);
500+
requestBodyObject->set("secret", secret);
501+
std::string requestBody;
502+
std::stringstream oss;
503+
requestBodyObject->stringify(oss);
504+
requestBody = oss.str();
505+
http::Request httpRequest(Poco::URI(requestUrl).getPathAndQuery());
506+
httpRequest.setVerb(http::Request::VERB_POST);
507+
httpRequest.setBody(requestBody, "application/json");
508+
std::shared_ptr<DocumentBroker> docBroker = getDocumentBroker();
509+
httpSession->asyncRequest(httpRequest, docBroker->getPoll());
510+
return true;
511+
}
512+
431513
bool ClientSession::_handleInput(const char *buffer, int length)
432514
{
433515
LOG_TRC("handling incoming [" << getAbbreviatedMessage(buffer, length) << ']');
@@ -1200,6 +1282,11 @@ bool ClientSession::_handleInput(const char *buffer, int length)
12001282
tokens.equals(0, "geta11ycaretposition") ||
12011283
tokens.equals(0, "getpresentationinfo"))
12021284
{
1285+
if (tokens.equals(0, "uno") && tokens.equals(1, ".uno:PrepareSignature"))
1286+
{
1287+
return handlePrepareSignature(tokens);
1288+
}
1289+
12031290
if (tokens.equals(0, "key"))
12041291
_keyEvents++;
12051292

wsd/ClientSession.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ class ClientSession final : public Session
289289

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

292+
bool handlePrepareSignature(const StringVector& tokens);
293+
292294
bool loadDocument(const char* buffer, int length, const StringVector& tokens,
293295
const std::shared_ptr<DocumentBroker>& docBroker);
294296
bool getStatus(const char* buffer, int length,

0 commit comments

Comments
 (0)