├── example ├── package.json ├── config.json ├── app.js └── index.html ├── package.json ├── LICENSE ├── README.md ├── worknotes.md └── appserver.js /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testdaveappserver", 3 | "description": "Bare bones demo app for daveappserver package.", 4 | "author": "Dave Winer ", 5 | "license": "MIT", 6 | "version": "0.4.0", 7 | "dependencies" : { 8 | "daveutils": "*", 9 | "daves3": "*", 10 | "davefilesystem": "*", 11 | "daveappserver": "*" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "testDaveAppserver", 3 | "productNameForDisplay": "Example App", 4 | 5 | "urlServerHomePageSource": "http://scripting.com/code/daveappserver/example/index.html", 6 | 7 | "prefsPath": "prefs.json", 8 | "docsPath": "myDocs/", 9 | 10 | "port": 1229, 11 | "flWebsocketEnabled": true, 12 | "websocketPort": 1230, 13 | 14 | "myDomain": "localhost:1229", 15 | 16 | "twitterConsumerKey": "abcdefghijklmnopqrstufwxy", 17 | "twitterConsumerSecret": "abcdefghijklmnopqrstufwxyabcdefghijklmnopqrstufwxy" 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daveappserver", 3 | "description": "Factored code that was appearing in all my servers.", 4 | "version": "0.8.3", 5 | "main": "appserver.js", 6 | "repository": { 7 | "type" : "git", 8 | "url" : "https://github.com/scripting/appServer" 9 | }, 10 | "license": "MIT", 11 | "files": [ 12 | "appserver.js" 13 | ], 14 | "dependencies" : { 15 | "querystring": "*", 16 | "request": "*", 17 | "ws": "*", 18 | "require-from-string": "*", 19 | "daveutils": "*", 20 | "davefilesystem": "*", 21 | "foldertojson": "*", 22 | "davetwitter": "*", 23 | "davezip": "*", 24 | "davesql": "*", 25 | "daves3": "*", 26 | "davehttp": "*", 27 | "davemail": "*", 28 | "wpidentity": "^0.5.27" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | const daveappserver = require ("daveappserver"); 2 | const utils = require ("daveutils"); 3 | 4 | var stats = { 5 | ctslogans: 0, 6 | whenLastSlogan: undefined 7 | }; 8 | var options = { 9 | everySecond: function () { 10 | }, 11 | everyMinute: function () { 12 | }, 13 | httpRequest: function (theRequest) { 14 | var now = new Date (); 15 | function returnPlainText (s) { 16 | theRequest.httpReturn (200, "text/plain", s.toString ()); 17 | } 18 | function returnData (jstruct) { 19 | if (jstruct === undefined) { 20 | jstruct = {}; 21 | } 22 | theRequest.httpReturn (200, "application/json", utils.jsonStringify (jstruct)); 23 | } 24 | switch (theRequest.lowerpath) { 25 | case "/slogan": 26 | stats.ctslogans++; 27 | stats.whenLastSlogan = now; 28 | daveappserver.saveStats (stats); 29 | returnData ({slogan: utils.getRandomSnarkySlogan ()}); 30 | return (true); 31 | } 32 | return (false); //not consumed 33 | } 34 | } 35 | daveappserver.start (options); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dave Winer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appserver 2 | 3 | A new release of nodeStorage that removes all the historical addons, streamlines configuration, only serves from the local file system, and serves the home page of the app. It could be thought of as v2.0 of nodeStorage except it is not backward compatible. 4 | 5 | ### Overview 6 | 7 | Over the years nodeStorage got a lot of appendages and add-ons that most apps don't use. 8 | 9 | The configuration file was confusing and not well documented. 10 | 11 | It was one of the first things I wrote for Node, and I learned a lot over time. 12 | 13 | FInally when one of my programming partners had difficulty understanding the code, I realizaed I did too, and decided to rebuild it from scratch using components I had already developed, such as davehttp, davetwitter, etc. 14 | 15 | I have converted Little Outliner to use this backend. The app was only modified in how it's configured. Everything else remains the same. It uses the same api glue file that it used to access nodeStorage. 16 | 17 | ### Configuring 18 | 19 | Below is an example of config.json file that goes in the same directory as the appserver.js file. 20 | 21 | ### Example 22 | 23 | ```json { "productName": "littleOutliner", "productNameForDisplay": "Little Outliner", "urlServerHomePageSource": "http://littleoutliner.com/testing/1.9.0/index.html", "prefsPath": "outlinerPrefs.json", "docsPath": "myOutlines/", "port": 1962, "flWebsocketEnabled": true, "websocketPort": 1963, "myDomain": "test.littleoutliner.com", "twitterConsumerKey": "1234567890123456789012345", "twitterConsumerSecret": "12345678901234567890123456789012345678901234567890" } ``` 24 | 25 | ### Explanation 26 | 27 | The first section provides configuration information about and to the app that's running on the server's home page. 28 | 29 | 1. productName is an id that can be used to identify the product. No specified use. 30 | 31 | 2. productNameForDisplay is what the app should use in its user interface. 32 | 33 | 3. urlServerHomePageSource is the address of an HTML page that is served through the home page of the server. It can contain macros that plug in values from the server. 34 | 35 | 4. prefsPath is the name of the prefs file for the app. This is the file you should read and save to, to maintain the state of the app for the user. 36 | 37 | 5. docsPath is where the documents for this app are stored in the user's storage on the server. 38 | 39 | 6. urlServerForClient is the address the app uses to call back to the server. 40 | 41 | 7. urlWebsocketServerForClient is the address of the web socket that reports on changes to files stored on the server. This address can be included in the app files so that apps that read the files can subbscribe to changes. 42 | 43 | The second section configures the HTTP server and the connection to Twitter for identity. 44 | 45 | 1. port is the port the HTTP server will run on, unless process.env.PORT is specified. It overrides the port choice in the config file. 46 | 47 | 2. flWebsocketEnabled, a boolean, determines whether or not we initialize the websocket server, if it's true then websocketPort is the port it's listening on. 48 | 49 | 3. myDomain, the domain assigned to this app. It's the domain you'd use to access the home page. If that includes a port, include the port in this value. 50 | 51 | 4. twitterConsumerKey and twitterConsumerSecret are the values that identify the app for Twitter, as assigned on developer.twitter.com. 52 | 53 | ### Updates 54 | 55 | #### v0.5.48 -- 3/18/22 by DW 56 | 57 | Exports three routines: fileExists, readWholeFile, writeWholeFile. 58 | 59 | #### v0.5.44 -- 3/18/22 by DW 60 | 61 | New callback, config.publishFile. If defined we call back with the file, screenname, relpath, flprivate, filetext and the url of the file if it's public. 62 | 63 | It's called when we handle a /publishfile or /writewholefile message. 64 | 65 | #### v0.5.32 -- 12/4/21 by DW 66 | 67 | Added a new callback, publicFileSaved, which is called when the user updates a public file. 68 | 69 | Fixed a bug where a user couldn't create a new file if their public files folder was empty. 70 | 71 | #### v0.5.32 -- 9/26/21 by DW 72 | 73 | Fixed the example app to require "../appserver.js" instead of "lib/daveappserver.js" which only exists on my development machine. 74 | 75 | #### v0.5.31 -- 9/15/21 by DW 76 | 77 | writeWholeFile was meant to be sure the path to the file exists before writing the file. It wasn't doing it, now it does. 78 | 79 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | [%productNameForDisplay%] 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 41 | 159 | 160 | 161 |
162 |
163 | 197 |
198 |
199 | 200 |
201 |
202 |

This is the home page for an example app for daveAppServer.

203 |
204 |
205 | 206 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /worknotes.md: -------------------------------------------------------------------------------- 1 | #### 10/31/25; 7:42:48 AM by DW -- v0.8.2 2 | 3 | Need to require new version of wpIdentity. 4 | 5 | Turned off console messages in notifySocketSubscribers. 6 | 7 | #### 5/25/25; 1:07:27 PM by DW -- v0.8.0 8 | 9 | I wrote the test app i mentioned in the previous note. Set it up to do what we do when sending notifications about new feed items, only instead of using the older websockets package, i use the new one. worked perfectly, no double-notifications. so now i'm going to convert to use the new package here. 10 | 11 | #### 5/21/25; 11:09:32 AM by DW -- v0.7.16 12 | 13 | I was trying to debug why we're getting double notification of new items via the websockets interface. 14 | 15 | It's a real mystery, but the problem is not in the database code, it's in the websockets code. 16 | 17 | I'm going to handle the problem by writing a special server app that just spools out new items via websockets, all new code, let's see if we can find the doubling-up that way. 18 | 19 | #### 10/6/24; 12:38:10 PM by DW 20 | 21 | wordpressHandleRequest/checkPendingConfirmation was trying to delete a non-existing confirmation record. Added a check for err that should have been there. 22 | 23 | #### 11/14/23; 5:24:09 PM by DW 24 | 25 | For WordPress identity to work on a multi-instance setup, we have to use the same confirmation approach we use for email signup. So every call to the WordPress event handler we add two callbacks to the options object to support creating an identification record in the database, and checking it on callback from WordPress that the value is correct in the state record then send back with the confirmation. 26 | 27 | Note -- to get this functionality you must set config.flUseDatabaseForConfirmations to true. 28 | 29 | #### 11/11/23; 1:01:55 PM by DW 30 | 31 | Change config.flEnableSupervisorMode to default true instead of false. 32 | 33 | #### 10/31/23; 9:06:37 AM by DW 34 | 35 | WordPress login. 36 | 37 | Previously, we added code that let WordPress have access to every http event, this made it possible to develop the WordPress verbs in Drummer scripting. 38 | 39 | Now we want to do more, allow an appserver client app to grab the logon event, when the user has given us permission and we've gotten an accessToken. 40 | 41 | So there's a callback, search for, that in turn calls a callback in the client app, that then does what it wants to with the login. 42 | 43 | In the case of FeedLand, the fit is pretty tight. FL needs what WP can provide -- a unique name and an email address. 44 | 45 | This connection is made here in daveappserver. 46 | 47 | #### 10/30/23; 1:52:52 PM by DW 48 | 49 | Fixed a crashing bug in http404Callback. 50 | 51 | #### 9/20/23; 10:33:40 AM by DW 52 | 53 | Two new optional callbacks, getStaticFile and publishStaticFile. 54 | 55 | FeedLand uses this to store static files in a database table instead of S3. 56 | 57 | #### 9/16/23; 10:46:52 AM by DW 58 | 59 | Reorganized returnServerHomePage so we read the template text after calling the callbacks. They can change the url of the home page source. This was needed when we added newsproduct rendering to the FeedLand server. 60 | 61 | #### 9/11/23; 10:34:58 AM by DW 62 | 63 | How to set up for WordPress 64 | 65 | 1. First set up a new app here. 66 | 67 | 2. There's a new section of config.json called wordpress. In it are these values: 68 | 69 | ```JavaScript "wordpress": { "clientId": 123456789, "clientSecret": "xxx", "urlRedirect": "https://myserver.com/callbackFromWordpress", "scope": "global" } ``` 70 | 71 | 3. Include this in the <head> section of your home page HTML 72 | 73 | <script src="//s3.amazonaws.com/scripting.com/code/wpidentity/client/api.js"></script> 74 | 75 | #### 9/10/23; 4:27:32 PM by DW -- v0.7.0 76 | 77 | WordPress functionality. An app running on daveappserver will be able to log the user on to WordPress, and perform basic operations like creating, updating and deleting posts, getting a list of all their sites. Using the wpidentity package. 78 | 79 | There are two points of integration, at startup and when handling an http request. 80 | 81 | #### 8/15/23; 6:54:54 PM by DW 82 | 83 | New config setting, flTraceOnError. Defaults false. If true, when davehttp handles a request, it gives you a stack trace that seems pretty useless and it hides where the actual error is happening. You can turn it back on if you feel it is useful. 84 | 85 | #### 8/14/23; 11:39:18 AM by DW 86 | 87 | We need the option of storing pending confirmations in a database, not in memory. 88 | 89 | new config setting: flUseDatabaseForConfirmations, default false 90 | 91 | config.database must also be defined, but we don't check. 92 | 93 | #### 7/27/23; 2:27:42 PM by DW 94 | 95 | dns.reverse can crash, so do the call in a try statement. 96 | 97 | #### 7/26/23; 3:15:15 PM by DW 98 | 99 | There was a stray bit of code that's first running today but was written on May 8 this year (that's the creation date on the outline element). 100 | 101 | Obviously a direction I thought of going in but didn't. 102 | 103 | It made it into the published package and into the NPM version. 104 | 105 | getPagetableForHomePage (function (err, pagetable) { 106 | 107 | }); 108 | 109 | #### 3/17/23; 12:27:58 PM by DW 110 | 111 | Only alpha, numeric and underscore characters allowed in user names. 112 | 113 | #### 3/8/23; 12:13:06 PM by DW 114 | 115 | email addresses become unicase. 116 | 117 | we do this by converting the email address as it enters the system from the client. 118 | 119 | sendConfirmingEmail 120 | 121 | callWithScreenname 122 | 123 | #### 3/3/23; 10:49:28 AM by DW 124 | 125 | I want to be able to send a confirming email from Electric Drummer and have it return with the emailMemory record to a localhost address. 126 | 127 | This means that the sendConfirmingEmail call must have a new parameter, urlredirect which, if specified, is where we redirect to on confirmation. 128 | 129 | #### 2/8/23; 10:03:25 AM by DW 130 | 131 | New config.json setting -- flSecureWebsocket. If true we initiate connections with wss:// otherwise ws:// 132 | 133 | #### 2/8/23; 8:57:32 AM by DW 134 | 135 | At startup we look for config.js in the same folder as the app, if it's present we require it, and use the result in place of config.json. 136 | 137 | This approach is needed in certain hosting situations including WordPress VIP. 138 | 139 | #### 1/30/23; 12:38:16 PM by DW 140 | 141 | If config.urlServerForClient is set, don't set it to the default. 142 | 143 | Same with config.urlWebsocketServerForClient. 144 | 145 | Made sure that config.urlServerForClient and urlWebsocketServerForClient were undefined if not specified in config.json, so we would set the defaults when needed. 146 | 147 | #### 1/23/23; 3:35:25 PM by DW 148 | 149 | Support SMTP mail sending in addition to SES. 150 | 151 | New values in config.json to support this: 152 | 153 | "smtpHost": "smtp.mailhost.com", 154 | 155 | "smtpPort": 587, 156 | 157 | "smtpUsername": "bullman", 158 | 159 | "smtpPassword": "getoutahere", 160 | 161 | #### 9/16/22 by DW -- 0.5.57 162 | 163 | Changed /useriswhitelisted call so that it reads config.json itself, so the system doesn't have to be rebooted to make a change to the whitelist. 164 | 165 | #### 8/14/22 by DW -- 0.5.56 166 | 167 | Exporting getFilePath so FeedLand server can find the user's prefs.json file. 168 | 169 | #### 8/14/22 by DW -- 0.5.55 170 | 171 | New userLogonCallback callback. Called when the user has successfully logged in via davetwitter. We send back the information about the login, the user's screenname, userid, token and secret. 172 | 173 | #### 8/10/22 by DW -- 0.5.53, 0.5.54 174 | 175 | In returnServerHomePage we add a new param to the addMacroToPagetable callback, the request object. 176 | 177 | #### 7/21/22 by DW -- 0.5.52 178 | 179 | New config setting, config.whitelist. Defaults to undefined. 180 | 181 | If undefined, the app doesn't have a whitelist and anyone is authorized to use it, ie everyone is whitelisted. 182 | 183 | If not undefined, it's an array of screennames of people who are authorized to use the software. 184 | 185 | There's a new call /useriswhitelisted that determines if a user is whitelisted. 186 | 187 | #### 7/3/22 by DW -- 0.5.49 188 | 189 | In writeWholeFile if config.publishFile was defined, we'd call back with a type param that wasn't defined. We defined it. 190 | 191 | Added this file to the project to be consistent with other new projects. 192 | 193 | -------------------------------------------------------------------------------- /appserver.js: -------------------------------------------------------------------------------- 1 | var myVersion = "0.8.3", myProductName = "daveAppServer"; 2 | 3 | exports.start = startup; 4 | exports.notifySocketSubscribers = notifySocketSubscribers; 5 | exports.saveStats = saveStats; 6 | exports.getStats = getStats; //6/28/21 by DW 7 | exports.getConfig = getConfig; 8 | exports.publishFile = publishFile; //12/13/21 by DW 9 | exports.readWholeFile = readWholeFile; //5/28/22 by DW 10 | exports.writeWholeFile = writeWholeFile; //5/28/22 by DW 11 | exports.getFilePath = getFilePath; //9/13/22 by DW 12 | 13 | const fs = require ("fs"); 14 | var dns = require ("dns"); 15 | var os = require ("os"); 16 | const request = require ("request"); 17 | const websocket = require ("ws"); 18 | const utils = require ("daveutils"); 19 | const davehttp = require ("davehttp"); 20 | const davetwitter = require ("davetwitter"); 21 | const filesystem = require ("davefilesystem"); 22 | const folderToJson = require ("foldertojson"); 23 | const zip = require ("davezip"); 24 | const s3 = require ("daves3"); 25 | const qs = require ("querystring"); 26 | const mail = require ("davemail"); 27 | const requireFromString = require ("require-from-string"); //2/10/23 by DW 28 | const davesql = require ("davesql"); //8/14/23 by DW 29 | const wordpress = require ("wpidentity"); //9/10/23 by DW 30 | 31 | const whenStart = new Date (); 32 | 33 | var config = { 34 | productName: "randomApp", 35 | productNameForDisplay: "Random App", 36 | version: myVersion, 37 | prefsPath: "prefs.json", 38 | docsPath: "myDocs/", 39 | flLogToConsole: true, 40 | port: process.env.PORT || 1420, 41 | websocketPort: 1422, 42 | flAllowAccessFromAnywhere: true, 43 | flPostEnabled: true, //12/21/20 by DW 44 | flWebsocketEnabled: true, 45 | flEnableLogin: true, //user can log in via twitter 46 | blockedAddresses: [], 47 | flForceTwitterLogin: true, 48 | flUseTwitterIdentity: false, //2/6/23 by DW 49 | flTraceOnError: false, //8/15/23 by DW 50 | 51 | flStorageEnabled: true, 52 | privateFilesPath: "privateFiles/users/", 53 | publicFilesPath: "publicFiles/users/", 54 | 55 | defaultContentType: "text/plain", //8/3/21 by DW 56 | 57 | userAgent: myProductName + " v" + myVersion, //11/8/21 by DW 58 | 59 | whitelist: undefined, //7/21/22 by DW 60 | 61 | confirmEmailSubject: "FeedLand confirmation", //12/7/22 by DW 62 | fnameEmailTemplate: "emailtemplate.html", 63 | operationToConfirm: "add your email address to your FeedLand user profile", 64 | mailSender: "dave@scripting.com", 65 | dataFolder: "data/", 66 | confirmationExpiresAfter: 60 * 60 * 24, //emails expire after 24 hours 67 | 68 | flSecureWebsocket: false, //2/8/23 by DW 69 | 70 | flUseS3ForStorage: false, //2/15/23 by DW 71 | 72 | flUseDatabaseForConfirmations: false, //8/14/23 by DW 73 | 74 | flAllowWordpressIdentity: true, //10/31/23 by DW 75 | 76 | flEnableSupervisorMode: true, //11/3/23 by DW 77 | 78 | findUserWithScreenname: function (screenname, callback) { //2/6/23 by DW 79 | callback (false); 80 | }, 81 | findUserWithEmail: function (emailaddress, callback) { //11/4/23 by DW 82 | callback (false); 83 | }, 84 | getScreenNameFromEmail: function (screenname, callback) { //2/7/23 by DW 85 | callback (undefined, screenname); 86 | }, 87 | 88 | isUserAdmin: function (emailaddress, callback) { //11/3/23 by DW 89 | callback (false); 90 | } 91 | }; 92 | const fnameConfig = "config.json"; 93 | 94 | var stats = { 95 | whenFirstStart: whenStart, ctStarts: 0, 96 | whenLastStart: undefined, 97 | ctWrites: 0, 98 | ctHits: 0, ctHitsToday: 0, ctHitsThisRun:0, 99 | whenLastHit: new Date (0), 100 | pendingConfirmations: new Array () //12/7/22 by DW 101 | }; 102 | const fnameStats = "stats.json"; 103 | 104 | function userIsWhitelisted (screenname, callback) { //9/16/22 by DW 105 | fs.readFile (fnameConfig, function (err, jsontext) { 106 | var flWhitelisted = false; 107 | if (!err) { 108 | var jstruct; 109 | try { 110 | jstruct = JSON.parse (jsontext); 111 | if (jstruct.whitelist === undefined) { //no whitelist 112 | flWhitelisted = true; 113 | } 114 | else { 115 | flWhitelisted = jstruct.whitelist.includes (screenname); 116 | } 117 | } 118 | catch (err) { 119 | } 120 | } 121 | console.log ("userIsWhitelisted: screenname == " + screenname + ", flWhitelisted == " + flWhitelisted); 122 | callback (undefined, {flWhitelisted}); 123 | }); 124 | } 125 | function statsChanged () { 126 | flStatsChanged = true; 127 | } 128 | function saveStats (theStats) { 129 | for (var x in theStats) { 130 | stats [x] = theStats [x]; 131 | } 132 | statsChanged (); 133 | } 134 | function getStats () { //6/28/21 by DW 135 | return (stats); 136 | } 137 | function getConfig () { 138 | return (config); 139 | } 140 | function httpReadUrl (url, callback) { 141 | request (url, function (err, response, data) { 142 | if (err) { 143 | callback (err); 144 | } 145 | else { 146 | if (response.statusCode != 200) { 147 | const errstruct = { 148 | message: "Can't read the URL, \"" + url + "\" because we received a status code of " + response.statusCode + ".", 149 | statusCode: response.statusCode 150 | }; 151 | callback (errstruct); 152 | } 153 | else { 154 | callback (undefined, data); 155 | } 156 | } 157 | }); 158 | } 159 | function httpFullRequest (jsontext, callback) { //11/5/21 by DW 160 | var theRequest; 161 | function isErrorStatusCode (theCode) { //11/8/21 by DW 162 | return ((theCode < 200) || (theCode > 299)); 163 | } 164 | try { 165 | theRequest = JSON.parse (jsontext); 166 | } 167 | catch (err) { 168 | callback (err); 169 | return; 170 | } 171 | request (theRequest, function (err, response, data) { 172 | if (err) { 173 | callback (err); 174 | } 175 | else { 176 | if (isErrorStatusCode (response.statusCode)) { //11/8/21 by DW 177 | const errstruct = { 178 | message: "Can't read the URL, \"" + theRequest.url + "\" because we received a status code of " + response.statusCode + ".", 179 | statusCode: response.statusCode, 180 | data //11/8/21 by DW 181 | }; 182 | callback (errstruct); 183 | } 184 | else { 185 | callback (undefined, data); 186 | } 187 | } 188 | }); 189 | } 190 | function checkPathForIllegalChars (path) { 191 | function isIllegal (ch) { 192 | if (utils.isAlpha (ch) || utils.isNumeric (ch)) { 193 | return (false); 194 | } 195 | switch (ch) { 196 | case "/": case "_": case "-": case ".": case " ": case "*": case "@": 197 | return (false); 198 | } 199 | return (true); 200 | } 201 | for (var i = 0; i < path.length; i++) { 202 | if (isIllegal (path [i])) { 203 | return (false); 204 | } 205 | } 206 | if (utils.stringContains (path, "./")) { 207 | return (false); 208 | } 209 | return (true); 210 | } 211 | function getDomainName (clientIp, callback) { //11/14/15 by DW 212 | if (clientIp === undefined) { 213 | if (callback !== undefined) { 214 | callback ("undefined"); 215 | } 216 | } 217 | else { 218 | try { //7/27/23 by DW 219 | dns.reverse (clientIp, function (err, domains) { 220 | var name = clientIp; 221 | if (!err) { 222 | if (domains.length > 0) { 223 | name = domains [0]; 224 | } 225 | } 226 | if (callback !== undefined) { 227 | callback (name); 228 | } 229 | }); 230 | } 231 | catch (err) { 232 | if (callback !== undefined) { 233 | callback (name); 234 | } 235 | } 236 | } 237 | } 238 | function getDomainNameVerb (clientIp, callback) { //2/27/21 by DW 239 | dns.reverse (clientIp, function (err, domains) { 240 | if (err) { 241 | callback (err); 242 | } 243 | else { 244 | var name = (domains.length > 0) ? name = domains [0] : clientIp; 245 | callback (undefined, {name}); 246 | } 247 | }); 248 | } 249 | function getDottedIdVerb (name, callback) { //2/27/21 by DW 250 | dns.lookup (name, null, function (err, dottedid) { 251 | if (err) { 252 | callback (err); 253 | } 254 | else { 255 | callback (undefined, {dottedid}); 256 | } 257 | }); 258 | } 259 | function cleanFileStats (stats) { //4/19/21 by DW 260 | function formatDate (d) { 261 | return (new Date (d).toUTCString ()); 262 | } 263 | var cleanStats = { 264 | size: stats.size, //number of bytes in file 265 | whenAccessed: formatDate (stats.atime), //when last read 266 | whenCreated: formatDate (stats.birthtime), 267 | whenModified: formatDate (stats.mtime), 268 | flPrivate: stats.flPrivate 269 | } 270 | return (cleanStats); 271 | } 272 | 273 | //sockets 274 | var theWsServer = undefined; 275 | 276 | function getWsProtocol () { //2/8/23 by DW 277 | const protocol = (utils.getBoolean (config.flSecureWebsocket)) ? "wss://" : "ws://"; 278 | return (protocol); 279 | } 280 | function notifySocketSubscribers (verb, payload, flPayloadIsString, callbackToQualify) { 281 | if (theWsServer !== undefined) { 282 | var ctUpdates = 0, now = new Date (), ctTotalSockets = 0; 283 | if (payload !== undefined) { 284 | if (!flPayloadIsString) { 285 | payload = utils.jsonStringify (payload); 286 | } 287 | } 288 | theWsServer.clients.forEach (function (conn, ix) { 289 | ctTotalSockets++; 290 | if (conn.appData !== undefined) { //it's one of ours 291 | var flnotify = true; 292 | if (callbackToQualify !== undefined) { 293 | flnotify = callbackToQualify (conn); 294 | } 295 | if (flnotify) { 296 | try { 297 | conn.send (verb + "\r" + payload); //5/25/25 by DW 298 | conn.appData.whenLastUpdate = now; 299 | conn.appData.ctUpdates++; 300 | ctUpdates++; 301 | } 302 | catch (err) { 303 | } 304 | } 305 | } 306 | }); 307 | } 308 | } 309 | function checkWebSocketCalls () { //expire timed-out calls 310 | } 311 | function countOpenSockets () { 312 | if (theWsServer === undefined) { //12/18/15 by DW 313 | return (0); 314 | } 315 | else { 316 | return (theWsServer.clients.length); 317 | } 318 | } 319 | function getOpenSocketsArray () { //return an array with data about open sockets 320 | var theArray = new Array (); 321 | function viewTime (when) { //5/21/25 by DW -- wanted to see more detail in time, include seconds 322 | return (new Date (when).toLocaleTimeString ()); 323 | } 324 | theWsServer.clients.forEach (function (conn, ix) { 325 | if (conn.appData !== undefined) { //it's one of ours 326 | theArray.push ({ 327 | lastVerb: conn.appData.lastVerb, 328 | urlToWatch: conn.appData.urlToWatch, 329 | domain: conn.appData.domain, 330 | whenStarted: viewTime (conn.appData.whenStarted), 331 | whenLastUpdate: viewTime (conn.appData.whenLastUpdate), 332 | emailAddress: conn.appData.emailAddress //5/21/25 by DW 333 | }); 334 | } 335 | }); 336 | return (theArray); 337 | } 338 | function handleWebSocketConnection (conn) { 339 | var now = new Date (); 340 | 341 | conn.appData = { //initialize 342 | whenStarted: now, 343 | ctUpdates: 0, 344 | whenLastUpdate: new Date (0), 345 | lastVerb: undefined, 346 | urlToWatch: undefined, 347 | domain: undefined 348 | }; 349 | 350 | function logToConsole (conn, verb, value) { 351 | getDomainName (conn._socket.remoteAddress, function (theName) { //log the request 352 | var freemem = utils.gigabyteString (os.freemem ()), method = "WS:" + verb, now = new Date (); 353 | if (theName === undefined) { 354 | theName = conn._socket.remoteAddress; 355 | } 356 | console.log (now.toLocaleTimeString () + " " + freemem + " " + method + " " + value + " " + theName); 357 | conn.appData.domain = theName; 358 | }); 359 | } 360 | 361 | function kissOtherLogonsGoodnight (screenname, theNewConnection) { //12/14/21 by DW 362 | theWsServer.clients.forEach (function (conn, ix) { 363 | if (conn.appData !== undefined) { //it's one of ours 364 | if (conn != theNewConnection) { //it's not the new one 365 | if (conn.appData.screenname == screenname) { 366 | console.log ("kissOtherLogonsGoodnight: \"" + conn.appData.screenname + "\" = \"" + screenname + "\""); //2/12/23 by DW 367 | conn.send ("goodnight"); //5/25/25 by DW 368 | } 369 | } 370 | } 371 | }); 372 | } 373 | 374 | conn.on ("message", function (s) { 375 | s = s.toString (); //5/25/25 by DW 376 | var words = s.split (" "); 377 | if (words.length > 1) { //new protocol as of 11/29/15 by DW 378 | conn.appData.whenLastUpdate = now; 379 | conn.appData.lastVerb = words [0]; 380 | switch (words [0]) { 381 | case "watch": 382 | conn.appData.urlToWatch = utils.trimWhitespace (words [1]); 383 | logToConsole (conn, conn.appData.lastVerb, conn.appData.urlToWatch); 384 | break; 385 | case "user": //9/29/21 by DW 386 | if (config.flUseTwitterIdentity) { //2/12/23 by DW 387 | var token = words [1], secret = words [2]; 388 | conn.appData.twOauthToken = token; 389 | conn.appData.twOauthTokenSecret = secret; 390 | conn.appData.urlToWatch = ""; 391 | davetwitter.getScreenName (token, secret, function (screenname) { 392 | conn.appData.screenname = screenname; 393 | kissOtherLogonsGoodnight (screenname, conn); //12/14/21 by DW 394 | logToConsole (conn, conn.appData.lastVerb, conn.appData.screenname); 395 | }); 396 | } 397 | else { 398 | var emailAddress = words [1], emailSecret = words [2]; 399 | config.findUserWithEmail (emailAddress, function (flInDatabase, userRec) { 400 | if (flInDatabase) { 401 | conn.appData.emailAddress = userRec.emailAddress; 402 | conn.appData.screenname = userRec.emailAddress; 403 | conn.appData.emailSecret = userRec.emailSecret; 404 | conn.appData.urlToWatch = ""; 405 | kissOtherLogonsGoodnight (conn.appData.screenname, conn); 406 | logToConsole (conn, conn.appData.lastVerb, conn.appData.screenname); 407 | } 408 | }); 409 | conn.appData.emailAddress = emailAddress; //5/21/25 by DW 410 | conn.appData.emailSecret = emailSecret; 411 | } 412 | break; 413 | 414 | } 415 | } 416 | else { 417 | conn.close (); 418 | } 419 | }); 420 | conn.on ("close", function () { 421 | }); 422 | conn.on ("error", function (err) { 423 | }); 424 | } 425 | function webSocketStartup () { 426 | if (config.flWebsocketEnabled) { 427 | try { 428 | theWsServer = new websocket.Server({port: config.websocketPort}); //5/25/25 by DW 429 | theWsServer.on ("connection", handleWebSocketConnection); //5/25/25 by DW 430 | console.log ("webSocketStartup: config.websocketPort == " + config.websocketPort); 431 | theWsServer.listen (config.websocketPort); 432 | } 433 | catch (err) { 434 | console.log ("webSocketStartup: err.message == " + err.message); 435 | } 436 | } 437 | } 438 | //s3 storage -- 2/14/23 by DW 439 | 440 | function getS3FilePath (screenname, relpath, flprivate) { 441 | const folder = (flprivate) ? config.privateFilesPath : config.publicFilesPath; 442 | const f = folder + screenname + "/" + relpath; 443 | const s3path = config.s3PathForStorage + f; 444 | return (s3path); 445 | } 446 | 447 | function getFileFromS3 (screenname, relpath, flprivate, callback) { 448 | function formatDate (d) { 449 | return (new Date (d).toUTCString ()); 450 | } 451 | s3.getObject (getS3FilePath (screenname, relpath, flprivate), function (err, data) { 452 | if (err) { 453 | callback (err); 454 | } 455 | else { 456 | var data = { 457 | filedata: data.Body.toString (), 458 | filestats: { 459 | whenModified: formatDate (data.LastModified) 460 | } 461 | }; 462 | callback (undefined, data); 463 | } 464 | }); 465 | } 466 | function saveFileToS3 (screenname, relpath, type, flprivate, filetext, callback) { 467 | const acl = undefined; //use the default 468 | s3.newObject (getS3FilePath (screenname, relpath, flprivate), filetext, type, acl, callback); 469 | } 470 | //storage functions 471 | function getFilePath (screenname, relpath, flprivate) { 472 | const folder = (flprivate) ? config.privateFilesPath : config.publicFilesPath; 473 | const f = folder + screenname + "/" + relpath; 474 | return (f); 475 | } 476 | function findFile (screenname, relpath, callback) { //4/1/21 by DW 477 | var f = getFilePath (screenname, relpath, false); //public version 478 | fs.stat (f, function (err, stats) { 479 | if (err) { 480 | f = getFilePath (screenname, relpath, true); //private version 481 | fs.stat (f, function (err, stats) { 482 | if (err) { 483 | callback (err); 484 | } 485 | else { 486 | stats.flPrivate = true; 487 | callback (undefined, stats); 488 | } 489 | }); 490 | } 491 | else { 492 | stats.flPrivate = false; 493 | callback (undefined, stats); 494 | } 495 | }); 496 | } 497 | function publishFile (screenname, relpath, type, flprivate, filetext, callback) { 498 | if (config.publishStaticFile !== undefined) { //9/20/23 by DW 499 | config.publishStaticFile (screenname, relpath, type, flprivate, filetext, callback); 500 | } 501 | else { 502 | if (config.flStorageEnabled) { 503 | if (config.flUseS3ForStorage) { 504 | saveFileToS3 (screenname, relpath, type, flprivate, filetext, callback); 505 | } 506 | else { 507 | var f = getFilePath (screenname, relpath, flprivate); 508 | utils.sureFilePath (f, function () { 509 | var now = new Date (); 510 | fs.writeFile (f, filetext, function (err) { 511 | if (err) { 512 | callback (err); 513 | } 514 | else { 515 | var url = (flprivate) ? undefined : config.urlServerForClient + screenname + "/" + relpath; 516 | if (config.publishFile !== undefined) { //3/18/22 by DW 517 | config.publishFile (f, screenname, relpath, type, flprivate, filetext, url); 518 | } 519 | if (!flprivate) { 520 | notifySocketSubscribers ("update", filetext, true, function (conn) { //3/6/2 by DW -- payload is a string 521 | if (conn.appData.urlToWatch == url) { 522 | return (true); 523 | } 524 | else { 525 | return (false); 526 | } 527 | }); 528 | } 529 | callback (undefined, { 530 | url, 531 | whenLastUpdate: now 532 | }); 533 | } 534 | }); 535 | }); 536 | } 537 | } 538 | else { 539 | callback ({message: "Can't publish the file because the feature is not enabled on the server."}); 540 | } 541 | } 542 | } 543 | function getFile (screenname, relpath, flprivate, callback) { 544 | function errcallback (err) { 545 | if (err.code == "ENOENT") { 546 | err.status = 500; 547 | err.code = "NoSuchKey"; 548 | } 549 | callback (err); 550 | } 551 | function getFromStaticFIlesystem () { 552 | if (config.flStorageEnabled) { 553 | if (config.flUseS3ForStorage) { 554 | getFileFromS3 (screenname, relpath, flprivate, callback); 555 | } 556 | else { 557 | var f = getFilePath (screenname, relpath, flprivate); 558 | fs.readFile (f, function (err, filetext) { 559 | if (err) { 560 | errcallback (err); 561 | } 562 | else { 563 | fs.stat (f, function (err, stats) { 564 | if (err) { 565 | errcallback (err); 566 | } 567 | else { 568 | var data = { 569 | filedata: filetext.toString (), 570 | filestats: cleanFileStats (stats) 571 | }; 572 | callback (undefined, data); 573 | } 574 | }); 575 | } 576 | }); 577 | } 578 | } 579 | else { 580 | callback ({message: "Can't get the file because the feature is not enabled on the server."}); 581 | } 582 | } 583 | if (config.getStaticFile !== undefined) { //9/20/23 by DW 584 | config.getStaticFile (screenname, relpath, flprivate, function (err, data) { 585 | if (err) { 586 | getFromStaticFIlesystem (); 587 | } 588 | else { 589 | callback (undefined, data); 590 | } 591 | }); 592 | } 593 | else { 594 | getFromStaticFIlesystem (); 595 | } 596 | } 597 | function getFileList (screenname, flprivate, callback) { 598 | var folder = getFilePath (screenname, "", flprivate); 599 | filesystem.getFolderInfo (folder, function (theList) { 600 | var returnedList = new Array (); 601 | theList.forEach (function (item) { 602 | var fname = utils.stringLastField (item.f, "/"); 603 | if (fname != ".DS_Store") { 604 | returnedList.push ({ 605 | path: utils.stringDelete (item.f, 1, folder.length), 606 | whenLastChange: item.whenModified, 607 | whenCreated: item.whenCreated, 608 | ctChars: item.size 609 | }); 610 | } 611 | }); 612 | if (callback != undefined) { 613 | callback (undefined, returnedList); 614 | } 615 | }); 616 | } 617 | 618 | function getPublicFileUrl (screenname, relpath) { //12/4/21 by DW 619 | var urlpublic = config.urlServerForClient + screenname + "/" + relpath; 620 | return (urlpublic); 621 | } 622 | 623 | function makeFilePublic (screenname, relpath, callback) { //2/20/21 by DW 624 | console.log ("makeFilePublic: relpath == " + relpath); 625 | getFile (screenname, relpath, false, function (err, data) { 626 | var urlpublic = config.urlServerForClient + screenname + "/" + relpath; 627 | if (err) { //public file doesn't exist, read the private file 628 | getFile (screenname, relpath, true, function (err, filetext) { 629 | if (err) { //file not there, can't make the file public 630 | var message = "Can't make the file public because we can't read the private file."; 631 | console.log ("makeFilePublic: err.message == " + err.message); 632 | callback ({message}); 633 | } 634 | else { 635 | publishFile (screenname, relpath, "text/plain", false, filetext, function (err, data) { 636 | if (err) { 637 | var message = "Can't make the file public because we can't write the new file."; 638 | callback ({message}); 639 | } 640 | else { 641 | callback (undefined, {url: urlpublic}); 642 | } 643 | }); 644 | } 645 | }); 646 | } 647 | else { //it exists, return the public url of the file 648 | callback (undefined, {url: urlpublic}); 649 | } 650 | }); 651 | } 652 | function getFileHierarchy (screenname, callback) { //2/21/21 by DW 653 | folderToJson.getObject (config.privateFilesPath + screenname + "/", function (err, privateSubs) { 654 | if (err) { 655 | callback (err); 656 | } 657 | else { 658 | folderToJson.getObject (config.publicFilesPath + screenname + "/", function (err, publicSubs) { 659 | function legitError (err) { 660 | if (err) { 661 | if (err.code == "ENOENT") { 662 | publicSubs = new Object (); 663 | return (false); 664 | } 665 | else { 666 | return (true); 667 | } 668 | } 669 | else { 670 | return (false); 671 | } 672 | } 673 | if (legitError (err)) { 674 | callback (err); 675 | } 676 | else { 677 | var theHierarchy = { 678 | publicFiles: { 679 | subs: publicSubs 680 | }, 681 | privateFiles: { 682 | subs: privateSubs 683 | } 684 | }; 685 | callback (undefined, theHierarchy); 686 | } 687 | }); 688 | } 689 | }); 690 | } 691 | function deleteFile (screenname, relpath, callback) { //2/23/21 by DW 692 | if (config.flStorageEnabled) { 693 | function deleteone (flprivate, callback) { 694 | var f = getFilePath (screenname, relpath, flprivate); 695 | fs.unlink (f, callback); 696 | } 697 | deleteone (true, function (errPrivate) { 698 | deleteone (false, function (errPublic) { 699 | if (errPrivate && errPublic) { 700 | callback ({message: "Can't delete the file because it doesn't exist."}); 701 | } 702 | else { 703 | callback (undefined); 704 | } 705 | }); 706 | }); 707 | } 708 | else { 709 | callback ({message: "Can't delete the file because the feature is not enabled on the server."}); 710 | } 711 | } 712 | function fileExists (screenname, relpath, callback) { //5/29/21 by DW 713 | readWholeFile (screenname, relpath, function (err, data) { 714 | var flExists = err === undefined; 715 | callback (undefined, {flExists}); 716 | }); 717 | } 718 | function readWholeFile (screenname, relpath, callback) { //2/24/21 by DW 719 | if (config.flStorageEnabled) { 720 | function readone (flprivate, callback) { 721 | var f = getFilePath (screenname, relpath, flprivate); 722 | fs.readFile (f, function (err, filetext) { 723 | if (err) { 724 | callback (err); 725 | } 726 | else { 727 | filetext = filetext.toString (); //it's a buffer 728 | callback (undefined, {filetext}); 729 | } 730 | }); 731 | } 732 | readone (false, function (err, fileinfo) { //look for public version first 733 | if (err) { 734 | readone (true, function (err, fileinfo) { //look for private version 735 | if (err) { 736 | callback ({message: "Can't read the file because it doesn't exist."}); 737 | } 738 | else { 739 | callback (undefined, fileinfo); 740 | } 741 | }); 742 | } 743 | else { 744 | callback (undefined, fileinfo); 745 | } 746 | }); 747 | } 748 | else { 749 | callback ({message: "Can't read the file because the feature is not enabled on the server."}); 750 | } 751 | } 752 | function storageMustBeEnabled (namefunction, httpReturn, callback) { 753 | if (config.flStorageEnabled) { 754 | callback (); 755 | } 756 | else { 757 | httpReturn ({message: "Can't " + namefunction + " the file because the feature is not enabled on the server."}); 758 | } 759 | } 760 | function writeWholeFile (screenname, relpath, filetext, callback) { 761 | storageMustBeEnabled ("write", callback, function () { 762 | function readone (flprivate, callback) { 763 | var f = getFilePath (screenname, relpath, flprivate); 764 | fs.readFile (f, function (err, filetext) { 765 | if (err) { 766 | callback (err); 767 | } 768 | else { 769 | filetext = filetext.toString (); //it's a buffer 770 | callback (undefined, {filetext}); 771 | } 772 | }); 773 | } 774 | function writethefile (flprivate) { 775 | var f = getFilePath (screenname, relpath, flprivate); 776 | utils.sureFilePath (f, function () { //9/15/21 by DW 777 | fs.writeFile (f, filetext, function (err) { 778 | if (err) { 779 | callback (err); 780 | } 781 | else { 782 | if (config.publishFile !== undefined) { //3/18/22 by DW 783 | const url = (flprivate) ? undefined : config.urlServerForClient + screenname + "/" + relpath; 784 | const type = utils.httpExt2MIME (utils.stringLastField (f, ".")); //7/3/22 by DW 785 | config.publishFile (f, screenname, relpath, type, flprivate, filetext, url); 786 | } 787 | callback (undefined); 788 | } 789 | }); 790 | }); 791 | } 792 | readone (false, function (err, data) { 793 | if (err) { //write a private file 794 | writethefile (true); 795 | } 796 | else { //public version exists 797 | writethefile (false); 798 | } 799 | }); 800 | }); 801 | } 802 | function getPublicUrl (screenname, relpath) { //8/24/21 by DW 803 | return (config.urlServerForClient + screenname + "/" + relpath); 804 | } 805 | function getFileInfo (screenname, relpath, callback) { //4/1/21 by DW 806 | if (config.flStorageEnabled) { 807 | findFile (screenname, relpath, function (err, stats) { 808 | if (err) { 809 | callback (err); 810 | } 811 | else { 812 | function formatDate (d) { 813 | return (new Date (d).toUTCString ()); 814 | } 815 | callback (undefined, { 816 | size: stats.size, //number of bytes in file 817 | whenAccessed: formatDate (stats.atime), //when last red 818 | whenCreated: formatDate (stats.birthtime), 819 | whenModified: formatDate (stats.mtime), 820 | flPrivate: stats.flPrivate, 821 | urlPublic: (stats.flPrivate) ? undefined : getPublicUrl (screenname, relpath) //8/24/21 by DW 822 | }); 823 | } 824 | }); 825 | } 826 | else { 827 | callback ({message: "Can't read the file because the feature is not enabled on the server."}); 828 | } 829 | } 830 | function getUserData (screenname, callback) { //4/14/20 by DW 831 | storageMustBeEnabled ("get user data", callback, function () { 832 | const tmpfolder = "tmp/", archivefile = tmpfolder + screenname + ".zip"; 833 | utils.sureFilePath (archivefile, function () { 834 | var theArchive = zip.createArchive (archivefile, function (err, data) { 835 | if (callback !== undefined) { 836 | callback (err, archivefile); 837 | } 838 | }); 839 | var pathPublicFiles = getFilePath (screenname, "", false); 840 | var pathPrivateFiles = getFilePath (screenname, "", true); 841 | theArchive.addDirectoryToArchive (pathPublicFiles, "Public Files"); 842 | theArchive.addDirectoryToArchive (pathPrivateFiles, "Private Files"); 843 | theArchive.finalize (); 844 | }); 845 | }); 846 | } 847 | //github -- 11/8/21 by DW 848 | function handleGithubOauthCallback (theCode, callback) { //11/8/21 by DW 849 | var params = { 850 | client_id: config.githubClientId, 851 | client_secret: config.githubClientSecret, 852 | code: theCode 853 | }; 854 | var apiUrl = "https://github.com/login/oauth/access_token?" + utils.buildParamList (params); 855 | var githubRequest = { 856 | method: "POST", 857 | url: apiUrl 858 | }; 859 | console.log ("handleGithubOauthCallback: githubRequest === " + utils.jsonStringify (githubRequest)); 860 | request (githubRequest, function (err, response, body) { 861 | if (err) { 862 | console.log ("handleGithubOauthCallback: err.message == " + err.message); 863 | callback (err); 864 | } 865 | else { 866 | var postbody = qs.parse (body); 867 | var urlRedirect = "/?githubaccesstoken=" + postbody.access_token; 868 | console.log ("handleGithubOauthCallback: urlRedirect = " + urlRedirect); 869 | callback (undefined, urlRedirect); 870 | } 871 | }); 872 | } 873 | function downloadFromGithub (username, repository, path, accessToken, callback) { //calls back with the JSON structure GitHub returns 874 | if (!utils.beginsWith (path, "/")) { 875 | path = "/" + path; 876 | } 877 | var url = "https://api.github.com/repos/" + username + "/" + repository + "/contents" + path; 878 | var theRequest = { 879 | url: url, 880 | jar: true, //"remember cookies for future use" 881 | maxRedirects: 5, 882 | headers: { 883 | "User-Agent": config.userAgent, 884 | "Authorization": "token " + accessToken 885 | } 886 | }; 887 | request (theRequest, function (err, response, jsontext) { 888 | if (err) { 889 | callback (err); 890 | } 891 | else { 892 | if (response.statusCode == 404) { 893 | callback ({message: "The file \"" + path + "\" was not found."}); 894 | } 895 | else { 896 | if (response.headers ["x-ratelimit-remaining"] == 0) { 897 | var theLimit = response.headers ["x-ratelimit-limit"]; 898 | callback ({"message": "GitHub reported a rate limit error. You are limited to " + theLimit + " calls per hour."}); 899 | } 900 | else { 901 | try { 902 | var jstruct = JSON.parse (jsontext); 903 | callback (undefined, jstruct); 904 | } 905 | catch (err) { 906 | callback (err); 907 | } 908 | } 909 | } 910 | } 911 | }); 912 | } 913 | function uploadToGithub (jsontext, data, callback) { 914 | var options; 915 | try { 916 | options = JSON.parse (jsontext); 917 | } 918 | catch (err) { 919 | callback (err); 920 | return; 921 | } 922 | options.data = data; 923 | if (options.userAgent === undefined) { 924 | options.userAgent = config.userAgent; 925 | } 926 | if (options.type === undefined) { 927 | options.type = utils.httpExt2MIME (options.path); 928 | } 929 | if (options.message === undefined) { 930 | options.message = utils.getRandomSnarkySlogan (); 931 | } 932 | 933 | var bodyStruct = { 934 | message: options.message, 935 | committer: options.committer, 936 | content: Buffer.from (options.data).toString ("base64") 937 | }; 938 | downloadFromGithub (options.username, options.repository, options.path, options.accessToken, function (err, jstruct) { 939 | if (jstruct !== undefined) { 940 | bodyStruct.sha = jstruct.sha; 941 | } 942 | var url = "https://api.github.com/repos/" + options.username + "/" + options.repository + "/contents/" + options.path; 943 | var theRequest = { 944 | method: "PUT", 945 | url, 946 | body: JSON.stringify (bodyStruct), 947 | headers: { 948 | "User-Agent": options.userAgent, 949 | "Authorization": "token " + options.accessToken, 950 | "Content-Type": options.type 951 | } 952 | }; 953 | request (theRequest, function (err, response, body) { 954 | if (err) { 955 | callback (err); 956 | } 957 | else { 958 | var rateLimitMessage; 959 | if (response.headers ["x-ratelimit-remaining"] == 0) { 960 | var theLimit = response.headers ["x-ratelimit-limit"]; 961 | rateLimitMessage = "GitHub reported a rate limit error. You are limited to " + theLimit + " calls per hour."; 962 | } 963 | var returnedStruct = JSON.parse (body); 964 | returnedStruct.statusCode = response.statusCode; 965 | returnedStruct.rateLimitMessage = rateLimitMessage; 966 | callback (undefined, returnedStruct); 967 | } 968 | }); 969 | }); 970 | } 971 | function getGithubDirectory (username, repository, path, accessToken, callback) { 972 | function loadDirectory (theArray, parentpath, callback) { 973 | function nextFile (ix) { 974 | if (ix < theArray.length) { 975 | var item = theArray [ix]; 976 | if (item.type == "dir") { 977 | getGithubDirectory (username, repository, item.path, accessToken, function (err, jstruct) { 978 | if (jstruct !== undefined) { //no error 979 | item.subs = jstruct; 980 | } 981 | nextFile (ix + 1); 982 | }); 983 | } 984 | else { 985 | nextFile (ix + 1); 986 | } 987 | } 988 | else { 989 | callback (); 990 | } 991 | } 992 | nextFile (0); 993 | } 994 | if (utils.beginsWith (path, "/")) { 995 | path = utils.stringDelete (path, 1, 1); 996 | } 997 | var theRequest = { 998 | method: "GET", 999 | url: "https://api.github.com/repos/" + username + "/" + repository + "/contents/" + path, 1000 | headers: { 1001 | "User-Agent": config.userAgent, 1002 | "Authorization": "token " + accessToken, 1003 | } 1004 | }; 1005 | request (theRequest, function (err, response, body) { 1006 | if (err) { 1007 | callback (err); 1008 | } 1009 | else { 1010 | try { 1011 | var jstruct = JSON.parse (body); 1012 | if (Array.isArray (jstruct)) { //it's a directory 1013 | loadDirectory (jstruct, path, function () { 1014 | callback (undefined, jstruct); 1015 | }); 1016 | } 1017 | else { 1018 | callback (undefined, jstruct); 1019 | } 1020 | } 1021 | catch (err) { 1022 | if (callback !== undefined) { 1023 | callback (err); 1024 | } 1025 | } 1026 | } 1027 | }); 1028 | } 1029 | function getGithubUserInfo (username, accessToken, callback) { 1030 | var url = "https://api.github.com/user"; 1031 | if (username !== undefined) { 1032 | url += "s/" + username 1033 | } 1034 | var theRequest = { 1035 | method: "GET", 1036 | url, 1037 | headers: { 1038 | "User-Agent": config.userAgent, 1039 | "Authorization": "token " + accessToken 1040 | } 1041 | }; 1042 | request (theRequest, function (err, response, body) { 1043 | if (err) { 1044 | callback (err); 1045 | } 1046 | else { 1047 | try { 1048 | var jstruct = JSON.parse (body); 1049 | callback (undefined, jstruct); 1050 | } 1051 | catch (err) { 1052 | callback (err); 1053 | } 1054 | } 1055 | }); 1056 | } 1057 | //email registration -- 12/7/22 by DW 1058 | function addPendingConfirmation (theConfirmation, callback) { //8/14/23 by DW 1059 | if (config.flUseDatabaseForConfirmations) { 1060 | theConfirmation.whenCreated = theConfirmation.when; 1061 | delete theConfirmation.when; 1062 | 1063 | var sqltext = "replace into pendingConfirmations " + davesql.encodeValues (theConfirmation); 1064 | davesql.runSqltext (sqltext, function (err, result) { 1065 | if (err) { 1066 | callback (err); 1067 | } 1068 | else { 1069 | console.log ("addPendingConfirmation: email == " + theConfirmation.email); 1070 | callback (undefined); 1071 | } 1072 | }); 1073 | } 1074 | else { 1075 | stats.pendingConfirmations.push (theConfirmation); 1076 | statsChanged (); 1077 | callback (undefined); //no error 1078 | } 1079 | } 1080 | function findPendingConfirmation (magicString, callback) { //8/14/23 by DW 1081 | if (config.flUseDatabaseForConfirmations) { 1082 | var sqltext = "select * from pendingConfirmations where magicString=" + davesql.encode (magicString) + ";"; 1083 | davesql.runSqltext (sqltext, function (err, result) { 1084 | if (err) { 1085 | console.log ("findPendingConfirmation: err.message == " + err.message); 1086 | callback (err); 1087 | } 1088 | else { 1089 | if (result.length == 0) { 1090 | const message = "Can't find the pending confirmation."; 1091 | callback ({message}); 1092 | } 1093 | else { 1094 | var theConfirmation = result [0]; 1095 | if (theConfirmation.urlRedirect == null) { 1096 | theConfirmation.urlRedirect = undefined; 1097 | } 1098 | callback (undefined, theConfirmation); 1099 | } 1100 | } 1101 | }); 1102 | } 1103 | else { 1104 | var flFoundConfirm = false; 1105 | stats.pendingConfirmations.forEach (function (item) { 1106 | if (item.magicString == magicString) { 1107 | callback (undefined, item); 1108 | flFoundConfirm = true; 1109 | } 1110 | }); 1111 | if (!flFoundConfirm) { 1112 | const message = "Can't find the pending confirmation."; 1113 | callback ({message}); 1114 | } 1115 | } 1116 | } 1117 | function deletePendingConfirmation (item, callback) { //8/14/23 by DW 1118 | if (config.flUseDatabaseForConfirmations) { 1119 | const sqltext = "delete from pendingConfirmations where magicString = " + davesql.encode (item.magicString) + ";"; //8/15/23 by DW 1120 | davesql.runSqltext (sqltext, function (err, result) { 1121 | if (err) { 1122 | if (callback !== undefined) { 1123 | callback (err); 1124 | } 1125 | } 1126 | else { 1127 | if (callback !== undefined) { 1128 | callback (undefined); 1129 | } 1130 | } 1131 | }); 1132 | } 1133 | else { 1134 | item.flDeleted = true; 1135 | } 1136 | } 1137 | function checkPendingConfirmations (callback) { //8/14/23 by DW 1138 | if (config.flUseDatabaseForConfirmations) { 1139 | const sqltext = "delete from pendingConfirmations where whenCreated < now() - interval " + config.confirmationExpiresAfter + " minute;"; 1140 | davesql.runSqltext (sqltext, function (err, result) { 1141 | if (err) { 1142 | if (callback !== undefined) { 1143 | callback (err); 1144 | } 1145 | } 1146 | else { 1147 | if (callback !== undefined) { 1148 | callback (undefined); 1149 | } 1150 | } 1151 | }); 1152 | } 1153 | else { 1154 | var flChanged = false; 1155 | var newArray = new Array (); 1156 | stats.pendingConfirmations.forEach (function (item) { 1157 | if ((!item.flDeleted) && (utils.secondsSince (item.when) < config.confirmationExpiresAfter)) { 1158 | newArray.push (item); 1159 | } 1160 | else { 1161 | flChanged = true; 1162 | } 1163 | }); 1164 | if (flChanged) { 1165 | stats.pendingConfirmations = newArray; 1166 | statsChanged (); 1167 | } 1168 | } 1169 | } 1170 | 1171 | function sendConfirmingEmail (email, screenname, flNewUser=false, urlRedirect, callback) { 1172 | email = utils.stringLower (email); //3/8/23 by DW 1173 | function getScreenname (callback) { 1174 | function containsIllegalCharacter (name) { //3/17/23 by DW 1175 | function isLegal (ch) { 1176 | return (utils.isAlpha (ch) || utils.isNumeric (ch) || (ch == "_") || (ch == ".") || (ch == "@")); 1177 | } 1178 | for (var i = 0; i < name.length; i++) { 1179 | let ch = name [i]; 1180 | if (!isLegal (ch)) { 1181 | return (true); 1182 | } 1183 | } 1184 | return (false); 1185 | } 1186 | if (flNewUser) { //the caller had to provide it 1187 | if (containsIllegalCharacter (screenname)) { //3/17/23 by DW 1188 | const message = "Can't create the user \"" + screenname + "\" because only alpha, numeric and underscore characters are allowed in names." 1189 | callback ({message}); 1190 | } 1191 | else { 1192 | config.findUserWithScreenname (screenname, function (flInDatabase) { 1193 | if (flInDatabase) { 1194 | const message = "Can't create the user \"" + screenname + "\" because there already is a user with that name." 1195 | callback ({message}); 1196 | } 1197 | else { 1198 | callback (undefined, screenname); 1199 | } 1200 | }); 1201 | } 1202 | } 1203 | else { //we have to look it up 1204 | config.getScreenNameFromEmail (email, callback); 1205 | } 1206 | } 1207 | getScreenname (function (err, screenname) { 1208 | if (err) { 1209 | callback (err); 1210 | } 1211 | else { 1212 | const magicString = utils.getRandomPassword (10); 1213 | const urlWebApp = config.urlServerForEmail; //2/6/23 by DW 1214 | console.log ("sendConfirmingEmail: email == " + email + ", urlWebApp == " + urlWebApp); 1215 | var obj = { 1216 | magicString: magicString, 1217 | email: email, 1218 | flDeleted: false, 1219 | screenname, 1220 | flNewUser, //1/7/23 by DW 1221 | urlRedirect, //3/3/23 by DW 1222 | when: new Date () 1223 | }; 1224 | addPendingConfirmation (obj, function (err) { //8/14/23 by DW 1225 | if (err) { 1226 | callback (err); 1227 | } 1228 | else { 1229 | console.log ("sendConfirmingEmail: obj == " + utils.jsonStringify (obj)); 1230 | var params = { 1231 | title: config.confirmEmailSubject, 1232 | operationToConfirm: config.operationToConfirm, 1233 | confirmationUrl: urlWebApp + "userconfirms?emailConfirmCode=" + encodeURIComponent (magicString) 1234 | }; 1235 | fs.readFile (config.fnameEmailTemplate, function (err, emailTemplate) { 1236 | if (err) { 1237 | const message = "Error reading email template."; 1238 | console.log ("sendConfirmingEmail: err.message == " + err.message); 1239 | callback ({message}); 1240 | } 1241 | else { 1242 | var mailtext = utils.multipleReplaceAll (emailTemplate.toString (), params, false, "[%", "%]"); 1243 | mail.send (email, params.title, mailtext, config.mailSender, function (err, data) { 1244 | if (err) { 1245 | callback (err); 1246 | } 1247 | else { 1248 | callback (undefined, {message: "Please check your email."}); 1249 | } 1250 | }); 1251 | const f = config.dataFolder + "lastmail.html"; 1252 | utils.sureFilePath (f, function () { 1253 | fs.writeFile (f, mailtext, function (err) { 1254 | }); 1255 | }); 1256 | } 1257 | }); 1258 | } 1259 | }); 1260 | } 1261 | }); 1262 | } 1263 | function receiveConfirmation (emailConfirmCode, callback) { 1264 | var urlWebApp = config.urlServerForClient; //2/5/23 by DW 1265 | var urlRedirect = undefined, flFoundConfirm = false; 1266 | function encode (s) { 1267 | return (encodeURIComponent (s)); 1268 | } 1269 | 1270 | findPendingConfirmation (emailConfirmCode, function (err, item) { 1271 | if (err) { 1272 | if (urlRedirect === undefined) { 1273 | urlRedirect = urlWebApp; 1274 | } 1275 | callback (urlRedirect + "?failedLogin=true&message=" + encode (err.message)); 1276 | } 1277 | else { 1278 | if (config.addEmailToUserInDatabase !== undefined) { 1279 | if (item.urlRedirect !== undefined) { //3/3/23 by DW 1280 | urlWebApp = item.urlRedirect; 1281 | } 1282 | config.addEmailToUserInDatabase (item.screenname, item.email, item.magicString, item.flNewUser, function (err, emailSecret) { 1283 | if (err) { 1284 | urlRedirect = urlWebApp + "?failedLogin=true&message=" + encode (err.message); 1285 | } 1286 | else { 1287 | urlRedirect = urlWebApp + "?emailconfirmed=true&email=" + encode (item.email) + "&code=" + encode (emailSecret) + "&screenname=" + encode (item.screenname); 1288 | deletePendingConfirmation (item); 1289 | } 1290 | callback (urlRedirect); 1291 | }); 1292 | } 1293 | } 1294 | }); 1295 | } 1296 | //wordpress -- 10/31/23 by DW 1297 | function wordpressHandleRequest (theRequest) { 1298 | function encode (s) { 1299 | return (encodeURIComponent (s)); 1300 | } 1301 | function returnRedirect (url, code=302) { 1302 | var headers = { 1303 | location: url 1304 | }; 1305 | theRequest.httpReturn (code, "text/plain", code + " REDIRECT", headers); 1306 | } 1307 | function useWordpressAccount (accessToken, theUserInfo) { 1308 | console.log ("useWordpressAccount: accessToken == " + accessToken); 1309 | console.log ("useWordpressAccount: theUserInfo == " + utils.jsonStringify (theUserInfo)); 1310 | config.loginWordpressUser (accessToken, theUserInfo, function (err, userRec) { 1311 | if (err) { 1312 | returnRedirect (config.urlServerForClient + "?failedLogin=true&message=" + encode (err.message)); 1313 | } 1314 | else { 1315 | returnRedirect (config.urlServerForClient + "?emailconfirmed=true&email=" + encode (userRec.emailAddress) + "&code=" + encode (userRec.emailSecret) + "&screenname=" + encode (userRec.screenname)); 1316 | } 1317 | }); 1318 | } 1319 | function createPendingConfirmation (callback) { //11/14/23 by DW 1320 | const obj = { 1321 | magicString: utils.getRandomPassword (10), 1322 | flDeleted: false, 1323 | when: new Date () 1324 | }; 1325 | addPendingConfirmation (obj, function (err) { 1326 | if (err) { 1327 | callback (err); 1328 | } 1329 | else { 1330 | callback (undefined, obj); 1331 | } 1332 | }); 1333 | } 1334 | function checkPendingConfirmation (confirmCode, callback) { //11/14/23 by DW 1335 | findPendingConfirmation (confirmCode, function (err, item) { 1336 | if (!err) { //10/6/24 by DW 1337 | deletePendingConfirmation (item); 1338 | } 1339 | callback (err); //return success or failure 1340 | }); 1341 | } 1342 | 1343 | var options = undefined; 1344 | if (config.flAllowWordpressIdentity) { 1345 | options = { 1346 | useWordpressAccount 1347 | }; 1348 | if (config.flUseDatabaseForConfirmations) { //11/14/23 by DW 1349 | options.createPendingConfirmation = createPendingConfirmation; 1350 | options.checkPendingConfirmation = checkPendingConfirmation; 1351 | } 1352 | } 1353 | 1354 | const flHandled = wordpress.handleHttpRequest (theRequest, options); 1355 | return (flHandled); 1356 | } 1357 | 1358 | function startup (options, callback) { 1359 | function readConfig (f, theConfig, flReportError, callback) { 1360 | fs.readFile (f, function (err, jsontext) { 1361 | if (err) { 1362 | if (flReportError) { //1/21/21 by DW 1363 | console.log ("readConfig: err.message == " + err.message); 1364 | } 1365 | } 1366 | else { 1367 | try { 1368 | var jstruct = JSON.parse (jsontext); 1369 | for (var x in jstruct) { 1370 | theConfig [x] = jstruct [x]; 1371 | } 1372 | } 1373 | catch (err) { 1374 | console.log ("readConfig: err.message == " + err.message); 1375 | } 1376 | } 1377 | callback (); 1378 | }); 1379 | } 1380 | function readConfigJson (callback) { //2/8/23 by DW 1381 | fs.readFile ("config.js", function (err, scripttext) { 1382 | var flReadConfigJson = true; 1383 | if (!err) { 1384 | try { 1385 | scripttext = scripttext.toString (); 1386 | var jstruct = requireFromString (scripttext); 1387 | for (var x in jstruct) { 1388 | config [x] = jstruct [x]; 1389 | } 1390 | flReadConfigJson = false; 1391 | callback (); 1392 | } 1393 | catch (err) { 1394 | console.log (err.message); 1395 | } 1396 | } 1397 | if (flReadConfigJson) { 1398 | readConfig (fnameConfig, config, true, callback); 1399 | } 1400 | }); 1401 | } 1402 | function startDavetwitter (httpRequestCallback) { //patch over a design problem in starting up davetwitter and davehttp -- 7/20/20 by DW 1403 | if (config.twitter === undefined) { 1404 | config.twitter = new Object (); 1405 | } 1406 | config.twitter.myPort = config.port; 1407 | config.twitter.httpPort = config.port; 1408 | config.twitter.myDomain = config.myDomain; 1409 | config.twitter.flLogToConsole = config.flLogToConsole; 1410 | config.twitter.flAllowAccessFromAnywhere = config.flAllowAccessFromAnywhere; 1411 | config.twitter.flPostEnabled = config.flPostEnabled; 1412 | config.twitter.flTraceOnError = config.flTraceOnError; //8/15/23 by DW 1413 | config.twitter.blockedAddresses = config.blockedAddresses; 1414 | config.twitter.httpRequestCallback = httpRequestCallback; 1415 | config.twitter.http404Callback = http404Callback; //1/24/21 by DW 1416 | config.twitter.twitterConsumerKey = config.twitterConsumerKey; 1417 | config.twitter.twitterConsumerSecret = config.twitterConsumerSecret; 1418 | config.twitter.userLogonCallback = userLogonCallback; //8/14/22 by DW 1419 | if (config.urlFavicon !== undefined) { //1/26/23 by DW 1420 | config.twitter.urlFavicon = config.urlFavicon; 1421 | } 1422 | davetwitter.start (config.twitter); 1423 | } 1424 | function startDavemail () { //1/23/23 by DW 1425 | var options; 1426 | if (config.smtpHost === undefined) { 1427 | options = { 1428 | flUseSes: true 1429 | }; 1430 | } 1431 | else { 1432 | options = { 1433 | flUseSes: false, 1434 | smtpHost: config.smtpHost, 1435 | port: config.smtpPort, 1436 | username: config.smtpUsername, 1437 | password: config.smtpPassword 1438 | }; 1439 | } 1440 | mail.start (options); 1441 | } 1442 | function handleHttpRequest (theRequest) { 1443 | const params = theRequest.params; 1444 | const token = params.oauth_token; 1445 | const secret = params.oauth_token_secret; 1446 | const flprivate = (params.flprivate === undefined) ? false : utils.getBoolean (params.flprivate); 1447 | 1448 | stats.ctHits++; 1449 | stats.ctHitsToday++; 1450 | stats.ctHitsThisRun++; 1451 | stats.whenLastHit = new Date (); 1452 | statsChanged (); 1453 | 1454 | function returnPlainText (s) { 1455 | theRequest.httpReturn (200, "text/plain", s.toString ()); 1456 | } 1457 | function returnData (jstruct) { 1458 | if (jstruct === undefined) { 1459 | jstruct = {}; 1460 | } 1461 | theRequest.httpReturn (200, "application/json", utils.jsonStringify (jstruct)); 1462 | } 1463 | function returnHtml (htmltext) { 1464 | theRequest.httpReturn (200, "text/html", htmltext); 1465 | } 1466 | function returnXml (xmltext) { 1467 | theRequest.httpReturn (200, "text/xml", xmltext); 1468 | } 1469 | function returnNotFound () { 1470 | theRequest.httpReturn (404, "text/plain", "Not found."); 1471 | } 1472 | function returnError (jstruct) { 1473 | theRequest.httpReturn (500, "application/json", utils.jsonStringify (jstruct)); 1474 | } 1475 | function httpReturn (err, jstruct) { 1476 | if (err) { 1477 | returnError (err); 1478 | } 1479 | else { 1480 | returnData (jstruct); 1481 | } 1482 | } 1483 | function httpReturnRedirect (url, code) { //9/30/20 by DW 1484 | var headers = { 1485 | location: url 1486 | }; 1487 | if (code === undefined) { 1488 | code = 302; 1489 | } 1490 | theRequest.httpReturn (code, "text/plain", code + " REDIRECT", headers); 1491 | } 1492 | 1493 | function httpReturnObject (err, jstruct) { 1494 | if (err) { 1495 | returnError (err); 1496 | } 1497 | else { 1498 | returnData (jstruct); 1499 | } 1500 | } 1501 | function httpReturnZipFile (f) { //4/13/20 by DW 1502 | fs.readFile (f, function (err, data) { 1503 | if (err) { 1504 | returnError (err); 1505 | } 1506 | else { 1507 | theRequest.httpReturn (200, "application/zip", data); 1508 | } 1509 | }); 1510 | } 1511 | function returnServerHomePage () { 1512 | var pagetable = { 1513 | productName: config.productName, 1514 | productNameForDisplay: config.productNameForDisplay, 1515 | version: config.version, 1516 | urlServerForClient: config.urlServerForClient, 1517 | urlWebsocketServerForClient: config.urlWebsocketServerForClient, 1518 | flEnableLogin: config.flEnableLogin, 1519 | prefsPath: config.prefsPath, 1520 | docsPath: config.docsPath, 1521 | flUseTwitterIdentity: config.flUseTwitterIdentity, //2/6/23 by DW 1522 | idGitHubClient: config.githubClientId, //11/9/21 by DW 1523 | flWebsocketEnabled: config.flWebsocketEnabled, //2/8/23 by DW 1524 | urlServerHomePageSource: config.urlServerHomePageSource, //9/16/23 by DW 1525 | pathServerHomePageSource: config.pathServerHomePageSource //9/16/23 by DW 1526 | }; 1527 | function getTemplateText (callback) { 1528 | if (pagetable.pathServerHomePageSource !== undefined) { 1529 | fs.readFile (pagetable.pathServerHomePageSource, function (err, templatetext) { 1530 | if (err) { 1531 | callback (err); 1532 | } 1533 | else { 1534 | callback (undefined, templatetext); 1535 | } 1536 | }); 1537 | } 1538 | else { 1539 | request (pagetable.urlServerHomePageSource, function (err, response, templatetext) { 1540 | if (err) { 1541 | callback (err); 1542 | } 1543 | else { 1544 | if ((response.statusCode >= 200) && (response.statusCode <= 299)) { 1545 | callback (undefined, templatetext); 1546 | } 1547 | else { 1548 | const message = "HTTP error == " + response.statusCode; 1549 | callback ({message}); 1550 | } 1551 | } 1552 | }); 1553 | } 1554 | } 1555 | function buildAndServeHomepage () { 1556 | getTemplateText (function (err, templatetext) { 1557 | if (err) { 1558 | returnError (err); 1559 | } 1560 | else { 1561 | const pagetext = utils.multipleReplaceAll (templatetext.toString (), pagetable, false, "[%", "%]"); 1562 | returnHtml (pagetext); 1563 | } 1564 | }); 1565 | } 1566 | if (theRequest.addToPagetable !== undefined) { //3/9/21 by DW 1567 | for (var x in theRequest.addToPagetable) { 1568 | pagetable [x] = theRequest.addToPagetable [x]; 1569 | } 1570 | } 1571 | if (config.addMacroToPagetable !== undefined) { 1572 | config.addMacroToPagetable (pagetable, theRequest); //8/10/22 by DW 1573 | } 1574 | if (config.asyncAddMacroToPagetable !== undefined) { //8/10/22 by DW 1575 | config.asyncAddMacroToPagetable (pagetable, theRequest, function () { 1576 | buildAndServeHomepage (); 1577 | }); 1578 | } 1579 | else { 1580 | buildAndServeHomepage (); 1581 | } 1582 | } 1583 | function callWithScreenname (callback) { 1584 | 1585 | function actingAsFilter (email, callback) { //11/3/23 by DW 1586 | if (config.flEnableSupervisorMode) { 1587 | if (params.actingas === undefined) { 1588 | callback (email); 1589 | } 1590 | else { 1591 | config.isUserAdmin (email, function (flAdmin, userRec) { 1592 | if (flAdmin) { 1593 | config.findUserWithScreenname (params.actingas, function (flInDatabase, userRec) { 1594 | if (flInDatabase) { 1595 | if (userRec.emailAddress === undefined) { 1596 | callback (email); 1597 | } 1598 | else { 1599 | callback (userRec.emailAddress); 1600 | } 1601 | } 1602 | else { 1603 | callback (email); 1604 | } 1605 | }); 1606 | } 1607 | else { 1608 | callback (email); 1609 | } 1610 | }); 1611 | } 1612 | } 1613 | else { 1614 | callback (email); 1615 | } 1616 | } 1617 | 1618 | if (config.flUseTwitterIdentity) { //2/6/23 by DW 1619 | if (config.getScreenname === undefined) { 1620 | davetwitter.getScreenName (token, secret, function (screenname) { 1621 | if (screenname === undefined) { 1622 | returnError ({message: "Can't do the thing you want because the accessToken is not valid."}); 1623 | } 1624 | else { 1625 | callback (screenname); 1626 | } 1627 | }); 1628 | } 1629 | else { 1630 | config.getScreenname (params, function (err, screenname) { //12/23/22 by DW 1631 | if (err) { 1632 | returnError (err); 1633 | } 1634 | else { 1635 | callback (screenname); 1636 | } 1637 | }); 1638 | } 1639 | } 1640 | else { 1641 | if ((params.emailaddress !== undefined) && (params.emailcode !== undefined)) { 1642 | const email = utils.stringLower (params.emailaddress); //3/8/23 by DW 1643 | config.findUserWithEmail (email, function (flInDatabase, userRec) { 1644 | if (flInDatabase) { 1645 | if (params.emailcode == userRec.emailSecret) { 1646 | actingAsFilter (email, function (whoActingAs) { 1647 | console.log ("\nappserver: whoActingAs == " + whoActingAs + "\n"); 1648 | callback (whoActingAs); 1649 | }); 1650 | } 1651 | else { 1652 | const message = "Can't do what you wanted the correct email authentication wasn't provided."; 1653 | returnError ({message}); 1654 | } 1655 | } 1656 | else { 1657 | const message = "Can't do what you wanted because the email address isn't in the database."; 1658 | returnError ({message}); 1659 | } 1660 | }); 1661 | } 1662 | else { 1663 | const message = "Can't do what you wanted because the call is missing email authentication."; 1664 | returnError ({message}); 1665 | } 1666 | } 1667 | } 1668 | 1669 | if (config.httpRequest !== undefined) { 1670 | if (config.httpRequest (theRequest)) { //consumed by callback 1671 | return (true); 1672 | } 1673 | } 1674 | 1675 | if (wordpressHandleRequest (theRequest)) { //9/10/23 by DW 1676 | return (true); 1677 | } 1678 | 1679 | switch (theRequest.lowermethod) { 1680 | case "post": 1681 | switch (theRequest.lowerpath) { 1682 | case "/publishfile": //1/22/21 by DW 1683 | callWithScreenname (function (screenname) { 1684 | publishFile (screenname, params.relpath, params.type, flprivate, theRequest.postBody.toString (), function (err, data) { 1685 | if (err) { 1686 | returnError (err); 1687 | } 1688 | else { //quirk in API, it wants a string, not a JSON struct 1689 | if (!flprivate) { 1690 | if (config.publicFileSaved !== undefined) { 1691 | config.publicFileSaved (token, secret, getPublicFileUrl (screenname, params.relpath)); 1692 | } 1693 | } 1694 | returnPlainText (utils.jsonStringify (data)); 1695 | } 1696 | }); 1697 | }); 1698 | return (true); 1699 | case "/writewholefile": //2/25/21 by DW -- special way to write a file, for scripting 1700 | callWithScreenname (function (screenname) { 1701 | writeWholeFile (screenname, params.relpath, theRequest.postBody.toString (), httpReturn); 1702 | }); 1703 | return (true); 1704 | case "/uploadtogithub": //11/9/21 by DW 1705 | uploadToGithub (params.options, theRequest.postBody, httpReturn); 1706 | return (true); 1707 | } 1708 | break; 1709 | case "get": 1710 | switch (theRequest.lowerpath) { 1711 | case "/": 1712 | returnServerHomePage (); 1713 | return (true); 1714 | case "/now": 1715 | returnPlainText (new Date ()); 1716 | return (true); 1717 | case "/version": 1718 | returnData ({ 1719 | productName: config.productName, 1720 | version: config.version 1721 | }); 1722 | return (true); 1723 | case "/stats": 1724 | returnData (stats); 1725 | return (true); 1726 | case "/getfile": 1727 | callWithScreenname (function (screenname) { 1728 | getFile (screenname, params.relpath, flprivate, httpReturn); 1729 | }); 1730 | return (true); 1731 | case "/getoptionalfile": 1732 | callWithScreenname (function (screenname) { 1733 | getFile (screenname, params.relpath, flprivate, function (err, data) { 1734 | if (err) { 1735 | returnData ({}); //return nothing 1736 | } 1737 | else { 1738 | returnData ({data}); 1739 | } 1740 | }); 1741 | }); 1742 | return (true); 1743 | case "/getfilelist": 1744 | callWithScreenname (function (screenname) { 1745 | getFileList (screenname, flprivate, httpReturn); 1746 | }); 1747 | return (true); 1748 | case "/makefilepublic": //2/20/21 by DW 1749 | callWithScreenname (function (screenname) { 1750 | makeFilePublic (screenname, params.relpath, httpReturn); 1751 | }); 1752 | return (true); 1753 | case "/getfilehierarchy": //2/21/21 by DW 1754 | callWithScreenname (function (screenname) { 1755 | getFileHierarchy (screenname, httpReturn); 1756 | }); 1757 | return (true); 1758 | case "/deletefile": //2/23/21 by DW 1759 | callWithScreenname (function (screenname) { 1760 | deleteFile (screenname, params.relpath, httpReturn); 1761 | }); 1762 | return (true); 1763 | case "/readwholefile": //2/24/21 by DW 1764 | callWithScreenname (function (screenname) { 1765 | readWholeFile (screenname, params.relpath, httpReturn); 1766 | }); 1767 | return (true); 1768 | case "/fileexists": //5/29/21 by DW 1769 | callWithScreenname (function (screenname) { 1770 | fileExists (screenname, params.relpath, httpReturn); 1771 | }); 1772 | return (true); 1773 | case "/httpreadurl": //2/26/21 by DW 1774 | callWithScreenname (function (screenname) { 1775 | httpReadUrl (params.url, httpReturn); 1776 | }); 1777 | return (true); 1778 | case "/httprequest": //11/5/21 by DW 1779 | callWithScreenname (function (screenname) { 1780 | httpFullRequest (params.request, httpReturn); 1781 | }); 1782 | return (true); 1783 | case "/getdomainname": //2/27/21 by DW 1784 | callWithScreenname (function (screenname) { 1785 | getDomainNameVerb (params.dottedid, httpReturn); 1786 | }); 1787 | return (true); 1788 | case "/getdottedid": //2/27/21 by DW 1789 | callWithScreenname (function (screenname) { 1790 | getDottedIdVerb (params.name, httpReturn); 1791 | }); 1792 | return (true); 1793 | case "/myfiles": //3/7/21 by DW 1794 | callWithScreenname (function (screenname) { 1795 | getUserData (screenname, function (err, zipfile) { 1796 | if (err) { 1797 | errorResponse (err); 1798 | } 1799 | else { 1800 | httpReturnZipFile (zipfile); 1801 | } 1802 | }); 1803 | }); 1804 | return (true); 1805 | case "/getfileinfo": //4/1/21 by DW 1806 | callWithScreenname (function (screenname) { 1807 | getFileInfo (screenname, params.relpath, httpReturn); 1808 | }); 1809 | return (true); 1810 | case "/githuboauthcallback": //11/8/21 by DW 1811 | handleGithubOauthCallback (params.code, function (err, urlRedirect) { 1812 | if (err) { 1813 | returnError (err); 1814 | } 1815 | else { 1816 | httpReturnRedirect (urlRedirect); 1817 | } 1818 | }); 1819 | return (true); 1820 | case "/downloadfromgithub": //11/8/21 by DW 1821 | downloadFromGithub (params.username, params.repository, params.path, params.accessToken, httpReturn); 1822 | return (true); 1823 | case "/githubgetdirectory": //11/10/21 by DW 1824 | getGithubDirectory (params.username, params.repository, params.path, params.accessToken, httpReturn); 1825 | return (true); 1826 | case "/githubgetuserinfo": //11/10/21 by DW 1827 | callWithScreenname (function (screenname) { 1828 | getGithubUserInfo (params.username, params.accessToken, httpReturn); 1829 | }); 1830 | return (true); 1831 | case "/useriswhitelisted": //7/21/22 by DW 1832 | callWithScreenname (function (screenname) { 1833 | userIsWhitelisted (screenname, httpReturn); //9/16/22 by DW 1834 | }); 1835 | return (true); 1836 | case "/sendconfirmingemail": //12/7/22 by DW 1837 | sendConfirmingEmail (params.email, undefined, false, params.urlredirect, httpReturn); //3/3/23 by DW 1838 | return (true); 1839 | case "/createnewuser": //1/7/23 by DW 1840 | sendConfirmingEmail (params.email, params.name, true, params.urlredirect, httpReturn); //3/3/23 by DW 1841 | return (true); 1842 | case "/userconfirms": //12/7/22 by DW 1843 | receiveConfirmation (params.emailConfirmCode, httpReturnRedirect); 1844 | return (true); 1845 | 1846 | } 1847 | break; 1848 | } 1849 | return (false); 1850 | } 1851 | function http404Callback (theRequest) { 1852 | function return404 () { 1853 | theRequest.httpReturn (404, "text/plain", "Not found."); 1854 | } 1855 | function returnPlainText (s) { 1856 | theRequest.httpReturn (200, "text/plain", s.toString ()); 1857 | } 1858 | if (config.flStorageEnabled) { 1859 | if (checkPathForIllegalChars (theRequest.path)) { 1860 | 1861 | function getFileContent (screenname, relpath, flprivate, callback) { //9/8/21 by DW 1862 | var f = getFilePath (screenname, relpath, flprivate); 1863 | fs.readFile (f, function (err, filetext) { 1864 | if (err) { 1865 | callback (err); 1866 | } 1867 | else { 1868 | callback (undefined, filetext); 1869 | } 1870 | }); 1871 | } 1872 | 1873 | var path = utils.stringDelete (theRequest.path, 1, 1); //delete leading slash 1874 | var screenname = utils.stringNthField (path, "/", 1); 1875 | var relpath = utils.stringDelete (path, 1, screenname.length + 1); 1876 | var flprivate = false; 1877 | getFileContent (screenname, relpath, flprivate, function (err, filedata) { 1878 | if (err) { 1879 | return404 (); 1880 | } 1881 | else { 1882 | const ext = utils.stringLastField (relpath, "."); //8/3/21 by DW 1883 | if (ext == relpath) { //no extension 1884 | type = config.defaultContentType; 1885 | } 1886 | else { 1887 | type = utils.httpExt2MIME (ext, config.defaultContentType); 1888 | } 1889 | theRequest.httpReturn (200, type, filedata); 1890 | } 1891 | }); 1892 | } 1893 | else { 1894 | return404 (); 1895 | } 1896 | return (true); //tell davetwitter we handled it 1897 | } 1898 | else { 1899 | return (false); //tell davetwitter we didn't handle it 1900 | } 1901 | } 1902 | function userLogonCallback (options) { //8/14/22 by DW 1903 | if (config.userLogonCallback !== undefined) { 1904 | config.userLogonCallback (options); 1905 | } 1906 | } 1907 | function everyMinute () { 1908 | var now = new Date (); 1909 | if (config.everyMinute !== undefined) { 1910 | config.everyMinute (); 1911 | } 1912 | if (now.getMinutes () == 0) { 1913 | console.log ("\n" + now.toLocaleTimeString () + ": " + config.productName + " v" + config.version + " running on port " + config.port + ".\n"); 1914 | checkPendingConfirmations (); //12/7/22 by DW 1915 | } 1916 | } 1917 | function everySecond () { 1918 | if (flStatsChanged) { 1919 | stats.ctWrites++; 1920 | flStatsChanged = false; 1921 | fs.writeFile (fnameStats, utils.jsonStringify (stats), function () { 1922 | }); 1923 | } 1924 | if (config.everySecond !== undefined) { 1925 | config.everySecond (); 1926 | } 1927 | } 1928 | 1929 | utils.copyScalars (options, config); //1/22/21 by DW 1930 | readConfigJson (function () { //readConfig (fnameConfig, config, true, function () { //anything can be overridden by config.json 1931 | readConfig (fnameStats, stats, false, function () { 1932 | if (process.env.PORT !== undefined) { //8/6/20 by DW 1933 | config.port = process.env.PORT; 1934 | } 1935 | stats.ctStarts++; 1936 | stats.ctHitsThisRun = 0; 1937 | stats.whenLastStart = whenStart; 1938 | statsChanged (); 1939 | console.log ("\n" + config.productName + " v" + config.version + " running on port " + config.port + ".\n"); 1940 | console.log ("config == " + utils.jsonStringify (config)); 1941 | startDavetwitter (handleHttpRequest); 1942 | startDavemail (); //1/23/23 by DW 1943 | 1944 | if (config.wordpress !== undefined) { //9/10/23 by DW 1945 | wordpress.start (config.wordpress, function () { 1946 | }); 1947 | } 1948 | 1949 | if (config.myDomain === undefined) { 1950 | console.log ("startup: can't start the server because config.myDomain is not defined."); 1951 | } 1952 | else { 1953 | if (config.urlServerForClient === undefined) { //1/30/23 by DW 1954 | config.urlServerForClient = "http://" + config.myDomain + "/"; 1955 | } 1956 | if (config.urlWebsocketServerForClient === undefined) { //1/30/23 by DW 1957 | config.urlWebsocketServerForClient = getWsProtocol () + utils.stringNthField (config.myDomain, ":", 1) + ":" + config.websocketPort + "/"; 1958 | } 1959 | webSocketStartup (); 1960 | setInterval (everySecond, 1000); 1961 | utils.runEveryMinute (everyMinute); 1962 | if (callback !== undefined) { 1963 | callback (config); 1964 | } 1965 | } 1966 | }); 1967 | }); 1968 | } 1969 | --------------------------------------------------------------------------------