middleware.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. var path = require('path')
  2. var fs = require('fs')
  3. var helpers = require('./helpers')
  4. var mime = require('mime')
  5. var terraform = require('candlewax')
  6. var pkg = require('../package.json')
  7. var skin = require('./skin')
  8. var connect = require('connect')
  9. var send = require('send')
  10. var utilsPause = require('pause')
  11. var utilsEscape = require('escape-html')
  12. var parse = require('parseurl')
  13. var url = require('url')
  14. exports.notMultihostURL = function(req, rsp, next){
  15. var host = req.headers.host
  16. var hostname = host.split(':')[0]
  17. var arr = hostname.split(".")
  18. var port = host.split(':')[1] ? ':' + host.split(':')[1] : ''
  19. if(hostname == "127.0.0.1" || hostname == "localhost"){
  20. rsp.statusCode = 307
  21. rsp.setHeader('Location', 'http://harp.nu' + port)
  22. rsp.end("redirecting you to http://harp.nu" + port)
  23. }else if(arr.length == 4){
  24. arr.pop()
  25. arr.push('io')
  26. var link = 'http://' + arr.join('.') + port
  27. var body = "Local server does not support history. Perhaps you are looking for <href='" + link + "'>" + link + "</a>."
  28. rsp.statusCode = 307
  29. rsp.end(body)
  30. }else if(arr.length > 4){
  31. arr.shift()
  32. var link = 'http://' + arr.join('.') + port
  33. rsp.statusCode = 307
  34. rsp.setHeader('Location', link)
  35. rsp.end("redirecting you to " + link)
  36. }else{
  37. next()
  38. }
  39. }
  40. var reservedDomains = ["harp.io", "harpdev.io", "harpapp.io"];
  41. exports.index = function(dirPath){
  42. return function(req, rsp, next){
  43. var host = req.headers.host;
  44. var hostname = host.split(':')[0];
  45. var arr = hostname.split(".");
  46. var port = host.split(':')[1] ? ':' + host.split(':')[1] : '';
  47. var poly = terraform.root(__dirname + "/templates");
  48. if(arr.length == 2){
  49. fs.readdir(dirPath, function(err, files){
  50. var projects = [];
  51. files.forEach(function(file){
  52. var local = file.split('.');
  53. var appPart = local.join("_");
  54. if (local.length > 2) {
  55. var domain = local.slice(Math.max(local.length - 2, 1)).join(".");
  56. if (reservedDomains.indexOf(domain) != -1) {
  57. appPart = local[0];
  58. }
  59. }
  60. // DOT files are ignored.
  61. if (file[0] !== ".") {
  62. projects.push({
  63. "name" : file,
  64. "localUrl" : 'http://' + appPart + "." + host,
  65. "localPath" : path.resolve(dirPath, file)
  66. });
  67. }
  68. });
  69. poly.render("index.jade", { pkg: pkg, projects: projects, layout: "_layout.jade" }, function(error, body){
  70. rsp.end(body)
  71. });
  72. })
  73. } else {
  74. next();
  75. }
  76. }
  77. }
  78. exports.hostProjectFinder = function(dirPath){
  79. return function(req, rsp, next){
  80. var host = req.headers.host;
  81. var hostname = host.split(':')[0];
  82. var matches = [];
  83. fs.readdir(dirPath, function(err, files){
  84. var appPart = hostname.split(".")[0];
  85. files.forEach(function(file){
  86. var fp = file.split('.');
  87. var filePart;
  88. // Check against Reserved Domains first.
  89. if (fp.length > 2) {
  90. var domain = fp.slice(Math.max(fp.length - 2, 1)).join(".");
  91. if (reservedDomains.indexOf(domain) != -1) {
  92. fp = fp.slice(0, Math.max(fp.length - 2))
  93. }
  94. }
  95. filePart = fp.join("_");
  96. if (appPart == filePart) {
  97. matches.push(file);
  98. }
  99. });
  100. if(matches.length > 0){
  101. req.projectPath = path.resolve(dirPath, matches[0]);
  102. next();
  103. } else {
  104. rsp.end("Cannot find project")
  105. }
  106. });
  107. }
  108. }
  109. exports.regProjectFinder = function(projectPath){
  110. return function(req, rsp, next){
  111. req.projectPath = projectPath
  112. next()
  113. }
  114. }
  115. /**
  116. * Fallbacks
  117. *
  118. * This is the logic behind rendering fallback files.
  119. *
  120. * 1. return static 200.html file
  121. * 2. compile and return 200.xxx
  122. * 3. return static 404.html file
  123. * 4. compile and return 404.xxx file
  124. * 5. default 404
  125. *
  126. * It is broken into two public functions `fallback`, and `notFound`
  127. *
  128. */
  129. var fallback = exports.fallback = function(req, rsp, next){
  130. skin(req, rsp, [custom200static, custom200dynamic, notFound], next)
  131. }
  132. var notFound = exports.notFound = function(req, rsp, next){
  133. skin(req, rsp, [custom404static, custom404dynamic, default404], next)
  134. }
  135. /**
  136. * Custom 200
  137. *
  138. * 1. return static 200.html file
  139. * 2. compile and return 200.xxx file
  140. *
  141. */
  142. var custom200static = function(req, rsp, next){
  143. fs.readFile(path.resolve(req.setup.publicPath, "200.html"), function(err, contents){
  144. if(contents){
  145. var body = contents.toString()
  146. var type = helpers.mimeType("html")
  147. var charset = mime.charsets.lookup(type)
  148. rsp.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
  149. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  150. rsp.statusCode = 200
  151. rsp.end(body)
  152. }else{
  153. next()
  154. }
  155. })
  156. }
  157. /**
  158. * Custom 200 (jade, md, ejs)
  159. *
  160. * 1. return static 200.html file
  161. * 2. compile and return 404.xxx file
  162. *
  163. */
  164. var custom200dynamic = function(req, rsp, next){
  165. skin(req, rsp, [poly], function(){
  166. var priorityList = terraform.helpers.buildPriorityList("200.html")
  167. var sourceFile = terraform.helpers.findFirstFile(req.setup.publicPath, priorityList)
  168. if(!sourceFile) return next()
  169. req.poly.render(sourceFile, function(error, body){
  170. if(error){
  171. // TODO: make this better
  172. rsp.statusCode = 404;
  173. rsp.end("There is an error in your " + sourceFile + " file")
  174. }else{
  175. if(!body) return next()
  176. var type = helpers.mimeType("html")
  177. var charset = mime.charsets.lookup(type)
  178. rsp.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
  179. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  180. rsp.statusCode = 200;
  181. rsp.end(body)
  182. }
  183. })
  184. })
  185. }
  186. /**
  187. * Custom 404 (html)
  188. *
  189. * 1. return static 404.html file
  190. * 2. compile and return 404.xxx file
  191. *
  192. * TODO: cache readFile IO
  193. *
  194. */
  195. var custom404static = function(req, rsp, next){
  196. fs.readFile(path.resolve(req.setup.publicPath, "404.html"), function(err, contents){
  197. if(contents){
  198. var body = contents.toString()
  199. var type = helpers.mimeType("html")
  200. var charset = mime.charsets.lookup(type)
  201. rsp.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''))
  202. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  203. rsp.statusCode = 404
  204. rsp.end(body)
  205. }else{
  206. next()
  207. }
  208. })
  209. }
  210. /**
  211. * Custom 404 (jade, md, ejs)
  212. *
  213. * 1. return static 404.html file
  214. * 2. compile and return 404.xxx file
  215. *
  216. */
  217. var custom404dynamic = function(req, rsp, next){
  218. skin(req, rsp, [poly], function(){
  219. var priorityList = terraform.helpers.buildPriorityList("404.html")
  220. var sourceFile = terraform.helpers.findFirstFile(req.setup.publicPath, priorityList)
  221. if(!sourceFile) return next()
  222. req.poly.render(sourceFile, function(error, body){
  223. if(error){
  224. // TODO: make this better
  225. rsp.statusCode = 404;
  226. rsp.end("There is an error in your " + sourceFile + " file")
  227. }else{
  228. if(!body) return next()
  229. var type = helpers.mimeType("html")
  230. var charset = mime.charsets.lookup(type)
  231. rsp.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
  232. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  233. rsp.statusCode = 404;
  234. rsp.end(body)
  235. }
  236. })
  237. })
  238. }
  239. /**
  240. * Default 404
  241. *
  242. * No 200 nor 404 files were found.
  243. *
  244. */
  245. var default404 = function(req, rsp, next){
  246. var locals = {
  247. project: req.headers.host,
  248. name: "Page Not Found",
  249. pkg: pkg
  250. }
  251. terraform.root(__dirname + "/templates").render("404.jade", locals, function(err, body){
  252. var type = helpers.mimeType("html")
  253. var charset = mime.charsets.lookup(type)
  254. rsp.setHeader('Content-Type', type + (charset ? '; charset=' + charset : ''));
  255. rsp.statusCode = 404
  256. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  257. rsp.end(body)
  258. })
  259. }
  260. /**
  261. * Underscore
  262. *
  263. * Returns 404 if path contains beginning underscore
  264. *
  265. */
  266. exports.underscore = function(req, rsp, next){
  267. if(terraform.helpers.shouldIgnore(req.url)){
  268. notFound(req, rsp, next)
  269. }else{
  270. next()
  271. }
  272. }
  273. /**
  274. * Modern Web Language
  275. *
  276. * Returns 404 if file is a precompiled
  277. *
  278. */
  279. exports.mwl = function(req, rsp, next){
  280. var ext = path.extname(req.url).replace(/^\./, '')
  281. req.originalExt = ext
  282. // This prevents the source files from being served, but also
  283. // has to factor in that in this brave new world, sometimes
  284. // `.html` (Handlebars, others), `.css` (PostCSS), and
  285. // `.js` (Browserify) are actually being used to specify
  286. // source files
  287. //if (['js'].indexOf(ext) === -1) {
  288. if (terraform.helpers.processors["html"].indexOf(ext) !== -1 || terraform.helpers.processors["css"].indexOf(ext) !== -1 || terraform.helpers.processors["js"].indexOf(ext) !== -1) {
  289. notFound(req, rsp, next)
  290. } else {
  291. next()
  292. }
  293. //} else {
  294. //next()
  295. //}
  296. }
  297. /**
  298. * Static
  299. *
  300. * Serves up static page (if it exists).
  301. *
  302. */
  303. exports.static = function(req, res, next) {
  304. var options = {}
  305. var redirect = true
  306. if ('GET' != req.method && 'HEAD' != req.method) return next()
  307. //if (['js'].indexOf(path.extname(req.url).replace(/^\./, '')) !== -1) return next()
  308. var pathn = parse(req).pathname;
  309. var pause = utilsPause(req);
  310. function resume() {
  311. next();
  312. pause.resume();
  313. }
  314. function directory() {
  315. if (!redirect) return resume();
  316. var pathname = url.parse(req.originalUrl).pathname;
  317. res.statusCode = 301;
  318. res.setHeader('Location', pathname + '/');
  319. res.end('Redirecting to ' + utilsEscape(pathname) + '/');
  320. }
  321. function error(err) {
  322. if (404 == err.status){
  323. // look for implicit `*.html` if we get a 404
  324. return path.extname(err.path) === ''
  325. ? serve(pathn + ".html")
  326. : resume()
  327. }
  328. next(err);
  329. }
  330. var serve = function(pathn){
  331. send(req, pathn, {
  332. maxage: options.maxAge || 0,
  333. root: req.setup.publicPath,
  334. hidden: options.hidden
  335. })
  336. .on('error', error)
  337. .on('directory', directory)
  338. .pipe(res)
  339. }
  340. serve(pathn)
  341. }
  342. /**
  343. * Opens the (optional) harp.json file and sets the config settings.
  344. */
  345. exports.setup = function(req, rsp, next){
  346. if(req.hasOwnProperty('setup')) return next()
  347. try{
  348. req.setup = helpers.setup(req.projectPath)
  349. }catch(error){
  350. error.stack = helpers.stacktrace(error.stack, { lineno: error.lineno })
  351. var locals = {
  352. project: req.headers.host,
  353. error: error,
  354. pkg: pkg
  355. }
  356. return terraform.root(__dirname + "/templates").render("error.jade", locals, function(err, body){
  357. rsp.statusCode = 500
  358. rsp.end(body)
  359. })
  360. }
  361. next()
  362. }
  363. /**
  364. * Basic Auth
  365. */
  366. exports.basicAuth = function(req, rsp, next){
  367. // default empty
  368. var creds = []
  369. // allow array
  370. if(req.setup.config.hasOwnProperty("basicAuth") && req.setup.config["basicAuth"] instanceof Array)
  371. creds = req.setup.config["basicAuth"]
  372. // allow string
  373. if(req.setup.config.hasOwnProperty("basicAuth") && typeof req.setup.config["basicAuth"] === 'string')
  374. creds = [req.setup.config["basicAuth"]]
  375. // move on if no creds
  376. if(creds.length === 0) return next()
  377. // use connect auth lib iterate over all creds provided
  378. connect.basicAuth(function(user, pass){
  379. return creds.some(function(cred){
  380. return cred === user + ":" + pass
  381. })
  382. })(req, rsp, next)
  383. }
  384. /**
  385. * Sets up the poly object
  386. */
  387. var poly = exports.poly = function(req, rsp, next){
  388. if(req.hasOwnProperty("poly")) return next()
  389. try{
  390. req.poly = terraform.root(req.setup.publicPath, req.setup.config.globals)
  391. }catch(error){
  392. error.stack = helpers.stacktrace(error.stack, { lineno: error.lineno })
  393. var locals = {
  394. project: req.headers.host,
  395. error: error,
  396. pkg: pkg
  397. }
  398. return terraform.root(__dirname + "/templates").render("error.jade", locals, function(err, body){
  399. rsp.statusCode = 500
  400. rsp.end(body)
  401. })
  402. }
  403. next()
  404. }
  405. /**
  406. * Asset Pipeline
  407. */
  408. exports.process = function(req, rsp, next){
  409. var normalizedPath = helpers.normalizeUrl(req.url)
  410. var priorityList = terraform.helpers.buildPriorityList(normalizedPath)
  411. var sourceFile = terraform.helpers.findFirstFile(req.setup.publicPath, priorityList)
  412. /**
  413. * We GTFO if we don't have a source file.
  414. */
  415. if(!sourceFile){
  416. if (path.basename(normalizedPath) === "index.html") {
  417. var pathAr = normalizedPath.split(path.sep); pathAr.pop() // Pop index.html off the list
  418. var prospectCleanPath = pathAr.join("/")
  419. var prospectNormalizedPath = helpers.normalizeUrl(prospectCleanPath)
  420. var prospectPriorityList = terraform.helpers.buildPriorityList(prospectNormalizedPath)
  421. prospectPriorityList.push(path.basename(prospectNormalizedPath + ".html"))
  422. sourceFile = terraform.helpers.findFirstFile(req.setup.publicPath, prospectPriorityList)
  423. if (!sourceFile) {
  424. return next()
  425. } else {
  426. // 301 redirect
  427. rsp.statusCode = 301
  428. rsp.setHeader('Location', prospectCleanPath)
  429. rsp.end('Redirecting to ' + utilsEscape(prospectCleanPath))
  430. }
  431. } else {
  432. return next()
  433. }
  434. } else {
  435. /**
  436. * Now we let terraform handle the asset pipeline.
  437. */
  438. req.poly.render(sourceFile, function(error, body){
  439. if(error){
  440. error.stack = helpers.stacktrace(error.stack, { lineno: error.lineno })
  441. var locals = {
  442. project: req.headers.host,
  443. error: error,
  444. pkg: pkg
  445. }
  446. if(terraform.helpers.outputType(sourceFile) == 'css'){
  447. var outputType = terraform.helpers.outputType(sourceFile)
  448. var mimeType = helpers.mimeType(outputType)
  449. var charset = mime.charsets.lookup(mimeType)
  450. var body = helpers.cssError(locals)
  451. rsp.statusCode = 200
  452. rsp.setHeader('Content-Type', mimeType + (charset ? '; charset=' + charset : ''))
  453. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  454. rsp.end(body)
  455. }else{
  456. // Make the paths relative but keep the root dir.
  457. // TODO: move to helper.
  458. //
  459. // var loc = req.projectPath.split(path.sep); loc.pop()
  460. // var loc = loc.join(path.sep) + path.sep
  461. // if(error.filename) error.filename = error.filename.replace(loc, "")
  462. terraform.root(__dirname + "/templates").render("error.jade", locals, function(err, body){
  463. var mimeType = helpers.mimeType('html')
  464. var charset = mime.charsets.lookup(mimeType)
  465. rsp.statusCode = 500
  466. rsp.setHeader('Content-Type', mimeType + (charset ? '; charset=' + charset : ''))
  467. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  468. rsp.end(body)
  469. })
  470. }
  471. }else{
  472. // 404
  473. if(!body) return next()
  474. var outputType = terraform.helpers.outputType(sourceFile)
  475. var mimeType = helpers.mimeType(outputType)
  476. var charset = mime.charsets.lookup(mimeType)
  477. rsp.statusCode = 200
  478. rsp.setHeader('Content-Type', mimeType + (charset ? '; charset=' + charset : ''))
  479. rsp.setHeader('Content-Length', Buffer.byteLength(body, charset));
  480. rsp.end(body);
  481. }
  482. })
  483. }
  484. }