├── lib ├── helpers.js └── myproxy.js ├── public ├── step3.png ├── index.css ├── featured.js ├── index.js └── index.html ├── deploy.config.js ├── package.json ├── .gitignore └── app.js /lib/helpers.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garageScript/FreeDomains/HEAD/public/step3.png -------------------------------------------------------------------------------- /lib/myproxy.js: -------------------------------------------------------------------------------- 1 | const adapter = {} 2 | adapter.getMappings = async () => { 3 | } 4 | 5 | module.exports = adapter 6 | -------------------------------------------------------------------------------- /deploy.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {"apps":{"name":"freedomains.dev","script":"npm run start:myproxy << /home/myproxy/.pm2/logs/freedomains.dev-out.log","instances":1,"autorestart":true,"watch":false,"max_memory_restart":"1G","env_production":{"NODE_ENV":"production","PORT":3002,"ADMIN":"A3uPmSvEs5H7FfgmjCUsqD3vsshwuRMwezneqXdA","WORKPATH":"/home/myproxy"},"error_file":"/home/myproxy/.pm2/logs/freedomains.dev-err.log","merge_logs":true}} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freedomains.dev", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start:myproxy": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "myproxy@freedomains.dev:/home/myproxy/freedomains.dev" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "express": "^4.21.2", 18 | "node-fetch": "^3.2.10", 19 | "uuid": "^3.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | .deleteButton { 2 | float: right; 3 | display: inline-block; 4 | cursor: pointer; 5 | } 6 | .actionContainer .loading { 7 | display: none; 8 | } 9 | .actionContainer.isLoading .loading { 10 | display: inline-block; 11 | } 12 | .actionContainer.isLoading .setUpButton { 13 | display: none; 14 | } 15 | .save { 16 | margin-bottom: 10px; 17 | } 18 | 19 | .inlineCode { 20 | display: inline-block; 21 | padding: 5px; 22 | background: #eee; 23 | color: #f55; 24 | } 25 | 26 | .indent { 27 | padding-left: 20px; 28 | } 29 | .indent2 { 30 | padding-left: 40px; 31 | } 32 | 33 | .groupStep { 34 | margin-top: 10px; 35 | margin-bottom: 10px; 36 | } 37 | 38 | #newPortButton { 39 | height: 38px; 40 | } 41 | 42 | .step2 { 43 | color: #181; 44 | } 45 | 46 | h5 { 47 | margin-top: 10px; 48 | } 49 | 50 | -------------------------------------------------------------------------------- /public/featured.js: -------------------------------------------------------------------------------- 1 | const appLink = document.querySelector('.appLink') 2 | 3 | const renderNext = (str, orig, cb, i = 0) => { 4 | if (i >= str.length && i >= orig.length) { 5 | return cb() 6 | } 7 | 8 | const oldChars = orig.split('') 9 | const newChar = i >= str.length ? ' ' : str[i] 10 | oldChars[i] = newChar 11 | 12 | const newDisplay = oldChars.join('') 13 | appLink.innerText = newDisplay 14 | 15 | setTimeout(() => { 16 | return renderNext(str, newDisplay, cb, i + 1) 17 | }, 100) 18 | } 19 | 20 | const doAll = (arr, i = 0) => { 21 | if (i >= arr.length) { 22 | i = 0 23 | } 24 | renderNext(arr[i].name, appLink.innerText, () => { 25 | appLink.href = arr[i].url 26 | setTimeout(() => { 27 | return doAll(arr, i + 1) 28 | }, 2000) 29 | }) 30 | } 31 | 32 | const data = [{ 33 | name: "Herman's Portfolio", 34 | url: 'https://herman.hireme.fun' 35 | }, { 36 | name: 'Corny Jokes', 37 | url: 'https://cornyjokes.learnjs.tips' 38 | }] 39 | 40 | doAll(data) 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | data.db 107 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const fs = require('fs') 3 | const fetch = require('node-fetch') 4 | const { exec } = require('child_process') 5 | const uuidv4 = require('uuid/v4') 6 | const app = express() 7 | app.use(express.static('public')) 8 | app.use(express.json()) 9 | 10 | const myProxyApi = process.env.MYPROXY_API 11 | const myProxyKey = process.env.MYPROXY_KEY 12 | 13 | const dataPath = './data.db' 14 | let data = {} 15 | fs.readFile(dataPath, (err, fileData) => { 16 | if (err) return 17 | try { 18 | data = JSON.parse(fileData) 19 | } catch (e) { 20 | console.log('parse error', e) 21 | } 22 | }) 23 | 24 | const getMappings = () => { 25 | return data.mappings || {} 26 | } 27 | 28 | const getFullDomain = (subDomain, domain) => { 29 | const prefix = subDomain ? `${subDomain}.` : '' 30 | return `${prefix}${domain}` 31 | } 32 | 33 | 34 | app.get('/isAvailable', (req, res) => { 35 | const { subDomain, domain } = req.query 36 | const fullDomain = getFullDomain(subDomain, domain) 37 | const allMappings = getMappings() 38 | const result = { 39 | isAvailable: true 40 | } 41 | if (allMappings[fullDomain]) { 42 | result.isAvailable = false 43 | } 44 | return res.json(result) 45 | }) 46 | 47 | app.get('/downloadConfig', (req, res) => { 48 | fetch(`${myProxyApi}/mappings/download/?fullDomain=${req.query.fullDomain}`, { 49 | headers: { 50 | authorization: myProxyKey 51 | } 52 | }).then(r => { 53 | r.body.pipe(res) 54 | res.setHeader('content-disposition', `attachment; filename="deploy.config.js"`) 55 | }) 56 | }) 57 | 58 | app.get('/api/logs/:type/:domain', (req, res) => { 59 | const { type, domain } = req.params 60 | fetch(`${myProxyApi}/logs/${type}/${domain}`, { 61 | headers: { 62 | authorization: myProxyKey 63 | } 64 | }).then(r => { 65 | res.setHeader('content-type', 'text/plain') 66 | r.body.pipe(res) 67 | }) 68 | }) 69 | 70 | app.delete('/api/logs/:domain', (req, res) => { 71 | const { domain } = req.params 72 | fetch(`${myProxyApi}/logs/${domain}`, { 73 | method: 'DELETE', 74 | headers: { 75 | authorization: myProxyKey 76 | } 77 | }).then(r => { 78 | r.body.pipe(res) 79 | }) 80 | }) 81 | 82 | app.get('/api/domains', (req, res) => { 83 | fetch (`${myProxyApi}/availableDomains`, { 84 | headers: { 85 | authorization: myProxyKey 86 | } 87 | }) 88 | .then(r => r.json()) 89 | .then(domains => res.json(domains)) 90 | }) 91 | 92 | app.use('/api/mappings', (req, res, next) => { 93 | const userId = req.headers.authorization 94 | if (!userId || !(data.users || {})[userId]) { 95 | return res.status(401).json({ message: 'user id is invalid' }) 96 | } 97 | req.user = { 98 | id: userId 99 | } 100 | next() 101 | }) 102 | 103 | app.get('/api/mappings', async (req, res) => { 104 | const originalMappings = await fetch(`${myProxyApi}/mappings`, { 105 | headers: { 106 | authorization: myProxyKey 107 | } 108 | }).then(r => r.json()) 109 | const originalMap = originalMappings.reduce((acc, mapping) => { 110 | acc[mapping.fullDomain] = mapping 111 | return acc 112 | }, {}) 113 | 114 | const allMappings = getMappings() 115 | const userMappings = Object.values(allMappings).filter(m => { 116 | return m.userId === req.user.id 117 | }).map((mapping) => { 118 | const original = originalMap[mapping.fullDomain] || {} 119 | mapping.status = original.status || mapping.status 120 | mapping.id = original.id || mapping.id 121 | return mapping 122 | }).sort((a, b) => b.createdAt - a.createdAt) 123 | res.json(userMappings) 124 | }) 125 | 126 | app.delete('/api/mappings/:id', async (req, res) => { 127 | const allMappings = getMappings() 128 | const mapping = Object.values(allMappings).find(m => { 129 | return m.id === req.params.id 130 | }) 131 | 132 | if (!mapping || mapping.userId !== req.user.id) { 133 | return res.status(401).json({ message: 'user id is invalid' }) 134 | } 135 | 136 | await fetch(`${myProxyApi}/mappings/${req.params.id}`, { 137 | method: 'DELETE', 138 | headers: { 139 | 'Content-Type': 'application/json', 140 | authorization: myProxyKey 141 | } 142 | }).then(r => r.json()).catch(e => { 143 | console.log('error for deleting mapping', e) 144 | }) 145 | 146 | delete allMappings[mapping.fullDomain] 147 | saveData() 148 | 149 | res.json(mapping) 150 | }) 151 | 152 | app.post('/api/mappings', async (req, res) => { 153 | const { subDomain, domain } = req.body 154 | if (!subDomain) return res.status(400).json({ message: 'Subdomain field is required.' }) 155 | 156 | const newMapping = await fetch(`${myProxyApi}/mappings`, { 157 | method: 'POST', 158 | headers: { 159 | 'Content-Type': 'application/json', 160 | authorization: myProxyKey 161 | }, 162 | body: JSON.stringify({ 163 | domain, subDomain 164 | }) 165 | }).then(r => r.json()).catch(e => { 166 | console.log('error for creating mapping', e) 167 | }) 168 | 169 | // DEV env: 170 | /* 171 | const newMapping = { 172 | fullDomain: getFullDomain(subDomain, domain), 173 | gitLink: 'demo.com', 174 | id: Date.now() 175 | } 176 | */ 177 | 178 | const { gitLink, id, fullDomain } = newMapping 179 | const mappings = getMappings() 180 | mappings[fullDomain] = { 181 | id, 182 | domain, 183 | subDomain, 184 | fullDomain, 185 | gitLink, 186 | userId: req.user.id, 187 | createdAt: Date.now() 188 | } 189 | data.mappings = mappings 190 | saveData() 191 | 192 | res.json(req.body) 193 | }) 194 | 195 | app.get('/api/users/:userId', (req, res) => { 196 | const users = data.users || {} 197 | res.json({ 198 | key: users[req.params.userId] 199 | }) 200 | }) 201 | 202 | const saveData = () => { 203 | return new Promise((resolve, reject) => { 204 | fs.writeFile(dataPath, JSON.stringify(data, null, 2), (err) => { 205 | if (err) return reject(err) 206 | resolve() 207 | }) 208 | }) 209 | } 210 | 211 | app.post('/api/sshKeys', async (req, res) => { 212 | let { userId, key } = req.body 213 | if (!key) return res.json({ error: 'invalid input' }) 214 | 215 | const users = data.users || {} 216 | 217 | const foundUser = Object.entries(users).find(([uid, sshKey]) => { 218 | return (sshKey === key) 219 | }) 220 | 221 | // User already exist 222 | if (foundUser && foundUser.length === 2) { 223 | return res.json({ 224 | userId: foundUser[0], 225 | key: foundUser[1] 226 | }) 227 | } 228 | 229 | const tmpFilePath = `${__dirname}/keys_${uuidv4()}` 230 | fs.writeFile(tmpFilePath, key, () => { 231 | exec(`ssh-keygen -lf ${tmpFilePath}`, (err, result) => { 232 | exec(`rm ${tmpFilePath}`, async () => { 233 | if (err) { 234 | return res.status(400).send({ 235 | message: 'SSH KEY is invalid. Run "cat ~/.ssh/id_rsa.pub" and submit the output of the command' 236 | }) 237 | } 238 | 239 | // Replace UserId 240 | if (users[userId]) { 241 | // Delete oldSshKey? 242 | // Decided not to, a user could have multiple 243 | // sshKeys on one browser 244 | } 245 | 246 | // Create new key 247 | await fetch(`${myProxyApi}/sshKeys`, { 248 | method: 'POST', 249 | headers: { 250 | 'Content-Type': 'application/json', 251 | authorization: myProxyKey 252 | }, 253 | body: JSON.stringify({ 254 | key 255 | }) 256 | }).then(r => r.json()).catch(e => { 257 | console.log('error for creating mapping', e) 258 | }) 259 | 260 | userId = uuidv4() 261 | users[userId] = key 262 | data.users = users 263 | await saveData() 264 | return res.json({ userId, key }) 265 | }) 266 | }) 267 | }) 268 | }) 269 | 270 | app.listen(process.env.PORT || 8123) 271 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | /* global fetch localStorage $, confirm, alert */ 2 | 3 | const hostSelector = document.querySelector('#hostSelector') 4 | const dropDownDomains = document.querySelector('.dropdown-menu') 5 | const createButton = document.querySelector('.create') 6 | const domainList = document.querySelector('.domainList') 7 | const sshKeySaveButton = document.querySelector('#saveSshKey') 8 | const sshKeyInput = document.querySelector('#sshKeyInput') 9 | const subDomain = document.querySelector('.subDomain') 10 | const invalidElement = document.querySelector('.invalid-feedback') 11 | const domainForm = document.getElementById('domainForm') 12 | 13 | let selectedHost = '' 14 | 15 | class DomainOption { 16 | constructor (domain) { 17 | const dropdownElement = document.createElement('button') 18 | dropdownElement.classList.add('dropdown-item') 19 | dropdownElement.textContent = domain 20 | dropdownElement.onclick = () => { 21 | hostSelector.innerText = domain 22 | selectedHost = domain 23 | checkAvailability() 24 | } 25 | 26 | dropDownDomains.appendChild(dropdownElement) 27 | } 28 | } 29 | 30 | class MappingItem { 31 | constructor (data, idx) { 32 | const mappingElement = document.createElement('li') 33 | let iconClass 34 | let iconColor 35 | // The variables below are to hide log related icons when pm2 is not 36 | // being used to monitor the apps. These apps will not have status since 37 | // they are not managed by pm2. 38 | let settingClass 39 | let logClass 40 | if (data.status === 'running') { 41 | iconClass = 'fa fa-circle mr-1 mt-1' 42 | iconColor = 'rgba(50,255,50,0.5)' 43 | logClass = 'fa fa-file-text-o ml-1 mt-1' 44 | settingClass = 'ml-1 fa fa-cog' 45 | } else if (data.status === 'not started') { 46 | iconClass = '' 47 | iconColor = 'transparent' 48 | } else { 49 | iconClass = 'fa fa-circle mr-1 mt-1' 50 | iconColor = 'rgba(255, 50, 50, 0.5)' 51 | logClass = 'fa fa-file-text-o ml-1 mt-1' 52 | settingClass = 'ml-1 fa fa-cog' 53 | } 54 | mappingElement.classList.add( 55 | 'list-group-item', 56 | 'd-flex', 57 | 'align-items-center' 58 | ) 59 | domainList.appendChild(mappingElement) 60 | 61 | const createdDate = new Date(data.createdAt || Date.now()) 62 | let step2Content = '' 63 | if (idx) { 64 | step2Content = ` 65 | 66 | ${data.gitLink} 67 | 68 | ` 69 | } else { 70 | step2Content = ` 71 | Step 2 --> 72 | git clone 73 | 74 | ${data.gitLink} 75 | 76 | 77 | ` 78 | } 79 | mappingElement.innerHTML = ` 80 |
26 | This is a shared resource, please use responsibily. To ensure fair use and reduce costs, inactive Apps will be automatically removed after 7 days. 27 |
28 |29 | This app is powered by 30 | MyProxy 31 |
32 | 33 |
34 | Featured Apps:
35 | Corny Jokes
36 |
37 | Want to be featured? Email Herman at
38 | hwong.0305@gmail.com
39 |
This will be the domain name that your friends and family will be putting into the browser to visit your website.
43 |
121 | const express = require('express')
122 | const app = express()
123 | app.use(express.static('public'))
124 | app.get('/', (req, res) => {
125 | res.send('hello');
126 | });
127 |
128 | app.listen(process.env.PORT || 8123);
129 |
130 |
143 | ...
144 | "scripts": {
145 | "start": "node app.js",
146 | ...
147 | }
148 | ...
149 |
150 | 180 | The video below shows you how to deploy your apps after setting up your SSH Key. The contents may contain more information than the steps above because they are taken from the MyProxy site, but it should be enough to help you understand each steps. 181 |
182 |