1/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2/* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5// sjs for remote about:newtab (bug 1226928)
6"use strict";
7
8const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
9Cu.import("resource://gre/modules/NetUtil.jsm");
10Cu.import("resource://gre/modules/FileUtils.jsm");
11Cu.importGlobalProperties(["URLSearchParams"]);
12
13const path = "browser/dom/security/test/contentverifier/";
14
15const goodFileName = "file_about_newtab.html";
16const goodFileBase = path + goodFileName;
17const goodFile = FileUtils.getDir("TmpD", [], true);
18goodFile.append(goodFileName);
19const goodSignature = path + "file_about_newtab_good_signature";
20const goodX5UString = "\"https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=default\"";
21
22const scriptFileName = "script.js";
23const cssFileName = "style.css";
24const badFile = path + "file_about_newtab_bad.html";
25const brokenSignature = path + "file_about_newtab_broken_signature";
26const badSignature = path + "file_about_newtab_bad_signature";
27const badX5UString = "\"https://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=bad\"";
28const httpX5UString = "\"http://example.com/browser/dom/security/test/contentverifier/file_contentserver.sjs?x5u=default\"";
29
30const sriFile = path + "file_about_newtab_sri.html";
31const sriSignature = path + "file_about_newtab_sri_signature";
32
33const badCspFile = path + "file_about_newtab_bad_csp.html";
34const badCspSignature = path + "file_about_newtab_bad_csp_signature";
35
36// This cert chain is copied from
37// security/manager/ssl/tests/unit/test_content_signing/
38// using the certificates
39// * content_signing_remote_newtab_ee.pem
40// * content_signing_int.pem
41// * content_signing_root.pem
42const goodCertChainPath = path + "goodChain.pem";
43
44const tempFileNames = [goodFileName, scriptFileName, cssFileName];
45
46// we copy the file to serve as newtab to a temp directory because
47// we modify it during tests.
48setupTestFiles();
49
50function setupTestFiles() {
51  for (let fileName of tempFileNames) {
52    let tempFile = FileUtils.getDir("TmpD", [], true);
53    tempFile.append(fileName);
54    if (!tempFile.exists()) {
55      let fileIn = getFileName(path + fileName, "CurWorkD");
56      fileIn.copyTo(FileUtils.getDir("TmpD", [], true), "");
57    }
58  }
59}
60
61function getFileName(filePath, dir) {
62  // Since it's relative to the cwd of the test runner, we start there and
63  // append to get to the actual path of the file.
64  let testFile =
65    Cc["@mozilla.org/file/directory_service;1"].
66      getService(Components.interfaces.nsIProperties).
67      get(dir, Components.interfaces.nsILocalFile);
68  let dirs = filePath.split("/");
69  for (let i = 0; i < dirs.length; i++) {
70    testFile.append(dirs[i]);
71  }
72  return testFile;
73}
74
75function loadFile(file) {
76  // Load a file to return it.
77  let testFileStream =
78    Cc["@mozilla.org/network/file-input-stream;1"]
79      .createInstance(Components.interfaces.nsIFileInputStream);
80  testFileStream.init(file, -1, 0, 0);
81  return NetUtil.readInputStreamToString(testFileStream,
82                                         testFileStream.available());
83}
84
85function appendToFile(aFile, content) {
86  try {
87    let file = FileUtils.openFileOutputStream(aFile, FileUtils.MODE_APPEND |
88                                                     FileUtils.MODE_WRONLY);
89    file.write(content, content.length);
90    file.close();
91  } catch (e) {
92    dump(">>> Error in appendToFile "+e);
93    return "Error";
94  }
95  return "Done";
96}
97
98function truncateFile(aFile, length) {
99  let fileIn = loadFile(aFile);
100  fileIn = fileIn.slice(0, -length);
101
102  try {
103    let file = FileUtils.openFileOutputStream(aFile, FileUtils.MODE_WRONLY |
104                                                     FileUtils.MODE_TRUNCATE);
105    file.write(fileIn, fileIn.length);
106    file.close();
107  } catch (e) {
108    dump(">>> Error in truncateFile "+e);
109    return "Error";
110  }
111  return "Done";
112}
113
114function cleanupTestFiles() {
115  for (let fileName of tempFileNames) {
116    let tempFile = FileUtils.getDir("TmpD", [], true);
117    tempFile.append(fileName);
118    tempFile.remove(true);
119  }
120}
121
122/*
123 * handle requests of the following form:
124 * sig=good&key=good&file=good&header=good&cached=no to serve pages with
125 * content signatures
126 *
127 * it further handles invalidateFile=yep and validateFile=yep to change the
128 * served file
129 */
130function handleRequest(request, response) {
131  let params = new URLSearchParams(request.queryString);
132  let x5uType = params.get("x5u");
133  let signatureType = params.get("sig");
134  let fileType = params.get("file");
135  let headerType = params.get("header");
136  let cached = params.get("cached");
137  let invalidateFile = params.get("invalidateFile");
138  let validateFile = params.get("validateFile");
139  let resource = params.get("resource");
140  let x5uParam = params.get("x5u");
141
142  if (params.get("cleanup")) {
143    cleanupTestFiles();
144    response.setHeader("Content-Type", "text/html", false);
145    response.write("Done");
146    return;
147  }
148
149  if (resource) {
150    if (resource == "script") {
151      response.setHeader("Content-Type", "application/javascript", false);
152      response.write(loadFile(getFileName(scriptFileName, "TmpD")));
153    } else { // resource == "css1" || resource == "css2"
154      response.setHeader("Content-Type", "text/css", false);
155      response.write(loadFile(getFileName(cssFileName, "TmpD")));
156    }
157    return;
158  }
159
160  // if invalidateFile is set, this doesn't actually return a newtab page
161  // but changes the served file to invalidate the signature
162  // NOTE: make sure to make the file valid again afterwards!
163  if (invalidateFile) {
164    let r = "Done";
165    for (let fileName of tempFileNames) {
166      if (appendToFile(getFileName(fileName, "TmpD"), "!") != "Done") {
167        r = "Error";
168      }
169    }
170    response.setHeader("Content-Type", "text/html", false);
171    response.write(r);
172    return;
173  }
174
175  // if validateFile is set, this doesn't actually return a newtab page
176  // but changes the served file to make the signature valid again
177  if (validateFile) {
178    let r = "Done";
179    for (let fileName of tempFileNames) {
180      if (truncateFile(getFileName(fileName, "TmpD"), 1) != "Done") {
181        r = "Error";
182      }
183    }
184    response.setHeader("Content-Type", "text/html", false);
185    response.write(r);
186    return;
187  }
188
189  // we have to return the certificate chain on request for the x5u parameter
190  if (x5uParam && x5uParam == "default") {
191    response.setHeader("Cache-Control", "max-age=216000", false);
192    response.setHeader("Content-Type", "text/plain", false);
193    response.write(loadFile(getFileName(goodCertChainPath, "CurWorkD")));
194    return;
195  }
196
197  // avoid confusing cache behaviours
198  if (!cached) {
199    response.setHeader("Cache-Control", "no-cache", false);
200  } else {
201    response.setHeader("Cache-Control", "max-age=3600", false);
202  }
203
204  // send HTML to test allowed/blocked behaviours
205  response.setHeader("Content-Type", "text/html", false);
206
207  // set signature header and key for Content-Signature header
208  /* By default a good content-signature header is returned. Any broken return
209   * value has to be indicated in the url.
210   */
211  let csHeader = "";
212  let x5uString = goodX5UString;
213  let signature = goodSignature;
214  let file = goodFile;
215  if (x5uType == "bad") {
216    x5uString = badX5UString;
217  } else if (x5uType == "http") {
218    x5uString = httpX5UString;
219  }
220  if (signatureType == "bad") {
221    signature = badSignature;
222  } else if (signatureType == "broken") {
223    signature = brokenSignature;
224  } else if (signatureType == "sri") {
225    signature = sriSignature;
226  } else if (signatureType == "bad-csp") {
227    signature = badCspSignature;
228  }
229  if (fileType == "bad") {
230    file = getFileName(badFile, "CurWorkD");
231  } else if (fileType == "sri") {
232    file = getFileName(sriFile, "CurWorkD");
233  } else if (fileType == "bad-csp") {
234    file = getFileName(badCspFile, "CurWorkD");
235  }
236
237  if (headerType == "good") {
238    // a valid content-signature header
239    csHeader = "x5u=" + x5uString + ";p384ecdsa=" +
240               loadFile(getFileName(signature, "CurWorkD"));
241  } else if (headerType == "error") {
242    // this content-signature header is missing ; before p384ecdsa
243    csHeader = "x5u=" + x5uString + "p384ecdsa=" +
244               loadFile(getFileName(signature, "CurWorkD"));
245  } else if (headerType == "errorInX5U") {
246    // this content-signature header is missing the keyid directive
247    csHeader = "x6u=" + x5uString + ";p384ecdsa=" +
248               loadFile(getFileName(signature, "CurWorkD"));
249  } else if (headerType == "errorInSignature") {
250    // this content-signature header is missing the p384ecdsa directive
251    csHeader = "x5u=" + x5uString + ";p385ecdsa=" +
252               loadFile(getFileName(signature, "CurWorkD"));
253  }
254
255  if (csHeader) {
256    response.setHeader("Content-Signature", csHeader, false);
257  }
258  let result = loadFile(file);
259
260  response.write(result);
261}
262