utils.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. "use strict";
  2. var fs = require("fs");
  3. var path = require("path");
  4. var join = require("path").join;
  5. var connect = require("connect");
  6. var Immutable = require("immutable");
  7. var http = require("http");
  8. var https = require("https");
  9. var Map = require("immutable").Map;
  10. var fromJS = require("immutable").fromJS;
  11. var List = require("immutable").List;
  12. var snippet = require("./../snippet").utils;
  13. var _ = require("../lodash.custom");
  14. var serveStatic = require("./serve-static-wrapper").default();
  15. var serveIndex = require("serve-index");
  16. var logger = require("../logger");
  17. var snippetUtils = require("../snippet").utils;
  18. var lrSnippet = require("resp-modifier");
  19. var certPath = join(__dirname, "..", "..", "certs");
  20. function getCa(options) {
  21. var caOption = options.getIn(["https", "ca"]);
  22. // if not provided, use Browsersync self-signed
  23. if (typeof caOption === "undefined") {
  24. return fs.readFileSync(join(certPath, "server.csr"));
  25. }
  26. // if a string was given, read that file from disk
  27. if (typeof caOption === "string") {
  28. return fs.readFileSync(caOption);
  29. }
  30. // if an array was given, read all
  31. if (List.isList(caOption)) {
  32. return caOption.toArray().map(function (x) {
  33. return fs.readFileSync(x);
  34. });
  35. }
  36. }
  37. function getKey(options) {
  38. return fs.readFileSync(options.getIn(["https", "key"]) || join(certPath, "server.key"));
  39. }
  40. function getCert(options) {
  41. return fs.readFileSync(options.getIn(["https", "cert"]) || join(certPath, "server.crt"));
  42. }
  43. function getHttpsServerDefaults(options) {
  44. return fromJS({
  45. key: getKey(options),
  46. cert: getCert(options),
  47. ca: getCa(options),
  48. passphrase: ""
  49. });
  50. }
  51. function getPFXDefaults(options) {
  52. return fromJS({
  53. pfx: fs.readFileSync(options.getIn(["https", "pfx"]))
  54. });
  55. }
  56. var serverUtils = {
  57. /**
  58. * @param options
  59. * @returns {{key, cert}}
  60. */
  61. getHttpsOptions: function (options) {
  62. var userOption = options.get("https");
  63. if (Map.isMap(userOption)) {
  64. if (userOption.has("pfx")) {
  65. return userOption.mergeDeep(getPFXDefaults(options));
  66. }
  67. return userOption.mergeDeep(getHttpsServerDefaults(options));
  68. }
  69. return getHttpsServerDefaults(options);
  70. },
  71. /**
  72. * Get either http or https server
  73. * or use the httpModule provided in options if present
  74. */
  75. getServer: function (app, options) {
  76. return {
  77. server: (function () {
  78. var httpModule = serverUtils.getHttpModule(options);
  79. if (options.get("scheme") === "https" ||
  80. options.get("httpModule") === "http2") {
  81. var opts = serverUtils.getHttpsOptions(options);
  82. return httpModule.createServer(opts.toJS(), app);
  83. }
  84. return httpModule.createServer(app);
  85. })(),
  86. app: app
  87. };
  88. },
  89. getHttpModule: function (options) {
  90. /**
  91. * Users may provide a string to be used by nodes
  92. * require lookup.
  93. */
  94. var httpModule = options.get("httpModule");
  95. if (typeof httpModule === "string") {
  96. /**
  97. * Note, this could throw, but let that happen as
  98. * the error message good enough.
  99. */
  100. var maybe = require.resolve(httpModule);
  101. return require(maybe);
  102. }
  103. if (options.get("scheme") === "https") {
  104. return https;
  105. }
  106. return http;
  107. },
  108. getMiddlewares: function (bs) {
  109. var clientJs = bs.pluginManager.hook("client:js", {
  110. port: bs.options.get("port"),
  111. options: bs.options
  112. });
  113. var scripts = bs.pluginManager.get("client:script")(bs.options.toJS(), clientJs, "middleware");
  114. var defaultMiddlewares = [
  115. {
  116. id: "Browsersync HTTP Protocol",
  117. route: require("../config").httpProtocol.path,
  118. handle: require("../http-protocol").middleware(bs)
  119. },
  120. {
  121. id: "Browsersync IE8 Support",
  122. route: "",
  123. handle: snippet.isOldIe(bs.options.get("excludedFileTypes").toJS())
  124. },
  125. {
  126. id: "Browsersync Response Modifier",
  127. route: "",
  128. handle: serverUtils.getSnippetMiddleware(bs)
  129. },
  130. {
  131. id: "Browsersync Client - versioned",
  132. route: bs.options.getIn(["scriptPaths", "versioned"]),
  133. handle: scripts
  134. },
  135. {
  136. id: "Browsersync Client",
  137. route: bs.options.getIn(["scriptPaths", "path"]),
  138. handle: scripts
  139. }
  140. ];
  141. /**
  142. * Add cors middleware to the front of the stack
  143. * if a user provided a 'cors' flag
  144. */
  145. if (bs.options.get("cors")) {
  146. defaultMiddlewares.unshift({
  147. id: "Browsersync CORS support",
  148. route: "",
  149. handle: serverUtils.getCorsMiddlewware()
  150. });
  151. }
  152. /**
  153. * Add connect-history-api-fallback if 'single' argument given
  154. */
  155. if (bs.options.get("single")) {
  156. defaultMiddlewares.unshift({
  157. id: "Browsersync SPA support",
  158. route: "",
  159. handle: require("connect-history-api-fallback")()
  160. });
  161. }
  162. /**
  163. * Add serve static middleware
  164. */
  165. if (bs.options.get("serveStatic")) {
  166. var ssMiddlewares = serverUtils.getServeStaticMiddlewares(bs.options.get("serveStatic"), bs.options.get("serveStaticOptions", Immutable.Map({})).toJS());
  167. var withErrors = ssMiddlewares.filter(function (x) {
  168. return x.get("errors").size > 0;
  169. });
  170. var withoutErrors = ssMiddlewares.filter(function (x) {
  171. return x.get("errors").size === 0;
  172. });
  173. if (withErrors.size) {
  174. withErrors.forEach(function (item) {
  175. logger.logger.error("{red:Warning!} %s", item.getIn(["errors", 0, "data", "message"]));
  176. });
  177. }
  178. if (withoutErrors.size) {
  179. withoutErrors.forEach(function (item) {
  180. defaultMiddlewares.push.apply(defaultMiddlewares, item.get("items").toJS());
  181. });
  182. }
  183. }
  184. /**
  185. * Add user-provided middlewares
  186. */
  187. var userMiddlewares = bs.options
  188. .get("middleware")
  189. .map(normaliseMiddleware)
  190. .toArray();
  191. var beforeMiddlewares = userMiddlewares.filter(function (x) {
  192. return x.override;
  193. });
  194. var afterMiddlewares = userMiddlewares
  195. .filter(function (x) {
  196. return !x.override;
  197. })
  198. .concat(bs.options.get("mode") !== "proxy" &&
  199. userMiddlewares.length === 0 && {
  200. id: "Browsersync 404/index support",
  201. route: "",
  202. handle: serveIndex(bs.options.get("cwd"), {
  203. icons: true,
  204. view: "details"
  205. })
  206. });
  207. var mwStack = []
  208. .concat(beforeMiddlewares, defaultMiddlewares, afterMiddlewares)
  209. .filter(Boolean);
  210. return mwStack;
  211. function normaliseMiddleware(item) {
  212. /**
  213. * Object given in options, which
  214. * ended up being a Map
  215. */
  216. if (Map.isMap(item)) {
  217. return item.toJS();
  218. }
  219. /**
  220. * Single function
  221. */
  222. if (typeof item === "function") {
  223. return {
  224. route: "",
  225. handle: item
  226. };
  227. }
  228. /**
  229. * Plain obj
  230. */
  231. if (item.route !== undefined && item.handle) {
  232. return item;
  233. }
  234. }
  235. },
  236. getBaseApp: function (bs) {
  237. var app = connect();
  238. var middlewares = serverUtils.getMiddlewares(bs);
  239. /**
  240. * Add all internal middlewares
  241. */
  242. middlewares.forEach(function (item) {
  243. app.stack.push(item);
  244. });
  245. return app;
  246. },
  247. getSnippetMiddleware: function (bs) {
  248. var rules = [];
  249. var blacklist = List([])
  250. .concat(bs.options.getIn(["snippetOptions", "ignorePaths"]))
  251. .concat(bs.options.getIn(["snippetOptions", "blacklist"]))
  252. .filter(Boolean);
  253. var whitelist = List([]).concat(bs.options.getIn(["snippetOptions", "whitelist"]));
  254. // Snippet
  255. rules.push(snippetUtils.getRegex(bs.options.get("snippet"), bs.options.get("snippetOptions")));
  256. // User
  257. bs.options.get("rewriteRules").forEach(function (rule) {
  258. if (Map.isMap(rule)) {
  259. rules.push(rule.toJS());
  260. }
  261. if (_.isPlainObject(rule)) {
  262. rules.push(rule);
  263. }
  264. });
  265. // Proxy
  266. if (bs.options.get("proxy")) {
  267. var proxyRule = require("./proxy-utils").rewriteLinks(bs.options.getIn(["proxy", "url"]).toJS());
  268. rules.push(proxyRule);
  269. }
  270. var lr = lrSnippet.create({
  271. rules: rules,
  272. blacklist: blacklist.toArray(),
  273. whitelist: whitelist.toArray()
  274. });
  275. return lr.middleware;
  276. },
  277. getCorsMiddlewware: function () {
  278. return function (req, res, next) {
  279. // Website you wish to allow to connect
  280. res.setHeader("Access-Control-Allow-Origin", "*");
  281. // Request methods you wish to allow
  282. res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
  283. // Request headers you wish to allow
  284. res.setHeader("Access-Control-Allow-Headers", "X-Requested-With,content-type");
  285. // Set to true if you need the website to include cookies in the requests sent
  286. // to the API (e.g. in case you use sessions)
  287. res.setHeader("Access-Control-Allow-Credentials", true);
  288. next();
  289. };
  290. },
  291. /**
  292. * @param ssOption
  293. * @param serveStaticOptions
  294. * @returns {*}
  295. */
  296. getServeStaticMiddlewares: function (ssOption, serveStaticOptions) {
  297. return ssOption.map(function (dir, i) {
  298. /**
  299. * When a user gives a plain string only, eg:
  300. * serveStatic: ['./temp']
  301. * ->
  302. * This means a middleware will be created with
  303. * route: ''
  304. * handle: serveStatic('./temp', options)
  305. */
  306. if (_.isString(dir)) {
  307. return getFromString(dir);
  308. }
  309. /**
  310. * If a user gave an object eg:
  311. * serveStatic: [{route: "", dir: ["test", "./tmp"]}]
  312. * ->
  313. * This means we need to create a middle for each route + dir combo
  314. */
  315. if (Immutable.Map.isMap(dir)) {
  316. return getFromMap(dir, i);
  317. }
  318. /**
  319. * At this point, an item in the serveStatic array was not a string
  320. * or an object so we return an error that can be logged
  321. */
  322. return fromJS({
  323. items: [],
  324. errors: [
  325. {
  326. type: "Invalid Type",
  327. data: {
  328. message: "Only strings and Objects (with route+dir) are supported for the ServeStatic option"
  329. }
  330. }
  331. ]
  332. });
  333. });
  334. /**
  335. * @param {string} x
  336. * @returns {string}
  337. */
  338. function getRoute(x) {
  339. if (x === "")
  340. return "";
  341. return x[0] === "/" ? x : "/" + x;
  342. }
  343. /**
  344. * @param dir
  345. * @returns {Map}
  346. */
  347. function getFromString(dir) {
  348. return fromJS({
  349. items: [
  350. {
  351. route: "",
  352. handle: serveStatic(dir, serveStaticOptions)
  353. }
  354. ],
  355. errors: []
  356. });
  357. }
  358. /**
  359. * @param dir
  360. * @returns {Map}
  361. */
  362. function getFromMap(dir) {
  363. var ssOptions = (function () {
  364. if (dir.get("options")) {
  365. return dir.get("options").toJS();
  366. }
  367. return {};
  368. })();
  369. var route = Immutable.List([])
  370. .concat(dir.get("route"))
  371. .filter(_.isString);
  372. var _dir = Immutable.List([])
  373. .concat(dir.get("dir"))
  374. .filter(_.isString);
  375. if (_dir.size === 0) {
  376. return fromJS({
  377. items: [],
  378. errors: [
  379. {
  380. type: "Invalid Object",
  381. data: {
  382. message: "Serve Static requires a 'dir' property when using an Object"
  383. }
  384. }
  385. ]
  386. });
  387. }
  388. var ssItems = (function () {
  389. /**
  390. * iterate over every 'route' item
  391. * @type {Immutable.List<any>|Immutable.List<*>|Immutable.List<any>|*}
  392. */
  393. var routeItems = (function () {
  394. /**
  395. * If no 'route' was given, assume we want to match all
  396. * paths
  397. */
  398. if (route.size === 0) {
  399. return _dir.map(function (dirString) {
  400. return Map({
  401. route: "",
  402. dir: dirString
  403. });
  404. });
  405. }
  406. return route.reduce(function (acc, routeString) {
  407. /**
  408. * For each 'route' item, also iterate through 'dirs'
  409. * @type {Immutable.Iterable<K, M>}
  410. */
  411. var perDir = _dir.map(function (dirString) {
  412. return Map({
  413. route: getRoute(routeString),
  414. dir: dirString
  415. });
  416. });
  417. return acc.concat(perDir);
  418. }, List([]));
  419. })();
  420. /**
  421. * Now create a serverStatic Middleware for each item
  422. */
  423. return routeItems.map(function (routeItem) {
  424. return routeItem.merge({
  425. handle: serveStatic(routeItem.get("dir"), ssOptions)
  426. });
  427. });
  428. })();
  429. return fromJS({
  430. items: ssItems,
  431. errors: []
  432. });
  433. }
  434. }
  435. };
  436. module.exports = serverUtils;
  437. //# sourceMappingURL=utils.js.map