├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── src ├── Credits.html ├── index.html ├── lib ├── api │ ├── api.js │ ├── class.js │ ├── requestbin.js │ ├── router.js │ ├── server.js │ ├── syslog.js │ └── utility.js ├── config.json ├── controller │ ├── bin.js │ ├── bootstrap.js │ └── main.js ├── css │ ├── animate.min.css │ ├── fenix-embedded.css │ ├── font │ │ ├── fenix.svg │ │ ├── opensans.woff │ │ └── opensanslight.woff │ ├── hint.min.css │ ├── main.css │ ├── prism.css │ ├── requestbin.css │ └── toolbar.css ├── icons │ ├── fenix.icns │ ├── fenix.ico │ ├── fenix.png │ ├── webhooks.icns │ └── webhooks.png ├── js │ ├── UI.js │ ├── editwizard.js │ ├── prism.js │ ├── router.js │ ├── wizard.js │ └── zepto.js ├── public │ ├── css │ │ └── style.css │ ├── directory.html │ ├── images │ │ ├── css.png │ │ ├── file.png │ │ ├── folder.png │ │ ├── image.png │ │ ├── office.png │ │ ├── php.png │ │ ├── script.png │ │ ├── sound.png │ │ ├── video.png │ │ ├── word.png │ │ ├── xml.png │ │ └── zip.png │ └── js │ │ └── sorttable.js └── view │ ├── about.html │ ├── bin.html │ ├── main.html │ └── splash.html └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [coreybutler] 4 | patreon: coreybutler 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | _* 3 | *.fnx 4 | components 5 | bower.json 6 | tmp 7 | node_modules 8 | !.gitignore 9 | !.jshintignore 10 | !.npmignore 11 | !.travis.yml 12 | *.db 13 | *.fnx 14 | dist 15 | npm-debug.log 16 | src/Gruntfile.js 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Before posting a question, please review the [FAQ](https://github.com/coreybutler/fenix/wiki/faq) and the [wiki](https://github.com/coreybutler/fenix/wiki). 2 | There is also a video walk-thru of Fenix on the [Fenix YouTube Channel](http://www.youtube.com/playlist?list=PL6u9ibuk0pbM68hZONUq-vY39ByaXoJj-). 3 | If you are interested in Fenix but don't quite understand how it could work for you, please see the [Love Localhost](https://medium.com/tech-recipes/f488940f3e38) article. 4 | 5 | Additionally, some great folks have written articles about how they're using Fenix: 6 | 7 | - [Hosting Locally With Fenix Web Server](http://calendee.com/2014/06/25/hosting-locally-with-fenix-web-server/) by Justin Noel for [Calendee](http://calendee.com) 8 | - [Easy and Shareable Web Servers With Fenix](http://flippinawesome.org/2014/06/30/easy-and-shareable-local-web-servers-with-fenix/) by Raymond Camden for [Flippin' Awesome](http://flippinawesome.org). 9 | 10 | You can also contact the author via [@goldglovecb on Twitter](http://twitter.com/goldglovecb). 11 | 12 | If you've exhausted all of those options, then post an issue here. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fenix Web Server 2 | 3 | Fenix is a desktop web server for developers. Check out [fenixwebserver.com](https://preview.fenixwebserver.com) for details. 4 | There are some [YouTube videos](http://www.youtube.com/playlist?list=PL6u9ibuk0pbM68hZONUq-vY39ByaXoJj-) of the old version. We do not yet have any screencasts of v3.0.0, but a [live demo for Bleeding Edge Web](https://www.youtube.com/watch?v=KsoNGVScd_c&t=5053s) was recorded during the early development days. 5 | 6 | ![Fenix 3.0.0](https://docs.fenixwebserver.com/assets/fenix-home.png) 7 | 8 | **Sponsors (as of 2020)** 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | --- 18 | 19 | If you're using Fenix, we'd love your [feedback](https://coreybutler.typeform.com/to/Vk0v2x)! 20 | 21 | **Fenix 3.0.0 [release candidate 13 for macOS and Windows](https://preview.fenixwebserver.com) is available.** 22 | 23 | [Join the Mailing List](https://fenixwebserver.com) (signup on the bottom of the page) 24 | 25 | [![Tweet](https://img.shields.io/badge/Tweet-Spread%20the%20Word-blue?style=for-the-badge&logo=twitter)](https://twitter.com/intent/tweet?hashtags=nodejs&original_referer=http%3A%2F%2Fgithub.com&text=Fenix%20Web%20Server%203.0.0%3A%20A%20desktop%20web%20server%20for%20JAM%20Stack%20Development%2C%20from%20%40author_io&tw_p=tweetbutton&url=http%3A%2F%2Fpreview.fenixwebserver.com&via=goldglovecb) 26 | ![v2.0 Downloads](https://img.shields.io/github/downloads/coreybutler/fenix/v2.0.0/total.svg?style=for-the-badge) 27 | ![v3.0.0 Downloads](https://img.shields.io/github/downloads/coreybutler/fenix/3.0.0-rc.13/total.svg?style=for-the-badge) 28 | 29 | --- 30 | **UPDATE 9/18/19** 31 | Fenix 3 is done, for both Mac and Windows. We had to cut a few things, like automatic updates (it will prompt you to download a new version when new updates are available). Unfortunately, the tools for updating an Electron app aren't really sufficient enough to support some of the new features (like the built-in CLI, updating the `PATH`, etc). We are working on a more streamlined autoupdate experience, which will power future versions. 32 | 33 | Fenix 3 is just one of several things we've been working on under the Author.io brand to make writing software a more efficient/enjoyable process. Since there are several efforts underway (and only 2 of us working on everything), we're also spending time to turn Author.io into a full-fledged company. Don't worry, Fenix will still be free... we're exploring other monetization options to support continued development, as well as sponsorship for the many open source efforts we're puring time into. 34 | 35 | We're also nearly finished with the following: 36 | 37 | - [NGN 2.0.0](https://github.com/ngnjs/ngn) - A JS library for building your own frameworks. 38 | - [Chassis](https://github.com/ngn-chassis) - A PostCSS pre-processing framework. 39 | - [Web Components](https://github.com/author-elements) - A web component library. 40 | - [Metadoc](https://github.com/author/metadoc) - A JS documentation utility that produces JSON. 41 | 42 | NGN, Chassis, and the web components were all used to build Fenix 3 and the associated websites. NGN has been battle-tested with clients like [TopGolf](https://topgolf.com), [Aunt Bertha](https://auntbertha.com), and several enterprises. We're actively working on Metadoc to produce better documentation for the Fenix 3 API libraries. 43 | 44 | We've also released the initial [Fenix 3 docs](https://docs.fenixwebserver.com). 45 | 46 | A placeholder website for [author.io](https://www.author.io), a Twitter account [@author_io](https://twitter.com/author_io) and an [Author.io Facebook Page](https://www.facebook.com/softwareauthor) are live. 47 | 48 | For those we invited to the early beta, thank you. Your feedback has been invaluable. I'd also like to publicly thank those of you who have donated. Your support means the world to us! 49 | 50 | We have some exciting new things coming in 3.0.0: 51 | 52 | **Base** 53 | - [x] Abstracted Foundation (i.e. our electron boilerplate) 54 | - [x] Middleware Plugin System (Internal Use Only) 55 | - [x] UI Plugin System (Internal Use Only) 56 | 57 | _The plugin system is only for internal use. We hope to expand this for developer use in a later edition._ 58 | 59 | **Open Core** 60 | - [x] Autoupdate macOS (evergreen) - No more ridiculously long delays between updates! 61 | - [x] Autoupdate Windows (evergreen) - 90% done. 62 | - [x] Brand new UI. 63 | - [x] Native CLI app (no need to `npm install fenix-cli` anymore). 64 | - [x] Automatic port management. 65 | - [x] Port conflict resolution via [porthog](https://github.com/coreybutler/porthog) 66 | - [x] Replace Growl w/ Native System Notifications. 67 | - [x] Optional JS/CSS minification. 68 | - [x] Optional GZip compression. 69 | - [x] Optionally Render Markdown as HTML (used to always do this, now you have a choice). 70 | - [x] Optional ETags. 71 | - [x] Optional CORS Support 72 | - [x] Optional JSON/XML/YAML Pretty-Print. 73 | - [x] Option to output logs to physical file. 74 | - [x] API 75 | - [x] Global Preferences 76 | - [x] Soft Delete of Servers 77 | - [x] "Pretty" names for SSH tunneling (i.e. myapp.localtunnel.me) 78 | - [x] SSH Tunneling Keepalive 79 | - [x] Light Theme 80 | - [x] Dark Theme 81 | - [x] System Tray Support 82 | - [x] "Run in Background" Mode 83 | - [x] Drag 'n' Drop Server Creation (App & System Tray) 84 | - [x] Installer (macOS pkg, Windows NSIS) 85 | - [x] New Responsive File Browser. 86 | - [x] Autodeployment (w/ badge service via author.io) 87 | 88 | There have been several requests for things like gzip compression, ETags, etc. These features don't typically make sense for the simplest form of local development, but modern UI development "done right" requires a little more emphasis on networking/transmission. These features become very useful when testing and troubleshooting, so we've made it possible to turn them on/off for each server. We're also extending the Fenix API to manage these things programmatically, and we anticipate releasing a gulp/grunt plugin to help automate local testing workflows. 89 | 90 | ~~**PRO Edition**~~ 91 | - [ ] Log Filtering 92 | - [ ] Advanced Live Reload 93 | - [x] Custom Response Headers. 94 | - [x] Multiple server root directories. 95 | - [x] Realtime connection monitoring & statistics 96 | - [x] SSL Support (Fenix CA) 97 | - [x] Fenix Certificate Authority 98 | - [x] Windows Trustchain Management 99 | - [x] OSX Trustchain Management 100 | - [x] Firefox Trustchain Management 101 | - [x] Automatic NIC Management & Synchronization 102 | 103 | Due to the unique and complex nature of some of these features, we are moving them into a separate project. They will likely resurface in 3.1.x or 3.2.x edition (possibly for free). 104 | 105 | 108 | 109 | 110 | 111 | The request browser will be released as it's own separate app, so it won't be in Fenix 3.0.0. I always felt it was a useful tool, and survey results agree... but it also doesn't fit in as well with the original scope of Fenix. Moving it to it's own project will help it get the attention it needs to be truly awesome. 112 | 113 | Finally, we're going "open core". Most of the features above will be free, but more advanced features are slated for a commercial release. As much has we'd like to make this free, devlopment has already grown into a full time effort. 114 | 115 | --- 116 | 117 | # Fenix 2.0 (OLD EDITION) 118 | 119 | **Main App:** 120 | 121 | ![Fenix](http://fenixwebserver.com/img/win32/banner_device.png) 122 | 123 | **Webhook Browser** 124 | 125 | ![Fenix Webhooks](http://fenixwebserver.com/img/win32/bin.png) 126 | 127 | The [wiki](https://github.com/coreybutler/fenix/wiki) has additional information about how Fenix works, how to hack on it, 128 | and how to use it on other platforms. The [release history](https://github.com/coreybutler/fenix/releases) has older versions and a changelog. 129 | 130 | ## Known Issues 131 | 132 | - The [OpenUI5 SDK](http://openui5.hana.ondemand.com) is known to cause an issue with Fenix. See [issue #15](https://github.com/coreybutler/fenix/issues/15) for details. 133 | 134 | ## Like Fenix? 135 | 136 | Making a donation will go towards the development of Fenix. At the moment, I'd love to reach a simple goal of $100 in _annual_ contributions so I can obtain an Apple Developer license for Fenix... which is the only application I'm distributing on Mac. This would help prevent the "Cannot install from unidentified developer" annoyance some OSX Mavericks users experience. Other contributions would go towards future efforts like hosting a public SSH tunnel (to take some load off localtunnel.me) and new feature development. 137 | 138 | [Support OSS Development via Stripe](https://coreybutler.typeform.com/to/ZY4pyp) or [Become a Patron](https://patreon.com/coreybutler) 139 | 140 | ## GPL License 141 | 142 | Fenix 2.0 is available under the GPL license. 143 | -------------------------------------------------------------------------------- /src/Credits.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 |

Credits

12 | 13 |

Collaborators

14 |
  • Corey Butler (coreybutler.com, @goldglovecb)
  • 15 | 16 |

    Contact information

    17 |

    18 | Website: www.fenixwebservers.com 19 | Company: www.ecorsystems.com 20 |

    21 | 22 |

    Community

    23 |

    24 | Issues: GitHub 25 |

    26 | 27 |

    License

    28 |

    29 | This program is free software: you can redistribute it and/or modify 30 | it under the terms of the GNU General Public License as published by 31 | the Free Software Foundation, either version 3 of the License, or 32 | (at your option) any later version. 33 |

    34 |

    35 | This program is distributed in the hope that it will be useful, 36 | but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 38 | GNU General Public License for more details. 39 |

    40 |

    41 | You should have received a copy of the GNU General Public License 42 | along with this program. If not, see 43 | http://www.gnu.org/licenses. 44 |

    45 | 46 | 47 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/api/api.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | api = express(), 3 | fs = require('fs'); 4 | 5 | api.use(express.json()); 6 | api.use(express.urlencoded()); 7 | //api.use(require('connect-multiparty')); 8 | 9 | api.use(function(req,res,next){ 10 | res.set({ 11 | 'x-powered-by': 'Fenix' 12 | }); 13 | next(); 14 | }); 15 | 16 | // Get a list of available servers 17 | // Returns an array of available servers. 18 | api.get('/server/list',function(req,res){ 19 | res.json(Object.keys(ROUTER.servers).map(function(id){ 20 | var server = ROUTER.getServer(id), tmp = server.json; 21 | tmp.running = server.running; 22 | tmp.shared = server.shared; 23 | if (tmp.shared){ 24 | tmp.publicUrl = server.publicUrl; 25 | } 26 | delete tmp.screenshot; 27 | delete tmp.cfg_paths; 28 | delete tmp.suppressnotices; 29 | return tmp; 30 | })); 31 | }); 32 | 33 | // Stop a server 34 | api.put('/server/:server/stop',function(req,res){ 35 | var server = ROUTER.getServer(req.params.server); 36 | if (!server) { 37 | server = ROUTER.getServer(Object.keys(ROUTER.servers).filter(function(id){ 38 | var s = ROUTER.getServer(id); 39 | if (s.name.trim().toLowerCase() === req.params.server.trim().toLowerCase()){ 40 | return true; 41 | } else if (s.path.trim().toLowerCase() === req.params.server.trim().toLowerCase()) { 42 | return true; 43 | } else if (s.port === parseInt(req.params.server)){ 44 | return true; 45 | } 46 | return false; 47 | })[0]); 48 | } 49 | if (!server) { 50 | res.send(404); 51 | return; 52 | } 53 | if (!server.running){ 54 | res.send(200); 55 | return; 56 | } 57 | server.stop(function(){ 58 | res.send(200); 59 | }); 60 | }); 61 | 62 | // Start a server 63 | api.put('/server/:server/start',function(req,res){ 64 | var server = ROUTER.getServer(req.params.server); 65 | if (!server) { 66 | server = ROUTER.getServer(Object.keys(ROUTER.servers).filter(function(id){ 67 | var s = ROUTER.getServer(id); 68 | if (s.name.trim().toLowerCase() === req.params.server.trim().toLowerCase()){ 69 | return true; 70 | } else if (s.path.trim().toLowerCase() === req.params.server.trim().toLowerCase()) { 71 | return true; 72 | } else if (s.port === parseInt(req.params.server)){ 73 | return true; 74 | } 75 | return false; 76 | })[0]); 77 | } 78 | if (!server) { 79 | res.send(404); 80 | return; 81 | } 82 | if (server.running){ 83 | var x = server.json; 84 | delete x.screenshot; 85 | delete x.cfg_paths; 86 | delete x.suppressnotices; 87 | x.shared = server.shared; 88 | x.publicUrl = server.publicUrl || null; 89 | res.json(x); 90 | return; 91 | } 92 | // Make sure the directory exists 93 | if (!require('fs').existsSync(server.path)){ 94 | res.send(410); 95 | } 96 | // Make sure there won't be port conflicts 97 | ROUTER.portscanner.checkPortStatus(server.port,'127.0.0.1',function(err,status){ 98 | if (status === 'open'){ 99 | res.send(409); 100 | return; 101 | } 102 | server.start(function(){ 103 | var x = server.json; 104 | delete x.screenshot; 105 | delete x.cfg_paths; 106 | delete x.suppressnotices; 107 | x.shared = server.shared; 108 | x.publicUrl = server.publicUrl || null; 109 | res.json(x); 110 | }); 111 | }); 112 | }); 113 | 114 | api.put('/server/:server/share',function(req,res){ 115 | var server = ROUTER.getServer(req.params.server); 116 | if (!server) { 117 | server = ROUTER.getServer(Object.keys(ROUTER.servers).filter(function(id){ 118 | var s = ROUTER.getServer(id); 119 | if (s.name.trim().toLowerCase() === req.params.server.trim().toLowerCase()){ 120 | return true; 121 | } else if (s.path.trim().toLowerCase() === req.params.server.trim().toLowerCase()) { 122 | return true; 123 | } else if (s.port === parseInt(req.params.server)){ 124 | return true; 125 | } 126 | return false; 127 | })[0]); 128 | } 129 | if (!server) { 130 | res.send(404); 131 | return; 132 | } 133 | var data = server.json; 134 | delete data.screenshot; 135 | delete data.cfg_paths; 136 | delete data.suppressnotices; 137 | if (server.shared) { 138 | data.publicUrl = server.publicUrl; 139 | res.json(data); 140 | return; 141 | } 142 | // If the server isn't running, start it first. 143 | if (!server.running){ 144 | ROUTER.portscanner.checkPortStatus(server.port,'127.0.0.1',function(err,status){ 145 | if (status === 'open'){ 146 | res.send(409); 147 | return; 148 | } 149 | server.start(function(){ 150 | server.once('share',function(){ 151 | data.publicUrl = server.publicUrl; 152 | res.json(data); 153 | }); 154 | setTimeout(function(){ 155 | server.share(); 156 | },500); 157 | }); 158 | }); 159 | } else { 160 | server.on('share',function(){ 161 | data.publicUrl = server.publicUrl; 162 | res.json(data); 163 | }); 164 | server.share(); 165 | } 166 | }); 167 | 168 | api.put('/server/:server/unshare',function(req,res){ 169 | var server = ROUTER.getServer(req.params.server); 170 | if (!server) { 171 | server = ROUTER.getServer(Object.keys(ROUTER.servers).filter(function(id){ 172 | var s = ROUTER.getServer(id); 173 | if (s.name.trim().toLowerCase() === req.params.server.trim().toLowerCase()){ 174 | return true; 175 | } else if (s.path.trim().toLowerCase() === req.params.server.trim().toLowerCase()) { 176 | return true; 177 | } else if (s.port === parseInt(req.params.server)){ 178 | return true; 179 | } 180 | return false; 181 | })[0]); 182 | } 183 | if (!server) { 184 | res.send(404); 185 | return; 186 | } 187 | if (!server.shared){ 188 | res.send(200); 189 | return; 190 | } 191 | server.unshare(function(){ 192 | res.send(200); 193 | }); 194 | }); 195 | 196 | api.put('/server/:server/status',function(req,res){ 197 | var server = ROUTER.getServer(req.params.server); 198 | if (!server) { 199 | server = ROUTER.getServer(Object.keys(ROUTER.servers).filter(function(id){ 200 | var s = ROUTER.getServer(id); 201 | if (s.name.trim().toLowerCase() === req.params.server.trim().toLowerCase()){ 202 | return true; 203 | } else if (s.path.trim().toLowerCase() === req.params.server.trim().toLowerCase()) { 204 | return true; 205 | } else if (s.port === parseInt(req.params.server)){ 206 | return true; 207 | } 208 | return false; 209 | })[0]); 210 | } 211 | if (!server) { 212 | res.send(404); 213 | return; 214 | } 215 | var x = server.json; 216 | delete x.screenshot; 217 | delete x.cfg_paths; 218 | delete x.suppressnotices; 219 | x.shared = server.shared; 220 | x.publicUrl = server.publicUrl || null; 221 | res.json(x); 222 | }); 223 | 224 | // Returns the details of a specific server. 225 | api.get('/server/:server/list',function(req,res){ 226 | var s = Object.keys(ROUTER.servers).filter(function(id){ 227 | var server = ROUTER.getServer(id); 228 | // Handle ports 229 | if (!isNaN(parseInt(req.params.server))){ 230 | return parseInt(req.params.server) === server.port; 231 | } 232 | return server.name.trim().toLowerCase() === req.params.server.trim().toLowerCase(); 233 | }).map(function(id){ 234 | var server = ROUTER.getServer(id), tmp = server.json; 235 | tmp.running = server.running; 236 | tmp.shared = server.shared; 237 | if (tmp.shared){ 238 | tmp.publicUrl = server.publicUrl; 239 | } 240 | delete tmp.screenshot; 241 | delete tmp.cfg_paths; 242 | delete tmp.suppressnotices; 243 | return tmp; 244 | })[0]; 245 | if (s === undefined){ 246 | res.send(404); 247 | } else { 248 | res.json(s); 249 | } 250 | }); 251 | 252 | api.post('/server',function(req,res){ 253 | if (!req.body.hasOwnProperty('path') || !req.body.hasOwnProperty('name')){ 254 | res.send(400); 255 | return; 256 | } 257 | // Create the new server 258 | ROUTER.getAvailablePort(function(aport){ 259 | var server = ROUTER.createServer({ 260 | name: req.body.name, 261 | path: req.body.path, 262 | port: aport 263 | }); 264 | if (!fs.existsSync(req.body.path)){ 265 | // If the server path doesn't exist but the server wasa created, notify with a 201 266 | res.send(201); 267 | return; 268 | } else { 269 | // Start a valid server 270 | server.on('start',function(){ 271 | setTimeout(function(){ 272 | var tmp = server.json; 273 | tmp.running = server.running; 274 | tmp.shared = server.shared; 275 | if (tmp.shared){ 276 | tmp.publicUrl = server.publicUrl; 277 | } 278 | delete tmp.screenshot; 279 | delete tmp.cfg_paths; 280 | delete tmp.suppressnotices; 281 | res.send(tmp); 282 | },1500); 283 | }); 284 | server.start(); 285 | } 286 | },parseInt(req.body.port)||80); 287 | }); 288 | 289 | // Delete a server 290 | api.del('/server/:server',function(req,res){ 291 | var s = Object.keys(ROUTER.servers).filter(function(id){ 292 | var server = ROUTER.getServer(id); 293 | // Handle ports 294 | if (!isNaN(parseInt(req.params.server))){ 295 | return parseInt(req.params.server) === server.port; 296 | } else if (req.params.server === server.path){ 297 | return true; 298 | } 299 | return server.name.trim().toLowerCase() === req.params.server.trim().toLowerCase(); 300 | }).map(function(id){ 301 | var server = ROUTER.getServer(id), tmp = server.json; 302 | tmp.running = server.running; 303 | tmp.shared = server.shared; 304 | if (tmp.shared){ 305 | tmp.publicUrl = server.publicUrl; 306 | } 307 | delete tmp.screenshot; 308 | delete tmp.cfg_paths; 309 | delete tmp.suppressnotices; 310 | return tmp; 311 | })[0]; 312 | if (s === undefined){ 313 | res.send(404); 314 | } else { 315 | ROUTER.on('deleteserver',function(svr){ 316 | if (svr.id = s.id){ 317 | res.send(200); 318 | } 319 | return; 320 | }); 321 | ROUTER.deleteServer(s.id) 322 | } 323 | }); 324 | 325 | // Close the desktop app 326 | api.put('/close',function(req,res){ 327 | setTimeout(function(){ 328 | var gui = require('nw.gui'); 329 | gui.Window.get(this).close(); 330 | },500); 331 | res.send(200); 332 | }); 333 | 334 | // Get the current worknig version 335 | api.get('/version',function(req,res){ 336 | res.send(global.pkg.version); 337 | }); 338 | 339 | 340 | //api.get('/server/list',function(req,res){}); 341 | //api.get('/server/list',function(req,res){}); 342 | //alert('Attempting to listen on port 33649'); 343 | api.listen(33649,function(){ 344 | console.log('Console server available on port 33649'); 345 | }); 346 | -------------------------------------------------------------------------------- /src/lib/api/class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Class 3 | * A base class providing a simple inheritance model for JavaScript classes. All 4 | * API classes are an extension of this model.

    5 | * **Example:** 6 | * // Superclass 7 | * var Vehicle = Class.extend({ 8 | * constructor: function (type) { 9 | * this.type = type; 10 | * }, 11 | * accelerate: function() { 12 | * return this.type+' is accelerating'; 13 | * } 14 | * }); 15 | * 16 | * // Subclass 17 | * var Car = Vehicle.extend({ 18 | * constructor: function (doorCount) { 19 | * Car.super.constructor.call(this, 'car'); 20 | * 21 | * Object.defineProperty(this,'doors',{ 22 | * value: doorCount || 4, // Default = 4 23 | * writable: true, 24 | * enumerable: true 25 | * }); 26 | * }, 27 | * accelerate: function () { 28 | * console.log('The '+this.doors+'-door '+ Car.super.accelerate.call(this)); 29 | * } 30 | * }); 31 | * 32 | * var mustang = new Car(2); 33 | * mustang.accelerate(); 34 | * 35 | * //Outputs: The 2-door car is accelerating. 36 | * 37 | * @docauthor Corey Butler 38 | */ 39 | var Class = { 40 | 41 | /** 42 | * @method extend 43 | * The properties of the object being extended. 44 | * // Subclass 45 | * var Car = Vehicle.extend({ 46 | * constructor: function (doors) { 47 | * Car.super.constructor.call(this, 'car'); 48 | * 49 | * Object.defineProperty(this,'doors',{ 50 | * value: doors || 4, 51 | * writable: true, 52 | * enumerable: true 53 | * }); 54 | * }, 55 | * accelerate: function () { 56 | * console.log('The '+this.doors+'-door '+ Car.super.accelerate.call(this)); 57 | * } 58 | * }); 59 | * @param {Object} obj 60 | * The object containing `constructor` and methods of the new object. 61 | * @returns {Object} 62 | */ 63 | extend: function ( obj ) { 64 | var parent = this.prototype || Class, 65 | prototype = Object.create(parent); 66 | 67 | Class.merge(obj, prototype); 68 | 69 | var _constructor = prototype.constructor; 70 | if (!(_constructor instanceof Function)) { 71 | throw Error("No constructor() method."); 72 | } 73 | 74 | /** 75 | * @property {Object} prototype 76 | * The prototype of all objects. 77 | * @protected 78 | */ 79 | _constructor.prototype = prototype; 80 | 81 | /** 82 | * @property super 83 | * Refers to the parent class. 84 | * @protected 85 | */ 86 | _constructor.super = parent; 87 | 88 | // Inherit class method 89 | _constructor.extend = this.extend; 90 | 91 | return _constructor; 92 | }, 93 | 94 | /** 95 | * @method merge 96 | * Merges the source to target 97 | * @private 98 | * @param {Object} [source] 99 | * Original object. 100 | * @param {Object} target 101 | * New object (this). 102 | * @param {Boolean} [force=false] 103 | * @returns {Object} 104 | */ 105 | merge: function(source, target, force) { 106 | target = target || this; 107 | force = force || false; 108 | Object.getOwnPropertyNames(source).forEach(function(attr) { 109 | 110 | // If the attribute already exists, 111 | // it will not be recreated, unless force is true. 112 | if (target.hasOwnProperty(attr)){ 113 | if (force) 114 | delete target[attr]; 115 | } 116 | 117 | if (!target.hasOwnProperty(attr)) 118 | Object.defineProperty(target, attr, Object.getOwnPropertyDescriptor(source, attr)); 119 | 120 | }); 121 | return target; 122 | } 123 | }; 124 | 125 | try { 126 | /* 127 | * EventEmitter v4.0.3 - git.io/ee 128 | * Oliver Caldwell 129 | * MIT license 130 | * https://github.com/Wolfy87/EventEmitter 131 | */ 132 | (function(){"use strict";function t(){}function r(t,n){for(var e=t.length;e--;)if(t[e].listener===n)return e;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var e=t.prototype,i=this,s=i.EventEmitter;e.getListeners=function(n){var r,e,t=this._getEvents();if(n instanceof RegExp){r={};for(e in t)t.hasOwnProperty(e)&&n.test(e)&&(r[e]=t[e])}else r=t[n]||(t[n]=[]);return r},e.flattenListeners=function(t){var e,n=[];for(e=0;e= 0){ 142 | return; 143 | } 144 | } 145 | this.emitEvent(eventname,[data]); 146 | } 147 | }); 148 | } catch (e) { 149 | EventEmitter = (require('events')).EventEmitter; 150 | EventEmitter.prototype.emit = function(){ 151 | if (this.suppressedEvents !== undefined){ 152 | if (this.suppressedEvents.indexOf(arguments[0]) >= 0){ 153 | return; 154 | } 155 | } 156 | EventEmitter.super_.prototype.emit.apply(this); 157 | }; 158 | EventEmitter.setMaxListeners(150); 159 | } 160 | 161 | /** 162 | * @method on 163 | * Add an event listener. For example: 164 | * 165 | * Class.on('someEvent',function(){ 166 | * alert('Heard someEvent'); 167 | * }); 168 | */ 169 | /** 170 | * @method once 171 | * Add an event listener that removes itself after 172 | * the event is fired (i.e. listens once). 173 | * For example: 174 | * 175 | * Class.once('someEvent',function(){ 176 | * alert('Heard someEvent'); 177 | * }); 178 | */ 179 | /** 180 | * @method off 181 | * Remove an event listener. 182 | * 183 | * API.off('someEvent'); 184 | */ 185 | /** 186 | * @method emit 187 | * Emit an event. 188 | * 189 | * API.emit('someEvent',{data:value}); 190 | * @protected 191 | */ 192 | Class.merge(EventEmitter.prototype); 193 | -------------------------------------------------------------------------------- /src/lib/api/requestbin.js: -------------------------------------------------------------------------------- 1 | // Configure the Express server 2 | var express = require('express'), 3 | localtunnel = require('localtunnel'), 4 | request = require('request'), 5 | app = express(); 6 | 7 | //app.use(express.json()); 8 | app.use(express.urlencoded()); 9 | app.use(function(req, res, next) { 10 | req.setEncoding('utf8'); 11 | var data=''; 12 | req.setEncoding('utf8'); 13 | req.on('data', function(chunk) { 14 | data += chunk; 15 | }); 16 | 17 | req.on('end', function() { 18 | req.body = data; 19 | next(); 20 | }); 21 | }); 22 | 23 | /** 24 | * @class RequestBin 25 | * The primary router. 26 | * @singleton 27 | * @extends Utility 28 | * @uses Server 29 | */ 30 | var RequestBin = Utility.extend({ 31 | 32 | constructor: function(config){ 33 | config = config || {}; 34 | 35 | RequestBin.super.constructor.call(this,config); 36 | 37 | Object.defineProperties(this,{ 38 | 39 | /** 40 | * @property {Boolean} running 41 | * Indicates whether the proxy server is running. 42 | * @readonly 43 | */ 44 | running: { 45 | enumerable: true, 46 | writable: true, 47 | configurable: false, 48 | value: false 49 | }, 50 | 51 | /** 52 | * @property {String} MAC 53 | * The MAC address of the computer running the request bin. 54 | * @readonly 55 | */ 56 | MAC: { 57 | enumerable: false, 58 | writable: true, 59 | configurable: false, 60 | value: null 61 | }, 62 | 63 | /** 64 | * @cfg {Number} [port=56789] 65 | * The port on which the request bin should be listening. 66 | * @private 67 | */ 68 | port: { 69 | enumerable: false, 70 | writable: false, 71 | configurable: false, 72 | value: config.port || 56789 73 | }, 74 | 75 | /** 76 | * @property {Object} app 77 | * The Express app used for receiving requests. 78 | * @private 79 | * @readonly 80 | */ 81 | app: { 82 | enumerable: false, 83 | writable: false, 84 | configurable: false, 85 | value: app 86 | }, 87 | 88 | /** 89 | * @property {String} subdomain 90 | * The requested subdomain on all localtunnel calls. 91 | * @private 92 | */ 93 | _subdomain: { 94 | enumerable: false, 95 | writable: true, 96 | configurable: false, 97 | value: config.subdomain || null 98 | }, 99 | 100 | subdomain: { 101 | enumerable: false, 102 | get: function(){ 103 | if (this._subdomain === null) { 104 | this._subdomain = require('crypto') 105 | .createHash('sha1', this.MAC) 106 | .update(this.MAC) 107 | .digest('hex').substr(0,8); 108 | } 109 | return this._subdomain; 110 | } 111 | }, 112 | 113 | /** 114 | * @property {String} publicUrl 115 | * The public URL assigned by localtunnel. 116 | * @private 117 | * @readonly 118 | */ 119 | publicUrl: { 120 | enumerable: false, 121 | writable: true, 122 | configurable: false, 123 | value: null 124 | }, 125 | 126 | /** 127 | * @property {Object} tunnel 128 | * The localtunnel object. 129 | * @private 130 | * @readonly 131 | */ 132 | tunnel: { 133 | enumerable: false, 134 | writable: true, 135 | configurable: false, 136 | value: null 137 | }, 138 | 139 | /** 140 | * @property {Object} tunnelmonitor 141 | * An interval that monitors the public tunnel. 142 | * @private 143 | * @readonly 144 | */ 145 | tunnelmonitor:{ 146 | enumerable: false, 147 | writable: true, 148 | configurable: false, 149 | value: { 150 | monitoring: false 151 | } 152 | }, 153 | 154 | /** 155 | * @property {Boolean} autoRestartTunnel 156 | * Automatically restarts the sharing if the public connection is lost. 157 | * @readonly 158 | * @private 159 | */ 160 | autoRestartTunnel: { 161 | enumerable: false, 162 | writable: true, 163 | configurable: false, 164 | value: true 165 | }, 166 | 167 | /** 168 | * @property {Object} requests 169 | * The last 100 requests (by timestamp) that have been logged. 170 | * @readonly 171 | */ 172 | requests: { 173 | enumerable: true, 174 | writable: true, 175 | configurable: true, 176 | value: {} 177 | }, 178 | 179 | /** 180 | * @property {Boolean} connecting 181 | * Indicates the request bin is attempting to establish a #tunnel. 182 | * @readonly 183 | */ 184 | connecting: { 185 | enumerable: true, 186 | writable: true, 187 | configurable: false, 188 | value: false 189 | }, 190 | 191 | /** 192 | * @property {Boolean} shared 193 | * Indicates whether the bin is shared publicly or not. 194 | * @private 195 | */ 196 | shared: { 197 | enumerable: false, 198 | writable: true, 199 | configurable: false, 200 | value: false 201 | } 202 | 203 | }); 204 | 205 | var me = this; 206 | 207 | // Get the computer's MAC address 208 | require('getmac').getMac(function(err,addr){ 209 | if (err) throw err; 210 | me.MAC = addr; 211 | me.computer = addr; 212 | 213 | // Handle all requests 214 | me.app.head('/ping_fenix',function(req,res){ 215 | res.send(200); 216 | return; 217 | }); 218 | me.app.all('/*',function(req,res){ 219 | if(req.url !== '/fenix_ping'){ 220 | me.addRequest({ 221 | method: req.method, 222 | headers: req.headers, 223 | body: req.body.replace(/\/gi,'>'), 224 | query: req.query, 225 | url: req.url, 226 | source: req.ip 227 | }); 228 | } 229 | res.send(200); 230 | }); 231 | 232 | /** 233 | * @event ready 234 | * Fired when the RequestBrowser is initialized. 235 | */ 236 | me.emit('ready'); 237 | }); 238 | }, 239 | 240 | /** 241 | * @method start 242 | * Start the routing service on the specified port (defaults to 80). 243 | * @param {Function} [callback] 244 | * Fired when the server is started. 245 | * @fires start 246 | * @fires share 247 | */ 248 | start: function(cb){ 249 | var me = this; 250 | 251 | // Start the server 252 | app.listen(this.port,function(){ 253 | me.running = true; 254 | 255 | /** 256 | * @event start 257 | * Fired when the server is started locally. 258 | */ 259 | me.emit('start',me.port); 260 | 261 | // Start listening on localtunnel 262 | cb && cb(); 263 | }); 264 | 265 | }, 266 | 267 | /** 268 | * @method stop 269 | * Stop the proxy server. 270 | * @param {Function} [callback] 271 | * Fired when the server is stopped. 272 | * @fires stop 273 | * @fires unshare 274 | */ 275 | stop: function(cb){ 276 | var me = this; 277 | 278 | this.app.once('close',function(){ 279 | cb && cb(); 280 | }); 281 | 282 | if (this.shared){ 283 | this.unshare(function(){ 284 | me.app.close(); 285 | }); 286 | } else { 287 | this.app.close(); 288 | } 289 | }, 290 | 291 | /** 292 | * @method restart 293 | * Restart the proxy server. 294 | * @param {Function} callback 295 | * Executed when the restart is complete. 296 | */ 297 | restart: function(cb){ 298 | var me = this; 299 | this.stop(function(){ 300 | me.start(function(){ 301 | cb && cb(); 302 | }); 303 | }); 304 | }, 305 | 306 | /** 307 | * @method pingtunnel 308 | * Ping the tunnel service. 309 | * @private 310 | */ 311 | pingtunnel: function(){ 312 | if (!this.shared){ 313 | clearInterval(me.tunnelmonitor.interval); 314 | return; 315 | } 316 | console.log('Checking '+this.publicUrl+' at '+(new Date()).toLocaleTimeString()); 317 | var me = this; 318 | request.head(me.publicUrl+'/ping_fenix',{ 319 | headers:{ 320 | 'user-agent':'fenix' 321 | } 322 | },function(err,res,body){ 323 | if (err){ 324 | me.tunnel.emit('error',err); 325 | return; 326 | } 327 | switch (parseInt(res.statusCode)){ 328 | case 504: 329 | me.tunnel.emit('error',new Error('The localtunnel service timed out.')); 330 | return; 331 | case 200: 332 | return; 333 | default: 334 | me.tunnel.emit('error',new Error('Unknown error with localtunnel. Status code: '+res.statusCode.toString())); 335 | return; 336 | } 337 | }); 338 | }, 339 | 340 | /** 341 | * @method share 342 | * Start the localtunnel connection. This makes the request bin publicly accessible. 343 | * @param {Function} callback 344 | * Executed when the restart is complete. 345 | * @fires connecting 346 | */ 347 | share: function(cb){ 348 | var me = this; 349 | 350 | this.connecting = true; 351 | setTimeout(function(){ 352 | if (me.shared || lt !== undefined){ 353 | return; 354 | } 355 | me.autoRestartTunnel = false; 356 | me.connecting = false; 357 | if (me.tunnel !== null){ 358 | me.tunnel = null; 359 | } 360 | /** 361 | * @event timeout 362 | * Fired when sharing cannot be established in a reasonable amount of time. 363 | */ 364 | me.emit('timeout'); 365 | },5000); 366 | /** 367 | * @event connecting 368 | * Fired when a localtunnel connection is initiated. 369 | */ 370 | this.emit('connecting'); 371 | var lt = localtunnel(me.port,{subdomain:me.subdomain},function(err, tunnel){ 372 | 373 | if (err) { 374 | me.emit('error',err); 375 | console.log(err); 376 | cb && cb(err); 377 | return; 378 | } 379 | 380 | // Autorestart on close 381 | tunnel.on('close', function(){ 382 | clearInterval(me.tunnelmonitor.interval); 383 | me.publicUrl = null; 384 | me.shared = false; 385 | /** 386 | * @event unshare 387 | * Fired when the server stops sharing publicly. 388 | */ 389 | me.emit('unshare'); 390 | if (me.autoRestartTunnel){ 391 | me.start(); 392 | } 393 | }); 394 | 395 | tunnel.on('error',function(e){ 396 | clearInterval(me.tunnelmonitor.interval); 397 | me.emit('error',e); 398 | }); 399 | 400 | me.publicUrl = tunnel.url; 401 | me.shared = true; 402 | me.tunnel = tunnel; 403 | me.connecting = false; 404 | 405 | // Monitor the connection with public requests every 2 minutes 406 | me.tunnelmonitor.interval = setInterval(function(){ 407 | me.pingtunnel(); 408 | },60*1000); 409 | me.pingtunnel(); 410 | 411 | /** 412 | * @event share 413 | * Fired when the server is started and available publicly. 414 | */ 415 | me.emit('share',tunnel.url); 416 | cb && cb(); 417 | }); 418 | }, 419 | 420 | /** 421 | * @method unshare 422 | * Stop the localtunnel connection. This does not stop the web server, so 423 | * requests can still be captured on localhost. 424 | * @param {Function} callback 425 | * Executed when the restart is complete. 426 | */ 427 | unshare: function(cb){ 428 | var me = this, ars = this.autoRestartTunnel; 429 | this.autoRestartTunnel = false; 430 | 431 | this.tunnel.once('close',function(){ 432 | me.autoRestartTunnel = ars; 433 | setTimeout(function(){ 434 | cb && cb(); 435 | },500); 436 | }); 437 | 438 | this.tunnel.close(); 439 | }, 440 | 441 | /** 442 | * @method getRequestDetails 443 | * Return an object containing the details of the request. 444 | */ 445 | getRequestDetails: function(dt){ 446 | return this.requests[dt] || null; 447 | }, 448 | 449 | /** 450 | * @method addRequest 451 | * Add a request to the queue 452 | */ 453 | addRequest: function(req){ 454 | var timestamp = (new Date()).toJSON(); 455 | 456 | this.requests[timestamp] = req; 457 | 458 | req.timestamp = timestamp; 459 | 460 | /** 461 | * @event request 462 | * Fires when a request is received. Sends the request as an argument to the event handler. 463 | */ 464 | this.emit('request',req); 465 | } 466 | 467 | }); 468 | -------------------------------------------------------------------------------- /src/lib/api/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Router 3 | * The primary router. 4 | * @singleton 5 | * @extends Utility 6 | * @uses Server 7 | */ 8 | var Router = Utility.extend({ 9 | 10 | constructor: function(config){ 11 | config = config || {}; 12 | 13 | Router.super.constructor.call(this,config); 14 | 15 | Object.defineProperties(this,{ 16 | 17 | /** 18 | * @property {Object} proxy 19 | * The proxy server object. 20 | * @private 21 | * @readonly 22 | */ 23 | proxy: { 24 | enumerable: false, 25 | writable: true, 26 | configurable: false, 27 | value: null 28 | }, 29 | 30 | /** 31 | * @property {Boolean} running 32 | * Indicates whether the proxy server is running. 33 | */ 34 | running: { 35 | enumerable: true, 36 | writable: true, 37 | configurable: false, 38 | value: false 39 | }, 40 | 41 | /** 42 | * @property {Boolean} loading 43 | * Indicates the router is loading servers. 44 | */ 45 | loading: { 46 | enumerable: true, 47 | writable: true, 48 | configurable: false, 49 | value: false 50 | }, 51 | 52 | /** 53 | * @property {Object} domainmap 54 | * A mappping of the domain names to ports. 55 | * 56 | * { 57 | * 'mydomain.com': '127.0.0.1:8080', 58 | * 'otherdomain.com': '127.0.0.1:8181' 59 | * } 60 | * @readonly 61 | * @private 62 | */ 63 | domainmap: { 64 | enumerable: false, 65 | get: function(){ 66 | var rt = {}, me=this; 67 | Object.keys(this.servers||{}).forEach(function(serverid){ 68 | if ((me.servers[serverid].domain||'').trim().length > 0){ 69 | rt[me.servers[serverid].domain] = '127.0.0.1:'+me.servers[serverid].port.toString(); 70 | } 71 | }); 72 | return rt; 73 | } 74 | }, 75 | 76 | /** 77 | * @property {Object} servers 78 | * Contains the servers managed by this instance of Fenix. 79 | * @private 80 | */ 81 | servers: { 82 | enumerable: false, 83 | writable: true, 84 | configurable: false, 85 | value: {} 86 | }, 87 | 88 | /** 89 | * @property {Object} paths 90 | * An object containing all of the paths used by the managed servers. 91 | * @private 92 | */ 93 | paths: { 94 | enumerable: false, 95 | get: function(){ 96 | var paths = {}, me = this; 97 | Object.keys(this.servers||{}).forEach(function(serverid){ 98 | Object.defineProperty(paths,me.servers[serverid].path,{ 99 | enumerable: false, 100 | get: function(){ 101 | return me.servers[serverid]; 102 | } 103 | }); 104 | }); 105 | return paths; 106 | } 107 | }, 108 | 109 | /** 110 | * @property {Object} ports 111 | * The object consists of keys made up of the ports in use. The value of each key 112 | * is the server it is associated with. 113 | */ 114 | ports: { 115 | enumerable: false, 116 | get: function(){ 117 | var ports = {}, me = this; 118 | Object.keys(this.servers||{}).forEach(function(serverid){ 119 | Object.defineProperty(ports,me.servers[serverid].port,{ 120 | enumerable: false, 121 | get: function(){ 122 | return me.servers[serverid]; 123 | } 124 | }); 125 | }); 126 | return ports; 127 | } 128 | }, 129 | 130 | /** 131 | * @property serverstore 132 | * The absolute path of the location/file where servers are persisted. 133 | * @private 134 | */ 135 | serverstore: { 136 | enumerable: false, 137 | get: function(){ 138 | var p = require('path'); 139 | switch (process.platform.toLowerCase()) { 140 | case 'win32': return p.join(process.env.APPDATA,'Fenix','servers.fnx'); 141 | case 'darwin': return '/Users/Shared/Fenix.localized/servers.fnx'; 142 | default: return p.resolve(p.join('./','servers.fnx')); 143 | } 144 | } 145 | }, 146 | 147 | /** 148 | * @property {String} MAC 149 | * The MAC address of the computer running the router. 150 | */ 151 | MAC: { 152 | enumerable: false, 153 | writable: true, 154 | configurable: false, 155 | value: null 156 | }, 157 | 158 | /** 159 | * @property {Object} portscanner 160 | * A port scanner used for introspective assesment of the servers. 161 | * @protected 162 | */ 163 | portscanner: { 164 | enumerable: false, 165 | get: function(){ 166 | return require('portscanner'); 167 | } 168 | }, 169 | 170 | _socket:{ 171 | enumerable: false, 172 | writable: true, 173 | configurable: false, 174 | value: null 175 | }, 176 | 177 | /** 178 | * @property {net.Socket} socket 179 | * The socket server that enables CLI & other remote tools. 180 | * @protected 181 | */ 182 | socket: { 183 | enumerable: false, 184 | get: function(){ 185 | if (this._socket == null){ 186 | var net = require('net'), me = this; 187 | this._socket = net.createServer(function(conn){ 188 | conn.on('connection',function(socket){ 189 | socket.on('connect',function(){ 190 | console.log(socket.address()); 191 | me.emit('socketconnect'); 192 | }); 193 | }); 194 | conn.on('close',function(){ 195 | me.emit('socketclose'); 196 | }); 197 | }); 198 | } 199 | return this._socket; 200 | } 201 | } 202 | 203 | }); 204 | 205 | var me = this; 206 | 207 | // Get the computer's MAC address 208 | require('getmac').getMac(function(err,addr){ 209 | if (err) { 210 | addr = "UNKNOWN"; 211 | } 212 | me.MAC = addr; 213 | me.computer = addr; 214 | }); 215 | 216 | // Boot up the socket server 217 | this.socket.listen(336490,function(){ 218 | me.emit('socketready'); 219 | }); 220 | }, 221 | 222 | /** 223 | * @method getServer 224 | * Return the Server instance for the provided Server#id. Returns `null` if no server is found 225 | * for the specified ID. 226 | * @param {String} serverid 227 | * The ID of the Server to return. 228 | * @returns {Server} 229 | */ 230 | getServer: function(serverid){ 231 | return this.servers[serverid] || null; 232 | }, 233 | 234 | /** 235 | * @method createServer 236 | * Create a new server 237 | * @param {Object} serverConfiguration 238 | * Provide a server configuration. 239 | * @fires createserver 240 | */ 241 | createServer: function(config){ 242 | config = config || {}; 243 | config.id = require('crypto').createHash('md5').update(config.path+(config.port||80).toString()).digest('hex');//this.generateUUID(); 244 | this.servers[config.id] = new Server(config); 245 | this.save(); 246 | /** 247 | * @event createserver 248 | * Fired when a new server is created. 249 | * @param {Server} server 250 | * The Server that was added to the router. 251 | */ 252 | this.emit('createserver',this.servers[config.id]); 253 | return this.servers[config.id]; 254 | }, 255 | 256 | /** 257 | * @method deleteServer 258 | * Remove a specific server. 259 | * @fires deleteserver 260 | */ 261 | deleteServer: function(id){ 262 | var me = this; 263 | if(!this.servers.hasOwnProperty(id)){ 264 | throw new Error('Server '+id+' does not exist or could not be found.'); 265 | return; 266 | } 267 | this.servers[id].on('stop',function(){ 268 | var s = me.servers[id]; 269 | delete me.servers[id]; 270 | me.save(); 271 | /** 272 | * @event deleteserver 273 | * Fired when a server is removed from the router. 274 | * @param {Server} server 275 | * The server object that was removed. 276 | */ 277 | me.emit('deleteserver', s); 278 | }); 279 | this.servers[id].stop(); 280 | }, 281 | 282 | /** 283 | * @method startAllWebServers 284 | * Start all of the servers. 285 | */ 286 | startAllWebServers: function(){ 287 | var me = this; 288 | Object.keys(this.servers||{}).forEach(function(id){ 289 | !me.servers[id].running && me.servers[id].start(); 290 | }); 291 | }, 292 | 293 | /** 294 | * @method stopAllWebServers 295 | * Stop all of the servers. 296 | */ 297 | stopAllWebServers: function(){ 298 | var me = this; 299 | Object.keys(this.servers||{}).forEach(function(id){ 300 | me.servers[id].running && me.servers[id].stop(); 301 | }); 302 | }, 303 | 304 | /** 305 | * @method getAvailablePort 306 | * Returns the first available unused port. If no port range is specified, a port between 80-10000 will be returned. 307 | * This method will not return a port that is registered for use by one of the servers this router manage, regardless 308 | * of whether the server is running or not (it just needs to be registered with the router). 309 | * @param {Function} callback (required) 310 | * The callback to execute when a port is found. 311 | * @param {Number} callback.port 312 | * The first available port returned from this operation. This will be `-1` of no available port is available. 313 | * @param {Number} [minPort=80] 314 | * The lower bound of the port range to find an available port in. 315 | * @param {Number} [maxPort=10000] 316 | * The upper bound of the port range to find an available port in. 317 | */ 318 | getAvailablePort: function(callback,min,max){ 319 | if (typeof callback !== 'function'){ 320 | throw new Error('getAvailablePort must be given a callback method.'); 321 | } 322 | var me = this; 323 | min = min || 80; 324 | max = max || 10000; 325 | this.portscanner.basePort = min; 326 | this.portscanner.findAPortNotInUse(min,max,'localhost',function(err,p){ 327 | if (err){ 328 | callback(-1); 329 | console.error(err.message); 330 | return; 331 | } 332 | if (me.ports.hasOwnProperty(p)){ 333 | me.getAvailablePort(callback,(p+1),max); 334 | } else { 335 | callback(p); 336 | } 337 | }); 338 | }, 339 | 340 | /** 341 | * @method start 342 | * Start the routing service on the specified port (defaults to 80). 343 | * @param {Number} [port=80] 344 | * The port to run the proxy server on. 345 | * @param {Function} [callback] 346 | * Run this after the router is started. 347 | * @fires startproxy 348 | */ 349 | start: function(port,cb){ 350 | 351 | port = port || 80; 352 | 353 | if (typeof port === 'function'){ 354 | cb = port; 355 | port = 80; 356 | } 357 | 358 | var httpProxy = require('http-proxy'); 359 | 360 | this.proxy = httpProxy.createServer({ 361 | hostnameOnly: true, 362 | router: this.domainmap 363 | }); 364 | 365 | this.proxy.listen(port,function(){ 366 | me.emit('startproxy',this.proxy); 367 | cb && cb(); 368 | }); 369 | }, 370 | 371 | /** 372 | * @method stop 373 | * Stop the proxy server. 374 | * @param {Function} [callback] 375 | * Fired when the proxy server is stopped. 376 | * @fires stopproxy 377 | */ 378 | stop: function(cb){ 379 | this.proxy.close(); 380 | this.emit('stopproxy'); 381 | cb && cb(); 382 | }, 383 | 384 | /** 385 | * @method restart 386 | * Restart the proxy server. 387 | * @param {Function} callback 388 | * Executed when the restart is complete. 389 | */ 390 | restart: function(cb){ 391 | var me = this; 392 | this.stop(function(){ 393 | me.start(function(){ 394 | cb && cb(); 395 | }); 396 | }); 397 | }, 398 | 399 | /** 400 | * @method save 401 | * Save the servers to the `servers.json` file. 402 | */ 403 | save: function() { 404 | var me = this; 405 | var data = []; 406 | Object.keys(this.servers).forEach(function(server){ 407 | var d = me.servers[server].json; 408 | d.path = me.servers[server].path; 409 | d.running = me.servers[server].running; 410 | delete d.cfg_paths; 411 | delete d.screenshot; 412 | delete d.suppressnotices; 413 | delete d.starting; 414 | delete d.stopping; 415 | d.port = parseInt(d.port); 416 | data.push(d); 417 | }); 418 | var _p = require('path').dirname(this.serverstore), fs = require('fs'), pth = require('path'); 419 | if (!fs.existsSync(pth.resolve(_p))){ 420 | fs.mkdirSync(pth.resolve(_p)); 421 | } 422 | fs.writeFileSync(pth.join(_p,'servers.fnx'),JSON.stringify(data,null,2)); 423 | }, 424 | 425 | /** 426 | * @method load 427 | * Load the saved state of servers. 428 | */ 429 | load: function(){ 430 | var me = this; 431 | this.loading = true; 432 | try { 433 | if (require('fs').existsSync(this.serverstore)){ 434 | var svrs = JSON.parse(require('fs').readFileSync(this.serverstore)); 435 | svrs.forEach(function(server){ 436 | me.servers[server.id] = new Server({ 437 | name: server.name, 438 | domain: server.domain, 439 | port: server.port, 440 | path: server.path, 441 | id: server.id 442 | }); 443 | if (server.running){ 444 | me.servers[server.id].start(); 445 | } 446 | /** 447 | * @event loadserver 448 | * Fired when a server is loaded from disk. 449 | */ 450 | me.emit('loadserver',me.servers[server.id]); 451 | }); 452 | } 453 | } catch (e) { 454 | alert(e.message); 455 | } 456 | this.loading = false; 457 | /** 458 | * @event loadcomplete 459 | * Fired when the load process is complete. 460 | */ 461 | this.emit('loadcomplete'); 462 | } 463 | 464 | }); 465 | -------------------------------------------------------------------------------- /src/lib/api/syslog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @Class Syslog 3 | * Represents a system log. 4 | * @extends Utility 5 | */ 6 | var Syslog = Utility.extend({ 7 | constructor: function(config){ 8 | config = config || {}; 9 | 10 | Syslog.super.constructor.call(this,config); 11 | 12 | Object.defineProperties(this,{ 13 | 14 | /** 15 | * @property {Object} log 16 | * The contents of the log. This is a key/value object with timestamps for keys and messages for values. 17 | * @private 18 | */ 19 | syslog: { 20 | enumerable: false, 21 | writable: true, 22 | configurable: false, 23 | value: {} 24 | }, 25 | 26 | /** 27 | * @method prelog 28 | * Preprocessing for log messages. 29 | * @private 30 | */ 31 | prelog: { 32 | enumerable: false, 33 | writable: false, 34 | configurable: false, 35 | value: function(msg){ 36 | var m = null; 37 | switch(typeof msg){ 38 | case 'function': 39 | m = '[Function]'; 40 | break; 41 | case 'object': 42 | m = JSON.stringify(msg,null,2); 43 | break; 44 | default: 45 | m = msg.toString(); 46 | break; 47 | } 48 | return m; 49 | } 50 | }, 51 | 52 | timestamp: { 53 | enumerable: false, 54 | get: function() { 55 | var now = new Date(), 56 | date = [ now.getMonth() + 1, now.getDate(), now.getFullYear() ], 57 | time = [ now.getHours(), now.getMinutes(), now.getSeconds() ], 58 | suffix = ( time[0] < 12 ) ? "AM" : "PM"; 59 | 60 | time[0] = ( time[0] < 12 ) ? time[0] : time[0] - 12; 61 | time[0] = time[0] || 12; 62 | 63 | for ( var i = 1; i < 3; i++ ) { 64 | if ( time[i] < 10 ) { 65 | time[i] = "0" + time[i]; 66 | } 67 | } 68 | return date.join("/") + " " + time.join(":") + " " + suffix; 69 | } 70 | } 71 | 72 | }); 73 | }, 74 | 75 | /** 76 | * @method log 77 | * Add an entry to the log. 78 | * @param {Any} message 79 | * The message or data to log. 80 | * @fires log 81 | */ 82 | log: function(msg) { 83 | msg = this.prelog('['+this.timestamp+'] '+msg); 84 | this.syslog[+new Date] = msg; 85 | this.emit('log',msg); 86 | }, 87 | 88 | /** 89 | * @method error 90 | * Add an error entry to the log. 91 | * @param {Any} message 92 | * The message or data to log. 93 | * @fires errormsg 94 | */ 95 | error: function(msg) { 96 | msg = this.prelog('['+this.timestamp+'] '+'ERROR: '+msg); 97 | this.syslog[+new Date] = msg; 98 | this.emit('errormsg',msg); 99 | }, 100 | 101 | /** 102 | * @method warn 103 | * Add a warning entry to the log. 104 | * @param {Any} message 105 | * The message or data to log. 106 | * @fires warn 107 | */ 108 | warn: function(msg) { 109 | msg = this.prelog('['+this.timestamp+'] '+'WARNING: '+msg); 110 | this.syslog[+new Date] = msg; 111 | this.emit('warn',msg); 112 | } 113 | }); -------------------------------------------------------------------------------- /src/lib/api/utility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class Utility 3 | * A utility class to be subclassed. 4 | */ 5 | var Utility = Class.extend({ 6 | 7 | constructor: function(config){ 8 | 9 | Object.defineProperties(this,{ 10 | /** 11 | * @property {Object} json 12 | * The JSON representation of all enumerable properties. 13 | * @private 14 | */ 15 | json: { 16 | enumerable: false, 17 | get: function(){ 18 | return this.serialize(this); 19 | } 20 | } 21 | }); 22 | 23 | }, 24 | 25 | /** 26 | * @method serialize 27 | * Serialize an Object as JSON. 28 | * @params {Object} obj 29 | * The object to serialize. 30 | */ 31 | serialize: function(obj){ 32 | var out = Array.isArray(obj) == true ? [] : {}; 33 | for (var attr in obj){ 34 | if (typeof obj[attr] !== 'function' && attr !== '_events' && obj[attr] !== undefined && obj.hasOwnProperty(attr)){ 35 | if (Array.isArray(obj[attr])) { 36 | // Handles Arrays 37 | out[attr] = []; 38 | for (var child=0; childhttp://localhost:"+RequestBrowser.port.toString()+"" 13 | +(RequestBrowser.connecting 14 | ? "
    Connecting...
    " 15 | : ""); 16 | $('#connect').click(function(e){ 17 | e.preventDefault(); 18 | RequestBrowser.share(); 19 | }); 20 | }, 21 | stop: function(){ 22 | UI.status.removeClass().addClass('offline').empty()[0].innerHTML = "Offline"; 23 | }, 24 | timeout: function(){ 25 | UI.status.removeClass().addClass('online').empty()[0].innerHTML = "Accepting requests at http://localhost:"+RequestBrowser.port.toString()+"" 26 | +(RequestBrowser.connecting 27 | ? "
    Connecting...
    " 28 | : "
    Remote server timed out.Try Again
    "); 29 | $('#connect').click(function(e){ 30 | e.preventDefault(); 31 | RequestBrowser.share(); 32 | }); 33 | UI.notify({ 34 | title: 'Connection Timeout', 35 | text: 'The request bin timed out while connecting to the remote localtunnel sharing server.' 36 | }); 37 | }, 38 | share: function(){ 39 | UI.status.removeClass().addClass('public').empty()[0].innerHTML = "Accepting requests at "+RequestBrowser.publicUrl+"" 40 | + ""; 41 | $('#disconnect').click(function(e){ 42 | e.preventDefault(); 43 | RequestBrowser.unshare(); 44 | }); 45 | }, 46 | unshare: function(){ 47 | UI.server.start(); 48 | //UI.status.removeClass().addClass('online').empty()[0].innerHTML = "Accepting requests at http://localhost:"+RequestBrowser.port.toString()+""; 49 | }, 50 | request: function(req){ 51 | UI.render.requestItem({ 52 | method: req.method, 53 | source: req.source, 54 | time: new Date(req.timestamp) 55 | }); 56 | UI.notify({ 57 | text: req.method.toUpperCase()+' request received from '+req.source, 58 | title: req.method.toUpperCase()+' Received' 59 | }); 60 | global.track.event('Webhook Receiver','request','Received '+req.method.toUpperCase()+' Request',1).send(); 61 | } 62 | }, 63 | render: { 64 | requestItem: function(req){ 65 | var dt = new Date(req.time); 66 | dt = dt.toLocaleDateString() + ' ' + dt.toLocaleTimeString(); 67 | var str = '
    '+req.method.toUpperCase()+''+req.source+''+dt+''; 68 | if (!$('body').hasClass('hasrequests')){ 69 | $('body').addClass('hasrequests'); 70 | } 71 | $('#requests').prepend(str); 72 | $('#requests > div').off('click').click(function(e){ 73 | e.preventDefault(); 74 | $(e.currentTarget.parentNode).find('.selected').removeClass('selected'); 75 | $(e.currentTarget).addClass('selected'); 76 | UI.render.request(e.currentTarget.id); 77 | }); 78 | // Auto-select the first post 79 | if ($('#requests > div.selected').length === 0 && $('#requests > div').length === 1){ 80 | $('#requests > div:first-child').addClass('selected'); 81 | UI.render.request(req.time.toJSON()); 82 | } 83 | gui.Window.get(this).requestAttention(true); 84 | }, 85 | request: function(timestamp){ 86 | req = RequestBrowser.getRequestDetails(timestamp); 87 | UI.request[0].setAttribute('method',req.method); 88 | UI.request[0].setAttribute('source',req.source); 89 | var type = req.headers.hasOwnProperty('content-type') ? (req.headers['content-type'].toLowerCase().indexOf('json') >= 0 ? 'javascript' : 'markup') : 'markup'; 90 | UI.request.empty()[0].innerHTML = 'Show Headers
    '+UI.render.headers(req.headers)+'
    '+UI.render.body(req.body)+'
    '; 91 | Prism.highlightAll();//Prism.highlightElement($('#code')[0]); 92 | $('a.url-link').forEach(function(el){ 93 | el.setAttribute('href','javascript:gui.Shell.openExternal("'+el.getAttribute('href')+'");'); 94 | }); 95 | $('#showheader').click(function(e){ 96 | e.preventDefault(); 97 | UI.render.toggleHeader(); 98 | }); 99 | $('#requests > div > span').off('click').click(function(e){ 100 | e.preventDefault(); 101 | UI.render.request(e.currentTarget.parentNode.id); 102 | }); 103 | }, 104 | headers: function(headers){ 105 | var str = ''; 106 | Object.keys(headers).forEach(function(type){ 107 | str += ''; 108 | }); 109 | return str += '
    HeaderValue
    '+type+':'+headers[type]+'
    '; 110 | }, 111 | body: function(body){ 112 | try { 113 | body = JSON.parse(body); 114 | } catch (e){ 115 | console.log(e); 116 | } 117 | return typeof body !== 'string' ? JSON.stringify(body,null,2) : body; 118 | }, 119 | showHeaders: function(){ 120 | $('#headers').removeClass('hide'); 121 | $('#showheader')[0].innerHTML = 'Hide Headers'; 122 | $('#headers').addClass('fadeInDown'); 123 | setTimeout(function(){ 124 | $('#headers').removeClass('fadeInDown'); 125 | },700); 126 | }, 127 | hideHeaders: function(){ 128 | $('#showheader')[0].innerHTML = 'Show Headers'; 129 | $('#headers').addClass('fadeOutUp'); 130 | setTimeout(function(){ 131 | $('#headers').addClass('hide'); 132 | $('#headers').removeClass('fadeOutUp'); 133 | },700); 134 | }, 135 | toggleHeader: function(){ 136 | if ($('#headers').hasClass('hide')){ 137 | UI.render.showHeaders(); 138 | } else { 139 | UI.render.hideHeaders(); 140 | } 141 | } 142 | }, 143 | _growl:{ 144 | app: new Growler.GrowlApplication('Fenix'), 145 | init: function(){ 146 | UI._growl.app.setNotifications({ 147 | 'Status': { 148 | displayname: 'Fenix Receiver', 149 | enabled: true, 150 | icon: require('fs').readFileSync('./lib/icons/webhooks.png') 151 | } 152 | }); 153 | UI._growl.app.register(); 154 | UI._growl.initialized = true; 155 | }, 156 | initialized: false 157 | }, 158 | notify: function(msg){ 159 | if (!UI._growl.initialized){ 160 | UI._growl.init(); 161 | } 162 | msg = msg || {}; 163 | if (typeof msg === 'string'){ 164 | msg = { 165 | text: msg, 166 | title: 'Fenix Alert' 167 | //sticky, 168 | //priority (1,2,3,4,5.... 2 = critical), 169 | //icon 170 | }; 171 | } 172 | msg.priority = msg.priority || 3; 173 | UI._growl.app.sendNotification(msg.type||'Status',msg); 174 | } 175 | }; 176 | UI._growl.init(); 177 | 178 | // When the browser is ready, start the server 179 | RequestBrowser.on('ready',function(){ 180 | RequestBrowser.start(); 181 | }); 182 | 183 | // When the server changes state, show the user. 184 | RequestBrowser.on('start',UI.server.start); 185 | RequestBrowser.on('share',function(){ 186 | UI.server.share.apply(this,arguments); 187 | UI.notify({ 188 | title: 'Fenix Receiver Shared', 189 | text: 'The Fenix Receiver is now shared publicly.' 190 | }); 191 | }); 192 | RequestBrowser.on('unshare',function(){ 193 | UI.server.unshare.apply(this,arguments); 194 | UI.notify({ 195 | title: 'Fenix Receiver Unshared', 196 | text: 'The Fenix Receiver is no longer shared publicly.' 197 | }); 198 | global.track.event('Webhook Receiver','unshare','Unshared Webhook Receiver',1).send(); 199 | }); 200 | RequestBrowser.on('stop', function(){ 201 | UI.server.stop.apply(this,arguments); 202 | UI.notify({ 203 | title: 'Fenix Receiver Stopped', 204 | text: 'The Fenix Receiver server has stopped.' 205 | }); 206 | }); 207 | RequestBrowser.on('request',UI.server.request); 208 | RequestBrowser.on('connecting',UI.server.start); 209 | 210 | RequestBrowser.on('timeout',UI.server.timeout); 211 | 212 | global.windows.bin.on('autoshare',function(){ 213 | RequestBrowser.share(); 214 | }); 215 | -------------------------------------------------------------------------------- /src/lib/controller/bootstrap.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | gui = require('nw.gui'), 4 | head = gui.Window.get(this), 5 | pkg = require(path.resolve('./','package.json')), 6 | configuration = require(path.resolve(path.join('./','lib','config.json'))), 7 | config = configuration, 8 | win = { 9 | splash: path.join('.','lib','view','splash.html'), 10 | main: path.join('.','lib','view','main.html') 11 | }, 12 | uuid = require('node-uuid') 13 | ua = require('universal-analytics'), 14 | active = null; 15 | 16 | process.on('uncaughtexception',function(e){ 17 | console.dir(e); 18 | alert(e.message); 19 | if (e.code ==='EACCES'){ 20 | alert('Insufficient permissions.'); 21 | } 22 | if (!head.isDevToolsOpen() && config.dev){ 23 | head.showDevTools(); 24 | } 25 | }); 26 | if (config.dev){ 27 | head.showDevTools(); 28 | } 29 | 30 | var ganoop = function(){ 31 | return {send:function(){}}; 32 | } 33 | 34 | // Generate global references 35 | Object.defineProperties(global,{ 36 | windows: { 37 | enumerable: true, 38 | get: function(){ 39 | return win; 40 | } 41 | }, 42 | OS: { 43 | enumerable: true, 44 | get: function(){ 45 | return require('os').platform(); 46 | } 47 | }, 48 | trigger: { 49 | enumerable: true, 50 | writable: false, 51 | configurable: false, 52 | value: function(controller,evt){ 53 | win[controller].emit(evt); 54 | } 55 | }, 56 | track: { 57 | enumerable: true, 58 | get: function(){ 59 | // Get ID 60 | if(localStorage.getItem('fid') === undefined){ 61 | localStorage.setItem('fid',uuid.v4()); 62 | } 63 | if (configuration.googleanalytics){ 64 | return { 65 | event: ganoop, 66 | pageview: ganoop 67 | }; 68 | } 69 | return ua(configuration.googleanalytics,localStorage.getItem('fid')); 70 | } 71 | }, 72 | pkg: { 73 | enumerable: true, 74 | writable: false, 75 | configurable: false, 76 | value: pkg 77 | } 78 | }); 79 | 80 | // Default values 81 | var autoshow = config.main.hasOwnProperty('show') ? config.main.show : true; 82 | config.main.hasOwnProperty('show') && delete config.main.show; 83 | config.dev = config.hasOwnProperty('dev') ? config.dev : false; 84 | 85 | // If a splash screen exists, use it 86 | fs.exists(win.splash,function(splashexists){ 87 | if (splashexists){ 88 | // Create the splash page 89 | var cfg = config.splash; 90 | win.splash = gui.Window.open(win.splash,{ 91 | frame: cfg.hasOwnProperty('frame') ? cfg.frame : false, 92 | toolbar: config.dev ? true : (cfg.hasOwnProperty('toolbar') ? cfg.toolbar : false), 93 | width: cfg.width || 600, 94 | height: cfg.height || 400, 95 | transparent: cfg.hasOwnProperty('transparent') ? cfg.transparent : true, 96 | show_in_taskbar: cfg.hasOwnProperty('show_in_taskbar') ? cfg.show_in_taskbar : false, 97 | icon: path.resolve(config.icon), 98 | frame: config.dev === true ? true : (cfg.hasOwnProperty('frame') ? cfg.frame : false), 99 | 'new-instance': false 100 | }); 101 | } 102 | 103 | // Load any additional windows required by the app 104 | Object.keys(config.windows||{}).forEach(function(w){ 105 | var cfg = config.windows[w]; 106 | cfg.show = cfg.hasOwnProperty('show') ? cfg.show : true; 107 | cfg.show_in_taskbar = cfg.hasOwnProperty('show_in_taskbar') ? cfg.show_in_taskbar : false; 108 | cfg.icon = cfg.icon || path.resolve(config.icon); 109 | cfg.toolbar = config.dev ? true : (cfg.hasOwnProperty('toolbar') ? cfg.toolbar : false); 110 | cfg.frame = config.dev ? true : (cfg.hasOwnProperty('frame') ? cfg.frame : true); 111 | cfg['new-instance'] = false; 112 | win[w] = new gui.Window.open(path.join('.','lib','view',w+'.html'),cfg); 113 | if (cfg.close){ 114 | win[w].on('close',function(){ 115 | this.hide(); 116 | }); 117 | } 118 | }); 119 | 120 | // Load the main window 121 | var cfg = config.main; 122 | win.main = new gui.Window.open(win.main,{ 123 | show: autoshow, 124 | frame: config.dev ? true : (cfg.hasOwnProperty('frame') ? cfg.frame : true), 125 | toolbar: config.dev ? true : (cfg.hasOwnProperty('toolbar') ? cfg.toolbar : false), 126 | width: cfg.width || pkg.window.width, 127 | height: cfg.height || pkg.window.height, 128 | transparent: cfg.hasOwnProperty('transparent') ? cfg.transparent : true, 129 | show_in_taskbar: cfg.hasOwnProperty('show_in_taskbar') ? cfg.show_in_taskbar : true, 130 | icon: path.resolve(config.icon), 131 | 'new-instance': false 132 | }); 133 | 134 | // When the main window is closed, the app should close 135 | win.main.on('close',function(){ 136 | head.close(); 137 | }); 138 | 139 | // When the custom "ready" event is fired from the main window, close the splash page if it 140 | // exists and unhide the main window. 141 | win.main.once('ready',function(){ 142 | win.main.show(); 143 | if (!(win.splash instanceof String)){ 144 | win.splash.close(); 145 | } 146 | win.main.focus(); 147 | global.track.event('Application','launch','Opened Desktop Application').send(); 148 | global.track.pageview('/app/webservers').send(); 149 | }); 150 | 151 | // If autoshow is not turned on (on by default), fire the main window ready event. 152 | win.main.on('loaded',function(){ 153 | if (autoshow){ 154 | win.main.emit('ready'); 155 | } 156 | }); 157 | 158 | }); 159 | 160 | // When this controller closes, the whole app should close. 161 | head.on('close',function(){ 162 | gui.App.closeAllWindows(); 163 | gui.App.quit(); 164 | }); 165 | 166 | global.windows.head = head; 167 | -------------------------------------------------------------------------------- /src/lib/controller/main.js: -------------------------------------------------------------------------------- 1 | var gui = require('nw.gui'), 2 | path = require('path'); 3 | 4 | // Handle menu clicks 5 | $('nav > li > ul > li').click(function(e){ 6 | e.preventDefault(); 7 | $('nav > li > ul').addClass('hide'); 8 | setTimeout(function(){ 9 | $('nav > li > ul').removeClass('hide'); 10 | },300); 11 | eval($(e.currentTarget).find('a')[0].href.replace('javascript:','')); 12 | }); 13 | 14 | $('nav > li > ul > li > a').click(function(e){ 15 | $('nav > li > ul').addClass('hide'); 16 | setTimeout(function(){ 17 | $('nav > li > ul').removeClass('hide'); 18 | },300); 19 | }); 20 | 21 | if (process.platform === 'darwin'){ 22 | $('nav').css('background-color','#B1B1B1'); 23 | $('nav').css('display','flex'); 24 | $('nav').css('font-weight','bold'); 25 | global.windows.main.on('blur',function(){ 26 | $('nav').css('background-color','#E1E1E1'); 27 | }); 28 | global.windows.main.on('focus',function(){ 29 | $('nav').css('background-color','#B1B1B1'); 30 | }); 31 | } 32 | 33 | binopened = false; 34 | var openRequestBin = function(){ 35 | global.track.event('Application','webhooks','Opened Webhooks').send(); 36 | global.track.pageview('/app/webhooks/open').send(); 37 | !binopened && global.windows.bin.emit('autoshare'); 38 | global.windows.bin.show(); 39 | global.windows.bin.focus(); 40 | binopened = true; 41 | }; 42 | 43 | var openAbout = function(){ 44 | global.track.event('Application','help','Opened Help').send(); 45 | global.track.pageview('/app/help/open').send(); 46 | global.windows.about.show(); 47 | global.windows.about.focus(); 48 | }; 49 | 50 | global.windows.main.on('close',function(){ 51 | ROUTER.save(); 52 | }); 53 | 54 | // When the main window is minimized, place the app in the system tray. 55 | var tray; 56 | global.windows.main.on('minimize', function() { 57 | this.hide(); 58 | 59 | var tooltip = global.pkg.window.title; 60 | 61 | tray = new gui.Tray({ 62 | icon: path.resolve(global.pkg.window.icon), 63 | title: tooltip 64 | }); 65 | tray.tooltip = tooltip; // On Windows the tooltip needs to be set after the Tray is created. 66 | 67 | tray.on('click', function() { 68 | global.windows.main.show(); 69 | this.remove(); 70 | tray = null; 71 | }); 72 | 73 | // Create the tray menu with some basic functionality. 74 | var menu = new gui.Menu(); 75 | menu.append(new gui.MenuItem({ 76 | label: 'View Servers', 77 | click: function() { tray.emit('click'); } 78 | })); 79 | menu.append(new gui.MenuItem({ type: 'separator' })); 80 | menu.append(new gui.MenuItem({ 81 | label: 'Start All', 82 | click: function() { ROUTER.startAllWebServers(); } 83 | })); 84 | menu.append(new gui.MenuItem({ 85 | label: 'Stop All', 86 | click: function() { ROUTER.stopAllWebServers(); } 87 | })); 88 | menu.append(new gui.MenuItem({ type: 'separator' })); 89 | menu.append(new gui.MenuItem({ 90 | label: 'Quit', 91 | click: function() { global.windows.main.emit('close'); } 92 | })); 93 | 94 | tray.menu = menu; 95 | }); 96 | 97 | if (process.platform === 'darwin'){ 98 | global.windows.main.setShowInTaskbar(true); 99 | } 100 | 101 | $(window).on('keyup',function(e){ 102 | if (e.keyCode === 192 && e.ctrlKey && e.shiftKey){ 103 | global.windows.main.showDevTools(); 104 | global.windows.head.showDevTools(); 105 | } 106 | }); 107 | 108 | // disable default drag/drop behavior 109 | window.ondragover = function(e) { e.preventDefault(); return false; }; 110 | window.ondrop = function(e) { e.preventDefault(); return false; }; 111 | 112 | var dragTargetArea = $('#servers'); 113 | dragTargetArea.on('dragenter', function() { 114 | $(this).addClass('dragHover'); 115 | return false; 116 | }); 117 | 118 | dragTargetArea.on('dragleave', function() { 119 | $(this).removeClass('dragHover'); 120 | return false; 121 | }); 122 | 123 | dragTargetArea.on('drop', function(e) { 124 | e.preventDefault(); 125 | $(this).removeClass('dragHover'); 126 | 127 | var files = e.dataTransfer.files; 128 | if (files.length > 0) { 129 | createServersFromDropped(files); 130 | } 131 | 132 | return false; 133 | }); 134 | 135 | /** Takes a FileList and creates a server for each folder in the list **/ 136 | function createServersFromDropped(toCreate, index) { 137 | if (toCreate.length < 1) { 138 | return; 139 | } 140 | 141 | var fileIndex = index || 0; 142 | if (fileIndex >= toCreate.length) { 143 | return; 144 | } 145 | 146 | var file = toCreate.item(fileIndex); 147 | if (file == null) { 148 | return; 149 | } 150 | 151 | var droppedPath = file.path; 152 | var serverName = file.name; 153 | var serverCreated = false; 154 | 155 | fs.lstat(droppedPath, function(err, stats) { 156 | if (err || !(stats.isDirectory() || stats.isFile())) { 157 | // if the path provided wasn't a directory or file, skip it. 158 | createServersFromDropped(toCreate, ++fileIndex); 159 | } else { 160 | if (stats.isFile()) { 161 | // if a file was dropped, get the folder it is in. 162 | droppedPath = path.dirname(droppedPath); 163 | serverName = path.basename(droppedPath); 164 | } 165 | // check if a .fenix.json file exists and use its properties. 166 | var fenixFilePath = path.join(droppedPath, '.fenix.json'); 167 | if (fs.existsSync(fenixFilePath)) { 168 | var fenixFileJSON = require(fenixFilePath); 169 | 170 | // if there is a valid name use it instead of the autogenerated one from the path. 171 | if (fenixFileJSON.name) { 172 | serverName = fenixFileJSON.name.trim().length > 0 ? fenixFileJSON.name : serverName; 173 | } 174 | 175 | // if there is a valid port go ahead and create our server. 176 | if (fenixFileJSON.port) { 177 | var serverPort = parseInt(fenixFileJSON.port); 178 | if (serverPort.toString().trim().length > 0) { 179 | serverCreated = true; 180 | UI.server.create({ name: serverName, path: droppedPath, port: serverPort }, function() { 181 | createServersFromDropped(toCreate, ++fileIndex); 182 | }); 183 | } 184 | } 185 | } 186 | if (!serverCreated) { // this flag gets toggled if a server is created from a provided .fenix.json file 187 | ROUTER.getAvailablePort(function(availablePort) { 188 | UI.server.create({ name: serverName, path: droppedPath, port: availablePort }, function() { 189 | createServersFromDropped(toCreate, ++fileIndex); 190 | }); 191 | }); 192 | } 193 | } 194 | }); 195 | } 196 | -------------------------------------------------------------------------------- /src/lib/css/fenix-embedded.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fenix'; 3 | src: url('./font/fenix.svg?24529710#fenix') format('svg'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | @font-face { 8 | font-family: 'fenix'; 9 | src: url('data:application/octet-stream;base64,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') format('woff'), 10 | url('data:application/octet-stream;base64,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') format('truetype'); 11 | } 12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 14 | /* 15 | @media screen and (-webkit-min-device-pixel-ratio:0) { 16 | @font-face { 17 | font-family: 'fenix'; 18 | src: url('../font/fenix.svg?24529710#fenix') format('svg'); 19 | } 20 | } 21 | */ 22 | 23 | [class^="icon-"]:before, [class*=" icon-"]:before { 24 | font-family: "fenix"; 25 | font-style: normal; 26 | font-weight: normal; 27 | speak: none; 28 | 29 | display: inline-block; 30 | text-decoration: inherit; 31 | width: 1em; 32 | margin-right: .2em; 33 | text-align: center; 34 | /* opacity: .8; */ 35 | 36 | /* For safety - reset parent styles, that can break glyph codes*/ 37 | font-variant: normal; 38 | text-transform: none; 39 | 40 | /* fix buttons height, for twitter bootstrap */ 41 | line-height: 1em; 42 | 43 | /* Animation center compensation - margins should be symmetric */ 44 | /* remove if not needed */ 45 | margin-left: .2em; 46 | 47 | /* you can be more comfortable with increased icons size */ 48 | /* font-size: 120%; */ 49 | 50 | /* Uncomment for 3D effect */ 51 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 52 | } 53 | .icon-menu:before { content: '\e80b'; } /* '' */ 54 | .icon-stop:before { content: '\e802'; } /* '' */ 55 | .icon-lock:before { content: '\e805'; } /* '' */ 56 | .icon-lock-open:before { content: '\e806'; } /* '' */ 57 | .icon-pencil:before { content: '\e803'; } /* '' */ 58 | .icon-attention:before { content: '\e807'; } /* '' */ 59 | .icon-trash:before { content: '\e804'; } /* '' */ 60 | .icon-minus:before { content: '\e80a'; } /* '' */ 61 | .icon-share:before { content: '\e800'; } /* '' */ 62 | .icon-window:before { content: '\e80d'; } /* '' */ 63 | .icon-down-open-mini:before { content: '\e808'; } /* '' */ 64 | .icon-up-open-mini:before { content: '\e809'; } /* '' */ 65 | .icon-play:before { content: '\e801'; } /* '' */ 66 | .icon-folder:before { content: '\e80c'; } /* '' */ -------------------------------------------------------------------------------- /src/lib/css/font/fenix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2013 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/lib/css/font/opensans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/css/font/opensans.woff -------------------------------------------------------------------------------- /src/lib/css/font/opensanslight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/css/font/opensanslight.woff -------------------------------------------------------------------------------- /src/lib/css/hint.min.css: -------------------------------------------------------------------------------- 1 | /*! Hint.css - v1.3.1 - 2013-11-23 2 | * http://kushagragour.in/lab/hint/ 3 | * Copyright (c) 2013 Kushagra Gour; Licensed MIT */ 4 | 5 | .hint,[data-hint]{position:relative;display:inline-block}.hint:before,.hint:after,[data-hint]:before,[data-hint]:after{position:absolute;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0);visibility:hidden;opacity:0;z-index:1000000;pointer-events:none;-webkit-transition:.3s ease;-moz-transition:.3s ease;transition:.3s ease}.hint:hover:before,.hint:hover:after,.hint:focus:before,.hint:focus:after,[data-hint]:hover:before,[data-hint]:hover:after,[data-hint]:focus:before,[data-hint]:focus:after{visibility:visible;opacity:1}.hint:before,[data-hint]:before{content:'';position:absolute;background:transparent;border:6px solid transparent;z-index:1000001}.hint:after,[data-hint]:after{content:attr(data-hint);background:#383838;color:#fff;text-shadow:0 -1px 0 #000;padding:8px 10px;font-size:12px;line-height:12px;white-space:nowrap;box-shadow:4px 4px 8px rgba(0,0,0,.3)}.hint--top:before{border-top-color:#383838}.hint--bottom:before{border-bottom-color:#383838}.hint--left:before{border-left-color:#383838}.hint--right:before{border-right-color:#383838}.hint--top:before{margin-bottom:-12px}.hint--top:after{margin-left:-18px}.hint--top:before,.hint--top:after{bottom:100%;left:50%}.hint--top:hover:after,.hint--top:hover:before,.hint--top:focus:after,.hint--top:focus:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--bottom:before{margin-top:-12px}.hint--bottom:after{margin-left:-18px}.hint--bottom:before,.hint--bottom:after{top:100%;left:50%}.hint--bottom:hover:after,.hint--bottom:hover:before,.hint--bottom:focus:after,.hint--bottom:focus:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--right:before{margin-left:-12px;margin-bottom:-6px}.hint--right:after{margin-bottom:-14px}.hint--right:before,.hint--right:after{left:100%;bottom:50%}.hint--right:hover:after,.hint--right:hover:before,.hint--right:focus:after,.hint--right:focus:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}.hint--left:before{margin-right:-12px;margin-bottom:-6px}.hint--left:after{margin-bottom:-14px}.hint--left:before,.hint--left:after{right:100%;bottom:50%}.hint--left:hover:after,.hint--left:hover:before,.hint--left:focus:after,.hint--left:focus:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--error:after{background-color:#b34e4d;text-shadow:0 -1px 0 #592726}.hint--error.hint--top:before{border-top-color:#b34e4d}.hint--error.hint--bottom:before{border-bottom-color:#b34e4d}.hint--error.hint--left:before{border-left-color:#b34e4d}.hint--error.hint--right:before{border-right-color:#b34e4d}.hint--warning:after{background-color:#c09854;text-shadow:0 -1px 0 #6c5328}.hint--warning.hint--top:before{border-top-color:#c09854}.hint--warning.hint--bottom:before{border-bottom-color:#c09854}.hint--warning.hint--left:before{border-left-color:#c09854}.hint--warning.hint--right:before{border-right-color:#c09854}.hint--info:after{background-color:#3986ac;text-shadow:0 -1px 0 #193b4d}.hint--info.hint--top:before{border-top-color:#3986ac}.hint--info.hint--bottom:before{border-bottom-color:#3986ac}.hint--info.hint--left:before{border-left-color:#3986ac}.hint--info.hint--right:before{border-right-color:#3986ac}.hint--success:after{background-color:#458746;text-shadow:0 -1px 0 #1a321a}.hint--success.hint--top:before{border-top-color:#458746}.hint--success.hint--bottom:before{border-bottom-color:#458746}.hint--success.hint--left:before{border-left-color:#458746}.hint--success.hint--right:before{border-right-color:#458746}.hint--always:after,.hint--always:before{opacity:1;visibility:visible}.hint--always.hint--top:after,.hint--always.hint--top:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--bottom:after,.hint--always.hint--bottom:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--left:after,.hint--always.hint--left:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--always.hint--right:after,.hint--always.hint--right:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}.hint--rounded:after{border-radius:4px}.hint--bounce:before,.hint--bounce:after{-webkit-transition:opacity .3s ease,visibility .3s ease,-webkit-transform .3s cubic-bezier(0.71,1.7,.77,1.24);-moz-transition:opacity .3s ease,visibility .3s ease,-moz-transform .3s cubic-bezier(0.71,1.7,.77,1.24);transition:opacity .3s ease,visibility .3s ease,transform .3s cubic-bezier(0.71,1.7,.77,1.24)} -------------------------------------------------------------------------------- /src/lib/css/main.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Open Sans'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Open Sans Light'), local('OpenSans-Light'), url(./font/opensanslight.woff) format('woff'); 6 | } 7 | @font-face { 8 | font-family: 'Open Sans'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Open Sans'), local('OpenSans'), url(./font/opensans.woff) format('woff'); 12 | } 13 | 14 | HTML, BODY { 15 | padding: 0; 16 | margin: 0; 17 | width: 100%; 18 | height: 100%; 19 | overflow: hidden; 20 | } 21 | BODY, * { 22 | font-family: 'Open Sans'; 23 | font-weight: 300; 24 | } 25 | BODY { 26 | color: #efefef; 27 | background-color: #131313; 28 | /*background-image: url(../../resources/fenix2.png); 29 | background-position: center 95%; 30 | background-repeat:no-repeat;*/ 31 | } 32 | 33 | BODY > div { 34 | position: absolute; 35 | top: 25px; 36 | bottom: 6px; 37 | right: 6px; 38 | left: 6px; 39 | z-index: 10; 40 | overflow: hidden; 41 | display: none; 42 | } 43 | 44 | BODY > div:nth-child(2):after { 45 | content: 'No Servers Available...'; 46 | font-family: Monaco, Consolas, 'Open Sans', Arial, sans-serif; 47 | color: #303030; 48 | text-shadow: -1px 0px 0px #222; 49 | position: fixed; 50 | z-index: 10; 51 | top: 25px !important; 52 | right: 0; 53 | left: 0; 54 | padding-top: 5px; 55 | text-align: center; 56 | vertical-align: top; 57 | } 58 | BODY.hasservers > div:nth-child(2):after { 59 | display: none; 60 | } 61 | 62 | ::-webkit-scrollbar-track { 63 | -webkit-box-shadow: inset 0 0 6px rgba(255,255,255,0.3); 64 | background-color: #000; 65 | border-radius: 2px; 66 | } 67 | 68 | ::-webkit-scrollbar { 69 | width: 6px; 70 | background-color: #000; 71 | } 72 | 73 | ::-webkit-scrollbar-thumb { 74 | background-color: #333; 75 | border-radius: 2px; 76 | } 77 | 78 | #servers { 79 | overflow-y: auto; 80 | display: block; 81 | padding: 0 6px 0 6px; 82 | margin-top: 0px; 83 | } 84 | #servers.dragHover { 85 | background-color: #5D6B6B; 86 | } 87 | #servers > div { 88 | background: #222; 89 | border-radius: 2px; 90 | padding: 4px; 91 | height: 40px; 92 | display: flex; 93 | flex-flow: row; 94 | box-shadow: 2px 1px 3px #111; 95 | margin: 4px 0 0 0; 96 | border-top: 1px solid #313131; 97 | clear: both; 98 | border-right: 4px solid #c0392b; 99 | border-left: 4px solid transparent; 100 | } 101 | #servers > div > img:first-child { 102 | min-width: 34px !important; 103 | width: 34px !important; 104 | height: 34px !important; 105 | border-radius: 2px; 106 | box-shadow: 1px 1px 2px #111; 107 | margin: 3px 8px 3px 3px; 108 | border: 0px; 109 | } 110 | #servers > div > div { 111 | cursor: pointer; 112 | color: #9c9c9c; 113 | } 114 | #servers > div > div:nth-child(2) { 115 | flex: 1 100%; 116 | width: auto; 117 | } 118 | #servers > div > div:nth-child(2) > div { 119 | display: inline-flex; 120 | font-size: 11px; 121 | font-weight: 300; 122 | max-width: 80%; 123 | } 124 | #servers > div > div:nth-child(2) > div:first-child { 125 | font-size: 16px; 126 | font-weight: 400; 127 | display: flex; 128 | color: #aaa; 129 | text-shadow: 0px 0px 6px #000; 130 | } 131 | #servers > div > div:nth-child(2) > div:last-child { 132 | text-overflow:ellipsis; 133 | overflow: hidden; 134 | cursor: default; 135 | } 136 | #servers > div:hover > div:nth-child(2) > div:last-child { 137 | cursor: default !important; 138 | } 139 | #servers > div > div:last-child { 140 | position: relative; 141 | display: none; 142 | line-height: 40px; 143 | vertical-align: middle; 144 | margin: 0 4px 0 0; 145 | } 146 | #servers > div > div:nth-child(2) > div:last-child, 147 | #servers > div > div:nth-child(2) > div:nth-child(3) { 148 | border-left: 0px solid #333; 149 | padding: 0 3px 0 6px; 150 | } 151 | #servers > div:hover > div:last-child { 152 | display: flex; 153 | } 154 | #servers > div > div:last-child > div { 155 | line-height: 40px; 156 | vertical-align: middle; 157 | font-size: 20px; 158 | margin: 0 5px 0 5px; 159 | } 160 | #servers > div > div:last-child > div:hover { 161 | color: #eee; 162 | } 163 | #servers > div > div:last-child > div:nth-last-child(2), 164 | #servers > div > div:last-child > div:last-child { 165 | position: absolute; 166 | bottom: 0; 167 | right: -10px; 168 | font-size: 14px; 169 | color: #5e5e5e; 170 | display: none; 171 | } 172 | #servers > div > div:last-child > div:nth-last-child(2) { 173 | right: 10px; 174 | } 175 | #servers > div > div:last-child > div:nth-last-child(2), 176 | #servers > div:not(.unavailable):hover > div:last-child > div:nth-child(1), 177 | #servers > div:hover > div:last-child > div:last-child { 178 | display: block; 179 | } 180 | #servers > div:hover > div:last-child > div:last-child:hover { 181 | color: #e74c3c; 182 | text-shadow: -1px -1px 2px #111; 183 | } 184 | #servers > div > div:last-child > div:nth-last-child(2):hover { 185 | color: #f1c40f; 186 | text-shadow: -1px -1px 2px #111; 187 | } 188 | #servers > div > div:last-child > div:nth-child(1), 189 | #servers > div > div:last-child > div:nth-child(2) { 190 | display: none; 191 | } 192 | #servers > div.running > div:last-child > div:nth-child(1), 193 | #servers > div.running > div:last-child > div:nth-child(2) { 194 | display: inline-flex; 195 | } 196 | /* Override the play button icon with the stop icon when the server is running */ 197 | #servers > div.running > div:last-child > div:nth-child(3) > div:before { 198 | content: '\e802' !important; 199 | } 200 | #servers > div.running { 201 | border-right: 4px solid #27ae60; 202 | border-left: 4px solid transparent; 203 | } 204 | #servers > div.running > div:nth-child(2) > div:first-child { 205 | color: #eee; 206 | text-shadow: 0 0 3px rgba(26,156,188,.65); 207 | } 208 | #servers > div.unavailable { 209 | background-color: #292929; 210 | border-right: 4px solid #f1c40f; 211 | } 212 | #servers > div.unavailable > div:nth-child(2) > div { 213 | cursor:default; 214 | } 215 | #servers > div > div:nth-child(2) > div.hint--bounce:after, 216 | #servers > div > div:nth-child(2) > div.hint--bounce:before { 217 | display: none; 218 | } 219 | #servers > div.unavailable > div:nth-child(2) > div { 220 | color: #444; 221 | text-shadow: none; 222 | } 223 | #servers > div.unavailable > div:last-child > div { 224 | display: none; 225 | } 226 | #servers > div.unavailable > div:last-child > div:nth-child(n+4) { 227 | display: block; 228 | color: #efefef; 229 | } 230 | #servers > div > div:nth-child(2) > div { 231 | cursor: default; 232 | } 233 | #servers > div.running > div:nth-child(2) > div:nth-child(n-1){ 234 | cursor: pointer; 235 | } 236 | #servers > div.running > div:nth-child(2) > div.hint--bounce:after, 237 | #servers > div.running > div:nth-child(2) > div.hint--bounce:before { 238 | display: block; 239 | } 240 | #servers > div.running.shared { 241 | position: relative; 242 | margin-bottom: 27px; 243 | margin-top: 4px; 244 | border-bottom: 1px solid transparent !important; 245 | } 246 | #servers > div.running.shared:hover { 247 | /*border-top: 1px solid rgba(54,151,255,.81);*/ 248 | } 249 | #servers > div.running.shared:before { 250 | position: absolute; 251 | bottom: -24px; 252 | left: -4px; 253 | right: 1px; 254 | color: #555; 255 | content: 'Open ' attr(shared-url) ' | Copy Link'; 256 | border: 1px solid #333; 257 | z-index: 1; 258 | font-size: 11px; 259 | padding: 0 24px 0 0px; 260 | background-color: /*rgba(54,151,255,.11)*/rgba(255,255,255,.05); 261 | border-bottom-right-radius: 2px; 262 | border-bottom-left-radius: 2px; 263 | border-top-right-radius: 0px; 264 | border-right: 4px solid transparent; 265 | border-left: 0px; 266 | border-top: 1px solid transparent; 267 | border-bottom: 1px solid transparent; 268 | height: 20px; 269 | line-height: 22px; 270 | vertical-align: middle; 271 | text-align: right; 272 | } 273 | #servers > div.running.shared.highlight:before { 274 | background-color: rgba(255,255,255,.15); 275 | } 276 | #servers > div.running.shared { 277 | cursor: pointer; 278 | } 279 | #servers > div.running.shared:hover > * { 280 | cursor: default; 281 | } 282 | #servers > div.running.shared:hover > div:last-child > div { 283 | cursor: pointer; 284 | } 285 | #servers > div.running.shared:hover:before{ 286 | color: #efefef; 287 | } 288 | #servers > div.running.shared:after { 289 | z-index: 15; 290 | content: '\e800'; 291 | position: absolute; 292 | bottom: -28px; 293 | right: -2px; 294 | width: 20px; 295 | height: 20px; 296 | font-family: fenix; 297 | color: #777;/*#27ae60;*/ 298 | text-shadow: 1px 1px 2px #000; 299 | /*transform: rotate(-5deg);*/ 300 | font-size: 14px; 301 | padding: 1px 2px 2px 2px; 302 | } 303 | #servers > div.running.shared:hover:after { 304 | color: #efefef; 305 | } 306 | #servers > div.running.shared > div:last-child > div:nth-child(2) { 307 | color: rgba(54,151,255,.9); 308 | } 309 | #servers > div.running > img:first-child { 310 | cursor: pointer !important; 311 | } 312 | 313 | #loader { 314 | z-index: 1000; 315 | background: rgba(0,0,0,.7); 316 | color: #fff; 317 | text-shadow: 1px 1px 3px #333; 318 | top: 0; 319 | right: 0; 320 | bottom: 0; 321 | left: 0; 322 | text-align: center; 323 | padding: 75px 25px 25px 0; 324 | } 325 | 326 | #editwizard, 327 | #wizard { 328 | z-index: 50; 329 | background: rgba(0,0,0,.91); 330 | color: #eee; 331 | padding: 2px 8px 8px 8px; 332 | } 333 | 334 | #editwizard > input, 335 | #wizard > input { 336 | position: relative; 337 | background: transparent; 338 | color: #fff; 339 | text-shadow: -1px -1px 1px #222; 340 | padding: 4px; 341 | font-size: 20px; 342 | border: 1px solid #333; 343 | border-radius: 2px; 344 | width: 100%; 345 | } 346 | #editwizard > input:nth-child(3), 347 | #wizard > input:nth-child(3) { 348 | padding-right: 37px; 349 | } 350 | #editwizard > input:focus, 351 | #wizard > input:focus { 352 | outline-color: #000; 353 | } 354 | #editwizard > input.optional, 355 | #wizard > input.optional { 356 | position: relative; 357 | border: 1px dashed #333; 358 | } 359 | #editwizard > button, 360 | #wizard > button { 361 | margin: 12px 0 0 0; 362 | border-radius: 2px; 363 | background: #27ae60; 364 | width: 100%; 365 | height: 40px; 366 | color: #ecf0f1; 367 | font-size: 20px; 368 | border: 1px solid #333; 369 | cursor: pointer; 370 | } 371 | ::-webkit-input-placeholder { /* WebKit browsers */ 372 | color: #444; 373 | } 374 | #editwizard > input.optional::-webkit-pinput-placeholder, 375 | #wizard > input.optional::-webkit-input-placeholder { /* WebKit browsers */ 376 | color: #232323; 377 | } 378 | #filebrowser2, 379 | #filebrowser { 380 | position: absolute; 381 | right: 10px; 382 | top: 140px; 383 | font-size: 20px; 384 | font-family: fenix; 385 | z-index: 100; 386 | min-height: 28px; 387 | min-width: 28px; 388 | line-height: 34px; 389 | vertical-align: middle; 390 | color: rgba(255,255,255,.3); 391 | } 392 | #filebrowser2:hover, 393 | #filebrowser:hover { 394 | cursor: pointer; 395 | color: rgba(255,255,255,.9); 396 | } 397 | 398 | #log { 399 | top: 25px; 400 | bottom: 15px; 401 | left: 10px; 402 | right: 10px; 403 | border: 2px solid #000; 404 | border-radius: 3px; 405 | z-index: 20; 406 | background: rgba(0,0,0,.91); 407 | color: #eee; 408 | padding: 2px 8px 8px 8px; 409 | overflow: hidden; 410 | } 411 | 412 | #log > h3 { 413 | margin: 2px 0 6px 0; 414 | color: gold; 415 | } 416 | 417 | #log > h3 > span { 418 | margin-left: 8px; 419 | color: gold; 420 | } 421 | 422 | #editwizard > a:last-child, 423 | #wizard > a:last-child, 424 | #log > a:last-child { 425 | position: absolute; 426 | top: 4px; 427 | right: 7px; 428 | font-size: 22px; 429 | color: #CCCCCC; 430 | text-decoration: none; 431 | } 432 | 433 | #log > div { 434 | position: absolute; 435 | top: 35px; 436 | bottom: 6px; 437 | left: 10px; 438 | right: 10px; 439 | overflow: auto; 440 | padding-right: 6px; 441 | } 442 | 443 | #log > div > p { 444 | margin: 0; 445 | padding: 4px 2px 4px 2px; 446 | border-top: 1px solid #111; 447 | font-family: Monaco, Consolas, "courier new"; 448 | font-size: small; 449 | color: #aaa; 450 | border-radius: 2px; 451 | border-left: 4px solid transparent; 452 | overflow-x: hidden; 453 | } 454 | #log > div > p:last-child { 455 | color: #ddd; 456 | } 457 | 458 | #log > div > p.error { 459 | background: rgba(192,58,43,.1) !important; 460 | border-left: 4px solid rgba(192,58,43,.95); 461 | } 462 | #log > div > p.warn { 463 | background: rgba(243,157,18,.1) !important; 464 | border-left: 4px solid rgba(243,157,18,.95); 465 | } 466 | 467 | #log > div > p:hover { 468 | color: #fff; 469 | background-color: #000; 470 | text-shadow: 1px 1px 1px #111; 471 | } 472 | 473 | #log > div > p.error:hover { 474 | background: rgba(192,58,43,.95) !important; 475 | border-left: 4px solid transparent; 476 | } 477 | #log > div > p.warn:hover { 478 | background: rgba(243,157,18,.95) !important; 479 | border-left: 4px solid transparent; 480 | } 481 | 482 | #log > div > p:first-child { 483 | border: 0; 484 | } 485 | 486 | #editServerBtn[disabled='true'], 487 | #createServerBtn[disabled='true'] { 488 | position: relative; 489 | color: rgba(255,255,255,.3); 490 | background-color: #222; 491 | cursor: default; 492 | } 493 | editServerBtn[disabled='true']:before, 494 | #createServerBtn[disabled='true']:before { 495 | content: 'Form must be completed.'; 496 | position: absolute; 497 | top: -15px; 498 | left: 0px; 499 | font-size: 11px; 500 | color: #777; 501 | } 502 | 503 | .show { 504 | display: block !important; 505 | } 506 | .hide { 507 | display: none !important; 508 | } 509 | 510 | #mask { 511 | z-index: 1000; 512 | position: absolute; 513 | background: rgba(0,0,0,.9); 514 | color: #555; 515 | left: -1000; 516 | text-align: center; 517 | } 518 | -------------------------------------------------------------------------------- /src/lib/css/prism.css: -------------------------------------------------------------------------------- 1 | /** 2 | * okaidia theme for JavaScript, CSS and HTML 3 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 4 | * @author ocodia 5 | */ 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"] { 9 | color: #f8f8f2; 10 | text-shadow: 0 1px rgba(0,0,0,0.3); 11 | font-family: Monaco, Consolas, 'Andale Mono', monospace; 12 | direction: ltr; 13 | text-align: left; 14 | white-space: pre; 15 | word-spacing: normal; 16 | word-break: normal; 17 | 18 | -moz-tab-size: 4; 19 | -o-tab-size: 4; 20 | tab-size: 4; 21 | 22 | -webkit-hyphens: none; 23 | -moz-hyphens: none; 24 | -ms-hyphens: none; 25 | hyphens: none; 26 | } 27 | 28 | /* Code blocks */ 29 | pre[class*="language-"] { 30 | padding: 1em; 31 | margin: .5em 0; 32 | overflow: auto; 33 | border-radius: 0.3em; 34 | } 35 | 36 | :not(pre) > code[class*="language-"], 37 | pre[class*="language-"] { 38 | background: #272822; 39 | } 40 | 41 | /* Inline code */ 42 | :not(pre) > code[class*="language-"] { 43 | padding: .1em; 44 | border-radius: .3em; 45 | } 46 | 47 | .token.comment, 48 | .token.prolog, 49 | .token.doctype, 50 | .token.cdata { 51 | color: slategray; 52 | } 53 | 54 | .token.punctuation { 55 | color: #f8f8f2; 56 | } 57 | 58 | .namespace { 59 | opacity: .7; 60 | } 61 | 62 | .token.property, 63 | .token.tag, 64 | .token.constant, 65 | .token.symbol { 66 | color: #f92672; 67 | } 68 | 69 | .token.boolean, 70 | .token.number{ 71 | color: #ae81ff; 72 | } 73 | 74 | .token.selector, 75 | .token.attr-name, 76 | .token.string, 77 | .token.builtin { 78 | color: #a6e22e; 79 | } 80 | 81 | 82 | .token.operator, 83 | .token.entity, 84 | .token.url, 85 | .language-css .token.string, 86 | .style .token.string, 87 | .token.variable { 88 | color: #f8f8f2; 89 | } 90 | 91 | .token.atrule, 92 | .token.attr-value 93 | { 94 | color: #e6db74; 95 | } 96 | 97 | 98 | .token.keyword{ 99 | color: #66d9ef; 100 | } 101 | 102 | .token.regex, 103 | .token.important { 104 | color: #fd971f; 105 | } 106 | 107 | .token.important { 108 | font-weight: bold; 109 | } 110 | 111 | .token.entity { 112 | cursor: help; 113 | } 114 | 115 | pre.line-numbers { 116 | position: relative; 117 | padding-left: 3.8em; 118 | counter-reset: linenumber; 119 | } 120 | 121 | pre.line-numbers > code { 122 | position: relative; 123 | } 124 | 125 | .line-numbers .line-numbers-rows { 126 | position: absolute; 127 | pointer-events: none; 128 | top: 0; 129 | font-size: 100%; 130 | left: -3.8em; 131 | width: 3em; /* works for line-numbers below 1000 lines */ 132 | letter-spacing: -1px; 133 | border-right: 1px solid #999; 134 | 135 | -webkit-user-select: none; 136 | -moz-user-select: none; 137 | -ms-user-select: none; 138 | user-select: none; 139 | 140 | } 141 | 142 | .line-numbers-rows > span { 143 | pointer-events: none; 144 | display: block; 145 | counter-increment: linenumber; 146 | } 147 | 148 | .line-numbers-rows > span:before { 149 | content: counter(linenumber); 150 | color: #999; 151 | display: block; 152 | padding-right: 0.8em; 153 | text-align: right; 154 | } 155 | .token a { 156 | color: inherit; 157 | } 158 | -------------------------------------------------------------------------------- /src/lib/css/requestbin.css: -------------------------------------------------------------------------------- 1 | BODY { 2 | position: relative; 3 | } 4 | 5 | BODY > div:after { 6 | position: fixed; 7 | content: 'Awaiting Requests...'; 8 | color: #555; 9 | top: 45px; 10 | right: 10px; 11 | } 12 | BODY.hasrequests > div:after { 13 | display: none !important; 14 | } 15 | #status { 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | right: 0; 20 | height: 28px; 21 | font-weight: 300; 22 | display: block; 23 | padding: 6px; 24 | line-height: 28px; 25 | vertical-align: middle; 26 | font-family: 'Open Sans'; 27 | } 28 | #status > a { 29 | text-decoration: none; 30 | font-weight: bold; 31 | } 32 | #status.public { 33 | background-color: #27ae60; 34 | color: rgba(255,255,255,.6); 35 | font-weight: bold; 36 | } 37 | #status.public > a { 38 | color: rgba(255,255,255,.95); 39 | } 40 | #status.online { 41 | background-color: #f1c40f; 42 | color: rgba(0,0,0,.6); 43 | font-weight: bold; 44 | } 45 | #status.online > a { 46 | color: rgba(0,0,0,.95); 47 | } 48 | #status.offline { 49 | background-color: #c0392b; 50 | color: rgba(255,255,255,.6); 51 | font-weight: bold; 52 | } 53 | #status.offline > a { 54 | color: rgba(255,255,255,.95); 55 | } 56 | #status.connecting { 57 | background-color: #333; 58 | color: rgba(255,255,255,.85); 59 | font-weight: bold; 60 | } 61 | #binshell { 62 | position: absolute; 63 | top: 45px; 64 | left: 6px; 65 | right: 6px; 66 | bottom: 6px; 67 | display: flex; 68 | flex: 1 100%; 69 | } 70 | #request, #requests { 71 | height: 100%; 72 | border-radius: 3px; 73 | background-color: #222222; 74 | border: 1px solid #111111; 75 | padding: 3px; 76 | color: rgba(255,255,255,.9); 77 | } 78 | #requests { 79 | flex: 1; 80 | overflow-y: auto; 81 | overflow-x: hidden; 82 | } 83 | #requests > div { 84 | position: relative; 85 | flex: 1; 86 | margin: 4px; 87 | border-radius: 3px; 88 | cursor: pointer; 89 | border-bottom: 1px solid #1c1c1c !important; 90 | padding: 6px; 91 | margin: 3px 0 0 0; 92 | height: 25px; 93 | } 94 | #requests > div.selected { 95 | background-color: #111111 !important; 96 | } 97 | #requests > div:hover { 98 | background-color: #1c1c1c; 99 | } 100 | #requests > div > span:first-child { 101 | background-color: #D2D7D3; 102 | border-radius: 3px; 103 | padding: 3px; 104 | font-weight: bold; 105 | font-size: 11px; 106 | margin: 0 8px 0 0; 107 | position: absolute; 108 | top: 9px; 109 | left: 6px; 110 | } 111 | #requests > div > span:nth-child(2) { 112 | font-size: 12px; 113 | position: absolute; 114 | right: 6px; 115 | top: 6px; 116 | } 117 | #requests > div > span:last-child { 118 | display: block; 119 | clear: both; 120 | font-size: 10px; 121 | color: #bbb; 122 | margin: 4px 0 0 0; 123 | position: absolute; 124 | right: 6px; 125 | bottom: 2px; 126 | } 127 | #request { 128 | position: relative; 129 | flex: 4; 130 | margin: 0 0 0 4px !important; 131 | background-color: transparent !important; 132 | border: 0px !important; 133 | } 134 | #request > div > table { 135 | padding: 0; 136 | margin: 0; 137 | } 138 | #request > div > table > tbody { 139 | padding: 0; 140 | margin: 0; 141 | border: 0; 142 | } 143 | #request > div > table > tbody > tr > th { 144 | text-align: left; 145 | padding: 2px; 146 | font-family: 'Open Sans', 'Arial', sans-serif; 147 | font-size: 14px; 148 | color: cyan; 149 | margin: 0; 150 | } 151 | #request > div > table > tbody > tr > th:first-child { 152 | min-width: 150px; 153 | } 154 | #request > div > table > tbody > tr:first-child > th:last-child { 155 | padding-left: 8px !important; 156 | } 157 | #request > div > table > tbody > tr > td { 158 | font-family: 'Open Sans'; 159 | font-size: 13px; 160 | font-weight: 300; 161 | background-color: #000; 162 | padding: 0; 163 | margin: 0; 164 | } 165 | #request > div > table > tbody > tr > td:first-child { 166 | font-weight: bold !important; 167 | color: #999; 168 | vertical-align: top; 169 | } 170 | #request > div > table > tbody > tr:nth-child(n+1) > td:last-child { 171 | font-family: "courier new", Monaco, Consolas, 'Open Sans'; 172 | font-weight: 100 !important; 173 | color: white !important; 174 | padding: 2 2 2 8; 175 | } 176 | #request > div > table > tbody > tr:hover > td { 177 | background-color: rgba(255,255,255,.1); 178 | } 179 | #request > .headers { 180 | position: relative; 181 | border-bottom: 1px dashed #212121; 182 | padding: 0 0 9px 0; 183 | } 184 | #showheader { 185 | position: absolute; 186 | right: 3px; 187 | top: 6px; 188 | font-size: 11px; 189 | background-color: #333; 190 | color: #ecf0f1; 191 | padding: 6px; 192 | border-radius: 3px; 193 | text-decoration: none; 194 | z-index: 100; 195 | } 196 | #showheader:hover { 197 | cursor: pointer; 198 | } 199 | #request > pre > code { 200 | font-family: Monaco, Consolas,"courier new" !important; 201 | font-size: 14px; 202 | color: #bbb; 203 | } 204 | #request { 205 | overflow-x: hidden; 206 | overflow-y: auto; 207 | } 208 | .hide { 209 | display: none; 210 | } 211 | #status > div:last-child { 212 | position: absolute; 213 | right: 7px; 214 | top: 7px; 215 | z-index: 500; 216 | } 217 | #status > div > a { 218 | padding: 4px 8px 4px 8px; 219 | border-radius: 3px; 220 | background-color: #333; 221 | color: #efefef; 222 | text-decoration: none; 223 | font-size: 14px; 224 | } 225 | span.get { 226 | background-color: #1E8BC3 !important; 227 | } 228 | span.put { 229 | background-color: #913D88 !important; 230 | } 231 | span.post { 232 | background-color: #9b59b6 !important; 233 | } 234 | span.delete { 235 | background-color: #D64541 !important; 236 | } 237 | span.head { 238 | background-color: #F9BF3B !important; 239 | } 240 | span.patch { 241 | background-color: #336E7B !important; 242 | } 243 | span.options { 244 | background-color: #DADFE1 !important; 245 | color: #333 !important; 246 | } 247 | -------------------------------------------------------------------------------- /src/lib/css/toolbar.css: -------------------------------------------------------------------------------- 1 | nav { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | left: 0; 6 | height: 20px; 7 | z-index: 1000000; 8 | padding: 2 8 0 8; 9 | background-color: #F5F6F7; 10 | border: 0; 11 | border-bottom: 1px solid #DADADA; 12 | box-shadow: 0px 7px 5px 0px rgba(0,0,0,0.46); 13 | color: #000; 14 | display: flex; 15 | } 16 | nav > li { 17 | list-style-type:none; 18 | color: #000; 19 | margin-right: 20px; 20 | } 21 | nav > li > h1 { 22 | font-size: 11px; 23 | margin: 0; 24 | color: #000000 !important; 25 | cursor: default; 26 | text-shadow: 0px 1px 1px rgba(255, 255, 255, 1); 27 | } 28 | nav > li > h1 > a { 29 | text-decoration: none; 30 | color: #000; 31 | } 32 | nav > li > h1 > a:hover { 33 | color: #111; 34 | text-shadow: 0px 1px 1px rgba(0, 0, 0, .03); 35 | } 36 | nav > li > ul { 37 | position: fixed; 38 | top: 16px; 39 | left: 0; 40 | right: 0; 41 | display: none; 42 | background-color: rgba(41,41,41,.96); 43 | margin: 5 -10 0 -10; 44 | padding: 3px 10px 3px 10px; 45 | border: 1px solid #333; 46 | box-shadow: 0px 7px 5px 0px rgba(0,0,0,0.46); 47 | } 48 | nav > li > ul:before { 49 | box-shadow: inset 0px 3px 12px 0px rgba(0,0,0,0.43); 50 | } 51 | nav > li:hover > ul { 52 | display: block; 53 | } 54 | nav > li > ul > li { 55 | list-style-type:none; 56 | /* margin: 3px 0 3px 0;*/ 57 | padding: 2px; 58 | } 59 | nav > li:hover > ul > li:hover { 60 | list-style-type:none; 61 | background-color: rgba(20,20,20,.8); 62 | cursor: pointer; 63 | } 64 | nav > li > ul > li > a { 65 | font-family:'Open Sans','Helvetica Neue', Helvetica, Arial, sans-serif; 66 | text-decoration: none; 67 | color: #cfcfcf; 68 | font-weight:100; 69 | font-size: 12px; 70 | padding: 0; 71 | margin: 0 0 0 6px; 72 | } 73 | nav > li > ul > li:hover > a { 74 | color: #fff; 75 | } 76 | -------------------------------------------------------------------------------- /src/lib/icons/fenix.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/icons/fenix.icns -------------------------------------------------------------------------------- /src/lib/icons/fenix.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/icons/fenix.ico -------------------------------------------------------------------------------- /src/lib/icons/fenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/icons/fenix.png -------------------------------------------------------------------------------- /src/lib/icons/webhooks.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/icons/webhooks.icns -------------------------------------------------------------------------------- /src/lib/icons/webhooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/icons/webhooks.png -------------------------------------------------------------------------------- /src/lib/js/UI.js: -------------------------------------------------------------------------------- 1 | var Converter = require('ansi-to-html'), 2 | Growler = require('growler'); 3 | 4 | var UI = { 5 | server: { 6 | create: function(cfg,cb){ 7 | // Add the server to the router 8 | ROUTER.createServer(cfg); 9 | cb && cb(); 10 | }, 11 | display: function(server,cb){ 12 | if (!document.getElementById(server.id)){ 13 | $('body').addClass('hasservers'); 14 | var fs = require('fs'); 15 | 16 | // Add the server to the list of managed servers. 17 | $('#servers > div:first-child').before('
    ' 18 | +'' 19 | +'
    ' 20 | + '
    '+server.name+'
    ' 21 | + '
    '+server.port.toString()+'
    ' 22 | + '
    '+server.path+'
    ' 23 | +'
    ' 24 | +'
    ' 25 | + '
    ' 26 | + '
    ' 27 | + '
    ' 28 | + '
    ' 29 | + '
    ' 30 | +'
    ' 31 | +'
    ' 32 | ); 33 | UI.server.listen(); 34 | UI.wizard.hide(cb); 35 | 36 | if (!server.running){ 37 | setTimeout(function(){ 38 | $('#servers > div:first-child').addClass('animated swing'); 39 | UI.wizard.reset(); 40 | },300); 41 | } else { 42 | server.captureScreen(); 43 | } 44 | } 45 | }, 46 | update: function(id){ 47 | var s = ROUTER.getServer(id), 48 | x = $('#'+id).find('div:nth-child(2) > div'); 49 | 50 | x[0].innerHTML = s.name; 51 | x[0].setAttribute('data-hint','Open http://127.0.0.1:'+s.port.toString()); 52 | x[1].innerHTML = s.port.toString(); 53 | x[2].innerHTML = s.path; 54 | }, 55 | start: function(id){ 56 | var s = ROUTER.getServer(id); 57 | svr = $('#'+id); 58 | if (svr.length === 0){ 59 | return; 60 | } 61 | if (!s.running) { 62 | s.start(function(){ 63 | svr.addClass('running'); 64 | svr.find('div:last-child > div:nth-child(3)')[0].setAttribute('data-hint','Stop Server'); 65 | }); 66 | } else { 67 | svr.addClass('running'); 68 | svr.find('div:last-child > div:nth-child(3)')[0].setAttribute('data-hint','Stop Server'); 69 | } 70 | }, 71 | stop: function(id){ 72 | var s = ROUTER.getServer(id); 73 | if (s.running) { 74 | s.once('stop',function(){ 75 | $( '#'+id).removeClass('running'); 76 | $('#'+id).find('div:last-child > div:nth-child(3)')[0].setAttribute('data-hint','Start Server'); 77 | }); 78 | s.stop(); 79 | } else { 80 | $('#'+id).removeClass('running'); 81 | $('#'+id).find('div:last-child > div:nth-child(3)')[0].setAttribute('data-hint','Start Server'); 82 | } 83 | }, 84 | listen: function(cb){ 85 | $('#servers > div > img').off('click').on('click',function(e){ 86 | window.open($(e.currentTarget)[0].getAttribute('src'),'_blank','width=600,height=600,menubar=no,titlebar=no,location=no'); 87 | }); 88 | $('#servers > div > div:nth-child(2) > div:first-child').off('click').on('click',function(e){ 89 | if (ROUTER.getServer(e.currentTarget.parentNode.parentNode.id).running){ 90 | gui.Shell.openExternal(e.currentTarget.getAttribute('data-hint').replace('Open ','')); 91 | } 92 | }); 93 | $('#servers > div > div:last-child > div:first-child').off('click').on('click',function(e){ 94 | UI.logview.show(e.currentTarget.parentNode.parentNode.id); 95 | }); 96 | $('#servers > div > div:last-child > div:nth-child(2)').off('click').on('click',function(e){ 97 | var s = ROUTER.getServer(e.currentTarget.parentNode.parentNode.id); 98 | s[(s.shared===false?'share':'unshare')](); 99 | }); 100 | $('#servers > div > div:last-child > div:nth-child(3)').off('click').on('click',function(e){ 101 | var s = ROUTER.getServer(e.currentTarget.parentNode.parentNode.id); 102 | UI.server[s.running===true?'stop':'start'](s.id); 103 | UI.server.mask(s.id,s.running?'Stopping...':'Starting...'); 104 | }); 105 | $('#servers > div > div:last-child > div:nth-child(4)').off('click').on('click',function(e){ 106 | var s = ROUTER.getServer(e.currentTarget.parentNode.parentNode.id); 107 | UI.editwizard.show(s); 108 | }); 109 | $('#servers > div > div:last-child > div:last-child').off('click').on('click',function(e){ 110 | ROUTER.deleteServer(e.currentTarget.parentNode.parentNode.id); 111 | }); 112 | $('#servers > div').on('click',function(e){ 113 | if ($(e.currentTarget).hasClass('shared') && (e.currentTarget.offsetTop+e.currentTarget.clientHeight+8) < e.y){ 114 | var target = e.currentTarget, url = ROUTER.getServer(target.id).publicUrl; 115 | if ((target.clientWidth-67) < e.x){ 116 | gui.Clipboard.get().set(url); 117 | $(target).addClass('highlight'); 118 | 119 | setTimeout(function(){ 120 | $(target).removeClass('highlight'); 121 | },200); 122 | } else { 123 | gui.Shell.openExternal(url); 124 | } 125 | } 126 | }); 127 | cb && cb(); 128 | }, 129 | mask: function(id,msg){ 130 | $('#mask')[0].innerHTML = msg||'...'; 131 | $('#mask')[0].setAttribute('for',id); 132 | var d = $('#'+id)[0]; 133 | UI.server.cover(d.offsetTop,d.offsetLeft,d.clientWidth,d.clientHeight); 134 | }, 135 | unmask: function(){ 136 | UI.server.uncover(); 137 | }, 138 | cover: function(top,left,width,height){ 139 | $('#mask').css({ 140 | top: top+24, 141 | left: left+4, 142 | width: width+10, 143 | height: height+4, 144 | 'line-height': (height+4).toString()+'px' 145 | }); 146 | $('#mask').addClass('show'); 147 | }, 148 | uncover: function(){ 149 | $('#mask').removeClass('show'); 150 | }, 151 | resizeMask: function(){ 152 | if ($('#mask').hasClass('show')){ 153 | UI.server.mask($('#mask')[0].getAttribute('for'),$('#mask')[0].innerHTML); 154 | } 155 | }, 156 | setThumbnail: function(id,img){ 157 | try { 158 | $('#'+id).find('img:first-child')[0].setAttribute('src',img); 159 | } catch (e) { 160 | console.log('Image not displayed because DOM could not be found.'); 161 | } 162 | }, 163 | share: function(server){ 164 | $('#'+server.id).addClass('shared'); 165 | $('#'+server.id)[0].setAttribute('shared-url',server.publicUrl); 166 | $($('#'+server.id)[0]).find('div.icon-share').parent()[0].setAttribute('data-hint','Disable Public View'); 167 | }, 168 | unshare: function(server){ 169 | $('#'+server.id).removeClass('shared'); 170 | $($('#'+server.id)[0]).find('div.icon-share').parent()[0].setAttribute('data-hint','Enable Public View'); 171 | } 172 | }, 173 | wizard: { 174 | hide: function(cb){ 175 | $('#wizard').addClass('bounceOutUp'); 176 | setTimeout(function(){ 177 | $('#wizard').removeClass('bounceInDown show'); 178 | },800); 179 | cb && cb(); 180 | }, 181 | show: function(){ 182 | if (!$('#wizard').hasClass('show')){ 183 | if ($('#log').hasClass('show')){ 184 | UI.logview.hide(); 185 | setTimeout(function(){ 186 | $('#wizard').addClass('bounceInDown show'); 187 | $('#wizard').removeClass('bounceOutUp'); 188 | },500); 189 | } else { 190 | $('#wizard').addClass('bounceInDown show'); 191 | $('#wizard').removeClass('bounceOutUp'); 192 | } 193 | $('#name').focus(); 194 | } 195 | }, 196 | browseFilepath: function(){ 197 | $('#choosefile').click(); 198 | }, 199 | reset: function(){ 200 | $('#wizard > input').val(null); 201 | ROUTER.getAvailablePort(function(port){ 202 | $('#port').val(port); 203 | }); 204 | UI.wizard.button.disable(); 205 | }, 206 | valid: function(){ 207 | return $('#name').val().trim().length > 0 208 | && $('#path').val().trim().length > 1 209 | && $('#port').val().toString().trim().length > 0; 210 | }, 211 | prepopulate: function(){ 212 | var file = require('path').join($('#path').val(),'.fenix.json'); 213 | if (require('fs').existsSync(file)){ 214 | var f = require(file); 215 | $('#name').val(f.name); 216 | $('#port').val(parseInt(f.port)); 217 | if(UI.wizard.valid()){ 218 | UI.wizard.button.enable(); 219 | } 220 | } 221 | }, 222 | button: { 223 | enable: function(){ 224 | $('#wizard > button')[0].removeAttribute('disabled'); 225 | }, 226 | disable: function(){ 227 | $('#wizard > button')[0].setAttribute('disabled',true); 228 | } 229 | } 230 | }, 231 | editwizard: { 232 | hide: function(cb){ 233 | $('#editwizard').addClass('bounceOutUp'); 234 | setTimeout(function(){ 235 | $('#editwizard').removeClass('bounceInDown show'); 236 | },800); 237 | cb && cb(); 238 | }, 239 | show: function(server){ 240 | if (!$('#editwizard').hasClass('show')){ 241 | $('#epath').val(server.path); 242 | $('#eport').val(server.port); 243 | $('#ename').val(server.name); 244 | $('#eserver').val(server.id); 245 | UI.editwizard.button.enable(); 246 | if ($('#log').hasClass('show')){ 247 | UI.logview.hide(); 248 | setTimeout(function(){ 249 | $('#editwizard').addClass('bounceInDown show'); 250 | $('#editwizard').removeClass('bounceOutUp'); 251 | },500); 252 | } else { 253 | $('#editwizard').addClass('bounceInDown show'); 254 | $('#editwizard').removeClass('bounceOutUp'); 255 | } 256 | $('#ename').focus(); 257 | } 258 | }, 259 | browseFilepath: function(){ 260 | $('#choosefile2').click(); 261 | }, 262 | reset: function(){ 263 | $('#editwizard > input').val(null); 264 | ROUTER.getAvailablePort(function(port){ 265 | $('#eport').val(port); 266 | }); 267 | UI.editwizard.button.disable(); 268 | }, 269 | valid: function(){ 270 | return $('#ename').val().trim().length > 0 271 | && $('#epath').val().trim().length > 1 272 | && $('#eport').val().toString().trim().length > 0; 273 | }, 274 | prepopulate: function(){ 275 | var file = require('path').join($('#epath').val(),'.fenix.json'); 276 | if (require('fs').existsSync(file)){ 277 | var f = require(file); 278 | $('#ename').val(f.name); 279 | $('#eport').val(parseInt(f.port)); 280 | if(UI.wizard.valid()){ 281 | UI.wizard.button.enable(); 282 | } 283 | } 284 | }, 285 | button: { 286 | enable: function(){ 287 | $('#editwizard > button')[0].removeAttribute('disabled'); 288 | }, 289 | disable: function(){ 290 | $('#editwizard > button')[0].setAttribute('disabled',true); 291 | } 292 | } 293 | }, 294 | logview: { 295 | converter: new Converter({newline:true}), 296 | currentlog: null, 297 | scrollToBottom: function(){ 298 | var el = $('#log > div')[0]; 299 | el.scrollTop = el.scrollHeight - $(el).height(); 300 | }, 301 | hide: function(){ 302 | var s = ROUTER.getServer(UI.logview.currentlog); 303 | $('#log').addClass('bounceOut'); 304 | 305 | s.syslog.removeEvent('log'); 306 | s.syslog.removeEvent('errormsg'); 307 | s.syslog.removeEvent('warn'); 308 | 309 | setTimeout(function(){ 310 | $('#log').removeClass('bounceIn show'); 311 | },800); 312 | }, 313 | show: function(serverid){ 314 | if (!$('#log').hasClass('show')){ 315 | var s = ROUTER.getServer(serverid); 316 | 317 | UI.logview.currentlog = serverid; 318 | 319 | s.syslog.on('log',function(msg){ 320 | $('#log > div').append('

    '+UI.logview.clean(msg)+'

    '); 321 | UI.logview.scrollToBottom(); 322 | }); 323 | s.syslog.on('errormsg',function(msg){ 324 | $('#log > div').append('

    '+UI.logview.clean(msg)+'

    '); 325 | UI.logview.scrollToBottom(); 326 | }); 327 | s.syslog.on('warn',function(msg){ 328 | $('#log > div').append('

    '+UI.logview.clean(msg)+'

    '); 329 | UI.logview.scrollToBottom(); 330 | }); 331 | 332 | $('#log > h3 > span')[0].innerHTML = s.name; 333 | $('#log > div').empty(); 334 | for(var rec in s.syslog.syslog){ 335 | $('#log > div').append('

    '+s.syslog.syslog[rec]+'

    '); 336 | } 337 | $('#log').addClass('bounceIn show'); 338 | $('#log').removeClass('bounceOut'); 339 | 340 | global.track.event('Web Server','log','Viewed Log',1).send(); 341 | } 342 | }, 343 | clean: function(msg){ 344 | return msg.replace('\n','
    ').trim(); 345 | } 346 | }, 347 | loader: { 348 | show: function(msg){ 349 | if (!$('#loader').hasClass('show')){ 350 | if (msg){ 351 | $('#loader')[0].innerHTML = msg; 352 | } 353 | $('#loader').removeClass('bounceOut'); 354 | $('#loader').addClass('show'); 355 | } 356 | }, 357 | hide: function(msg){ 358 | if ($('#loader').hasClass('show')){ 359 | $('#loader').addClass('bounceOut'); 360 | $('#loader').removeClass('show'); 361 | $('#loader')[0].innerHTML = 'Loading...'; 362 | } 363 | } 364 | }, 365 | _growl:{ 366 | app: new Growler.GrowlApplication('Fenix'), 367 | init: function(){ 368 | UI._growl.app.setNotifications({ 369 | 'Status': { 370 | displayname: 'Fenix Server Status', 371 | enabled: true, 372 | icon: require('fs').readFileSync('./lib/icons/fenix.png') 373 | } 374 | }); 375 | UI._growl.app.register(); 376 | UI._growl.initialized = true; 377 | }, 378 | initialized: false, 379 | lastmessage: null 380 | }, 381 | lastgrowl: null, 382 | notify: function(msg){ 383 | if (!UI._growl.initialized){ 384 | UI._growl.init(); 385 | } 386 | if (ROUTER.loading || typeof global.windows.main === 'string'){ 387 | return; 388 | } 389 | msg = msg || {}; 390 | if (typeof msg === 'string'){ 391 | msg = { 392 | text: msg, 393 | title: 'Fenix Alert' 394 | //sticky, 395 | //priority (1,2,3,4,5.... 2 = critical), 396 | //icon 397 | }; 398 | } 399 | // Prevent duplication 400 | var checksum = (msg.type||'Status')+JSON.stringify(msg); 401 | if (checksum !== UI._growl.lastmessage){ 402 | UI._growl.lastmessage = checksum; 403 | setTimeout(function(){ 404 | UI._growl.lastmessage = null; 405 | },300); 406 | UI._growl.app.sendNotification(msg.type||'Status',msg); 407 | } else { 408 | console.log('Duplicate notification error:',(msg.type||'Status'),msg); 409 | } 410 | }, 411 | updateAvailable: function(){ 412 | $('BODY').addClass('updateavailable'); 413 | } 414 | }; 415 | UI._growl.init(); 416 | 417 | window.onresize = function(){ 418 | UI.server.resizeMask(); 419 | }; 420 | -------------------------------------------------------------------------------- /src/lib/js/editwizard.js: -------------------------------------------------------------------------------- 1 | // Wizard Controller 2 | $('#editwizard > button').on('click',function(e){ 3 | var id = $(e.currentTarget.parentNode).find('input[type="hidden"]').val(); 4 | var s = ROUTER.getServer(id); 5 | var running = s.running, shared = s.shared, recapture = false; 6 | s.suppressnotices = true; 7 | s.stop(function(){ 8 | if (s.path !== $('#epath').val()){ 9 | s.syslog.log('Path changed from '+s.path+' to '+$('#epath').val()); 10 | } 11 | if (s.name !== $('#ename').val()){ 12 | s.syslog.log('Name changed from '+s.name+' to '+$('#ename').val()); 13 | } 14 | if (s.port !== parseInt($('#eport').val())){ 15 | s.syslog.log('Path changed from '+s.port.toString()+' to '+$('#eport').val().toString()); 16 | } 17 | s.path = $('#epath').val(); 18 | s.port = $('#eport').val(); 19 | s.name = $('#ename').val(); 20 | 21 | UI.server.update(id); 22 | 23 | if (!require('fs').existsSync(s.path)){ 24 | $('#'+id).addClass('unavailable'); 25 | s.suppressnotices = false; 26 | } else { 27 | if (running){ 28 | s.once('start',function(){ 29 | if (shared){ 30 | s.share(); 31 | } 32 | s.suppressnotices = false; 33 | UI.notify({ 34 | title: 'Server Modified', 35 | text: s.name+' was modified and restarted. Now running '+s.path+' on port '+s.port.toString() 36 | }); 37 | }); 38 | s.start(); 39 | ROUTER.save(); 40 | } else { 41 | s.suppressnotices = true; 42 | } 43 | } 44 | UI.editwizard.hide(); 45 | }); 46 | }); 47 | 48 | $('#editwizard > a').on('click',function(e){ 49 | e.preventDefault(); 50 | UI.editwizard.hide(); 51 | }); 52 | 53 | $('#filebrowser2').on('click',function(e){ 54 | e.preventDefault(); 55 | UI.editwizard.browseFilepath(); 56 | }); 57 | 58 | document.querySelector('#choosefile2').addEventListener('change',function(evt){ 59 | $('#epath').val(this.value); 60 | UI.editwizard.prepopulate(); 61 | },false); 62 | 63 | $('#eport').on('keypress',function(evt){ 64 | evt = (evt) ? evt : window.event; 65 | var charCode = (evt.which) ? evt.which : evt.keyCode; 66 | if (charCode > 31 && (charCode < 48 || charCode > 57)) { 67 | return false; 68 | } 69 | return true; 70 | }); 71 | 72 | var efn = function(){ 73 | UI.editwizard.prepopulate(); 74 | }; 75 | 76 | var edelay = null; 77 | 78 | $('#epath').on('keyup',function(e){ 79 | clearTimeout(edelay); 80 | edelay = setTimeout(efn,900); 81 | }); 82 | 83 | $('#editwizard > input').on('keyup',function(e){ 84 | if (UI.editwizard.valid()){ 85 | UI.editwizard.button.enable(); 86 | } else { 87 | UI.editwizard.button.disable(); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /src/lib/js/prism.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prism: Lightweight, robust, elegant syntax highlighting 3 | * MIT license http://www.opensource.org/licenses/mit-license.php/ 4 | * @author Lea Verou http://lea.verou.me 5 | */(function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{type:function(e){return Object.prototype.toString.call(e).match(/\[object (\w+)\]/)[1]},clone:function(e){var n=t.util.type(e);switch(n){case"Object":var r={};for(var i in e)e.hasOwnProperty(i)&&(r[i]=t.util.clone(e[i]));return r;case"Array":return e.slice()}return e}},languages:{extend:function(e,n){var r=t.util.clone(t.languages[e]);for(var i in n)r[i]=n[i];return r},insertBefore:function(e,n,r,i){i=i||t.languages;var s=i[e],o={};for(var u in s)if(s.hasOwnProperty(u)){if(u==n)for(var a in r)r.hasOwnProperty(a)&&(o[a]=r[a]);o[u]=s[u]}return i[e]=o},DFS:function(e,n){for(var r in e){n.call(e,r,e[r]);t.util.type(e)==="Object"&&t.languages.DFS(e[r],n)}}},highlightAll:function(e,n){var r=document.querySelectorAll('code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code');for(var i=0,s;s=r[i++];)t.highlightElement(s,e===!0,n)},highlightElement:function(r,i,s){var o,u,a=r;while(a&&!e.test(a.className))a=a.parentNode;if(a){o=(a.className.match(e)||[,""])[1];u=t.languages[o]}if(!u)return;r.className=r.className.replace(e,"").replace(/\s+/g," ")+" language-"+o;a=r.parentNode;/pre/i.test(a.nodeName)&&(a.className=a.className.replace(e,"").replace(/\s+/g," ")+" language-"+o);var f=r.textContent;if(!f)return;f=f.replace(/&/g,"&").replace(/e.length)break e;if(p instanceof i)continue;a.lastIndex=0;var d=a.exec(p);if(d){l&&(c=d[1].length);var v=d.index-1+c,d=d[0].slice(c),m=d.length,g=v+m,y=p.slice(0,v+1),b=p.slice(g+1),w=[h,1];y&&w.push(y);var E=new i(u,f?t.tokenize(d,f):d);w.push(E);b&&w.push(b);Array.prototype.splice.apply(s,w)}}}return s},hooks:{all:{},add:function(e,n){var r=t.hooks.all;r[e]=r[e]||[];r[e].push(n)},run:function(e,n){var r=t.hooks.all[e];if(!r||!r.length)return;for(var i=0,s;s=r[i++];)s(n)}}},n=t.Token=function(e,t){this.type=e;this.content=t};n.stringify=function(e,r,i){if(typeof e=="string")return e;if(Object.prototype.toString.call(e)=="[object Array]")return e.map(function(t){return n.stringify(t,r,e)}).join("");var s={type:e.type,content:n.stringify(e.content,r,i),tag:"span",classes:["token",e.type],attributes:{},language:r,parent:i};s.type=="comment"&&(s.attributes.spellcheck="true");t.hooks.run("wrap",s);var o="";for(var u in s.attributes)o+=u+'="'+(s.attributes[u]||"")+'"';return"<"+s.tag+' class="'+s.classes.join(" ")+'" '+o+">"+s.content+""};if(!self.document){self.addEventListener("message",function(e){var n=JSON.parse(e.data),r=n.language,i=n.code;self.postMessage(JSON.stringify(t.tokenize(i,t.languages[r])));self.close()},!1);return}var r=document.getElementsByTagName("script");r=r[r.length-1];if(r){t.filename=r.src;document.addEventListener&&!r.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)}})();; 6 | Prism.languages.markup={comment:/<!--[\w\W]*?-->/g,prolog:/<\?.+?\?>/,doctype:/<!DOCTYPE.+?>/,cdata:/<!\[CDATA\[[\w\W]*?]]>/i,tag:{pattern:/<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|\w+))?\s*)*\/?>/gi,inside:{tag:{pattern:/^<\/?[\w:-]+/i,inside:{punctuation:/^<\/?/,namespace:/^[\w-]+?:/}},"attr-value":{pattern:/=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,inside:{punctuation:/=|>|"/g}},punctuation:/\/?>/g,"attr-name":{pattern:/[\w:-]+/g,inside:{namespace:/^[\w-]+?:/}}}},entity:/&#?[\da-z]{1,8};/gi};Prism.hooks.add("wrap",function(e){e.type==="entity"&&(e.attributes.title=e.content.replace(/&/,"&"))});; 7 | Prism.languages.css={comment:/\/\*[\w\W]*?\*\//g,atrule:{pattern:/@[\w-]+?.*?(;|(?=\s*{))/gi,inside:{punctuation:/[;:]/g}},url:/url\((["']?).*?\1\)/gi,selector:/[^\{\}\s][^\{\};]*(?=\s*\{)/g,property:/(\b|\B)[\w-]+(?=\s*:)/ig,string:/("|')(\\?.)*?\1/g,important:/\B!important\b/gi,ignore:/&(lt|gt|amp);/gi,punctuation:/[\{\};:]/g};Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{style:{pattern:/(<|<)style[\w\W]*?(>|>)[\w\W]*?(<|<)\/style(>|>)/ig,inside:{tag:{pattern:/(<|<)style[\w\W]*?(>|>)|(<|<)\/style(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.css}}});; 8 | Prism.languages.clike={comment:{pattern:/(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,lookbehind:!0},string:/("|')(\\?.)*?\1/g,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,"boolean":/\b(true|false)\b/g,"function":{pattern:/[a-z0-9_]+\(/ig,inside:{punctuation:/\(/}}, number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|(&){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,ignore:/&(lt|gt|amp);/gi,punctuation:/[{}[\];(),.:]/g}; 9 | ; 10 | Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue)\b/g,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g});Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,lookbehind:!0}});Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/(<|<)script[\w\W]*?(>|>)[\w\W]*?(<|<)\/script(>|>)/ig,inside:{tag:{pattern:/(<|<)script[\w\W]*?(>|>)|(<|<)\/script(>|>)/ig,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript}}});; 11 | Prism.hooks.add("after-highlight",function(e){var t=e.element.parentNode;if(!t||!/pre/i.test(t.nodeName)||t.className.indexOf("line-numbers")===-1){return}var n=1+e.code.split("\n").length;var r;lines=new Array(n);lines=lines.join("");r=document.createElement("span");r.className="line-numbers-rows";r.innerHTML=lines;if(t.hasAttribute("data-start")){t.style.counterReset="linenumber "+(parseInt(t.getAttribute("data-start"),10)-1)}e.element.appendChild(r)}) 12 | ; 13 | (function(){if(!self.Prism)return;var e=/\b([a-z]{3,7}:\/\/|tel:)[\w-+%~/.:]+/,t=/\b\S+@[\w.]+[a-z]{2}/,n=/\[([^\]]+)]\(([^)]+)\)/,r=["comment","url","attr-value","string"];for(var i in Prism.languages){var s=Prism.languages[i];Prism.languages.DFS(s,function(i,s){if(r.indexOf(i)>-1){s.pattern||(s=this[i]={pattern:s});s.inside=s.inside||{};i=="comment"&&(s.inside["md-link"]=n);s.inside["url-link"]=e;s.inside["email-link"]=t}});s["url-link"]=e;s["email-link"]=t}Prism.hooks.add("wrap",function(e){if(/-link$/.test(e.type)){e.tag="a";var t=e.content;if(e.type=="email-link")t="mailto:"+t;else if(e.type=="md-link"){var r=e.content.match(n);t=r[2];e.content=r[1]}e.attributes.href=t}})})(); 14 | ; 15 | -------------------------------------------------------------------------------- /src/lib/js/router.js: -------------------------------------------------------------------------------- 1 | var ROUTER = new Router(); 2 | 3 | var initServer = function(server){ 4 | if (!server.initialized){ 5 | server.on('initialized',function(){ 6 | if (!document.getElementById(server.id)){ 7 | UI.server.display(server,function(){ 8 | UI.loader.hide(); 9 | }); 10 | } 11 | }); 12 | server.on('error',function(e){ 13 | console.dir(e); 14 | UI.notify({ 15 | title: server.name, 16 | msg: (e.message|| 'Unknown Error')+' (Code: '+e.code.toString()+')' 17 | }); 18 | }); 19 | server.off('startfailure').on('startfailure',function(code){ 20 | if (server.assistingstart){ 21 | return; 22 | } 23 | if (ROUTER.loading){ 24 | alert('Could not start '+server.name+'. The user account has insufficient privileges to run a server on port '+server.port.toString()); 25 | return; 26 | } 27 | server.assistingstart = confirm('Insufficient user privileges.\nCannot run the server on port '+server.port.toString()+'.\n\nSome systems require elevated permissions (sudo/admin) to run servers on certain ports. Would you like Fenix to automatically find and use a supported port to start the server on?'); 28 | if (server.assistingstart){ 29 | server.once('start',function(){ 30 | server.assistingstart = false; 31 | }); 32 | ROUTER.getAvailablePort(function(port){ 33 | server.port = port; 34 | UI.server.update(server.id); 35 | UI.server.unmask(); 36 | server.start(); 37 | },1025); 38 | } else { 39 | UI.server.unmask(); 40 | } 41 | }); 42 | server.on('screencapture',function(server){ 43 | UI.server.setThumbnail(server.id,server.screenshot); 44 | }); 45 | server.on('share',function(){ 46 | UI.server.share(server); 47 | !server.supressnotices && UI.notify({ 48 | title: server.name, 49 | text: 'Publicy sharing server at '+server.publicUrl 50 | }); 51 | global.track.event('Web Server','share','Shared Web Server').send(); 52 | }); 53 | server.on('unshare',function(){ 54 | UI.server.unshare(server); 55 | !server.supressnotices && UI.notify({ 56 | title: server.name, 57 | text: 'Stopped sharing server publicly' 58 | }); 59 | }); 60 | server.on('start',function(){ 61 | if (server.running){ 62 | UI.server.start(server.id); 63 | // Capture the initial screenshot. 64 | server.captureScreen(); 65 | UI.server.unmask(server.id); 66 | !server.supressnotices && UI.notify({ 67 | title: server.name, 68 | text: 'Started on port '+server.port.toString() 69 | }); 70 | } 71 | }); 72 | server.on('stop',function(){ 73 | if (!server.running){ 74 | UI.server.stop(server.id); 75 | UI.server.unmask(server.id); 76 | !server.supressnotices && UI.notify({ 77 | title: server.name, 78 | text: 'Stopped' 79 | }); 80 | } 81 | }); 82 | server.on('filecreated',function(obj){ 83 | if (!obj.server.running){ 84 | return; 85 | } 86 | // The main index page was created, so recreate the screenshot 87 | if (obj.filename.replace(obj.server.path,'').replace('/','') === 'index.html'){ 88 | setTimeout(function(){ 89 | obj.server.captureScreen(); 90 | },1000); 91 | } 92 | }); 93 | server.on('filechanged',function(obj){ 94 | if (!obj.server.running){ 95 | return; 96 | } 97 | // The main index page was modified, so recreate the screenshot 98 | if (obj.filename.replace(obj.server.path,'').replace('/','') === 'index.html'){ 99 | setTimeout(function(){ 100 | obj.server.captureScreen(); 101 | },1000); 102 | } 103 | }); 104 | server.on('fileremoved',function(obj){ 105 | if (!obj.server.running){ 106 | return; 107 | } 108 | // The main index page was removed, so recreate the screenshot (it will show an error) 109 | if (obj.filename.replace(obj.server.path,'').replace('/','') === 'index.html'){ 110 | setTimeout(function(){ 111 | obj.server.captureScreen(); 112 | },1000); 113 | } 114 | }); 115 | global.track.event('Web Server','create','Created Web Server').send(); 116 | } else { 117 | UI.server.display(server,function(){ 118 | UI.loader.hide(); 119 | }); 120 | } 121 | }; 122 | 123 | ROUTER.on('createserver',function(server){ 124 | UI.loader.show('Configuring the new server.'); 125 | initServer(server); 126 | }); 127 | 128 | ROUTER.on('deleteserver',function(server){ 129 | $('#'+server.id).addClass('bounceOut'); 130 | setTimeout(function(){ 131 | $('#'+server.id).remove(); 132 | UI.notify({ 133 | title: server.name, 134 | text: 'Server Deleted' 135 | }); 136 | if (Object.keys(ROUTER.servers).length === 0){ 137 | $('body').removeClass('hasservers'); 138 | } 139 | },1000); 140 | global.track.event('Web Server','delete','Deleted Web Server').send(); 141 | }); 142 | 143 | ROUTER.on('loadserver',function(server){ 144 | initServer(server); 145 | }); 146 | 147 | ROUTER.on('loadcomplete',function(){ 148 | require('request').get('https://api.github.com/repos/coreybutler/fenix/releases',{ 149 | headers: { 150 | 'user-agent':'Fenix' 151 | } 152 | },function(error,response,body){ 153 | try { 154 | if (body){ 155 | data = JSON.parse(body); 156 | data = (data || [])[0]; 157 | 158 | if(data !== undefined){ 159 | var semver = require('semver'); 160 | var pkg = require('./package.json').version; 161 | localStorage.setItem('updateavailable',semver.lt(pkg,data.tag_name)); 162 | } else { 163 | localStorage.setItem('updateavailable',false); 164 | } 165 | 166 | setTimeout(function(){ 167 | if (localStorage.getItem('updateavailable')==='true'){ 168 | UI.updateAvailable(); 169 | UI.notify({ 170 | title: 'Update Available', 171 | text: 'Version '+data.tag_name+' is available.' 172 | }); 173 | } 174 | global.windows.main.emit('ready'); 175 | },2500+(localStorage.getItem('updateavailable')==='true'?3000:0)); 176 | 177 | } else { 178 | setTimeout(function(){ 179 | global.windows.splash && global.windows.splash.hide(); 180 | global.windows.main.show(); 181 | },1500); 182 | } 183 | } catch (e){ 184 | alert('ERROR: '+e.message); 185 | global.windows.main.emit('ready'); 186 | winloaded = true; 187 | } 188 | }); 189 | 190 | }); 191 | 192 | ROUTER.load(); 193 | -------------------------------------------------------------------------------- /src/lib/js/wizard.js: -------------------------------------------------------------------------------- 1 | // Wizard Controller 2 | $('#wizard > button').on('click',function(e){ 3 | UI.server.create({ 4 | name: $('#wizard > #name').val(), 5 | port: parseInt($('#wizard > #port').val()), 6 | path: $('#wizard > #path').val() 7 | }); 8 | }); 9 | 10 | $('#wizard > a').on('click',function(e){ 11 | e.preventDefault(); 12 | UI.wizard.hide(); 13 | }); 14 | 15 | ROUTER.getAvailablePort(function(port){ 16 | $('#port').val(port); 17 | }); 18 | 19 | $('#filebrowser').on('click',function(e){ 20 | e.preventDefault(); 21 | UI.wizard.browseFilepath(); 22 | }); 23 | 24 | document.querySelector('#choosefile').addEventListener('change',function(evt){ 25 | $('#path').val(this.value); 26 | UI.wizard.prepopulate(); 27 | if ($('#name').val().trim().length < 1) { 28 | $('#name').val(path.basename(this.value)); 29 | } 30 | UI.wizard.valid() && UI.wizard.button.enable(); 31 | },false); 32 | 33 | $('#port').on('keypress',function(evt){ 34 | evt = (evt) ? evt : window.event; 35 | var charCode = (evt.which) ? evt.which : evt.keyCode; 36 | if (charCode > 31 && (charCode < 48 || charCode > 57)) { 37 | return false; 38 | } 39 | return true; 40 | }); 41 | 42 | var fn = function(){ 43 | UI.wizard.prepopulate(); 44 | }; 45 | 46 | var delay = null; 47 | 48 | $('#path').on('keyup',function(e){ 49 | clearTimeout(delay); 50 | delay = setTimeout(fn,900); 51 | }); 52 | 53 | $('#wizard > input').on('keyup',function(e){ 54 | if (UI.wizard.valid()){ 55 | UI.wizard.button.enable(); 56 | } else { 57 | UI.wizard.button.disable(); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /src/lib/public/css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding:0; 3 | margin:0; 4 | } 5 | 6 | body { 7 | color: #333; 8 | font: 14px Sans-Serif; 9 | padding: 50px; 10 | background: #eee; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | padding: 20px 0 12px 0; 16 | margin: 0; 17 | font-family: Helvetica,; 18 | font-weight: 100; 19 | } 20 | h2 { 21 | font-size: 16px; 22 | text-align: center; 23 | padding: 0 0 12px 0; 24 | font-family: Helvetica, Arial; 25 | font-weight: 100; 26 | } 27 | 28 | #container { 29 | box-shadow: 0 5px 10px -5px rgba(0,0,0,0.5); 30 | position: relative; 31 | background: #121212; 32 | border-top-left-radius: 3px; 33 | border-top-right-radius: 3px; 34 | color: #efefef; 35 | font-weight: 300; 36 | } 37 | 38 | table { 39 | background-color: #F3F3F3; 40 | border-collapse: collapse; 41 | width: 100%; 42 | margin: 15px 0; 43 | } 44 | 45 | th { 46 | background-color: #333; 47 | color: #FFF; 48 | cursor: pointer; 49 | padding: 5px 10px; 50 | font-family: Helvetica, Arial; 51 | font-weight: 100; 52 | } 53 | 54 | th small { 55 | font-size: 9px; 56 | } 57 | 58 | td, th { 59 | text-align: left; 60 | width: 25%; 61 | } 62 | 63 | .dir > td:nth-child(2) > a, 64 | .dir > td:nth-child(3) > a { 65 | color: #aaa; 66 | } 67 | 68 | a { 69 | text-decoration: none; 70 | } 71 | 72 | td a { 73 | color: #663300; 74 | display: block; 75 | padding: 5px 10px; 76 | } 77 | th a { 78 | padding-left: 0 79 | } 80 | 81 | td:first-of-type a { 82 | background: url(../images/file.png) no-repeat 10px 50%; 83 | padding-left: 35px; 84 | } 85 | th:first-of-type { 86 | padding-left: 35px; 87 | } 88 | 89 | td:not(:first-of-type) a { 90 | background-image: none !important; 91 | } 92 | 93 | tr:nth-of-type(odd) { 94 | background-color: #E6E6E6; 95 | } 96 | 97 | tr:hover td { 98 | background-color:#CACACA; 99 | } 100 | 101 | tr:hover td a { 102 | color: #000; 103 | } 104 | 105 | 106 | 107 | 108 | 109 | /* icons for file types (icons by famfamfam) */ 110 | 111 | /* images */ 112 | table tr td:first-of-type a[href$=".jpg"], 113 | table tr td:first-of-type a[href$=".png"], 114 | table tr td:first-of-type a[href$=".gif"], 115 | table tr td:first-of-type a[href$=".svg"], 116 | table tr td:first-of-type a[href$=".jpeg"] 117 | {background-image: url(../images/image.png);} 118 | 119 | /* zips */ 120 | table tr td:first-of-type a[href$=".zip"] 121 | {background-image: url(../images/zip.png);} 122 | 123 | /* css */ 124 | table tr td:first-of-type a[href$=".css"] 125 | {background-image: url(../images/css.png);} 126 | 127 | /* docs */ 128 | table tr td:first-of-type a[href$=".doc"], 129 | table tr td:first-of-type a[href$=".docx"], 130 | table tr td:first-of-type a[href$=".ppt"], 131 | table tr td:first-of-type a[href$=".pptx"], 132 | table tr td:first-of-type a[href$=".pps"], 133 | table tr td:first-of-type a[href$=".ppsx"], 134 | table tr td:first-of-type a[href$=".xls"], 135 | table tr td:first-of-type a[href$=".xlsx"] 136 | {background-image: url(../images/office.png)} 137 | 138 | /* videos */ 139 | table tr td:first-of-type a[href$=".avi"], 140 | table tr td:first-of-type a[href$=".wmv"], 141 | table tr td:first-of-type a[href$=".mp4"], 142 | table tr td:first-of-type a[href$=".mov"], 143 | table tr td:first-of-type a[href$=".m4a"] 144 | {background-image: url(../images/video.png);} 145 | 146 | /* audio */ 147 | table tr td:first-of-type a[href$=".mp3"], 148 | table tr td:first-of-type a[href$=".ogg"], 149 | table tr td:first-of-type a[href$=".aac"], 150 | table tr td:first-of-type a[href$=".wma"] 151 | {background-image: url(../images/audio.png);} 152 | 153 | /* web pages */ 154 | table tr td:first-of-type a[href$=".html"], 155 | table tr td:first-of-type a[href$=".htm"], 156 | table tr td:first-of-type a[href$=".xml"] 157 | {background-image: url(../images/xml.png);} 158 | 159 | table tr td:first-of-type a[href$=".php"] 160 | {background-image: url(../images/php.png);} 161 | 162 | table tr td:first-of-type a[href$=".js"] 163 | {background-image: url(../images/script.png);} 164 | 165 | /* directories */ 166 | table tr.dir td:first-of-type a 167 | {background-image: url(../images/folder.png);} 168 | -------------------------------------------------------------------------------- /src/lib/public/directory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Directory Contents 6 | 7 | 8 | 9 | 10 | 11 |
    12 |

    Directory Contents

    13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
    FilenameTypeSize (kb)Date Modified
    .. (Parent Directory)<Directory>
    32 |
    33 | 34 | 35 | -------------------------------------------------------------------------------- /src/lib/public/images/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/css.png -------------------------------------------------------------------------------- /src/lib/public/images/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/file.png -------------------------------------------------------------------------------- /src/lib/public/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/folder.png -------------------------------------------------------------------------------- /src/lib/public/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/image.png -------------------------------------------------------------------------------- /src/lib/public/images/office.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/office.png -------------------------------------------------------------------------------- /src/lib/public/images/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/php.png -------------------------------------------------------------------------------- /src/lib/public/images/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/script.png -------------------------------------------------------------------------------- /src/lib/public/images/sound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/sound.png -------------------------------------------------------------------------------- /src/lib/public/images/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/video.png -------------------------------------------------------------------------------- /src/lib/public/images/word.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/word.png -------------------------------------------------------------------------------- /src/lib/public/images/xml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/xml.png -------------------------------------------------------------------------------- /src/lib/public/images/zip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coreybutler/fenix/e8df968fc2c601777e24151345530ceee190b9f5/src/lib/public/images/zip.png -------------------------------------------------------------------------------- /src/lib/public/js/sorttable.js: -------------------------------------------------------------------------------- 1 | /* 2 | SortTable 3 | version 2 4 | 7th April 2007 5 | Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ 6 | 7 | Instructions: 8 | Download this file 9 | Add to your HTML 10 | Add class="sortable" to any table you'd like to make sortable 11 | Click on the headers to sort 12 | 13 | Thanks to many, many people for contributions and suggestions. 14 | Licenced as X11: http://www.kryogenix.org/code/browser/licence.html 15 | This basically means: do what you want with it. 16 | */ 17 | 18 | 19 | var stIsIE = /*@cc_on!@*/false; 20 | 21 | sorttable = { 22 | init: function() { 23 | // quit if this function has already been called 24 | if (arguments.callee.done) return; 25 | // flag this function so we don't do the same thing twice 26 | arguments.callee.done = true; 27 | // kill the timer 28 | if (_timer) clearInterval(_timer); 29 | 30 | if (!document.createElement || !document.getElementsByTagName) return; 31 | 32 | sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; 33 | 34 | forEach(document.getElementsByTagName('table'), function(table) { 35 | if (table.className.search(/\bsortable\b/) != -1) { 36 | sorttable.makeSortable(table); 37 | } 38 | }); 39 | 40 | }, 41 | 42 | makeSortable: function(table) { 43 | if (table.getElementsByTagName('thead').length == 0) { 44 | // table doesn't have a tHead. Since it should have, create one and 45 | // put the first table row in it. 46 | the = document.createElement('thead'); 47 | the.appendChild(table.rows[0]); 48 | table.insertBefore(the,table.firstChild); 49 | } 50 | // Safari doesn't support table.tHead, sigh 51 | if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; 52 | 53 | if (table.tHead.rows.length != 1) return; // can't cope with two header rows 54 | 55 | // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as 56 | // "total" rows, for example). This is B&R, since what you're supposed 57 | // to do is put them in a tfoot. So, if there are sortbottom rows, 58 | // for backwards compatibility, move them to tfoot (creating it if needed). 59 | sortbottomrows = []; 60 | for (var i=0; i5' : ' ▴'; 104 | this.appendChild(sortrevind); 105 | return; 106 | } 107 | if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { 108 | // if we're already sorted by this column in reverse, just 109 | // re-reverse the table, which is quicker 110 | sorttable.reverse(this.sorttable_tbody); 111 | this.className = this.className.replace('sorttable_sorted_reverse', 112 | 'sorttable_sorted'); 113 | this.removeChild(document.getElementById('sorttable_sortrevind')); 114 | sortfwdind = document.createElement('span'); 115 | sortfwdind.id = "sorttable_sortfwdind"; 116 | sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; 117 | this.appendChild(sortfwdind); 118 | return; 119 | } 120 | 121 | // remove sorttable_sorted classes 122 | theadrow = this.parentNode; 123 | forEach(theadrow.childNodes, function(cell) { 124 | if (cell.nodeType == 1) { // an element 125 | cell.className = cell.className.replace('sorttable_sorted_reverse',''); 126 | cell.className = cell.className.replace('sorttable_sorted',''); 127 | } 128 | }); 129 | sortfwdind = document.getElementById('sorttable_sortfwdind'); 130 | if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } 131 | sortrevind = document.getElementById('sorttable_sortrevind'); 132 | if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } 133 | 134 | this.className += ' sorttable_sorted'; 135 | sortfwdind = document.createElement('span'); 136 | sortfwdind.id = "sorttable_sortfwdind"; 137 | sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; 138 | this.appendChild(sortfwdind); 139 | 140 | // build an array to sort. This is a Schwartzian transform thing, 141 | // i.e., we "decorate" each row with the actual sort key, 142 | // sort based on the sort keys, and then put the rows back in order 143 | // which is a lot faster because you only do getInnerText once per row 144 | row_array = []; 145 | col = this.sorttable_columnindex; 146 | rows = this.sorttable_tbody.rows; 147 | for (var j=0; j 12) { 184 | // definitely dd/mm 185 | return sorttable.sort_ddmm; 186 | } else if (second > 12) { 187 | return sorttable.sort_mmdd; 188 | } else { 189 | // looks like a date, but we can't tell which, so assume 190 | // that it's dd/mm (English imperialism!) and keep looking 191 | sortfn = sorttable.sort_ddmm; 192 | } 193 | } 194 | } 195 | } 196 | return sortfn; 197 | }, 198 | 199 | getInnerText: function(node) { 200 | // gets the text we want to use for sorting for a cell. 201 | // strips leading and trailing whitespace. 202 | // this is *not* a generic getInnerText function; it's special to sorttable. 203 | // for example, you can override the cell text with a customkey attribute. 204 | // it also gets .value for fields. 205 | 206 | hasInputs = (typeof node.getElementsByTagName == 'function') && 207 | node.getElementsByTagName('input').length; 208 | 209 | if (node.getAttribute("sorttable_customkey") != null) { 210 | return node.getAttribute("sorttable_customkey"); 211 | } 212 | else if (typeof node.textContent != 'undefined' && !hasInputs) { 213 | return node.textContent.replace(/^\s+|\s+$/g, ''); 214 | } 215 | else if (typeof node.innerText != 'undefined' && !hasInputs) { 216 | return node.innerText.replace(/^\s+|\s+$/g, ''); 217 | } 218 | else if (typeof node.text != 'undefined' && !hasInputs) { 219 | return node.text.replace(/^\s+|\s+$/g, ''); 220 | } 221 | else { 222 | switch (node.nodeType) { 223 | case 3: 224 | if (node.nodeName.toLowerCase() == 'input') { 225 | return node.value.replace(/^\s+|\s+$/g, ''); 226 | } 227 | case 4: 228 | return node.nodeValue.replace(/^\s+|\s+$/g, ''); 229 | break; 230 | case 1: 231 | case 11: 232 | var innerText = ''; 233 | for (var i = 0; i < node.childNodes.length; i++) { 234 | innerText += sorttable.getInnerText(node.childNodes[i]); 235 | } 236 | return innerText.replace(/^\s+|\s+$/g, ''); 237 | break; 238 | default: 239 | return ''; 240 | } 241 | } 242 | }, 243 | 244 | reverse: function(tbody) { 245 | // reverse the rows in a tbody 246 | newrows = []; 247 | for (var i=0; i=0; i--) { 251 | tbody.appendChild(newrows[i]); 252 | } 253 | delete newrows; 254 | }, 255 | 256 | /* sort functions 257 | each sort function takes two parameters, a and b 258 | you are comparing a[0] and b[0] */ 259 | sort_numeric: function(a,b) { 260 | aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); 261 | if (isNaN(aa)) aa = 0; 262 | bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); 263 | if (isNaN(bb)) bb = 0; 264 | return aa-bb; 265 | }, 266 | sort_alpha: function(a,b) { 267 | if (a[0].toLowerCase()==b[0].toLowerCase()) return 0; 268 | if (a[0].toLowerCase() 0 ) { 314 | var q = list[i]; list[i] = list[i+1]; list[i+1] = q; 315 | swap = true; 316 | } 317 | } // for 318 | t--; 319 | 320 | if (!swap) break; 321 | 322 | for(var i = t; i > b; --i) { 323 | if ( comp_func(list[i], list[i-1]) < 0 ) { 324 | var q = list[i]; list[i] = list[i-1]; list[i-1] = q; 325 | swap = true; 326 | } 327 | } // for 328 | b++; 329 | 330 | } // while(swap) 331 | } 332 | } 333 | 334 | /* ****************************************************************** 335 | Supporting functions: bundled here to avoid depending on a library 336 | ****************************************************************** */ 337 | 338 | // Dean Edwards/Matthias Miller/John Resig 339 | 340 | /* for Mozilla/Opera9 */ 341 | if (document.addEventListener) { 342 | document.addEventListener("DOMContentLoaded", sorttable.init, false); 343 | } 344 | 345 | /* for Internet Explorer */ 346 | /*@cc_on @*/ 347 | /*@if (@_win32) 348 | document.write(" 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/lib/view/bin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fenix Request Browser 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | Launching request browser... 17 |
    18 |
    19 |
    20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/lib/view/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Fenix Web Servers 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 37 |
    38 |
    39 |
    40 |
    41 |
    42 |

    New Server

    43 | 44 |
    45 | 46 | 47 | 48 | × 49 |
    50 |
    51 |

    Modify Server

    52 | 53 |
    54 | 55 | 56 | 57 | 58 | × 59 |
    60 |
    61 |

    Log:

    62 |
    63 | × 64 |
    65 |
    Loading...
    66 |
    67 | 68 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/lib/view/splash.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 74 | 75 | 76 |
    Update Available
    77 |
    78 |
    79 | Created by Corey Butler.
    80 | Copyright © 2014 Ecor Ventures, LLC. All Rights Reserved. 81 |
    82 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "index.html", 3 | "name": "fenix", 4 | "description": "Beautifully simple static web servers.", 5 | "version": "2.0.0", 6 | "keywords": [ 7 | "fenix", 8 | "send", 9 | "web", 10 | "server", 11 | "static", 12 | "bin", 13 | "request" 14 | ], 15 | "author": { 16 | "name": "Corey Butler", 17 | "email": "corey@coreybutler.com", 18 | "url": "http://coreybutler.com" 19 | }, 20 | "contributors": [], 21 | "no-edit-menu": true, 22 | "window": { 23 | "title": "Fenix Web Servers", 24 | "icon": "./lib/icons/fenix.png", 25 | "toolbar": false, 26 | "width": 400, 27 | "height": 600, 28 | "position": "center", 29 | "min_width": 400, 30 | "min_height": 250, 31 | "show": false, 32 | "resizable": true, 33 | "single-instance": true 34 | }, 35 | "dependencies": { 36 | "getmac": "1.0.6", 37 | "localtunnel": "1.0.0", 38 | "watch": "0.8.0", 39 | "portscanner": "0.1.3", 40 | "send": "0.1.4", 41 | "marked": "0.2.10", 42 | "ansi-to-html": "0.1.1", 43 | "growler": "0.0.1", 44 | "request": "2.30.0", 45 | "express": "3.4.8", 46 | "semver": "2.2.1", 47 | "node-uuid": "1.4.1", 48 | "universal-analytics": "0.3.4" 49 | }, 50 | "devDependencies": { 51 | "grunt": "0.4.4", 52 | "grunt-contrib-copy": "0.5.0", 53 | "grunt-contrib-jshint": "0.9.2", 54 | "colors": "0.6.2", 55 | "rimraf": "2.2.6", 56 | "grunt-text-replace": "0.3.11", 57 | "grunt-usemin": "2.1.0", 58 | "grunt-contrib-uglify": "0.4.0", 59 | "grunt-contrib-cssmin": "0.9.0", 60 | "grunt-node-webkit-builder": "0.1.18", 61 | "grunt-contrib-concat": "^0.4.0" 62 | } 63 | } 64 | --------------------------------------------------------------------------------