├── .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 | 
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 | [](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 | 
27 | 
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 | 
122 |
123 | **Webhook Browser**
124 |
125 | 
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 |
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 | ? "