diff options
Diffstat (limited to 'browser/components/translation/test/bing.sjs')
-rw-r--r-- | browser/components/translation/test/bing.sjs | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/browser/components/translation/test/bing.sjs b/browser/components/translation/test/bing.sjs new file mode 100644 index 0000000000..ec385e7fb6 --- /dev/null +++ b/browser/components/translation/test/bing.sjs @@ -0,0 +1,256 @@ +/* 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/. */ + +"use strict"; + +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +Cu.importGlobalProperties(["DOMParser"]); + +function handleRequest(req, res) { + try { + reallyHandleRequest(req, res); + } catch (ex) { + res.setStatusLine("1.0", 200, "AlmostOK"); + let msg = "Error handling request: " + ex + "\n" + ex.stack; + log(msg); + res.write(msg); + } +} + +function log(msg) { + // dump("BING-SERVER-MOCK: " + msg + "\n"); +} + +const statusCodes = { + 400: "Bad Request", + 401: "Unauthorized", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 500: "Internal Server Error", + 501: "Not Implemented", + 503: "Service Unavailable", +}; + +function HTTPError(code = 500, message) { + this.code = code; + this.name = statusCodes[code] || "HTTPError"; + this.message = message || this.name; +} +HTTPError.prototype = new Error(); +HTTPError.prototype.constructor = HTTPError; + +function sendError(res, err) { + if (!(err instanceof HTTPError)) { + err = new HTTPError( + typeof err == "number" ? err : 500, + err.message || typeof err == "string" ? err : "" + ); + } + res.setStatusLine("1.1", err.code, err.name); + res.write(err.message); +} + +function parseQuery(query) { + let ret = {}; + for (let param of query.replace(/^[?&]/, "").split("&")) { + param = param.split("="); + if (!param[0]) { + continue; + } + ret[unescape(param[0])] = unescape(param[1]); + } + return ret; +} + +function getRequestBody(req) { + let avail; + let bytes = []; + let body = new BinaryInputStream(req.bodyInputStream); + + while ((avail = body.available()) > 0) { + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + } + + return String.fromCharCode.apply(null, bytes); +} + +function sha1(str) { + let converter = Cc[ + "@mozilla.org/intl/scriptableunicodeconverter" + ].createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + // `result` is an out parameter, `result.value` will contain the array length. + let result = {}; + // `data` is an array of bytes. + let data = converter.convertToByteArray(str, result); + let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(ch.SHA1); + ch.update(data, data.length); + let hash = ch.finish(false); + + // Return the two-digit hexadecimal code for a byte. + function toHexString(charCode) { + return ("0" + charCode.toString(16)).slice(-2); + } + + // Convert the binary hash data to a hex string. + return Array.from(hash, (c, i) => toHexString(hash.charCodeAt(i))).join(""); +} + +function parseXml(body) { + let parser = new DOMParser(); + let xml = parser.parseFromString(body, "text/xml"); + if (xml.documentElement.localName == "parsererror") { + throw new Error("Invalid XML"); + } + return xml; +} + +function getInputStream(path) { + let file = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + for (let part of path.split("/")) { + file.append(part); + } + let fileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fileStream.init(file, 1, 0, false); + return fileStream; +} + +function checkAuth(req) { + let err = new Error("Authorization failed"); + err.code = 401; + + if (!req.hasHeader("Authorization")) { + throw new HTTPError(401, "No Authorization header provided."); + } + + let auth = req.getHeader("Authorization"); + if (!auth.startsWith("Bearer ")) { + throw new HTTPError( + 401, + "Invalid Authorization header content: '" + auth + "'" + ); + } + + // Rejecting inactive subscriptions. + if (auth.includes("inactive")) { + const INACTIVE_STATE_RESPONSE = + "<html><body><h1>TranslateApiException</h1><p>Method: TranslateArray()</p><p>Message: The Azure Market Place Translator Subscription associated with the request credentials is not in an active state.</p><code></code><p>message id=5641.V2_Rest.TranslateArray.48CC6470</p></body></html>"; + throw new HTTPError(401, INACTIVE_STATE_RESPONSE); + } +} + +function reallyHandleRequest(req, res) { + log("method: " + req.method); + if (req.method != "POST") { + sendError( + res, + "Bing only deals with POST requests, not '" + req.method + "'." + ); + return; + } + + let body = getRequestBody(req); + log("body: " + body); + + // First, we'll see if we're dealing with an XML body: + let contentType = req.hasHeader("Content-Type") + ? req.getHeader("Content-Type") + : null; + log("contentType: " + contentType); + + if (contentType.startsWith("text/xml")) { + try { + // For all these requests the client needs to supply the correct + // authentication headers. + checkAuth(req); + + let xml = parseXml(body); + let method = xml.documentElement.localName; + log("invoking method: " + method); + // If the requested method is supported, delegate it to its handler. + if (methodHandlers[method]) { + methodHandlers[method](res, xml); + } else { + throw new HTTPError(501); + } + } catch (ex) { + sendError(res, ex, ex.code); + } + } else { + // Not XML, so it must be a query-string. + let params = parseQuery(body); + + // Delegate an authentication request to the correct handler. + if ("grant_type" in params && params.grant_type == "client_credentials") { + methodHandlers.authenticate(res, params); + } else { + sendError(res, 501); + } + } +} + +const methodHandlers = { + authenticate(res, params) { + // Validate a few required parameters. + if (params.scope != "http://api.microsofttranslator.com") { + sendError(res, "Invalid scope."); + return; + } + if (!params.client_id) { + sendError(res, "Missing client_id param."); + return; + } + if (!params.client_secret) { + sendError(res, "Missing client_secret param."); + return; + } + + // Defines the tokens for certain client ids. + const TOKEN_MAP = { + testInactive: "inactive", + testClient: "test", + }; + let token = "test"; // Default token. + if (params.client_id in TOKEN_MAP) { + token = TOKEN_MAP[params.client_id]; + } + let content = JSON.stringify({ + access_token: token, + expires_in: 600, + }); + + res.setStatusLine("1.1", 200, "OK"); + res.setHeader("Content-Length", String(content.length)); + res.setHeader("Content-Type", "application/json"); + res.write(content); + }, + + TranslateArrayRequest(res, xml, body) { + let from = xml.querySelector("From").firstChild.nodeValue; + let to = xml.querySelector("To").firstChild.nodeValue; + log("translating from '" + from + "' to '" + to + "'"); + + res.setStatusLine("1.1", 200, "OK"); + res.setHeader("Content-Type", "text/xml"); + + let hash = sha1(body).substr(0, 10); + log("SHA1 hash of content: " + hash); + let inputStream = getInputStream( + "browser/browser/components/translation/test/fixtures/result-" + + hash + + ".txt" + ); + res.bodyOutputStream.writeFrom(inputStream, inputStream.available()); + inputStream.close(); + }, +}; |