├── .all-contributorsrc ├── .circleci └── config.yml ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .nycrc.json ├── .prettierrc.js ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── _config.yml ├── dataMigration.js ├── docs ├── _config.yml └── index.md ├── jest.config.js ├── package-lock.json ├── package.json ├── scripts ├── Dockerfile ├── deploy.sh ├── gitignore ├── no-interactive-login ├── post-receive ├── pre-receive ├── prod.config.js └── setup.sh ├── src ├── .env.example ├── admin │ └── index.ts ├── api │ ├── accessToken.ts │ ├── admin.ts │ ├── index.ts │ ├── logs.ts │ ├── mapping.ts │ └── sshKeys.ts ├── app.ts ├── auth │ └── index.ts ├── helpers │ ├── SNICallback.ts │ ├── authentication.ts │ ├── authorizedKeys.ts │ ├── cache.ts │ ├── crypto.ts │ ├── docker.ts │ ├── domainSetup.ts │ ├── environment.ts │ ├── getGitUser.ts │ └── httpRequest.ts ├── lib │ └── data.ts ├── providers │ ├── goDaddy.ts │ ├── helpers.ts │ ├── index.ts │ ├── namecheap.ts │ └── namecom.ts ├── public │ ├── accessTokens.ts │ ├── client.ts │ ├── helper.ts │ ├── keys.ts │ ├── manageDomain.ts │ └── providers.ts ├── server │ └── server.ts ├── tests │ ├── api.test.ts │ ├── helpers │ │ ├── accessTokensAdapter.ts │ │ ├── logAdapter.ts │ │ └── mappingAdapter.ts │ └── integration │ │ ├── accessToken.test.ts │ │ ├── log.test.ts │ │ ├── mapping.test.ts │ │ └── sshKeys.test.ts └── types │ ├── admin.ts │ ├── docker.ts │ ├── general.ts │ └── tests.ts ├── tsconfig.json └── views ├── admin ├── accessTokens.ejs └── providers.ejs ├── client.ejs ├── error.ejs ├── index.ejs ├── layout ├── footer.ejs └── head.ejs ├── login.ejs ├── manageDomain.ejs └── sshKeys.ejs /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "myProxy", 3 | "projectOwner": "garageScript", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "songz", 15 | "name": "Song Zheng", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/686933?v=4", 17 | "profile": "https://www.c0d3.com/", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "ideas", 22 | "infra", 23 | "mentoring", 24 | "projectManagement", 25 | "review" 26 | ] 27 | }, 28 | { 29 | "login": "hwong0305", 30 | "name": "Herman Wong", 31 | "avatar_url": "https://avatars1.githubusercontent.com/u/7990856?v=4", 32 | "profile": "https://www.devwong.com/", 33 | "contributions": [ 34 | "code", 35 | "review" 36 | ] 37 | }, 38 | { 39 | "login": "rkalra247", 40 | "name": "rkalra247", 41 | "avatar_url": "https://avatars1.githubusercontent.com/u/27792256?v=4", 42 | "profile": "https://github.com/rkalra247", 43 | "contributions": [ 44 | "code" 45 | ] 46 | }, 47 | { 48 | "login": "Wolfy64", 49 | "name": "David De Wulf", 50 | "avatar_url": "https://avatars3.githubusercontent.com/u/25457563?v=4", 51 | "profile": "https://dewulfdavid.com/", 52 | "contributions": [ 53 | "code" 54 | ] 55 | }, 56 | { 57 | "login": "SahilKalra98", 58 | "name": "SahilKalra98", 59 | "avatar_url": "https://avatars1.githubusercontent.com/u/23374591?v=4", 60 | "profile": "https://github.com/SahilKalra98", 61 | "contributions": [ 62 | "code" 63 | ] 64 | }, 65 | { 66 | "login": "albertoelopez", 67 | "name": "albertoelopez", 68 | "avatar_url": "https://avatars2.githubusercontent.com/u/40315201?v=4", 69 | "profile": "https://github.com/albertoelopez", 70 | "contributions": [ 71 | "code" 72 | ] 73 | }, 74 | { 75 | "login": "allopez7", 76 | "name": "Alberto Lopez", 77 | "avatar_url": "https://avatars3.githubusercontent.com/u/29881336?v=4", 78 | "profile": "https://c0d3.com/", 79 | "contributions": [ 80 | "code" 81 | ] 82 | }, 83 | { 84 | "login": "bryanjenningz", 85 | "name": "Bryan Jennings", 86 | "avatar_url": "https://avatars2.githubusercontent.com/u/7637655?v=4", 87 | "profile": "https://github.com/bryanjenningz", 88 | "contributions": [ 89 | "code" 90 | ] 91 | }, 92 | { 93 | "login": "joshgreenwell", 94 | "name": "Josh Greenwell", 95 | "avatar_url": "https://avatars0.githubusercontent.com/u/31043400?v=4", 96 | "profile": "https://github.com/joshgreenwell", 97 | "contributions": [ 98 | "code" 99 | ] 100 | }, 101 | { 102 | "login": "coltonehrman", 103 | "name": "Colton Ehrman", 104 | "avatar_url": "https://avatars1.githubusercontent.com/u/12456288?v=4", 105 | "profile": "https://coltonehrman.github.io/react-portfolio/", 106 | "contributions": [ 107 | "code", 108 | "review" 109 | ] 110 | }, 111 | { 112 | "login": "ggwadera", 113 | "name": "Guilherme Gwadera", 114 | "avatar_url": "https://avatars2.githubusercontent.com/u/16023489?v=4", 115 | "profile": "https://www.linkedin.com/in/guilherme-gwadera/", 116 | "contributions": [ 117 | "code", 118 | "maintenance" 119 | ] 120 | }, 121 | { 122 | "login": "Cijin", 123 | "name": "Cijin Cherian", 124 | "avatar_url": "https://avatars0.githubusercontent.com/u/1990966?v=4", 125 | "profile": "https://github.com/Cijin", 126 | "contributions": [ 127 | "doc" 128 | ] 129 | } 130 | ], 131 | "contributorsPerLine": 7 132 | } 133 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 3 | 4 | version: 2.1 5 | 6 | commands: 7 | checkout_and_restore_cache: 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | keys: 12 | - v1-dependencies-{{ checksum "package.json" }} 13 | - v1-dependencies- 14 | workspace: 15 | steps: 16 | - attach_workspace: 17 | at: ~/circleci 18 | 19 | executors: 20 | node: 21 | docker: 22 | - image: circleci/node:12.8.0 23 | working_directory: ~/circleci 24 | 25 | jobs: 26 | install: 27 | executor: node 28 | steps: 29 | - checkout_and_restore_cache 30 | - run: ./scripts/setup.sh 31 | - persist_to_workspace: 32 | root: . 33 | paths: 34 | - . 35 | - save_cache: 36 | key: source-v1-{{ checksum "package.json" }} 37 | paths: 38 | - ./node_modules 39 | - ./acme.sh 40 | lint: 41 | executor: node 42 | steps: 43 | - workspace 44 | - checkout_and_restore_cache 45 | - run: npm install 46 | - run: npm run lint 47 | - run: npm run build 48 | test: 49 | executor: node 50 | steps: 51 | - workspace 52 | - checkout_and_restore_cache 53 | - run: npm run test 54 | deploy: 55 | executor: node 56 | steps: 57 | - add_ssh_keys: 58 | fingerprints: 59 | - "a1:09:f8:12:0e:f2:aa:75:39:62:1f:31:55:d3:0c:a8" 60 | - workspace 61 | - checkout_and_restore_cache 62 | - run: 63 | name: Deploy Over SSH 64 | command: ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST "/home/dev/prodProxy/scripts/deploy.sh" 65 | 66 | workflows: 67 | version: 2 68 | integration: 69 | jobs: 70 | - install 71 | - lint: 72 | requires: 73 | - install 74 | - test: 75 | requires: 76 | - install 77 | - deploy: 78 | requires: 79 | - lint 80 | - test 81 | filters: 82 | branches: 83 | only: master 84 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier", 11 | "prettier/@typescript-eslint" 12 | ], 13 | "globals": { 14 | "Atomics": "readonly", 15 | "SharedArrayBuffer": "readonly" 16 | }, 17 | "parser": "@typescript-eslint/parser", 18 | "plugins": ["@typescript-eslint"], 19 | "parserOptions": { 20 | "ecmaVersion": 2018, 21 | "sourceType": "module" 22 | }, 23 | "rules": { 24 | "indent": [ 25 | "error", 26 | 2 27 | ], 28 | "linebreak-style": [ 29 | "error", 30 | "unix" 31 | ], 32 | "quotes": [ 33 | "error", 34 | "single" 35 | ], 36 | "semi": [ 37 | "error", 38 | "never" 39 | ], 40 | "camelcase": "off", 41 | "@typescript-eslint/camelcase": [ 42 | "error", { "properties": "never" } 43 | ], 44 | "@typescript-eslint/no-explicit-any": [ "warn", { 45 | "fixToUnknown": true, 46 | "ignoreRestArgs": true 47 | } ] 48 | } 49 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,code,macos,windows 3 | # Edit at https://www.gitignore.io/?templates=node,code,macos,windows 4 | 5 | ### database ### 6 | data.db 7 | 8 | ### Acme ### 9 | acme.sh/ 10 | 11 | ### Code ### 12 | .vscode/* 13 | !.vscode/settings.json 14 | !.vscode/tasks.json 15 | !.vscode/launch.json 16 | !.vscode/extensions.json 17 | 18 | ### macOS ### 19 | # General 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### Node ### 47 | # Logs 48 | logs 49 | *.log 50 | npm-debug.log* 51 | yarn-debug.log* 52 | yarn-error.log* 53 | lerna-debug.log* 54 | 55 | # Diagnostic reports (https://nodejs.org/api/report.html) 56 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 57 | 58 | # Runtime data 59 | pids 60 | *.pid 61 | *.seed 62 | *.pid.lock 63 | 64 | # Directory for instrumented libs generated by jscoverage/JSCover 65 | lib-cov 66 | 67 | # Coverage directory used by tools like istanbul 68 | coverage 69 | *.lcov 70 | 71 | # nyc test coverage 72 | .nyc_output 73 | 74 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 75 | .grunt 76 | 77 | # Bower dependency directory (https://bower.io/) 78 | bower_components 79 | 80 | # node-waf configuration 81 | .lock-wscript 82 | 83 | # Compiled binary addons (https://nodejs.org/api/addons.html) 84 | build/Release 85 | 86 | # Dependency directories 87 | node_modules/ 88 | jspm_packages/ 89 | 90 | # TypeScript v1 declaration files 91 | typings/ 92 | build/ 93 | 94 | # TypeScript cache 95 | *.tsbuildinfo 96 | 97 | # Optional npm cache directory 98 | .npm 99 | 100 | # Optional eslint cache 101 | .eslintcache 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variables file 113 | .env 114 | .env.test 115 | 116 | # parcel-bundler cache (https://parceljs.org/) 117 | .cache 118 | 119 | # next.js build output 120 | .next 121 | 122 | # nuxt.js build output 123 | .nuxt 124 | 125 | # vuepress build output 126 | .vuepress/dist 127 | 128 | # Serverless directories 129 | .serverless/ 130 | 131 | # FuseBox cache 132 | .fusebox/ 133 | 134 | # DynamoDB Local files 135 | .dynamodb/ 136 | 137 | ### Windows ### 138 | # Windows thumbnail cache files 139 | Thumbs.db 140 | Thumbs.db:encryptable 141 | ehthumbs.db 142 | ehthumbs_vista.db 143 | 144 | # Dump file 145 | *.stackdump 146 | 147 | # Folder config file 148 | [Dd]esktop.ini 149 | 150 | # Recycle Bin used on file shares 151 | $RECYCLE.BIN/ 152 | 153 | # Windows Installer files 154 | *.cab 155 | *.msi 156 | *.msix 157 | *.msm 158 | *.msp 159 | 160 | # Windows shortcuts 161 | *.lnk 162 | 163 | # End of https://www.gitignore.io/api/node,code,macos,windows -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-typescript", 3 | "extension": [ 4 | ".ts", 5 | ".tsx" 6 | ], 7 | "exclude": [ 8 | "**/*.d.ts" 9 | ], 10 | "reporter": [ 11 | "html" 12 | ] 13 | } -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | parser: 'typescript', 5 | trailingComma: "none" 6 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 garageScript 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 | # [MyProxy](https://garagescript.github.io/myProxy/) · [![CircleCI](https://circleci.com/gh/garageScript/myProxy.svg?style=svg)](https://circleci.com/gh/garageScript/myproxy) 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors-) 4 | 5 | MyProxy is an alternative to Nginx that allows automatic domain provider integration, ssl support for all domains, dynamic port proxy 6 | and automatic git deployment. 7 | 8 | MyProxy helps you quickly and easily: 9 | * Connect to your Domain provider 10 | * Set up A and CNAME records for your selected domains 11 | * Create and serve SSL certificates for your selected domains 12 | * Run an unlimited number of applications on your subdomains 13 | 14 | Watch the following videos to understand how MyProxy works: 15 | 16 | [Using MyProxy to deploy apps](https://www.youtube.com/watch?v=Tjx0BtpZmPc) 17 | 18 | [Setting up MyProxy on your server](https://www.youtube.com/watch?v=q3uSyMfaRP4) 19 | 20 | ## Try it out? 21 | Try it out on one of our open source partners providing a heroku alternative: https://freedomains.dev/ 22 | 23 | ## Why? 24 | Setting up a server is hard - especially setting up DNS records, managing certificates, and deployment. So we setup to build a simple and easy-to-use app that helps us build applications quickly. 25 | 26 | We are new to software engineering so if you find areas where this app could be improved, please let us know by [creating an issue](https://github.com/garageScript/myproxy/issues). We are excited to learn! 27 | 28 | Also, we are currently seeking jobs. If your team needs software engineers, please reach out: 29 | * [Alberto Lopez](https://www.linkedin.com/in/albertolopez-siliconvalley/) - Available immediately 30 | * [David De Wulf](https://dewulfdavid.com) - Open to new opportunities 31 | * [Rahul Kalra](https://www.linkedin.com/in/voterknow) - Available immediately 32 | * [Sahil Kalra](https://www.linkedin.com/in/s1kalra/) - UC San Diego senior, graduating June 2020 33 | * [Herman Wong](https://www.linkedin.com/in/hw335/) - Open to new opportunities 34 | 35 | ## Prerequisites 36 | To use `MyProxy`, you need 3 things: 37 | 1. A domain name. MyProxy uses [acme.sh](https://github.com/Neilpang/acme.sh/wiki/dnsapi), so you would have to buy the domains from any of the [DNS APIs listed there](https://github.com/Neilpang/acme.sh/wiki/dnsapi) (includes all of the major providers like namecheap, goDaddy, etc.) 38 | 2. A server's IP address that you have root access to. You can use your home server or get one from [AWS EC2](https://aws.amazon.com/ec2/?hp=tile&so-exp=below), [DigitalOcean](https://www.digitalocean.com/), [GoogleCloud](https://cloud.google.com/), etc. 39 | 3. Docker needs to be installed on the server. MyProxy uses Docker to run deployed apps inside containers. 40 | 41 | # Installation and Usage 42 | 43 | ## Server setup 44 | We tested MyProxy on the AWS and Google Cloud platforms. If you use them, please follow the configurations and setup below to make sure MyProxy works well for you. 45 | 46 | ### AWS Setup 47 | You will need to configure the VM's firewall per table below during security group setup on AWS EC2 instance. 48 | 49 | | Type | Protocol | Port Range | Source | 50 | |:---:|:--------:|:----------: | :------: | 51 | | HTTP | TCP | 80 | 0.0.0.0/0 | 52 | | HTTPS| TCP | 443 | 0.0.0.0/0 | 53 | | SSH | TCP | 22 | 0.0.0.0/0 | 54 | | Custom TCP Rule | TCP | 3000 | 0.0.0.0/0 | 55 | | Custom TCP Rule | TCP | 9418 | 0.0.0.0/0 | 56 | 57 | ### Google Cloud Setup 58 | 59 | - Target: `specify target tags` 60 | - Target Tags: `myproxy` 61 | - Source Filter: `IP ranges` 62 | - Source IP: `0.0.0.0/0` 63 | - Specify Protocol and Ports: `tcp: 3000` 64 | 65 | Update Google VMs to specify `myproxy http-server https-server` in network tags 66 | 67 | ## Installation 68 | 69 | 1. Connect to your server: `ssh root@your-server-ip-address` 70 | * **AWS Users Only** Change to root user `sudo su root` and change to home folder `cd ~` 71 | 2. Install Docker on your server, follow the instructions for your OS at the [Docker docs](https://docs.docker.com/engine/install/). 72 | 3. Clone the app: `git clone https://github.com/garageScript/myProxy.git` 73 | 4. Go to the MyProxy folder: `cd myProxy` 74 | 5. Run the setup script `./scripts/setup.sh` 75 | * Installs `nodeJS` and `npm` if system does not have them. 76 | * Enables firewall port `3000` (for the admin page UI), `80` and `443`. 77 | * Installs application dependencies 78 | * For a complete list of commands the script runs, [look here](https://github.com/garageScript/myProxy/blob/master/scripts/setup.sh) 79 | 6. Run the App: `ADMIN=YOUR_ADMIN_PASSWORD npm run server` 80 | * You can also run the app under your own defined port by setting a `PORT` environment variable 81 | 7. Exit from server `exit` 82 | 83 | ## Usage 84 | 85 | 1. Go to your server url: `http://your-server-ip-address:3000`. You will be prompted to enter your admin password and your domain provider's API Key and Secret, [find out how here](https://github.com/Neilpang/acme.sh/wiki/dnsapi) 86 | 2. All your domain names in that provider will show up. Click the **setup** button next to the domain you wish to setup (could take up to 5 minutes) 87 | 3. After your domain is setup, you will be able to generate as many subdomain repositories as you want! To do that: 88 | 1. Go to your server URL: `http://your-server-ip-address:3000` 89 | 2. Create a subdomain. IP and port are optional. You should see a git link that was created for you. 90 | 3. `git clone` the app, then build the app locally. Find out how in the **Building Your Local App section** below. 91 | 4. When you are done, `git push origin master` and watch your app run in production! 92 | 93 | ## Building-Your-Local-App 94 | 1. In the terminal, run `git clone ` to clone your app folder. 95 | 2. Enter your repo `cd ` 96 | 3. Run `npm init -y` 97 | 4. Run `npm i express --save` 98 | 5. Run `touch app.js` 99 | 6. Copy the following code into app.js. 100 | 101 | ```javascript 102 | const express = require('express'); 103 | const app = express(); 104 | app.use(express.static('public')); 105 | 106 | app.get('/', (req, res) => { 107 | res.send('hello'); 108 | }); 109 | 110 | app.listen(process.env.PORT || 8123); 111 | ``` 112 | 113 | 7. Update the `scripts` section in `package.json` with your app entry point command, under `start`: `"start": "node app.js"` 114 | 8. Run `git add .` 115 | 9. Run `git commit -m "Initial Commit"` 116 | 10. Run `git push origin master` 117 | 118 | # API Reference 119 | 120 | Users can use Access Tokens to manage their domain mappings from a 3rd party server. 121 | [See available endpoints](https://github.com/garageScript/myProxy/wiki/API) 122 | 123 | # Development & Contribution 124 | The following steps will guide you through how to setup your development environment to send pull requests or build your own custom features. 125 | 126 | You need to install node and typescript 127 | 128 | 1. Fork and clone the repository. 129 | 2. Install dependencies: `npm install` or `yarn` 130 | 3. Run the app: `yarn start` or `npm run start` 131 | * You can also run the app under your own defined port by setting a `PORT` environment variable 132 | 133 | ## Adding a new provider 134 | If your company sells domain names and you want your service to be supported on MyProxy, make sure you integrate with [acme.sh][DNS_API_integration] first. 135 | 136 | [Sample integration](https://github.com/garageScript/myProxy/pull/355) for Name.com that you can follow along. 137 | 138 | 1. All implemented providers are listed in [`src/providers/index.ts`](https://github.com/garageScript/myProxy/blob/master/src/providers/index.ts). Add your service to `providerList`, following the your [`acme.sh`](DNS_API_integration) integration's naming convention. 139 | * `path` should be the location of the file you create in the next step. 140 | 2. Create a file that exports the following functions: `getDomains` and `setRecord`. 141 | 3. Depending on your API in the previous step, you may need to create types. Types should be added to [`src/types/general.ts`](https://github.com/garageScript/myProxy/blob/master/src/types/general.ts) 142 | 143 | You are done! Get a beer 🍺 144 | 145 | ## Before sending a Pull Request: 146 | 1. Run `npm run autofix`: make sure there are no errors / warnings 147 | 148 | # License 149 | 150 | MyProxy is [MIT licensed](https://github.com/garageScript/myProxy/blob/master/LICENSE) 151 | 152 | [DNS_API_integration]: https://github.com/Neilpang/acme.sh#8-automatic-dns-api-integration 153 | 154 | ## Contributors ✨ 155 | 156 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 |

Song Zheng

💻 📖 🤔 🚇 🧑‍🏫 📆 👀

Herman Wong

💻 👀

rkalra247

💻

David De Wulf

💻

SahilKalra98

💻

albertoelopez

💻

Alberto Lopez

💻

Bryan Jennings

💻

Josh Greenwell

💻

Colton Ehrman

💻 👀

Guilherme Gwadera

💻 🚧

Cijin Cherian

📖
179 | 180 | 181 | 182 | 183 | 184 | 185 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /dataMigration.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const file = fs.readFileSync('./data.db') 3 | const fileData = JSON.parse(file.toString() || '{}') 4 | fileData.mappings = fileData.mappings.map(mapping => ({ 5 | ...mapping, 6 | domain: mapping.domain.toLowerCase(), 7 | subDomain: mapping.subDomain.toLowerCase(), 8 | fullDomain: mapping.fullDomain.toLowerCase() 9 | })) 10 | const fileDataString = `${JSON.stringify(fileData, null, 2)}` 11 | fs.writeFile('./data.db', fileDataString, err => { 12 | if (err) { 13 | return console.log('writing to DB failed', err) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | title: MyProxy 3 | description: MyProxy is an application that helps you connect to your Domain provider, setup A and CNAME records, create and serve SSL certificates, and run an unlimited number of applications on your subdomains. 4 | future: true 5 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # MyProxy · [![CircleCI](https://circleci.com/gh/garageScript/myProxy.svg?style=svg)](https://circleci.com/gh/garageScript/myproxy) 2 | MyProxy is an application that helps you quickly and easily: 3 | * Helps you connect to your Domain provider 4 | * Set up A and CNAME records for your selected domains 5 | * Create and serve SSL certificates for your selected domains 6 | * Run an unlimited number of applications on your subdomains 7 | 8 | To understand what MyProxy does for you, the video below walks you through how to create and deploy a new application as well as an existing application: 9 | 10 |
11 | 12 |
13 | 14 | If you want to install MyProxy into your own server, setup instructions are simple. Install MyProxy into your server and then connect the your domain name. 15 | 16 |
17 | 18 |
19 | 20 | ## Why? 21 | Setting up a server is hard - especially setting up DNS records, managing certificates, and deployment. So we setup to build a simple and easy-to-use app that helps us build applications quickly. 22 | 23 | We are new to software engineering so if you find areas where this app could be improved, please let us know by [creating an issue](https://github.com/garageScript/myproxy/issues). We are excited to learn! 24 | 25 | Also, we are currently seeking jobs. If your team needs software engineers, please reach out: 26 | * [Alberto Lopez](https://www.linkedin.com/in/albertolopez-siliconvalley/) - Available immediately 27 | * [David De Wulf](https://dewulfdavid.com) - Open to new opportunities 28 | * [Rahul Kalra](https://www.linkedin.com/in/voterknow) - Available immediately 29 | * [Sahil Kalra](https://www.linkedin.com/in/s1kalra/) - UC San Diego senior, graduating June 2020 30 | * [Herman Wong](https://www.linkedin.com/in/hw335/) - Open to new opportunities 31 | 32 | ## Prerequisites 33 | To use `MyProxy`, you need 2 things: 34 | 1. A domain name. MyProxy uses [acme.sh](https://github.com/Neilpang/acme.sh/wiki/dnsapi), so you would have to buy the domains from any of the [DNS APIs listed there](https://github.com/Neilpang/acme.sh/wiki/dnsapi) (includes all of the major providers like namecheap, goDaddy, etc.) 35 | 2. A server's IP address that you have root access to. You can use your home server or get one from [AWS EC2](https://aws.amazon.com/ec2/?hp=tile&so-exp=below), [DigitalOcean](https://www.digitalocean.com/), [GoogleCloud](https://cloud.google.com/), etc. 36 | 37 | # Installation and Usage 38 | 39 | ## Server setup 40 | We tested MyProxy on the AWS and Google Cloud platforms. If you use them, please follow the configurations and setup below to make sure MyProxy works well for you. 41 | 42 | ### AWS Setup 43 | You will need to configure the VM's firewall per table below during security group setup on AWS EC2 instance. 44 | 45 | | Type | Protocol | Port Range | Source | 46 | |:---:|:--------:|:----------: | :------: | 47 | | HTTP | TCP | 80 | 0.0.0.0/0 | 48 | | HTTPS| TCP | 443 | 0.0.0.0/0 | 49 | | SSH | TCP | 22 | 0.0.0.0/0 | 50 | | Custom TCP Rule | TCP | 3000 | 0.0.0.0/0 | 51 | | Custom TCP Rule | TCP | 9418 | 0.0.0.0/0 | 52 | 53 | ### Google Cloud Setup 54 | 55 | - Target: `specify target tags` 56 | - Target Tags: `myproxy` 57 | - Source Filter: `IP ranges` 58 | - Source IP: `0.0.0.0/0` 59 | - Specify Protocol and Ports: `tcp: 3000` 60 | 61 | Update Google VMs to specify `myproxy http-server https-server` in network tags 62 | 63 | ## Installation 64 | 65 | 1. Connect to your server: `ssh root@your-server-ip-address` 66 | * **AWS Users Only** Change to root user `sudo su root` and change to home folder `cd ~` 67 | 2. Clone the app: `git clone https://github.com/garageScript/myProxy.git` 68 | 3. Go to the MyProxy folder: `cd myProxy` 69 | 4. Run the setup script `./scripts/setup.sh` 70 | * Installs `nodeJS` and `npm` if system does not have them. 71 | * Enables firewall port `3000` (for the admin page UI), `80` and `443`. 72 | * Installs application dependencies 73 | * For a complete list of commands the script runs, [look here](https://github.com/garageScript/myProxy/blob/master/scripts/setup.sh) 74 | 5. Run the App: `ADMIN=YOUR_ADMIN_PASSWORD npm run server` 75 | * You can also run the app under your own defined port by setting a `PORT` environment variable 76 | 6. Exit from server `exit` 77 | 78 | ## Usage 79 | 80 | 1. Go to your server url: `http://your-server-ip-address:3000`. You will be prompted to enter your admin password and your domain provider's API Key and Secret, [find out how here](https://github.com/Neilpang/acme.sh/wiki/dnsapi) 81 | 2. All your domain names in that provider will show up. Click the **setup** button next to the domain you wish to setup (could take up to 5 minutes) 82 | 3. After your domain is setup, you will be able to generate as many subdomain repositories as you want! To do that: 83 | 1. Go to your server URL: `http://your-server-ip-address:3000` 84 | 2. Create a subdomain. IP and port are optional. You should see a git link that was created for you. 85 | 3. `git clone` the app, then build the app locally. Find out how in the **Building Your Local App section** below. 86 | 4. When you are done, `git push origin master` and watch your app run in production! 87 | 88 | ## Building-Your-Local-App 89 | 1. In the terminal, run `git clone ` to clone your app folder. 90 | 2. Enter your repo `cd ` 91 | 3. Run `npm init -y` 92 | 4. Run `npm i express --save` 93 | 5. Run `touch app.js` 94 | 6. Copy the following code into app.js. 95 | 96 | ```javascript 97 | const express = require('express'); 98 | const app = express(); 99 | app.use(express.static('public')); 100 | 101 | app.get('/', (req, res) => { 102 | res.send('hello'); 103 | }); 104 | 105 | app.listen(process.env.PORT || 8123); 106 | ``` 107 | 108 | 7. Update scripts section of `package.json` with `"start:myproxy": "node app.js"` 109 | 8. Run `git add .` 110 | 9. Run `git commit -m "Initial Commit"` 111 | 10. Run `git push origin master` 112 | 113 | # API Reference 114 | 115 | Users can use Access Tokens to manage their domain mappings from a 3rd party server. 116 | [See available endpoints](https://github.com/garageScript/myProxy/wiki/API) 117 | 118 | # Development & Contribution 119 | The following steps will guide you through how to setup your development environment to send pull requests or build your own custom features. 120 | 121 | You need to install node and typescript 122 | 123 | 1. Fork and clone the repository. 124 | 2. Install dependencies: `npm install` or `yarn` 125 | 3. Run the app: `yarn start` or `npm run start` 126 | * You can also run the app under your own defined port by setting a `PORT` environment variable 127 | 128 | ## Adding a new provider 129 | If your company sells domain names and you want your service to be supported on MyProxy, make sure you integrate with [acme.sh][DNS_API_integration] first. 130 | 131 | [Sample integration](https://github.com/garageScript/myProxy/pull/355) for Name.com that you can follow along. 132 | 133 | 1. All implemented providers are listed in [`src/providers/index.ts`](https://github.com/garageScript/myProxy/blob/master/src/providers/index.ts). Add your service to `providerList`, following the your [`acme.sh`](DNS_API_integration) integration's naming convention. 134 | * `path` should be the location of the file you create in the next step. 135 | 2. Create a file that exports the following functions: `getDomains` and `setRecord`. 136 | 3. Depending on your API in the previous step, you may need to create types. Types should be added to [`src/types/general.ts`](https://github.com/garageScript/myProxy/blob/master/src/types/general.ts) 137 | 138 | You are done! Get a beer 🍺 139 | 140 | ## Before sending a Pull Request: 141 | 1. Run `npm run autofix`: make sure there are no errors / warnings 142 | 143 | # License 144 | 145 | MyProxy is [MIT licensed](https://github.com/garageScript/myProxy/blob/master/LICENSE) 146 | 147 | [DNS_API_integration]: https://github.com/Neilpang/acme.sh#8-automatic-dns-api-integration 148 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "/src" 4 | ], 5 | "transform": { 6 | "^.+\\.tsx?$": "ts-jest" 7 | }, 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "myproxy", 3 | "version": "1.0.0", 4 | "description": "Application that proxies requests to other servers", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "jest --coverage", 8 | "test:dev": "jest --coverage --watch", 9 | "start": "tsc -p . --watch & nodemon build/app.js", 10 | "build": "tsc -p .", 11 | "build:live": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/app.ts", 12 | "prettier:base": "./node_modules/.bin/prettier --parser typescript --single-quote --no-semi", 13 | "prettier:check": "npm run prettier:base -- --check \"src/**/*.{ts,tsx}\"", 14 | "prettier:write": "npm run prettier:base -- --write \"src/**/*.{ts,tsx}\"", 15 | "coverage": "nyc npm run test", 16 | "eslint:check": "./node_modules/.bin/eslint src/ --ext .ts", 17 | "eslint:fix": "./node_modules/.bin/eslint src/ --ext .ts --fix", 18 | "lint": "npm run prettier:check && npm run eslint:check", 19 | "autofix": "npm run prettier:write && npm run eslint:fix", 20 | "server": "./scripts/setup.sh; pm2 startOrRestart ./scripts/prod.config.js --env production --update-env", 21 | "server:stop": "pm2 stop myProxy-prod", 22 | "migrate": "node ./dataMigration.js" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/garageScript/myproxy.git" 27 | }, 28 | "author": "", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/garageScript/myproxy/issues" 32 | }, 33 | "homepage": "https://github.com/garageScript/myproxy#readme", 34 | "dependencies": { 35 | "@types/cookie-parser": "^1.4.1", 36 | "@types/express": "^4.17.0", 37 | "@types/node-fetch": "^2.5.0", 38 | "@types/uuid": "^3.4.5", 39 | "cookie-parser": "^1.4.4", 40 | "dockerode": "^3.2.1", 41 | "dotenv": "^8.1.0", 42 | "ejs": "^2.6.2", 43 | "express": "^4.17.1", 44 | "http-proxy": "^1.18.1", 45 | "husky": "^3.0.3", 46 | "namecheap-api": "^1.0.5", 47 | "node-fetch": "^2.6.1", 48 | "pretty-quick": "^1.11.1", 49 | "uuid": "^3.3.3" 50 | }, 51 | "devDependencies": { 52 | "@types/dockerode": "^2.5.34", 53 | "@types/jest": "^24.0.17", 54 | "@types/node": "^12.7.1", 55 | "@typescript-eslint/eslint-plugin": "^2.0.0", 56 | "@typescript-eslint/parser": "^2.0.0", 57 | "eslint": "^6.2.2", 58 | "eslint-config-prettier": "^6.1.0", 59 | "jest": "^24.8.0", 60 | "nyc": "^14.1.1", 61 | "prettier": "^1.18.2", 62 | "source-map-support": "^0.5.13", 63 | "ts-jest": "^24.0.2", 64 | "typescript": "^3.5.3" 65 | }, 66 | "husky": { 67 | "hooks": { 68 | "pre-commit": "npm run lint", 69 | "pre-push": "npm run lint && npm run build && npm test" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /scripts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | RUN apk add git --no-cache 4 | RUN git config --global user.email "myproxy@garagescript.org" 5 | RUN git config --global user.name "myproxy" 6 | 7 | ENTRYPOINT ["docker-entrypoint.sh"] 8 | CMD ["node"] -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /home/dev/prodProxy 4 | git pull origin master 5 | npm run server 6 | -------------------------------------------------------------------------------- /scripts/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 | # Deploy config file 107 | deploy.config.js -------------------------------------------------------------------------------- /scripts/no-interactive-login: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | printf '%s\n' "Hi! You've successfully authenticated, but myProxy does not provide interactive shell access." 3 | exit 128 -------------------------------------------------------------------------------- /scripts/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd .. 3 | GIT_DIR='.git' 4 | DOMAIN=$(basename $(pwd)) 5 | umask 002 && git reset --hard 6 | git clean -f 7 | git checkout master 8 | git branch -D prod 9 | npm install 10 | echo "Starting the container..." 11 | curl --silent --unix-socket /var/run/docker.sock -X POST "http:/localhost/containers/${DOMAIN}/restart" 12 | echo "Your app is running at https://${DOMAIN}" 13 | -------------------------------------------------------------------------------- /scripts/pre-receive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SAFE_GITENV="env -u GIT_QUARANTINE_PATH" 3 | $SAFE_GITENV git checkout -b prod 4 | -------------------------------------------------------------------------------- /scripts/prod.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'myProxy-prod', 5 | script: './build/app.js', 6 | instances: 1, 7 | autorestart: false, 8 | cron_restart: '0 0 1 * *', 9 | watch: false, 10 | max_memory_restart: '1G', 11 | env_production: { 12 | NODE_ENV: process.env.NODE_ENV || 'production', 13 | PORT: process.env.PORT || 3000, 14 | ADMIN: process.env.ADMIN, 15 | WORKPATH: process.env.WORKPATH || '/home/myproxy' 16 | }, 17 | }, 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Helper functions 4 | command_exists() { 5 | command -v "$@" > /dev/null 2>&1 6 | } 7 | 8 | user_exists() { 9 | id "$1" &> /dev/null 10 | } 11 | 12 | # Check if docker is installed and stop the script if it's not 13 | if ! command_exists docker; then 14 | echo "myProxy requires Docker to run" 15 | echo "Docker installation instructions: https://docs.docker.com/engine/install/" 16 | exit 17 | fi 18 | 19 | if ! command_exists node; then 20 | echo "Installing node" 21 | sudo apt-get install curl 22 | curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - 23 | sudo apt-get install -y nodejs 24 | fi 25 | 26 | if ! command_exists pm2; then 27 | echo "Installing pm2" 28 | npm install pm2 -g 29 | fi 30 | 31 | npm install 32 | 33 | if [ ! -d "./acme.sh" ] ; then 34 | git clone https://github.com/Neilpang/acme.sh.git 35 | cd ./acme.sh 36 | ./acme.sh --install 37 | ./acme.sh --upgrade --auto-upgrade 38 | cd ../ 39 | fi 40 | 41 | sudo groupadd -f docker 42 | 43 | if ! user_exists myproxy; then 44 | echo "Creating user: myproxy" 45 | sudo useradd -m -c "myproxy" myproxy -s /bin/bash -p $(echo $ADMIN | openssl passwd -1 -stdin) -d "/home/myproxy" 46 | sudo usermod -aG docker myproxy 47 | mkdir -p /home/myproxy/.ssh 48 | mkdir -p /home/myproxy/.scripts 49 | fi 50 | 51 | if ! user_exists git; then 52 | echo "Creating user: git" 53 | sudo useradd -m -G myproxy -s $(which git-shell) -p $(echo $ADMIN | openssl passwd -1 -stdin) git 54 | sudo usermod -aG docker git 55 | mkdir -p /home/git/.ssh 56 | # Disable SSH MOTD message for git user 57 | touch /home/git/.hushlogin 58 | # Add git-shell message 59 | mkdir -p /home/git/git-shell-commands 60 | cp ./scripts/no-interactive-login /home/git/git-shell-commands/no-interactive-login 61 | chmod +x /home/git/git-shell-commands/no-interactive-login 62 | fi 63 | 64 | if [ -f "~/.ssh/authorized_keys" ]; then 65 | cp ~/.ssh/authorized_keys /home/myproxy/.ssh/authorized_keys 66 | cp ~/.ssh/authorized_keys /home/git/.ssh/authorized_keys 67 | # Prepend ssh options for authorized keys 68 | sed -i '/^ssh-rsa/s/^/no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty /' /home/git/.ssh/authorized_keys 69 | else 70 | touch /home/myproxy/.ssh/authorized_keys 71 | touch /home/git/.ssh/authorized_keys 72 | fi 73 | 74 | cp ./scripts/post-receive /home/myproxy/.scripts/post-receive 75 | cp ./scripts/pre-receive /home/myproxy/.scripts/pre-receive 76 | cp ./scripts/gitignore /home/myproxy/.scripts/.gitignore 77 | 78 | # fix file permissions 79 | chown myproxy:myproxy -R /home/myproxy/ 80 | chown git:git -R /home/git/ 81 | # set the group permissions for /home/myproxy 82 | # 2 = set the setgid bit for the files so group permissions are inherited 83 | # 775 = set read+write permissions for the user and group 84 | chmod 2775 -R /home/myproxy/ 85 | 86 | npm run build 87 | 88 | if [ ! -f "./data.db" ] ; then 89 | touch data.db 90 | fi 91 | 92 | # build docker iamge 93 | if docker ps > /dev/null 2>&1; then 94 | docker pull node:alpine 95 | docker build -t myproxy-node ./scripts 96 | else 97 | echo "WARNING: Couldn't run docker commands" 98 | echo "WARNING: Make sure your user has the right permissions" 99 | echo "WARNING: Go to this link to setup docker to run without root" 100 | echo "WARNING: https://docs.docker.com/engine/install/linux-postinstall/" 101 | fi -------------------------------------------------------------------------------- /src/.env.example: -------------------------------------------------------------------------------- 1 | ##### APP ##### 2 | PORT="APP_PORT" 3 | ADMIN="ADMIN_PASSWORD" -------------------------------------------------------------------------------- /src/admin/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { validUIAccess } from '../helpers/authentication' 3 | 4 | const adminRouter = express.Router() 5 | 6 | adminRouter.use(validUIAccess) 7 | adminRouter.get('/', (req, res) => { 8 | res.render('admin/providers') 9 | }) 10 | adminRouter.get('/accessTokens', (req, res) => { 11 | res.render('admin/accessTokens') 12 | }) 13 | 14 | export { adminRouter } 15 | -------------------------------------------------------------------------------- /src/api/accessToken.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/camelcase: 0 */ 2 | import express, { Response, NextFunction } from 'express' 3 | import uuidv4 from 'uuid/v4' 4 | import { AccessToken, AuthenticatedRequest } from '../types/general' 5 | import { setData, getAccessTokens } from '../lib/data' 6 | 7 | const accessTokensRouter = express.Router() 8 | 9 | accessTokensRouter.use( 10 | (req: AuthenticatedRequest, res: Response, next: NextFunction): void => { 11 | if (!req.user.isAdmin) { 12 | res.status(401).send('Unauthorized') 13 | return 14 | } 15 | return next() 16 | } 17 | ) 18 | 19 | accessTokensRouter.post('/', (req, res) => { 20 | if (req.body.name.length < 2) { 21 | return res.status(400).json({ 22 | message: 'invalid name' 23 | }) 24 | } 25 | const allAccessTokens = getAccessTokens() 26 | const existingToken = allAccessTokens.find(e => e.name === req.body.name) 27 | if (existingToken) { 28 | return res.status(400).json({ 29 | message: 'This token already exits' 30 | }) 31 | } 32 | const tokensObject: AccessToken = { 33 | name: req.body.name, 34 | id: `${uuidv4()}` 35 | } 36 | allAccessTokens.push(tokensObject) 37 | setData('accessTokens', allAccessTokens) 38 | res.json(tokensObject) 39 | }) 40 | 41 | accessTokensRouter.get('/', (req, res) => { 42 | const tokens = getAccessTokens() 43 | res.json(tokens) 44 | }) 45 | 46 | accessTokensRouter.get('/:id', (req, res) => { 47 | const tokens = getAccessTokens() 48 | const found = tokens.find(t => t.id === req.params.id) 49 | res.json(found || {}) 50 | }) 51 | 52 | accessTokensRouter.delete('/:id', (req, res) => { 53 | const tokens = getAccessTokens() 54 | tokens.find((e, i) => { 55 | if (e.id === req.params.id) { 56 | tokens.splice(i, 1) 57 | res.json(e) 58 | return true 59 | } 60 | }) 61 | setData('accessTokens', tokens) 62 | }) 63 | 64 | export default accessTokensRouter 65 | -------------------------------------------------------------------------------- /src/api/admin.ts: -------------------------------------------------------------------------------- 1 | import express, { Response, NextFunction } from 'express' 2 | import uuid4 from 'uuid/v4' 3 | 4 | import { ServiceKey } from '../types/admin' 5 | import { Domain, ServiceResponse, AuthenticatedRequest } from '../types/general' 6 | import { getAvailableDomains, setData, getProviderKeys } from '../lib/data' 7 | import { createSslCerts, setCnameRecords } from '../helpers/domainSetup' 8 | import providers, { providerList } from '../providers' 9 | import environment from '../helpers/environment' 10 | 11 | const { isProduction } = environment 12 | const app = express.Router() 13 | 14 | app.use( 15 | (req: AuthenticatedRequest, res: Response, next: NextFunction): void => { 16 | if (!req.user.isAdmin) { 17 | res.status(401).send('Unauthorized') 18 | return 19 | } 20 | return next() 21 | } 22 | ) 23 | 24 | app.post('/sslCerts', async (req, res) => { 25 | const { service, selectedDomain } = req.body 26 | const serviceResponse: ServiceResponse = { 27 | success: true, 28 | message: 'SSL Certs and domain name records successfully created' 29 | } 30 | 31 | if (isProduction()) { 32 | await createSslCerts(serviceResponse, service, selectedDomain).catch( 33 | error => { 34 | console.error('sslCertResponse', error) 35 | serviceResponse.success = false 36 | serviceResponse.message = 'createSslCerts error' 37 | } 38 | ) 39 | 40 | await setCnameRecords(service, selectedDomain, serviceResponse).catch( 41 | error => { 42 | console.error('cnameResponse', error) 43 | serviceResponse.success = false 44 | serviceResponse.message = 'setCnameRecords error' 45 | } 46 | ) 47 | 48 | if (serviceResponse.success) { 49 | const domains = getAvailableDomains() 50 | const domain: Domain = { 51 | domain: selectedDomain, 52 | expiration: '', 53 | provider: service 54 | } 55 | domains.push(domain) 56 | setData('availableDomains', domains) 57 | } 58 | } 59 | 60 | return res.json(serviceResponse) 61 | }) 62 | 63 | app.patch('/sslCerts/:selectedDomain', async (req, res) => { 64 | const service = req.body.service 65 | const selectedDomain = req.params.selectedDomain 66 | const serviceResponse: ServiceResponse = { 67 | success: true, 68 | message: 69 | 'SSL Certs and domain name records have successfully been reconfigured' 70 | } 71 | try { 72 | if (isProduction()) { 73 | const sslCertResponse = await createSslCerts( 74 | serviceResponse, 75 | service, 76 | selectedDomain 77 | ) 78 | if (!sslCertResponse.success) return res.json(sslCertResponse) 79 | 80 | const cnameResponse = await setCnameRecords( 81 | service, 82 | selectedDomain, 83 | serviceResponse 84 | ) 85 | if (!cnameResponse.success) return res.json(cnameResponse) 86 | } 87 | return res.json(serviceResponse) 88 | } catch (err) { 89 | serviceResponse.success = false 90 | serviceResponse.message = `Error: ${JSON.stringify(err)}` 91 | return res.json(serviceResponse) 92 | } 93 | }) 94 | 95 | app.post('/providerKeys', (req, res) => { 96 | // create service keys 97 | const serviceKeys = getProviderKeys() 98 | const providerKey: ServiceKey = { 99 | id: uuid4(), 100 | ...req.body 101 | } 102 | serviceKeys.push(providerKey) 103 | setData('serviceKeys', serviceKeys) 104 | res.json(providerKey) 105 | }) 106 | 107 | app.get('/providerKeys', (req, res) => { 108 | // get all servicekeys 109 | const serviceKeys = getProviderKeys() 110 | res.json(serviceKeys) 111 | }) 112 | 113 | app.get('/providerKeys/:id', (req, res) => { 114 | // grab one servicekey 115 | const serviceKeys = getProviderKeys() 116 | const selectedKey = serviceKeys.find( 117 | (element: ServiceKey) => element.id === req.params.id 118 | ) 119 | res.json(selectedKey) 120 | }) 121 | 122 | app.delete('/providerKeys/:id', (req, res) => { 123 | // delete a servicekey 124 | const serviceKeys = getProviderKeys() 125 | const updatedKeys = serviceKeys.filter( 126 | (element: ServiceKey) => element.id !== req.params.id 127 | ) 128 | setData('serviceKeys', updatedKeys) 129 | const updatedKey = serviceKeys.find( 130 | (element: ServiceKey) => element.id === req.params.id 131 | ) 132 | res.json(updatedKey) 133 | }) 134 | 135 | app.put('/providerKeys/:id', (req, res) => { 136 | // replace servicekey info 137 | const serviceKeys = getProviderKeys() 138 | const id: string = req.params.id 139 | const updatedKeys = serviceKeys.map((element: ServiceKey) => { 140 | if (element.id === id) element = { id, ...req.body } 141 | return element 142 | }) 143 | setData('serviceKeys', updatedKeys) 144 | const updatedKey = serviceKeys.find( 145 | (element: ServiceKey) => element.id === id 146 | ) 147 | res.json(updatedKey) 148 | }) 149 | 150 | app.patch('/providerKeys/:id', (req, res) => { 151 | // edit servicekey info 152 | const serviceKeys = getProviderKeys() 153 | const id: string = req.params.id 154 | const updatedKeys = serviceKeys.map((element: ServiceKey) => { 155 | if (element.id === id) { 156 | if (req.body.key) element.key = req.body.key 157 | if (req.body.service) element.service = req.body.service 158 | if (req.body.value) element.value = req.body.value 159 | } 160 | return element 161 | }) 162 | setData('serviceKeys', updatedKeys) 163 | const updatedKey = serviceKeys.find( 164 | (element: ServiceKey) => element.id === id 165 | ) 166 | res.json(updatedKey) 167 | }) 168 | 169 | app.get('/providers', async (_, res) => { 170 | const requests = providerList.map(provider => 171 | providers[provider.dns].getDomains() 172 | ) 173 | const data = await Promise.all(requests) 174 | const filteredData = data.map(domainElement => { 175 | if (domainElement.domains.code) domainElement.domains = [] 176 | return domainElement 177 | }) 178 | 179 | return res.json(filteredData) // Data send to view providers ? 180 | }) 181 | 182 | export default { app } 183 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Response, NextFunction } from 'express' 2 | import adminRouter from './admin' 3 | import logsRouter from './logs' 4 | import mappingRouter from './mapping' 5 | import sshKeyRouter from './sshKeys' 6 | import accessTokensRouter from './accessToken' 7 | import { getAvailableDomains } from '../lib/data' 8 | import { AuthenticatedRequest } from '../types/general' 9 | 10 | const apiRouter = express.Router() 11 | 12 | apiRouter.use( 13 | (req: AuthenticatedRequest, res: Response, next: NextFunction): void => { 14 | if (!req.user || (!req.user.isPseudoAdmin && !req.user.isAdmin)) { 15 | res.status(401).send('Unauthorized') 16 | return 17 | } 18 | return next() 19 | } 20 | ) 21 | 22 | apiRouter.use('/admin', adminRouter.app) 23 | apiRouter.use('/logs', logsRouter) 24 | apiRouter.use('/mappings', mappingRouter) 25 | apiRouter.use('/sshKeys', sshKeyRouter) 26 | apiRouter.use('/accessTokens', accessTokensRouter) 27 | 28 | apiRouter.get('/availableDomains', (req, res) => { 29 | const domains = getAvailableDomains() 30 | res.json(domains) 31 | }) 32 | 33 | export { apiRouter } 34 | -------------------------------------------------------------------------------- /src/api/logs.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import environment from '../helpers/environment' 3 | import { getMappingByDomain } from '../lib/data' 4 | import { getContainerLogs } from '../helpers/docker' 5 | 6 | const logsRouter = express.Router() 7 | const { isProduction } = environment 8 | 9 | logsRouter.get('/:stream/:domain', async (req, res) => { 10 | const { stream, domain } = req.params 11 | const { follow, tail } = req.query 12 | 13 | // Stream validation 14 | if (stream !== 'stdout' && stream !== 'stderr') { 15 | return res 16 | .status(400) 17 | .json({ message: 'stream param must be stdout or stderr' }) 18 | } 19 | 20 | if (isProduction()) { 21 | // Only search for domain when running in production. The test does not 22 | // require a valid domain since it only verifies the endpoint 23 | const { fullDomain } = getMappingByDomain(domain) 24 | // Pipes the log to res 25 | res.setHeader('content-type', 'text/plain') 26 | const logStream = await getContainerLogs(fullDomain, { 27 | follow: Boolean(follow), 28 | tail: Number(tail), 29 | [stream]: true 30 | }) 31 | logStream.pipe(res) 32 | } else { 33 | res.send('OK') 34 | } 35 | }) 36 | 37 | export default logsRouter 38 | -------------------------------------------------------------------------------- /src/api/mapping.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/camelcase: 0 */ 2 | import express from 'express' 3 | import uuid4 from 'uuid/v4' 4 | import util from 'util' 5 | import cp from 'child_process' 6 | import { 7 | setData, 8 | getMappings, 9 | getMappingByDomain, 10 | getMappingById, 11 | deleteDomain, 12 | updateDomain 13 | } from '../lib/data' 14 | import { Mapping } from '../types/general' 15 | import { getGitUserId, getGitGroupId } from '../helpers/getGitUser' 16 | import environment from '../helpers/environment' 17 | import { 18 | getContainersList, 19 | createContainer, 20 | startContainer, 21 | stopContainer, 22 | removeContainer, 23 | inspectContainer 24 | } from '../helpers/docker' 25 | import { DockerError } from '../types/docker' 26 | const mappingRouter = express.Router() 27 | const exec = util.promisify(cp.exec) 28 | const getNextPort = (map, start = 3002): number => { 29 | if (!map[start]) return start 30 | if (map[start]) start += 1 31 | return getNextPort(map, start) 32 | } 33 | 34 | const { WORKPATH, isProduction } = environment 35 | 36 | mappingRouter.post('/', async (req, res) => { 37 | const domainKeys = getMappings() 38 | if (parseInt(req.body.port) < 3001) { 39 | return res.status(400).json({ message: 'Port cannot be smaller than 3001' }) 40 | } 41 | const fullDomain = req.body.subDomain 42 | ? `${req.body.subDomain}.${req.body.domain}`.toLowerCase() 43 | : `${req.body.domain}`.toLowerCase() 44 | const existingSubDomain = getMappingByDomain(fullDomain) 45 | if (existingSubDomain) 46 | return res.status(400).json({ 47 | message: 'Subdomain already exists' 48 | }) 49 | const map = domainKeys.reduce((acc, e) => { 50 | acc[e.port] = true 51 | return acc 52 | }, {}) 53 | const portCounter = getNextPort(map) 54 | const port = parseInt(req.body.port || portCounter, 10) 55 | const scriptPath = '.scripts' 56 | 57 | // Create a new container and get the id 58 | const id = isProduction() ? await createContainer(fullDomain, port) : uuid4() 59 | 60 | const respond = (): void => { 61 | const mappingObject: Mapping = { 62 | domain: req.body.domain.toLowerCase(), 63 | subDomain: req.body.subDomain.toLowerCase(), 64 | port: port.toString(), 65 | ip: req.body.ip || '127.0.0.1', 66 | id, 67 | gitLink: `git@${req.body.domain}:${WORKPATH}/${fullDomain}`, 68 | fullDomain 69 | } 70 | domainKeys.push(mappingObject) 71 | setData('mappings', domainKeys) 72 | res.json(mappingObject) 73 | } 74 | 75 | if (!isProduction()) { 76 | return respond() 77 | } 78 | 79 | // get user and group id to execute the commands with the correct permissions 80 | const gitUserId = await getGitUserId() 81 | const gitGroupId = await getGitGroupId() 82 | 83 | exec( 84 | ` 85 | umask 002 86 | cd ${WORKPATH} 87 | mkdir ${fullDomain} 88 | git init ${fullDomain} 89 | cp ${scriptPath}/post-receive ${fullDomain}/.git/hooks/ 90 | cp ${scriptPath}/pre-receive ${fullDomain}/.git/hooks/ 91 | cp ${scriptPath}/.gitignore ${fullDomain}/.gitignore 92 | cd ${fullDomain} 93 | git config user.email "root@ipaddress" 94 | git config user.name "user" 95 | git add . 96 | git commit -m "Initial Commit" 97 | `, 98 | { uid: gitUserId, gid: gitGroupId } 99 | ) 100 | .then(() => { 101 | respond() 102 | }) 103 | .catch(error => console.error(`mappingRouter.post exec: ${error}`)) 104 | }) 105 | 106 | mappingRouter.get('/', async (req, res) => { 107 | const domains = getMappings() 108 | 109 | if (!isProduction()) 110 | return res.json(domains.map(el => ({ ...el, status: 'not started' }))) 111 | 112 | const data = await getContainersList() 113 | const statusData = data.reduce( 114 | (statusObj, el) => ({ 115 | ...statusObj, 116 | [el.Names[0].replace('/', '')]: el.State 117 | }), 118 | {} 119 | ) 120 | const fullDomainStatusMapping = domains.map(el => { 121 | if (statusData[el.fullDomain]) { 122 | return { ...el, status: statusData[el.fullDomain] } 123 | } else { 124 | return { ...el, status: 'not started' } 125 | } 126 | }) 127 | 128 | res.json(fullDomainStatusMapping) 129 | }) 130 | 131 | mappingRouter.delete('/:id', async (req, res) => { 132 | const deletedDomain = getMappingById(req.params.id) 133 | deleteDomain(deletedDomain.fullDomain) 134 | if (!isProduction()) return res.json(deletedDomain) 135 | 136 | // stop and remove container 137 | removeContainer(deletedDomain.fullDomain) 138 | .then(() => { 139 | // delete the domain folder 140 | exec(` 141 | cd ${WORKPATH} 142 | rm -rf ${deletedDomain.fullDomain} 143 | `).then(() => { 144 | res.json(deletedDomain) 145 | }) 146 | }) 147 | .catch(err => res.status(err.statusCode).json(err.json)) 148 | }) 149 | 150 | mappingRouter.get('/:id', (req, res) => { 151 | const foundDomain = getMappingById(req.params.id) 152 | res.json(foundDomain || {}) 153 | }) 154 | 155 | mappingRouter.get('/:id/start', (req, res) => { 156 | const { id } = req.params 157 | startContainer(id) 158 | .then(() => res.sendStatus(204)) 159 | .catch((err: DockerError) => res.status(err.statusCode).json(err.json)) 160 | }) 161 | 162 | mappingRouter.get('/:id/stop', (req, res) => { 163 | const { id } = req.params 164 | stopContainer(id) 165 | .then(() => res.sendStatus(204)) 166 | .catch((err: DockerError) => res.status(err.statusCode).json(err.json)) 167 | }) 168 | 169 | mappingRouter.get('/:fullDomain/environment', (req, res) => { 170 | const { fullDomain } = req.params 171 | inspectContainer(fullDomain) 172 | .then(info => { 173 | const envVars = info.Config.Env 174 | const defaultEnvs = new Set([ 175 | 'NODE_ENV', 176 | 'PORT', 177 | 'PATH', 178 | 'NODE_VERSION', 179 | 'YARN_VERSION' 180 | ]) 181 | const nonDefaultEnvs = envVars.reduce((acc, envVar) => { 182 | const [name, value] = envVar.split('=') 183 | if (!defaultEnvs.has(name)) { 184 | acc[name] = value 185 | } 186 | return acc 187 | }, {}) 188 | res.json({ variables: nonDefaultEnvs }) 189 | }) 190 | .catch((err: DockerError) => res.status(err.statusCode).json(err.json)) 191 | }) 192 | 193 | mappingRouter.put('/:fullDomain/environment', (req, res) => { 194 | const { fullDomain } = req.params 195 | const { variables } = req.body 196 | if (Object.entries(variables).some(([name, value]) => !name || !value)) { 197 | return res.status(400).json({ message: 'Fields must not be blank.' }) 198 | } 199 | // convert to the format Docker expects: NAME=VALUE 200 | const envVars = Object.entries(variables).map( 201 | ([name, value]) => `${name}=${value}` 202 | ) 203 | const mapping = getMappingByDomain(fullDomain) 204 | removeContainer(fullDomain) 205 | .then(() => createContainer(fullDomain, Number(mapping.port), envVars)) 206 | .then(id => { 207 | // since docker generates a new ID for the container 208 | // the domain record needs to be updated 209 | mapping.id = id 210 | updateDomain(fullDomain, mapping) 211 | return startContainer(id) 212 | }) 213 | .then(() => res.sendStatus(201)) 214 | .catch((err: DockerError) => res.status(err.statusCode).json(err.json)) 215 | }) 216 | 217 | export default mappingRouter 218 | -------------------------------------------------------------------------------- /src/api/sshKeys.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { 4 | authorizedKeys, 5 | addAuthorizedKey, 6 | removeAuthorizedKey 7 | } from '../helpers/authorizedKeys' 8 | 9 | const sshKeyRouter = express.Router() 10 | 11 | sshKeyRouter.get('/', (req, res) => { 12 | res.json(authorizedKeys.map(v => v.split(' ')[2])) 13 | }) 14 | 15 | sshKeyRouter.post('/', (req, res) => { 16 | const { key } = req.body 17 | addAuthorizedKey(key) 18 | res.json(authorizedKeys.map(v => v.split(' ')[2])) 19 | }) 20 | 21 | sshKeyRouter.delete('/', (req, res) => { 22 | const { id } = req.body 23 | removeAuthorizedKey(id) 24 | res.json(authorizedKeys.map(v => v.split(' ')[2])) 25 | }) 26 | 27 | export default sshKeyRouter 28 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { startAppServer, startProxyServer } from './server/server' 2 | import environment from './helpers/environment' 3 | 4 | const { PORT, ADMIN_PASS, isProduction } = environment 5 | 6 | startAppServer(PORT, ADMIN_PASS).catch(error => 7 | console.error(`startAppServer: ${error}`) 8 | ) 9 | 10 | /** 11 | * Proxy Server will create SSL Certificates on the server 12 | * for your domains in production. 13 | * Development mode do not have to run the proxy server. 14 | * */ 15 | if (isProduction()) startProxyServer() 16 | -------------------------------------------------------------------------------- /src/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { hashPass } from '../helpers/crypto' 2 | import { getTokenById } from '../lib/data' 3 | let pass = '' 4 | 5 | const isCorrectCredentials = (password: string, correct: string): boolean => { 6 | const adminPassword = hashPass(password) 7 | const userPassword = hashPass(correct) 8 | return userPassword === adminPassword 9 | } 10 | 11 | const isValidAccessToken = (token: string): boolean => !!getTokenById(token) 12 | 13 | const setPass = (password: string): void => { 14 | pass = password 15 | } 16 | 17 | const setupAuth = (req, res, next): void => { 18 | const { adminPass } = req.cookies 19 | const { authorization = '' } = req.headers 20 | if ( 21 | adminPass === hashPass(pass) || 22 | isCorrectCredentials(authorization, pass) 23 | ) { 24 | req.user = { isAdmin: true, isPseudoAdmin: true } 25 | return next() 26 | } 27 | if (isValidAccessToken(authorization)) { 28 | { 29 | req.user = { isPseudoAdmin: true } 30 | } 31 | return next() 32 | } 33 | return next() 34 | } 35 | 36 | export { setupAuth, setPass, isCorrectCredentials } 37 | -------------------------------------------------------------------------------- /src/helpers/SNICallback.ts: -------------------------------------------------------------------------------- 1 | // escape characters required or readFileSync will not find file 2 | /* eslint-disable */ 3 | import fs from 'fs' 4 | import tls from 'tls' 5 | import util from 'util' 6 | 7 | import environment from '../helpers/environment' 8 | 9 | const { HOME } = environment 10 | const acmePath = `${HOME}/.acme.sh` 11 | const readFileAsync = util.promisify(fs.readFile) 12 | const keyCache: { [key: string]: string } = {} 13 | const certCache: { [key: string]: string } = {} 14 | 15 | const readFile = async (path: string) => { 16 | const data = await readFileAsync(path, 'utf8') 17 | return data || null 18 | } 19 | 20 | const SNICallback = async (host, cb) => { 21 | // TLD -> Top-Level Domain | SLD -> Second-Level Domain 22 | const [TLD, SLD, ...subDomains] = host.split('.').reverse() 23 | const domain = `${SLD}.${TLD}` 24 | const wildstar = subDomains.length > 0 ? '*.' : '' 25 | const key = `${acmePath}${wildstar}${domain}` 26 | const keyPath = `${acmePath}/${wildstar}${domain}/${wildstar}${domain}\.key` 27 | const certPath = `${acmePath}/${wildstar}${domain}/fullchain.cer` 28 | if (!keyCache[key]) keyCache[key] = await readFile(keyPath) 29 | if (!certCache[key]) certCache[key] = await readFile(certPath) 30 | const secureContext = tls.createSecureContext({ 31 | key: keyCache[key], 32 | cert: certCache[key] 33 | }) 34 | if (cb) return cb(null, secureContext) 35 | return secureContext 36 | } 37 | 38 | export { SNICallback } 39 | -------------------------------------------------------------------------------- /src/helpers/authentication.ts: -------------------------------------------------------------------------------- 1 | const validUIAccess = (req, res, next): void => { 2 | if (!req.user) { 3 | return res.redirect('/login') 4 | } 5 | return next() 6 | } 7 | 8 | export { validUIAccess } 9 | -------------------------------------------------------------------------------- /src/helpers/authorizedKeys.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import environment from '../helpers/environment' 3 | 4 | const { isProduction } = environment 5 | const sshOptions = 6 | 'no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty' 7 | let authorizedKeys: Array = [] 8 | 9 | const updateSSHKey = (): void => { 10 | if (isProduction()) { 11 | const file = fs.createWriteStream('/home/git/.ssh/authorized_keys') 12 | file.on('error', err => { 13 | console.log(err) 14 | }) 15 | authorizedKeys.forEach(v => { 16 | file.write(`${sshOptions} ${v}\n`) 17 | }) 18 | file.end() 19 | } 20 | } 21 | 22 | const addAuthorizedKey = (key: string): void => { 23 | authorizedKeys.push(key) 24 | updateSSHKey() 25 | } 26 | 27 | const removeAuthorizedKey = (id: number): void => { 28 | authorizedKeys.splice(id, 1) 29 | updateSSHKey() 30 | } 31 | 32 | const setAuthorizedKeys = (keys: Array): void => { 33 | authorizedKeys = keys 34 | } 35 | 36 | export { 37 | addAuthorizedKey, 38 | authorizedKeys, 39 | removeAuthorizedKey, 40 | setAuthorizedKeys 41 | } 42 | -------------------------------------------------------------------------------- /src/helpers/cache.ts: -------------------------------------------------------------------------------- 1 | import { Mapping, MappingById } from '../types/general' 2 | 3 | const createDomainCache = (records: Mapping[]): MappingById => { 4 | return records.reduce( 5 | (obj, item) => ({ 6 | ...obj, 7 | [item.fullDomain]: item 8 | }), 9 | {} 10 | ) 11 | } 12 | 13 | const mapById = ( 14 | records: T[] 15 | ): { [id: string]: T } => { 16 | return records.reduce((obj, item) => ({ ...obj, [item.id]: item }), {}) 17 | } 18 | 19 | export { createDomainCache, mapById } 20 | -------------------------------------------------------------------------------- /src/helpers/crypto.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | const hashPass = (string: string): string => { 4 | return crypto 5 | .createHash('sha256') 6 | .update(string) 7 | .digest('hex') 8 | } 9 | 10 | export { hashPass } 11 | -------------------------------------------------------------------------------- /src/helpers/docker.ts: -------------------------------------------------------------------------------- 1 | import Docker from 'dockerode' 2 | import path from 'path' 3 | import { Readable, PassThrough } from 'stream' 4 | import environment from '../helpers/environment' 5 | 6 | const docker = new Docker({ socketPath: '/var/run/docker.sock' }) 7 | 8 | const getContainersList = async (): Promise => { 9 | const containers = await docker.listContainers({ all: true }) 10 | return containers 11 | } 12 | 13 | const getContainerLogs = async ( 14 | id: string, 15 | options: Docker.ContainerLogsOptions 16 | ): Promise => { 17 | const container = docker.getContainer(id) 18 | let logs = await container.logs(options) 19 | if (!options.follow) { 20 | // if follow = false, the logs are returned as a buffer 21 | // so we need to convert into a stream 22 | logs = Readable.from(logs) 23 | } 24 | const demuxedStream = new PassThrough() 25 | container.modem.demuxStream(logs, demuxedStream, demuxedStream) 26 | logs.on('end', () => demuxedStream.end()) 27 | return demuxedStream 28 | } 29 | 30 | const createContainer = async ( 31 | fullDomain: string, 32 | port: number, 33 | environmentVariables: string[] = [] 34 | ): Promise => { 35 | const workPath = path.resolve(environment.WORKPATH, fullDomain) 36 | return docker 37 | .createContainer({ 38 | Image: 'myproxy-node:latest', 39 | name: fullDomain, 40 | User: 'node', 41 | ExposedPorts: { 42 | '3000/tcp': {} 43 | }, 44 | Tty: false, 45 | WorkingDir: '/home/node/app', 46 | Env: ['NODE_ENV=production', 'PORT=3000', ...environmentVariables], 47 | HostConfig: { 48 | Binds: [`${workPath}:/home/node/app`], 49 | RestartPolicy: { 50 | Name: 'on-failure', 51 | MaximumRetryCount: 3 52 | }, 53 | LogConfig: { 54 | Type: 'json-file', 55 | Config: { 56 | 'max-size': '10m', 57 | 'max-file': '1' 58 | } 59 | }, 60 | PortBindings: { 61 | '3000/tcp': [ 62 | { 63 | HostIp: '', 64 | HostPort: port.toString() 65 | } 66 | ] 67 | } 68 | }, 69 | Cmd: ['npm', 'run', 'start'] 70 | }) 71 | .then(container => container.id) 72 | .catch(err => err) 73 | } 74 | 75 | const startContainer = async (id: string): Promise => { 76 | const container = docker.getContainer(id) 77 | return container.restart() 78 | } 79 | 80 | const stopContainer = async (id: string): Promise => { 81 | const container = docker.getContainer(id) 82 | return container.stop() 83 | } 84 | 85 | const removeContainer = async (id: string): Promise => { 86 | const container = docker.getContainer(id) 87 | return container.remove({ v: true, force: true }) 88 | } 89 | 90 | const inspectContainer = (id: string): Promise => { 91 | const container = docker.getContainer(id) 92 | return container.inspect() 93 | } 94 | 95 | export { 96 | getContainersList, 97 | getContainerLogs, 98 | createContainer, 99 | startContainer, 100 | stopContainer, 101 | removeContainer, 102 | inspectContainer 103 | } 104 | -------------------------------------------------------------------------------- /src/helpers/domainSetup.ts: -------------------------------------------------------------------------------- 1 | import cp from 'child_process' 2 | import util from 'util' 3 | 4 | import { getProviderKeys } from '../lib/data' 5 | import { ServiceResponse, ProviderService } from '../types/general' 6 | import provider, { providerList } from '../providers' 7 | 8 | const exec = util.promisify(cp.exec) 9 | const createSslCerts = async ( 10 | serviceResponse, 11 | service, 12 | selectedDomain 13 | ): Promise => { 14 | const serviceKeys = getProviderKeys().filter(d => d.service === service) 15 | const { keys } = providerList.find(provider => provider.dns === service) 16 | let envVars = keys.reduce((acc: string, key: string) => { 17 | const { value } = serviceKeys.find(d => d.key === key) || { value: '' } 18 | return acc + `${key}=${value} ` 19 | }, '') 20 | 21 | const { stdout: ipaddress } = await exec('curl ifconfig.me') 22 | if (service === 'dns_namecheap') { 23 | envVars += `NAMECHEAP_SOURCEIP=${ipaddress}` 24 | } 25 | 26 | const acme = `./acme.sh/acme.sh --issue --dns ${service} --server letsencrypt` 27 | const cert1 = `${acme} -d ${selectedDomain} --force` 28 | const cert2 = `${acme} -d *.${selectedDomain} --force` 29 | const cert1Response = await exec(`${envVars} ${cert1}`) 30 | const cert2Response = await exec(`${envVars} ${cert2}`) 31 | if (cert1Response.stderr || cert2Response.stderr) { 32 | serviceResponse.success = false 33 | serviceResponse.message = `Could not create SSL Certs. Error: ${ 34 | cert2Response.stderr 35 | ? JSON.stringify(cert2Response.stderr) 36 | : JSON.stringify(cert1Response.stderr) 37 | }` 38 | } 39 | return serviceResponse 40 | } 41 | 42 | const setCnameRecords = async ( 43 | service, 44 | selectedDomain, 45 | serviceResponse 46 | ): Promise => { 47 | const { stdout: ipaddress } = await exec('curl ifconfig.me') 48 | const providerService = provider[service] as ProviderService 49 | if (!providerService) { 50 | serviceResponse.success = false 51 | serviceResponse.message = 'Provider not found' 52 | return serviceResponse 53 | } 54 | const setRecords: ServiceResponse = await providerService.setRecord( 55 | selectedDomain, 56 | ipaddress 57 | ) 58 | return setRecords 59 | } 60 | 61 | export { createSslCerts, setCnameRecords } 62 | -------------------------------------------------------------------------------- /src/helpers/environment.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | ENV: process.env.NODE_ENV || process.env.ENV || 'development', 3 | PORT: process.env.PORT || process.env.ENV || 3000, 4 | ADMIN_PASS: process.env.ADMIN || process.env.ENV || null, 5 | HOME: process.env.HOME || process.env.ENV || null, 6 | WORKPATH: process.env.WORKPATH || '/home/myproxy', 7 | isProduction: (): boolean => 8 | (process.env.NODE_ENV || process.env.ENV) === 'production' 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/getGitUser.ts: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | import cp from 'child_process' 3 | const exec = util.promisify(cp.exec) 4 | 5 | const getGitUserId = async (): Promise => { 6 | const gitId = await exec('id -u myproxy') 7 | return parseInt(gitId.stdout, 10) 8 | } 9 | 10 | const getGitGroupId = async (): Promise => { 11 | const gitId = await exec('getent group myproxy | cut -d: -f3') 12 | return parseInt(gitId.stdout, 10) 13 | } 14 | 15 | export { getGitUserId, getGitGroupId } 16 | -------------------------------------------------------------------------------- /src/helpers/httpRequest.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | 3 | const sendRequest = async (url: string, options: object): Promise => { 4 | const response = await fetch(url, options) 5 | const body = await response.text() 6 | return JSON.parse(body) 7 | } 8 | 9 | export { sendRequest } 10 | -------------------------------------------------------------------------------- /src/lib/data.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { createDomainCache, mapById } from '../helpers/cache' 3 | import { DB, ServiceKey } from '../types/admin' 4 | import { 5 | Mapping, 6 | MappingById, 7 | Domain, 8 | AccessToken, 9 | AccessTokenById 10 | } from '../types/general' 11 | 12 | const data: DB = { 13 | serviceKeys: [], 14 | mappings: [], 15 | availableDomains: [], 16 | accessTokens: [] 17 | } 18 | 19 | let domainToMapping: MappingById = {} 20 | let idToMapping: MappingById = {} 21 | let idToAccessToken: AccessTokenById = {} 22 | 23 | const updateCache = (table: keyof DB): void => { 24 | if (table === 'mappings') { 25 | domainToMapping = createDomainCache(data.mappings) 26 | idToMapping = mapById(data.mappings) 27 | } 28 | if (table === 'accessTokens') { 29 | idToAccessToken = mapById(data.accessTokens) 30 | } 31 | } 32 | 33 | try { 34 | const file = fs.readFileSync('./data.db') 35 | const fileData: DB = JSON.parse(file.toString() || '{}') 36 | data.serviceKeys = fileData.serviceKeys || [] 37 | data.mappings = fileData.mappings || [] 38 | data.availableDomains = fileData.availableDomains || [] 39 | data.accessTokens = fileData.accessTokens || [] 40 | 41 | domainToMapping = createDomainCache(data.mappings) 42 | idToMapping = mapById(data.mappings) 43 | idToAccessToken = mapById(data.accessTokens) 44 | } catch (err) { 45 | console.log( 46 | 'File does not exist, but do not worry. File will be created on first save', 47 | err 48 | ) 49 | } 50 | 51 | const getData = (table: T): DB[T] | undefined => { 52 | return data[table] 53 | } 54 | 55 | const setData = (table: T, records: DB[T]): void => { 56 | data[table] = records 57 | updateCache(table) 58 | 59 | const fileData = `${JSON.stringify(data, null, 2)}` 60 | 61 | fs.writeFile('./data.db', fileData, err => { 62 | if (err) { 63 | return console.log('writing to DB failed', err) 64 | } 65 | }) 66 | } 67 | 68 | const getProviderKeys = (): ServiceKey[] => { 69 | const initialData = getData('serviceKeys') 70 | return initialData || [] 71 | } 72 | 73 | const getMappings = (): Mapping[] => { 74 | const initialData = getData('mappings') 75 | return initialData || [] 76 | } 77 | 78 | const getAvailableDomains = (): Domain[] => { 79 | const initialData = getData('availableDomains') 80 | return initialData || [] 81 | } 82 | 83 | const getAccessTokens = (): AccessToken[] => { 84 | const initialData = getData('accessTokens') 85 | return initialData || [] 86 | } 87 | 88 | const getMappingByDomain = (domain: string): Mapping => { 89 | return domainToMapping[domain] 90 | } 91 | 92 | const getMappingById = (id: string): Mapping | undefined => { 93 | return idToMapping[id] 94 | } 95 | 96 | const getTokenById = (id: string): AccessToken | undefined => { 97 | return idToAccessToken[id] 98 | } 99 | 100 | const deleteDomain = (domain: string): void => { 101 | delete domainToMapping[domain] 102 | setData('mappings', Object.values(domainToMapping)) 103 | } 104 | 105 | const updateDomain = (domain: string, updatedMapping: Mapping): void => { 106 | domainToMapping[domain] = updatedMapping 107 | setData('mappings', Object.values(domainToMapping)) 108 | } 109 | 110 | export { 111 | getData, 112 | setData, 113 | getProviderKeys, 114 | getMappings, 115 | getAvailableDomains, 116 | getAccessTokens, 117 | getMappingByDomain, 118 | getMappingById, 119 | getTokenById, 120 | deleteDomain, 121 | updateDomain 122 | } 123 | -------------------------------------------------------------------------------- /src/providers/goDaddy.ts: -------------------------------------------------------------------------------- 1 | import { sendRequest } from '../helpers/httpRequest' 2 | import { Provider, ServiceResponse } from '../types/general' 3 | import fetch from 'node-fetch' 4 | import { providerList } from './' 5 | import { getKeys, findKey } from './helpers' 6 | 7 | const provider = providerList.find(provider => provider.name === 'GoDaddy') 8 | 9 | const { name, dns, keys, service } = provider 10 | 11 | export const getDomains = async (): Promise => { 12 | const providerKeys = getKeys(provider) 13 | 14 | let domains = [] 15 | const url = `${service}/v1/domains?statuses=ACTIVE` 16 | const options = { 17 | headers: { 18 | Authorization: `sso-key ${findKey(provider, keys[0])}:${findKey( 19 | provider, 20 | keys[1] 21 | )}`, 22 | 'Content-Type': 'application/json' 23 | } 24 | } 25 | domains = await sendRequest>(url, options) 26 | 27 | return { 28 | id: dns, 29 | service, 30 | name, 31 | keys: providerKeys, 32 | domains 33 | } 34 | } 35 | 36 | export const setRecord = async ( 37 | domain: string, 38 | ipaddress: string 39 | ): Promise => { 40 | const url = `${service}/v1/domains/${domain}/records/A/@` 41 | const data = [ 42 | { 43 | data: ipaddress, 44 | ttl: 600 45 | } 46 | ] 47 | const options = { 48 | method: 'PUT', 49 | headers: { 50 | Authorization: `sso-key ${findKey(provider, keys[0])}:${findKey( 51 | provider, 52 | keys[1] 53 | )}`, 54 | 'Content-Type': 'application/json' 55 | }, 56 | body: JSON.stringify(data) 57 | } 58 | const cnameUrl = `${service}/v1/domains/${domain}/records/CNAME/*` 59 | const cnameData = [ 60 | { 61 | data: '@', 62 | ttl: 600 63 | } 64 | ] 65 | const cnameOptions = { 66 | method: 'PUT', 67 | headers: { 68 | Authorization: `sso-key ${findKey(provider, keys[0])}:${findKey( 69 | provider, 70 | keys[1] 71 | )}`, 72 | 'Content-Type': 'application/json' 73 | }, 74 | body: JSON.stringify(cnameData) 75 | } 76 | const response: ServiceResponse = { 77 | success: true, 78 | message: 'Successfully set CNAME records for wildcard domain' 79 | } 80 | 81 | // eslint-disable-next-line 82 | await Promise.all([fetch(url, options), fetch(cnameUrl, cnameOptions)]).catch( 83 | error => { 84 | console.error('Error setting CNAME records', error) 85 | response.success = false 86 | response.message = 'Error setting CNAME records' 87 | } 88 | ) 89 | 90 | return response 91 | } 92 | -------------------------------------------------------------------------------- /src/providers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { ProviderInfo } from '../types/general' 2 | import { getProviderKeys } from '../lib/data' 3 | import { ServiceKey } from '../types/admin' 4 | 5 | export const getKeys = (provider: ProviderInfo): ServiceKey[] => { 6 | const { dns, keys } = provider 7 | const keysDefault: { key: string }[] = [{ key: keys[0] }, { key: keys[1] }] 8 | const providerKeys = keysDefault.map(key => { 9 | const serviceKeys = getProviderKeys() 10 | return serviceKeys.find(k => k.service === dns && k.key === key.key) || key 11 | }) 12 | return providerKeys as ServiceKey[] 13 | } 14 | 15 | export const findKey = (provider: ProviderInfo, key: string): string => { 16 | return (getKeys(provider).find(k => k.key === key) || { value: '' }).value 17 | } 18 | -------------------------------------------------------------------------------- /src/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceConfig, ProviderInfo } from '../types/general' 2 | 3 | /** 4 | * Currently acme.sh supports most of the dns providers 5 | * https://github.com/Neilpang/acme.sh/wiki/dnsapi 6 | * Updated on December 20 2019 7 | * 8 | * To add a new provider please 9 | * follow acme.sh naming convention 10 | * { 11 | * name -> Provider name 12 | * dns -> dns_provider 13 | * key -> [PROVIDER_Key, ...etc] 14 | * service: Provider API 15 | * path: relative path of the provider file in "./src" 16 | * } 17 | */ 18 | export const providerList = [ 19 | { 20 | name: 'GoDaddy', 21 | dns: 'dns_gd', 22 | keys: ['GD_Key', 'GD_Secret'], 23 | service: 'https://api.godaddy.com', 24 | path: './goDaddy' 25 | }, 26 | { 27 | name: 'Name.com', 28 | dns: 'dns_namecom', 29 | keys: ['Namecom_Username', 'Namecom_Token'], 30 | service: 'https://api.name.com', 31 | path: './namecom' 32 | }, 33 | { 34 | name: 'Namecheap.com', 35 | dns: 'dns_namecheap', 36 | keys: ['NAMECHEAP_USERNAME', 'NAMECHEAP_API_KEY'], 37 | path: './namecheap' 38 | } 39 | ] as ProviderInfo[] 40 | 41 | /** 42 | * This code below add each provider file 43 | * to the default export 44 | * eg: providers = { 45 | * dns_gd: require(./goDaddy) 46 | * ...other providers 47 | * } 48 | */ 49 | const providers = {} as ServiceConfig 50 | providerList.forEach( 51 | provider => (providers[provider.dns] = require(provider.path)) 52 | ) 53 | 54 | export default providers 55 | -------------------------------------------------------------------------------- /src/providers/namecheap.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import { Provider, ServiceResponse } from '../types/general' 3 | import namecheapApi from 'namecheap-api' 4 | import { providerList } from '.' 5 | import { getKeys, findKey } from './helpers' 6 | import _ from 'lodash' 7 | 8 | const provider = providerList.find( 9 | provider => provider.name === 'Namecheap.com' 10 | ) 11 | 12 | const { name, dns, keys, service } = provider 13 | 14 | export const getDomains = async (): Promise => { 15 | const providerKeys = getKeys(provider) 16 | const domains = [] 17 | 18 | if (findKey(provider, keys[0]) && findKey(provider, keys[1])) { 19 | const ifconfigRes = await fetch('https://ifconfig.me/ip') 20 | 21 | namecheapApi.config.set('ApiUser', findKey(provider, keys[0])) 22 | namecheapApi.config.set('ApiKey', findKey(provider, keys[1])) 23 | namecheapApi.config.set('ClientIp', await ifconfigRes.text()) 24 | 25 | const { response } = await namecheapApi.apiCall( 26 | 'namecheap.domains.getList', 27 | {} 28 | ) 29 | 30 | _.get(response, '[0].DomainGetListResult[0].Domain', []).forEach(domain => { 31 | domains.push({ domain: domain['$'].Name }) 32 | }) 33 | } 34 | 35 | return { 36 | id: dns, 37 | service, 38 | name, 39 | keys: providerKeys, 40 | domains 41 | } 42 | } 43 | 44 | export const setRecord = async ( 45 | domain: string, 46 | ipaddress: string 47 | ): Promise => { 48 | const response: ServiceResponse = { 49 | success: true, 50 | message: `Successfully set A records for @ and * for ${domain}` 51 | } 52 | 53 | try { 54 | await namecheapApi.apiCall('namecheap.domains.dns.setHosts', { 55 | SLD: domain.split('.')[0], 56 | TLD: domain.split('.')[1], 57 | HostName1: '@', 58 | RecordType1: 'A', 59 | Address1: ipaddress, 60 | HostName2: '*', 61 | RecordType2: 'A', 62 | Address2: ipaddress 63 | }) 64 | } catch (err) { 65 | response.success = false 66 | response.message = `Error setting A records for ${domain}` 67 | } 68 | 69 | return response 70 | } 71 | -------------------------------------------------------------------------------- /src/providers/namecom.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import { sendRequest } from '../helpers/httpRequest' 3 | import { Provider, ServiceResponse } from '../types/general' 4 | import { RequestForName } from '../types/general' 5 | import { providerList } from './' 6 | import { getKeys, findKey } from './helpers' 7 | 8 | const provider = providerList.find(provider => provider.name === 'Name.com') 9 | 10 | const { name, dns, keys, service } = provider 11 | 12 | export const getDomains = async (): Promise => { 13 | const providerKeys = getKeys(provider) 14 | let domains = [] 15 | const options = { 16 | headers: { 17 | Authorization: `Basic ${Buffer.from( 18 | `${findKey(provider, keys[0])}:${findKey(provider, keys[1])}` 19 | ).toString('base64')}` 20 | } 21 | } 22 | const url = `${service}/v4/domains` 23 | const request = await sendRequest(url, options).catch(err => { 24 | console.error(`getDomains Error: ${err}`) 25 | return { domains: [] } 26 | }) 27 | 28 | if (request.domains) domains = [...request.domains] 29 | 30 | return { 31 | id: dns, 32 | service, 33 | name, 34 | keys: providerKeys, 35 | domains: domains.map(el => ({ ...el, domain: el.domainName })) 36 | } 37 | } 38 | 39 | export const setRecord = async ( 40 | domain: string, 41 | ipaddress: string 42 | ): Promise => { 43 | const url = `${service}/v4/domains/${domain}/records` 44 | const rootHost = { 45 | host: '', 46 | domainName: domain, 47 | type: 'A', 48 | answer: ipaddress, 49 | ttl: 300 50 | } 51 | const subdomainHost = { 52 | host: '*', 53 | domainName: domain, 54 | type: 'A', 55 | answer: ipaddress, 56 | ttl: 300 57 | } 58 | 59 | const options = { 60 | method: 'POST', 61 | headers: { 62 | Authorization: `Basic ${Buffer.from( 63 | `${findKey(provider, keys[0])}:${findKey(provider, keys[1])}` 64 | ).toString('base64')}` 65 | } 66 | } 67 | 68 | const rootHostOptions = { 69 | ...options, 70 | body: JSON.stringify(rootHost) 71 | } 72 | 73 | const subdomainHostOptions = { 74 | ...options, 75 | body: JSON.stringify(subdomainHost) 76 | } 77 | 78 | const response: ServiceResponse = { 79 | success: true, 80 | message: 'Successfully set CNAME records for wildcard domain' 81 | } 82 | 83 | await Promise.all([ 84 | fetch(url, rootHostOptions), 85 | fetch(url, subdomainHostOptions) 86 | ]).catch(error => { 87 | console.error('Error setting CNAME records', error) 88 | response.success = false 89 | response.message = 'Error setting CNAME records' 90 | }) 91 | 92 | return response 93 | } 94 | -------------------------------------------------------------------------------- /src/public/accessTokens.ts: -------------------------------------------------------------------------------- 1 | /* global helper */ 2 | 3 | type AccessToken = { 4 | name: string 5 | id: string 6 | } 7 | 8 | const submit: HTMLElement = helper.getElement('.createToken') 9 | const tokensList: HTMLElement = helper.getElement('.tokensList') 10 | 11 | class AccessTokens { 12 | constructor(data: AccessToken) { 13 | const token = document.createElement('li') 14 | token.classList.add('list-group-item', 'd-flex', 'align-items-center') 15 | tokensList.appendChild(token) 16 | token.innerHTML = ` 17 |
18 |
19 | 20 | Token Name: ${data.name} 21 | 22 | 23 | Token ID: ${data.id} 24 | 25 |
26 |
27 | 33 | ` 34 | const delButton = helper.getElement('.deleteButton', token) 35 | delButton.onclick = (): void => { 36 | if (confirm('Are you sure you want to delete this token?')) { 37 | fetch(`/api/accessTokens/${data.id}`, { 38 | method: 'DELETE', 39 | body: JSON.stringify({ data }), 40 | headers: { 41 | 'Content-Type': 'application/json' 42 | } 43 | }).then(() => { 44 | window.location.reload() 45 | }) 46 | } 47 | } 48 | } 49 | } 50 | 51 | fetch('/api/accessTokens') 52 | .then(r => r.json()) 53 | .then(tokens => { 54 | tokensList.innerHTML = '' 55 | tokens.reverse() 56 | tokens.forEach(e => new AccessTokens(e)) 57 | }) 58 | 59 | submit.onclick = (): void => { 60 | const tokenName = helper.getElement('.inputBox') as HTMLInputElement 61 | fetch('/api/accessTokens', { 62 | method: 'POST', 63 | body: JSON.stringify({ 64 | name: tokenName.value 65 | }), 66 | headers: { 67 | 'Content-Type': 'application/json' 68 | } 69 | }).then(() => { 70 | window.location.reload() 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/public/client.ts: -------------------------------------------------------------------------------- 1 | /* global helper */ 2 | type Mapping = { 3 | domain: string 4 | subDomain: string 5 | ip: string 6 | port: string 7 | id: string 8 | gitLink: string 9 | fullDomain: string 10 | status?: string 11 | } 12 | 13 | type Status = { 14 | fullDomain: string 15 | status: string 16 | } 17 | 18 | type ContainerResponse = { 19 | message?: string 20 | } 21 | 22 | const create: HTMLElement = helper.getElement('.create') 23 | const hostSelector: HTMLElement = helper.getElement('#hostSelector') 24 | const domainList: HTMLElement = helper.getElement('.domainList') 25 | const dropDownDomains: HTMLElement = helper.getElement('.dropdown-menu') 26 | let selectedHost = '' 27 | 28 | // eslint-disable-next-line 29 | class DomainOption { 30 | constructor(domain: string) { 31 | const dropdownElement = document.createElement('button') 32 | dropdownElement.classList.add('dropdown-item') 33 | dropdownElement.textContent = domain 34 | dropdownElement.onclick = (): void => { 35 | hostSelector.innerText = domain 36 | selectedHost = domain 37 | } 38 | 39 | dropDownDomains.appendChild(dropdownElement) 40 | } 41 | } 42 | 43 | class MappingItem { 44 | constructor(data: Mapping) { 45 | const mappingElement = document.createElement('li') 46 | let iconClass 47 | let iconColor 48 | // The variables below are to hide log related icons when pm2 is not 49 | // being used to monitor the apps. These apps will not have status since 50 | // they are not managed by pm2. 51 | let logClass 52 | if (data.status === 'running') { 53 | iconClass = 'fa fa-circle mr-1 mt-1' 54 | iconColor = 'rgba(50,255,50,0.5)' 55 | logClass = 'fa fa-file-text-o ml-1 mt-1' 56 | } else if (data.status === 'not started') { 57 | iconClass = '' 58 | iconColor = 'transparent' 59 | } else { 60 | iconClass = 'fa fa-circle mr-1 mt-1' 61 | iconColor = 'rgba(255, 50, 50, 0.5)' 62 | logClass = 'fa fa-file-text-o ml-1 mt-1' 63 | } 64 | mappingElement.classList.add( 65 | 'list-group-item', 66 | 'd-flex', 67 | 'align-items-center' 68 | ) 69 | domainList.appendChild(mappingElement) 70 | mappingElement.innerHTML = ` 71 |
72 |
73 | 74 | 75 | 76 | ${data.fullDomain} 77 | 78 | 79 | PORT: ${data.port} 80 | 81 | 86 | 87 | 92 | 93 |
94 | 95 | ${data.gitLink} 96 | 97 |
98 | 103 | Manage 104 | 105 | 111 | 117 | 123 | ` 124 | 125 | const startButton = helper.getElement('.start-button', mappingElement) 126 | startButton.onclick = (): void => { 127 | if (confirm('Are you sure want to start/restart this domain?')) { 128 | fetch(`/api/mappings/${data.id}/start`) 129 | .then(response => (response.status !== 204 ? response.json() : {})) 130 | .then((body: ContainerResponse) => 131 | body.message 132 | ? alert(`ERROR: ${body.message}`) 133 | : window.location.reload() 134 | ) 135 | } 136 | } 137 | const stopButton = helper.getElement('.stop-button', mappingElement) 138 | stopButton.onclick = (): void => { 139 | if (confirm('Are you sure want to stop this domain?')) { 140 | fetch(`/api/mappings/${data.id}/stop`) 141 | .then(response => (response.status !== 204 ? response.json() : {})) 142 | .then((body: ContainerResponse) => 143 | body.message 144 | ? alert(`ERROR: ${body.message}`) 145 | : window.location.reload() 146 | ) 147 | } 148 | } 149 | const delButton = helper.getElement('.delete-button', mappingElement) 150 | delButton.onclick = (): void => { 151 | if (confirm('Are you sure want to delete this domain?')) { 152 | fetch(`/api/mappings/${data.id}`, { 153 | method: 'DELETE', 154 | body: JSON.stringify({ data }), 155 | headers: { 156 | 'Content-Type': 'application/json' 157 | } 158 | }).then(() => { 159 | window.location.reload() 160 | }) 161 | } 162 | } 163 | } 164 | } 165 | 166 | fetch('/api/mappings') 167 | .then(r => r.json()) 168 | .then(mappings => { 169 | domainList.innerHTML = '' 170 | mappings 171 | .reverse() 172 | .filter( 173 | e => e.domain && e.port && e.id && e.gitLink && e.fullDomain && e.status 174 | ) 175 | .forEach(e => { 176 | new MappingItem(e) 177 | }) 178 | }) 179 | 180 | create.onclick = (): void => { 181 | const subDomain = helper.getElement('.subDomain') as HTMLInputElement 182 | const port = helper.getElement('.port') as HTMLInputElement 183 | const ipAddress = helper.getElement('.ipAddress') as HTMLInputElement 184 | const portValue = port.value 185 | const domain = selectedHost 186 | const ipValue = ipAddress.value 187 | const subDomainValue = subDomain.value 188 | 189 | fetch('/api/mappings', { 190 | method: 'POST', 191 | body: JSON.stringify({ 192 | domain: domain, 193 | subDomain: subDomainValue, 194 | port: portValue, 195 | ip: ipValue 196 | }), 197 | headers: { 198 | 'Content-Type': 'application/json' 199 | } 200 | }).then(res => { 201 | if (res.status === 400) { 202 | return res.json().then(response => { 203 | alert(response.message) 204 | }) 205 | } 206 | window.location.reload() 207 | }) 208 | port.value = '' 209 | ipAddress.value = '' 210 | subDomain.value = '' 211 | } 212 | 213 | fetch('/api/availableDomains') 214 | .then(r => r.json()) 215 | .then((data: Mapping[]) => { 216 | const domains = data.map(el => el.domain).sort() 217 | domains.forEach(domain => new DomainOption(domain)) 218 | selectedHost = domains[0] 219 | hostSelector.innerText = domains[0] 220 | }) 221 | -------------------------------------------------------------------------------- /src/public/helper.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const helper = { 3 | getElement: (query: string, root?: HTMLElement): HTMLElement => { 4 | if (!root) { 5 | return ( 6 | document.querySelector(query) || 7 | (document.createElement('div') as HTMLElement) 8 | ) 9 | } 10 | return ( 11 | root.querySelector(query) || 12 | (document.createElement('div') as HTMLElement) 13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/public/keys.ts: -------------------------------------------------------------------------------- 1 | const keyList = document.querySelector('.sshKeysList') 2 | const newKey = document.getElementById('sshKey') as HTMLInputElement 3 | const submitSSHKey = document.getElementById('newSshKey') 4 | 5 | const title = document.getElementById('title') as HTMLInputElement 6 | 7 | newKey.oninput = (e): void => { 8 | const { value } = e.target as HTMLInputElement 9 | title.value = value.split(' ')[2] || '' 10 | } 11 | 12 | submitSSHKey.onclick = (): void => { 13 | fetch('/api/sshKeys', { 14 | method: 'POST', 15 | body: JSON.stringify({ key: newKey.value }), 16 | headers: { 17 | 'Content-Type': 'application/json' 18 | } 19 | }).then(() => { 20 | window.location.reload() 21 | }) 22 | } 23 | 24 | fetch('/api/sshKeys') 25 | .then(r => r.json()) 26 | .then(data => { 27 | data.forEach((key, index) => { 28 | const newKey = document.createElement('Li') 29 | const removeButton = document.createElement('button') 30 | removeButton.className = 'btn btn-xs pull-right btn-outline-danger' 31 | removeButton.innerText = 'Delete' 32 | removeButton.onclick = (): void => { 33 | fetch('/api/sshKeys', { 34 | method: 'DELETE', 35 | body: JSON.stringify({ id: index }), 36 | headers: { 37 | 'Content-Type': 'application/json' 38 | } 39 | }).then(() => { 40 | window.location.reload() 41 | }) 42 | } 43 | newKey.className = 'list-group-item' 44 | newKey.innerHTML = key 45 | newKey.appendChild(removeButton) 46 | keyList.appendChild(newKey) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/public/manageDomain.ts: -------------------------------------------------------------------------------- 1 | const envList = helper.getElement('.envList') 2 | const environmentVariables: EnvironmentItem[] = [] 3 | const fullDomain = window.location.pathname.split('/').pop() 4 | document.getElementById('full-domain').innerText = fullDomain 5 | 6 | class EnvironmentItem { 7 | name: string 8 | value: string 9 | isValid: boolean 10 | 11 | private listItemElement: HTMLLIElement 12 | private nameInputElement: HTMLInputElement 13 | private valueInputElement: HTMLInputElement 14 | private removeButtonElement: HTMLButtonElement 15 | 16 | private readonly IS_INVALID = 'is-invalid' 17 | private readonly LETTER_NUMBER_REGEX = /^[A-Z0-9_]+$/g 18 | private readonly ELEMENT_HTML = ` 19 |
20 | 26 |
27 | = 28 |
29 | 35 |
36 | 42 |
43 |
44 | Name must contain only letters, number, or underscore. 45 |
46 |
47 | ` 48 | 49 | constructor(name?: string, value?: string) { 50 | this.name = name 51 | this.value = value 52 | this.createElement() 53 | this.setupEventListeners() 54 | if (name && value) { 55 | this.nameInputElement.value = this.name 56 | this.valueInputElement.value = this.value 57 | this.setValid(true) 58 | } else { 59 | this.setValid(false) 60 | } 61 | this.nameInputElement.focus() 62 | } 63 | 64 | /** 65 | * Sets the valid status for the object and updates the input DOM element. 66 | * @param isValid 67 | */ 68 | private setValid(isValid: boolean): void { 69 | if (isValid === this.isValid) return 70 | this.isValid = isValid 71 | if (isValid) this.nameInputElement.classList.remove(this.IS_INVALID) 72 | else this.nameInputElement.classList.add(this.IS_INVALID) 73 | } 74 | 75 | /** 76 | * Create the list element and append to the DOM 77 | */ 78 | private createElement(): void { 79 | this.listItemElement = document.createElement('li') 80 | this.listItemElement.innerHTML = this.ELEMENT_HTML 81 | this.listItemElement.classList.add( 82 | 'list-group-item', 83 | 'd-flex', 84 | 'align-items-center' 85 | ) 86 | envList.appendChild(this.listItemElement) 87 | this.nameInputElement = this.listItemElement.querySelector('.name-input') 88 | this.valueInputElement = this.listItemElement.querySelector('.value-input') 89 | this.removeButtonElement = this.listItemElement.querySelector( 90 | '.remove-button' 91 | ) 92 | } 93 | 94 | /** 95 | * Setup the event listeners for inputs and button 96 | */ 97 | private setupEventListeners(): void { 98 | this.nameInputElement.addEventListener('input', () => 99 | this.validateAndSetName(this.nameInputElement.value) 100 | ) 101 | this.valueInputElement.addEventListener( 102 | 'input', 103 | () => (this.value = this.valueInputElement.value) 104 | ) 105 | this.removeButtonElement.addEventListener('click', () => 106 | this.removeElement() 107 | ) 108 | } 109 | 110 | /** 111 | * Validates the variable name with regex and sets the field if it's valid 112 | * @param value input value 113 | */ 114 | private validateAndSetName(value: string): void { 115 | const upperCaseValue = value.toUpperCase() 116 | if (upperCaseValue.match(this.LETTER_NUMBER_REGEX)) { 117 | this.name = upperCaseValue 118 | this.setValid(true) 119 | } else { 120 | this.setValid(false) 121 | } 122 | } 123 | 124 | /** 125 | * Removes the list item element from the DOM and the items list. 126 | * @param element list item element 127 | */ 128 | private removeElement(): void { 129 | this.listItemElement.remove() 130 | environmentVariables.splice(environmentVariables.indexOf(this), 1) 131 | } 132 | } 133 | 134 | document.getElementById('addEnvButton').onclick = (): void => { 135 | environmentVariables.push(new EnvironmentItem()) 136 | } 137 | 138 | document.getElementById('submitEnvButton').onclick = (): void => { 139 | // check if there's at least one variable to submit 140 | if (environmentVariables.length === 0) { 141 | return alert('Error: add at least one variable before submitting.') 142 | } 143 | // check if all variables are valid 144 | if (environmentVariables.some(env => !env.isValid)) { 145 | return alert( 146 | 'Error: some fields are not valid, please fix them before submitting.' 147 | ) 148 | } 149 | // confirm if it's OK to restart the app and send the request 150 | if ( 151 | confirm( 152 | 'Submitting will recreate the container for your app.' + 153 | 'All data not saved inside the main app folder will be lost. Is this OK?' 154 | ) 155 | ) { 156 | // convert to the request body format 157 | // { variables: { NAME: value, ... }} 158 | const body = environmentVariables.reduce((acc, { name, value }) => { 159 | acc[name] = value 160 | return acc 161 | }, {}) 162 | // send the PUT request 163 | // show alert with error if request is not successful 164 | fetch(`/api/mappings/${fullDomain}/environment`, { 165 | method: 'PUT', 166 | headers: { 167 | 'Content-Type': 'application/json' 168 | }, 169 | body: JSON.stringify({ variables: body }) 170 | }) 171 | .then(response => (response.ok ? {} : response.json())) 172 | .then((body: ContainerResponse) => 173 | body.message 174 | ? alert(`ERROR: ${body.message}`) 175 | : window.location.reload() 176 | ) 177 | } 178 | } 179 | 180 | // Populate the list with existing environment variables 181 | fetch(`/api/mappings/${fullDomain}/environment`) 182 | .then(r => r.json()) 183 | .then(({ variables }) => { 184 | Object.entries(variables).forEach(([name, value]) => { 185 | environmentVariables.push( 186 | new EnvironmentItem(name as string, value as string) 187 | ) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /src/public/providers.ts: -------------------------------------------------------------------------------- 1 | /* global */ 2 | const providerList: HTMLElement = helper.getElement('.providerList') 3 | 4 | type Provider = { 5 | id?: string 6 | name: string 7 | service: string 8 | keys: ProviderKey[] 9 | domains: Domain[] 10 | } 11 | 12 | type ProviderKey = { 13 | id?: string 14 | service?: string 15 | value?: string 16 | key: string 17 | } 18 | 19 | type Domain = { 20 | domain: string 21 | expires?: string 22 | provider?: string 23 | } 24 | 25 | class DomainElement { 26 | constructor( 27 | domainObj: Domain, 28 | domainService: string, 29 | availableDomains: Domain[], 30 | container: HTMLElement 31 | ) { 32 | const domainElement = document.createElement('li') 33 | domainElement.classList.add('list-group-item') 34 | const foundDomain = availableDomains.find( 35 | e => e.domain === domainObj.domain 36 | ) 37 | const checkDomain = foundDomain 38 | ? '' 39 | : '' 40 | const isSetup = foundDomain ? 'Reconfigure' : 'Setup' 41 | const setUpButtonClass = 42 | isSetup === 'Reconfigure' ? 'btn-outline-danger' : 'btn-outline-primary' 43 | domainElement.innerHTML = ` 44 |
49 |
${domainObj.domain} ${checkDomain}
50 | 58 | 61 | 65 |
66 | ` 67 | const actionContainer: HTMLElement = helper.getElement( 68 | '.actionContainer', 69 | domainElement 70 | ) 71 | const setUpButton = helper.getElement('.setUpButton', domainElement) 72 | const method = foundDomain ? 'PATCH' : 'POST' 73 | const selectedDomain = foundDomain ? domainObj.domain : '' 74 | setUpButton.onclick = (): void => { 75 | actionContainer.classList.add('isLoading') 76 | fetch(`/api/admin/sslCerts/${selectedDomain}`, { 77 | method, 78 | body: JSON.stringify({ 79 | service: domainService, 80 | selectedDomain: domainObj.domain 81 | }), 82 | headers: { 83 | 'Content-Type': 'application/json' 84 | } 85 | }) 86 | .then(res => { 87 | return res.json() 88 | }) 89 | .then(() => { 90 | window.location.reload() 91 | }) 92 | } 93 | container.appendChild(domainElement) 94 | } 95 | } 96 | 97 | class ProviderKeyElement { 98 | constructor( 99 | providerKey: ProviderKey, 100 | providerId: string, 101 | providerKeysContainer: HTMLElement 102 | ) { 103 | const providerKeyElement = document.createElement('div') 104 | const isNew = !providerKey.id 105 | const buttonText = isNew ? 'Create' : 'Update' 106 | providerKeyElement.innerHTML = ` 107 |
108 |
109 | 112 | ${providerKey.key.replace('_', ' ')} 113 | 114 |
115 | 122 |
123 | 128 |
129 |
130 | ` 131 | providerKeyElement.className = 'list-group-item' 132 | const createOrUpdate = helper.getElement( 133 | '.createOrUpdateButton', 134 | providerKeyElement 135 | ) 136 | createOrUpdate.onclick = (): void => { 137 | const keyInput = helper.getElement( 138 | '.keyInput', 139 | providerKeyElement 140 | ) as HTMLInputElement 141 | const method = isNew ? 'POST' : 'PATCH' 142 | fetch(`/api/admin/providerKeys/${providerKey.id || ''}`, { 143 | method, 144 | body: JSON.stringify({ 145 | key: providerKey.key, 146 | value: keyInput.value, 147 | service: providerId 148 | }), 149 | headers: { 150 | 'Content-Type': 'application/json' 151 | } 152 | }).then(() => window.location.reload()) 153 | } 154 | providerKeysContainer.appendChild(providerKeyElement) 155 | } 156 | } 157 | 158 | class ProviderElement { 159 | constructor(providerId: string, provider: Provider) { 160 | const providerContainer = document.createElement('li') 161 | providerContainer.classList.add('list-group-item') 162 | providerContainer.innerHTML = ` 163 |

${provider.name}

164 |
165 |
166 |
167 |

Domains

168 |
    169 |
    170 | ` 171 | const providerKeysContainer = helper.getElement( 172 | '.providerKeysContainer', 173 | providerContainer 174 | ) 175 | const domainListContainer = helper.getElement( 176 | '.domainList', 177 | providerContainer 178 | ) 179 | provider.keys.map((providerKey: ProviderKey) => { 180 | return new ProviderKeyElement( 181 | providerKey, 182 | providerId, 183 | providerKeysContainer 184 | ) 185 | }) 186 | fetch('/api/availableDomains') 187 | .then(res => res.json()) 188 | .then(availableDomains => { 189 | provider.domains.map(domain => { 190 | return new DomainElement( 191 | domain, 192 | providerId, 193 | availableDomains, 194 | domainListContainer 195 | ) 196 | }) 197 | }) 198 | providerList.appendChild(providerContainer) 199 | } 200 | } 201 | 202 | fetch('/api/admin/providers') 203 | .then(res => res.json()) 204 | .then(providerList => { 205 | providerList.map((provider: Provider) => { 206 | const providerId = provider.id || '' 207 | return new ProviderElement(providerId, provider) 208 | }) 209 | }) 210 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | import express from 'express' 3 | import fs from 'fs' 4 | import https from 'https' 5 | import httpProxy from 'http-proxy' 6 | import path from 'path' 7 | import cookieParser from 'cookie-parser' 8 | 9 | import { adminRouter } from '../admin/index' 10 | import { apiRouter } from '../api/index' 11 | import { validUIAccess } from '../helpers/authentication' 12 | import { hashPass } from '../helpers/crypto' 13 | import { getAvailableDomains, getMappingByDomain } from '../lib/data' 14 | import { setPass, setupAuth, isCorrectCredentials } from '../auth' 15 | import { ProxyMapping } from '../types/general' 16 | import { SNICallback } from '../helpers/SNICallback' 17 | import { setAuthorizedKeys } from '../helpers/authorizedKeys' 18 | import environment from '../helpers/environment' 19 | 20 | const { isProduction } = environment 21 | 22 | // The steps below are covered by the setup script. This is not necessary. 23 | const cyan = '\x1b[36m\u001b[1m%s\x1b[0m' 24 | const red = '\x1b[31m\u001b[1m%s\x1b[0m' 25 | const errorMsg = 26 | 'ERROR: App cannot be started because you must set an ADMIN password in the environment variable when running this app. Visit https://github.com/garageScript/myproxy#how-to-install' 27 | 28 | const startAppServer = ( 29 | port: string | number, 30 | adminPass: string 31 | ): Promise => { 32 | return new Promise((resolve, reject) => { 33 | if (!adminPass) { 34 | console.error(red, errorMsg) 35 | return reject(errorMsg) 36 | } 37 | setPass(adminPass) 38 | 39 | if (isProduction()) { 40 | fs.readFile('/home/git/.ssh/authorized_keys', (error, data) => { 41 | if (error) { 42 | console.log(error) 43 | } 44 | setAuthorizedKeys( 45 | data 46 | .toString() 47 | .split('\n') 48 | .filter(e => e !== '') 49 | ) 50 | }) 51 | } 52 | 53 | const app = express() 54 | app.use(express.json()) 55 | app.use(express.urlencoded({ extended: true })) 56 | app.use(cookieParser()) 57 | app.use(express.static(path.join(__dirname, '../public'))) 58 | app.use(setupAuth) 59 | app.use('/admin', adminRouter) 60 | app.use('/api', apiRouter) 61 | app.set('view engine', 'ejs') 62 | app.set('views', path.join(__dirname, '../../views')) 63 | 64 | app.get('/', validUIAccess, (_, res) => { 65 | getAvailableDomains().length > 0 66 | ? res.render('client') 67 | : res.redirect('/admin') 68 | }) 69 | app.get('/login', (req, res) => res.render('login', { error: '' })) 70 | 71 | app.post('/login', (req, res) => { 72 | if (isCorrectCredentials(req.body.adminPass, adminPass)) { 73 | res.cookie('adminPass', hashPass(adminPass), { httpOnly: true }) 74 | return res.redirect('/admin') 75 | } 76 | 77 | return res.render('login', { error: 'Wrong Admin Password' }) 78 | }) 79 | 80 | app.get('/sshKeys', validUIAccess, (req, res) => { 81 | res.render('sshKeys') 82 | }) 83 | 84 | app.get('/manage/:domain', validUIAccess, (req, res) => { 85 | res.render('manageDomain') 86 | }) 87 | 88 | const server = app.listen(port, () => { 89 | console.log(cyan, `myProxy is running on port ${port}!`) 90 | resolve(server) 91 | }) 92 | }) 93 | } 94 | 95 | const startProxyServer = (): void => { 96 | const proxy = httpProxy.createProxyServer({ xfwd: true }) 97 | proxy.on('error', err => console.error('Proxy error', err)) 98 | 99 | const server = https.createServer({ SNICallback }, (req, res) => { 100 | try { 101 | const { ip, port }: ProxyMapping = 102 | getMappingByDomain(req.headers.host) || {} 103 | if (!port || !ip) return res.end('Not Found') 104 | proxy.web( 105 | req, 106 | res, 107 | { 108 | target: `http://${ip}:${port}`, 109 | xfwd: true, 110 | preserveHeaderKeyCase: true 111 | }, 112 | err => { 113 | console.error('Error communicating with server', err) 114 | res.end( 115 | `Error communicating with server that runs ${req.headers.host}` 116 | ) 117 | } 118 | ) 119 | } catch (err) { 120 | console.error('Error: proxy failed', err) 121 | return res.end(`Error: failed to create proxy ${req.headers.host}`) 122 | } 123 | }) 124 | 125 | server.on('upgrade', function(req, socket) { 126 | const { ip, port }: ProxyMapping = 127 | getMappingByDomain(req.headers.host) || {} 128 | if (port) return proxy.ws(req, socket, { target: `http://${ip}:${port}` }) 129 | }) 130 | server.listen(443) 131 | const httpApp = express() 132 | httpApp.get('/*', (req, res) => { 133 | const paramCheck = req.headers.host.split('?')[1] 134 | const params = paramCheck ? `?${paramCheck}` : '' 135 | res.redirect(`https://${req.headers.host}${req.path}${params}`) 136 | }) 137 | httpApp.listen(80) 138 | } 139 | 140 | export { startProxyServer, startAppServer } 141 | -------------------------------------------------------------------------------- /src/tests/api.test.ts: -------------------------------------------------------------------------------- 1 | import { startAppServer } from '../server/server' 2 | import fetch from 'node-fetch' 3 | 4 | const TEST_PORT = process.env.PORT || 4998 5 | const ADMIN = process.env.ADMIN || '4995' 6 | const apiUrl = `http://127.0.0.1:${TEST_PORT}` 7 | 8 | describe('/api', () => { 9 | let server 10 | 11 | beforeAll(async () => { 12 | server = await startAppServer(TEST_PORT, ADMIN) 13 | }) 14 | 15 | afterAll(() => { 16 | server.close() 17 | }) 18 | 19 | describe('/api/admin', () => { 20 | it('Should respond with 200 if pw matches', async () => { 21 | const response = await fetch(`${apiUrl}/api/admin/providerKeys`, { 22 | headers: { 23 | authorization: ADMIN 24 | } 25 | }) 26 | expect(response.status).toEqual(200) 27 | }) 28 | 29 | it('Should respond with 401 if pw does not match', async () => { 30 | const response = await fetch(`${apiUrl}/api/admin/providerKeys`, { 31 | headers: { 32 | authorization: 'oaeuou aoueHello' 33 | } 34 | }) 35 | expect(response.status).toEqual(401) 36 | }) 37 | it('Mappings should respond with 200 if token matches', async () => { 38 | const createTokenResponse = await fetch(`${apiUrl}/api/accessTokens`, { 39 | method: 'POST', 40 | headers: { 41 | authorization: ADMIN, 42 | 'Content-Type': 'application/json' 43 | }, 44 | body: JSON.stringify({ name: 'PaulWalker' }) 45 | }) 46 | const token = await createTokenResponse.json() 47 | const fetchResponse = await fetch(`${apiUrl}/api/mappings`, { 48 | headers: { 49 | authorization: token.id 50 | } 51 | }) 52 | expect(fetchResponse.status).toEqual(200) 53 | await fetch(`${apiUrl}/api/accessTokens/${token.id}`, { 54 | method: 'DELETE', 55 | headers: { 56 | authorization: ADMIN 57 | } 58 | }) 59 | }) 60 | it('Admin should respond with 401 with token ', async () => { 61 | const createTokenResponse = await fetch(`${apiUrl}/api/accessTokens`, { 62 | method: 'POST', 63 | headers: { 64 | authorization: ADMIN, 65 | 'Content-Type': 'application/json' 66 | }, 67 | body: JSON.stringify({ name: 'PaulWalker' }) 68 | }) 69 | const token = await createTokenResponse.json() 70 | const fetchResponse = await fetch(`${apiUrl}/api/admin/providerKeys`, { 71 | headers: { 72 | authorization: token.id 73 | } 74 | }) 75 | expect(fetchResponse.status).toEqual(401) 76 | await fetch(`${apiUrl}/api/accessTokens/${token.id}`, { 77 | method: 'DELETE', 78 | headers: { 79 | authorization: ADMIN 80 | } 81 | }) 82 | }) 83 | }) 84 | 85 | describe('api/availableDomains', () => { 86 | it('Should respond with 200 and return an array', async () => { 87 | const response = await fetch(`${apiUrl}/api/availableDomains`, { 88 | headers: { 89 | authorization: ADMIN 90 | } 91 | }) 92 | expect(response.status).toEqual(200) 93 | const data = await response.json() 94 | expect(data).toBeInstanceOf(Array) 95 | }) 96 | }) 97 | }) 98 | -------------------------------------------------------------------------------- /src/tests/helpers/accessTokensAdapter.ts: -------------------------------------------------------------------------------- 1 | const TEST_PORT = process.env.PORT || 50605 2 | const ADMIN = process.env.ADMIN || 'hjhj' 3 | const apiURL = `http://127.0.0.1:${TEST_PORT}` 4 | import { Options, Headers } from '../../types/tests' 5 | import fetch, { Response } from 'node-fetch' 6 | 7 | const reqHeaders: Headers = { 8 | authorization: ADMIN, 9 | 'Content-Type': 'application/json' 10 | } 11 | 12 | const accessTokensAdapter = ( 13 | path = '/', 14 | method: string, 15 | body?: object 16 | ): Promise => { 17 | const options: Options = { 18 | method, 19 | headers: reqHeaders 20 | } 21 | if (body) { 22 | options.body = JSON.stringify(body) 23 | } 24 | return fetch(`${apiURL}/api/accessTokens${path}`, options) 25 | } 26 | 27 | export { accessTokensAdapter } 28 | -------------------------------------------------------------------------------- /src/tests/helpers/logAdapter.ts: -------------------------------------------------------------------------------- 1 | const TEST_PORT = process.env.PORT || 50608 2 | const ADMIN = process.env.ADMIN || '123' 3 | const apiURL = `http://127.0.0.1:${TEST_PORT}` 4 | import { Options, Headers } from '../../types/tests' 5 | import fetch, { Response } from 'node-fetch' 6 | 7 | const reqHeaders: Headers = { 8 | authorization: ADMIN, 9 | 'Content-Type': 'application/json' 10 | } 11 | 12 | const logAdapter = (path = '/', method: string): Promise => { 13 | const options: Options = { 14 | method, 15 | headers: reqHeaders 16 | } 17 | return fetch(`${apiURL}/api/logs${path}`, options) 18 | } 19 | 20 | export { logAdapter } 21 | -------------------------------------------------------------------------------- /src/tests/helpers/mappingAdapter.ts: -------------------------------------------------------------------------------- 1 | const TEST_PORT = process.env.PORT || 50604 2 | const ADMIN = process.env.ADMIN || 'hjhj' 3 | const apiURL = `http://127.0.0.1:${TEST_PORT}` 4 | import { Options, Headers } from '../../types/tests' 5 | import fetch, { Response } from 'node-fetch' 6 | 7 | const reqHeaders: Headers = { 8 | authorization: ADMIN, 9 | 'Content-Type': 'application/json' 10 | } 11 | 12 | const mappingAdapter = ( 13 | path = '/', 14 | method: string, 15 | body?: object 16 | ): Promise => { 17 | const options: Options = { 18 | method, 19 | headers: reqHeaders 20 | } 21 | if (body) { 22 | options.body = JSON.stringify(body) 23 | } 24 | return fetch(`${apiURL}/api/mappings${path}`, options) 25 | } 26 | 27 | export { mappingAdapter } 28 | -------------------------------------------------------------------------------- /src/tests/integration/accessToken.test.ts: -------------------------------------------------------------------------------- 1 | import { startAppServer } from '../../server/server' 2 | import uuidv4 from 'uuid/v4' 3 | import { accessTokensAdapter } from '../helpers/accessTokensAdapter' 4 | 5 | const TEST_PORT = process.env.PORT || 50605 6 | const ADMIN = process.env.ADMIN || 'hjhj' 7 | 8 | describe('/api/accessTokens', () => { 9 | let server 10 | 11 | beforeAll(async () => { 12 | server = await startAppServer(TEST_PORT, ADMIN) 13 | }) 14 | 15 | afterAll(() => { 16 | server.close() 17 | }) 18 | 19 | it('should create an access token successfully', async () => { 20 | const name = `c0d3access${uuidv4()}` 21 | 22 | const postResponse = await accessTokensAdapter('/', 'POST', { 23 | name 24 | }) 25 | const postAccessToken = await postResponse.json() 26 | expect(postAccessToken.name).toEqual(name) 27 | const getTokens = await accessTokensAdapter('/', 'GET').then(r => r.json()) 28 | const foundToken = getTokens.find(e => e.name === name) 29 | expect(foundToken.name).toEqual(name) 30 | await accessTokensAdapter(`/${postAccessToken.id}`, 'DELETE') 31 | }) 32 | 33 | it('should not allow duplicate tokens', async () => { 34 | const firstName = `c0d3access${uuidv4()}` 35 | const postResponse = await accessTokensAdapter('/', 'POST', { 36 | name: firstName 37 | }) 38 | expect(postResponse.status).toEqual(200) 39 | const secondName = firstName 40 | const duplicatePostResponse = await accessTokensAdapter('/', 'POST', { 41 | name: secondName 42 | }) 43 | expect(duplicatePostResponse.status).toEqual(400) 44 | const updatedTokens = await accessTokensAdapter('/', 'GET').then(r => 45 | r.json() 46 | ) 47 | const allTokens = updatedTokens.filter(e => e.name === firstName) 48 | expect(allTokens.length).toEqual(1) 49 | await accessTokensAdapter(`/${allTokens[0].id}`, 'DELETE') 50 | }) 51 | 52 | it('should delete the token', async () => { 53 | const name = `c0d3accessDELETE${uuidv4()}` 54 | const postResponse = await accessTokensAdapter('/', 'POST', { 55 | name 56 | }) 57 | expect(postResponse.status).toEqual(200) 58 | 59 | const postResult = await postResponse.json() 60 | const selectedToken = await accessTokensAdapter( 61 | `/${postResult.id}`, 62 | 'GET' 63 | ).then(r => r.json()) 64 | expect(selectedToken.name).toEqual(name) 65 | 66 | const deleteToken = await accessTokensAdapter(`/${postResult.id}`, 'DELETE') 67 | const postDeleteResult = await deleteToken.json() 68 | expect(postResult.name).toEqual(postDeleteResult.name) 69 | 70 | const allTokens = await accessTokensAdapter('/', 'GET').then(r => r.json()) 71 | const findDeletedToken = allTokens.find(e => e.name === postResult.name) 72 | expect(findDeletedToken).toEqual(undefined) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /src/tests/integration/log.test.ts: -------------------------------------------------------------------------------- 1 | import { startAppServer } from '../../server/server' 2 | import { logAdapter } from '../helpers/logAdapter' 3 | 4 | const TEST_PORT = process.env.PORT || 50608 5 | const ADMIN = process.env.ADMIN || '123' 6 | 7 | describe('/api/logs', () => { 8 | let server 9 | 10 | beforeAll(async () => { 11 | server = await startAppServer(TEST_PORT, ADMIN) 12 | }) 13 | 14 | afterAll(() => { 15 | server.close() 16 | }) 17 | 18 | it('checks that output logs endpoint exists', async () => { 19 | const fullDomain = 'Cloud.Walker.com' 20 | const logResponse = await logAdapter(`/stdout/${fullDomain}`, 'GET') 21 | expect(logResponse.status).toEqual(200) 22 | }) 23 | 24 | it('checks that error logs endpoint exists', async () => { 25 | const fullDomain = 'Luke.Walker.com' 26 | const logResponse = await logAdapter(`/stderr/${fullDomain}`, 'GET') 27 | expect(logResponse.status).toEqual(200) 28 | }) 29 | 30 | it('checks that unknown stream param returns an error', async () => { 31 | const fullDomain = 'Luke.Walker.com' 32 | const logResponse = await logAdapter(`/somestream/${fullDomain}`, 'GET') 33 | expect(logResponse.status).toEqual(400) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/tests/integration/mapping.test.ts: -------------------------------------------------------------------------------- 1 | import { startAppServer } from '../../server/server' 2 | import uuidv4 from 'uuid/v4' 3 | import { mappingAdapter } from '../helpers/mappingAdapter' 4 | import { getMappingByDomain } from '../../lib/data' 5 | 6 | const TEST_PORT = process.env.PORT || 50604 7 | const ADMIN = process.env.ADMIN || 'hjhj' 8 | 9 | describe('/api', () => { 10 | let server 11 | 12 | beforeAll(async () => { 13 | server = await startAppServer(TEST_PORT, ADMIN) 14 | }) 15 | 16 | afterAll(() => { 17 | server.close() 18 | }) 19 | 20 | it('checks mappings for newly added mapping and it converts domain and subdomain to lowercase', async () => { 21 | const subDomain = `Testing${uuidv4()}` 22 | const domain = 'Rahul' 23 | const port = '5678' 24 | const postResponse = await mappingAdapter('/', 'POST', { 25 | domain, 26 | subDomain, 27 | port 28 | }) 29 | const postMapping = await postResponse.json() 30 | expect(postMapping.port).toEqual(port) 31 | expect(postMapping.subDomain).toEqual(subDomain.toLowerCase()) 32 | expect(postMapping.domain).toEqual(domain.toLowerCase()) 33 | expect(postMapping.fullDomain).toEqual( 34 | `${subDomain}.${domain}`.toLowerCase() 35 | ) 36 | const deleteResponse = await mappingAdapter(`/${postMapping.id}`, 'DELETE') 37 | expect(deleteResponse.status).toEqual(200) 38 | const getMapping = await mappingAdapter(`/${postMapping.id}`, 'GET') 39 | expect(getMapping.status).toEqual(200) 40 | const mappingData = await getMapping.json() 41 | expect(Object.keys(mappingData).length).toEqual(0) 42 | }) 43 | 44 | it('checks mappings for newly added root domain', async () => { 45 | const subDomain = '' 46 | const domain = `rahul${Date.now()}` 47 | const port = '5612' 48 | const postResponse = await mappingAdapter('/', 'POST', { 49 | subDomain, 50 | domain, 51 | port 52 | }) 53 | const postMapping = await postResponse.json() 54 | expect(postMapping.port).toEqual(port) 55 | expect(postMapping.domain).toEqual(domain) 56 | expect(postMapping.subDomain).toEqual(subDomain) 57 | expect(postMapping.fullDomain).toEqual(`${domain}`) 58 | 59 | const mappingResponse = await mappingAdapter(`/${postMapping.id}`, 'GET') 60 | const mappingData = await mappingResponse.json() 61 | 62 | expect(mappingData.port).toEqual(port) 63 | expect(mappingData.domain).toEqual(domain) 64 | expect(mappingData.subDomain).toEqual(subDomain) 65 | expect(mappingData.fullDomain).toEqual(`${domain}`) 66 | 67 | const deleteResponse = await mappingAdapter(`/${postMapping.id}`, 'DELETE') 68 | expect(deleteResponse.status).toEqual(200) 69 | }) 70 | 71 | it('Delete mapping', async () => { 72 | const subDomain = `delete${uuidv4()}` 73 | const domain = 'albertow' 74 | const port = '4500' 75 | const createMapping = await mappingAdapter('/', 'POST', { 76 | domain, 77 | subDomain, 78 | port 79 | }) 80 | expect(createMapping.status).toEqual(200) 81 | const mapping = await createMapping.json() 82 | const delMapping = await mappingAdapter(`/${mapping.id}`, 'DELETE') 83 | expect(delMapping.status).toEqual(200) 84 | const deletedMapping = await delMapping.json() 85 | expect(deletedMapping.port).toEqual(port) 86 | expect(deletedMapping.subDomain).toEqual(subDomain) 87 | expect(deletedMapping.domain).toEqual(domain) 88 | expect(deletedMapping.fullDomain).toEqual(`${subDomain}.${domain}`) 89 | expect(deletedMapping.id).toEqual(mapping.id) 90 | const getMapping = await mappingAdapter(`/${mapping.id}`, 'GET') 91 | expect(getMapping.status).toEqual(200) 92 | const mappingData = await getMapping.json() 93 | expect(Object.keys(mappingData).length).toEqual(0) 94 | }) 95 | 96 | it('checks no duplicate subdomain is created for same domain', async () => { 97 | const subDomain = `testing${uuidv4()}` 98 | const domain = 'Sahil' 99 | const port = '3522' 100 | const postResponse = await mappingAdapter('/', 'POST', { 101 | domain, 102 | subDomain, 103 | port 104 | }) 105 | expect(postResponse.status).toEqual(200) 106 | const duplicatePostResponse = await mappingAdapter('/', 'POST', { 107 | domain, 108 | subDomain, 109 | port 110 | }) 111 | expect(duplicatePostResponse.status).toEqual(400) 112 | const postMapping = await postResponse.json() 113 | const deleteResponse = await mappingAdapter(`/${postMapping.id}`, 'DELETE') 114 | expect(deleteResponse.status).toEqual(200) 115 | 116 | const getMapping = await mappingAdapter(`/${postMapping.id}`, 'GET') 117 | expect(getMapping.status).toEqual(200) 118 | const mappingData = await getMapping.json() 119 | expect(Object.keys(mappingData).length).toEqual(0) 120 | }) 121 | 122 | it('checks same subdomain can be used for different domains', async () => { 123 | const subDomain = `testing${uuidv4()}` 124 | const domain = 'VinDiesel' 125 | const port = '3522' 126 | await mappingAdapter('/', 'POST', { 127 | domain, 128 | subDomain, 129 | port 130 | }) 131 | 132 | const secondDomain = 'PaulWalker' 133 | const nextPort = '3523' 134 | await mappingAdapter('/', 'POST', { 135 | domain: secondDomain, 136 | port: nextPort, 137 | subDomain 138 | }) 139 | 140 | const firstFullDomain = `${subDomain}.${domain}`.toLowerCase() 141 | const secondFullDomain = `${subDomain}.${secondDomain}`.toLowerCase() 142 | const match1 = getMappingByDomain(firstFullDomain) 143 | const match2 = getMappingByDomain(secondFullDomain) 144 | 145 | expect(match1.fullDomain).toEqual(firstFullDomain) 146 | expect(match2.fullDomain).toEqual(secondFullDomain) 147 | await mappingAdapter(`/${match1.id}`, 'DELETE') 148 | await mappingAdapter(`/${match2.id}`, 'DELETE') 149 | }) 150 | 151 | it('checks status is returned when querying mappings', async () => { 152 | const subDomain = uuidv4() 153 | const domain = 'VinDiesel' 154 | const port = '3533' 155 | await mappingAdapter('/', 'POST', { 156 | domain, 157 | subDomain, 158 | port 159 | }) 160 | 161 | const getResponse = await mappingAdapter('/', 'GET') 162 | const getMappings = await getResponse.json() 163 | 164 | expect(getMappings[0].status).toEqual('not started') 165 | await mappingAdapter(`/${getMappings[0].id}`, 'DELETE') 166 | }) 167 | }) 168 | -------------------------------------------------------------------------------- /src/tests/integration/sshKeys.test.ts: -------------------------------------------------------------------------------- 1 | import { startAppServer } from '../../server/server' 2 | import fetch from 'node-fetch' 3 | import { authorizedKeys } from '../../helpers/authorizedKeys' 4 | 5 | const TEST_PORT = process.env.PORT || 5001 6 | const ADMIN = process.env.ADMIN || '123' 7 | 8 | const reqHeaders = { 9 | authorization: ADMIN, 10 | 'Content-Type': 'application/json' 11 | } 12 | 13 | describe('/api/sshKeys', () => { 14 | let server 15 | 16 | beforeAll(async () => { 17 | server = await startAppServer(TEST_PORT, ADMIN) 18 | }) 19 | 20 | afterAll(() => { 21 | server.close() 22 | }) 23 | 24 | it('checking SSH keys can be retrieved', async () => { 25 | const res = await fetch(`http://localhost:${TEST_PORT}/api/sshKeys`, { 26 | headers: reqHeaders 27 | }) 28 | const data = await res.json() 29 | expect(res.status).toEqual(200) 30 | expect(data).toBeInstanceOf(Array) 31 | }) 32 | 33 | it('check if SSH keys can be added', async () => { 34 | const currentSSHKeyLength = authorizedKeys.length 35 | const res = await fetch(`http://localhost:${TEST_PORT}/api/sshKeys`, { 36 | method: 'POST', 37 | body: JSON.stringify({ 38 | key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAkl schacon@mylaptop.local' 39 | }), 40 | headers: reqHeaders 41 | }) 42 | const data = await res.json() 43 | expect(authorizedKeys.length).toEqual(currentSSHKeyLength + 1) 44 | expect(data).toContain('schacon@mylaptop.local') 45 | }) 46 | 47 | it('check if SSH keys can be deleted', async () => { 48 | const res = await fetch(`http://localhost:${TEST_PORT}/api/sshKeys`, { 49 | method: 'DELETE', 50 | body: JSON.stringify({ 51 | id: 0 52 | }), 53 | headers: reqHeaders 54 | }) 55 | 56 | const data = await res.json() 57 | expect(res.status).toEqual(200) 58 | expect(data).toStrictEqual([]) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /src/types/admin.ts: -------------------------------------------------------------------------------- 1 | import { Mapping, Domain, AccessToken } from './general' 2 | 3 | type ServiceKey = { 4 | id?: string 5 | key: string 6 | value: string 7 | service: string 8 | } 9 | 10 | type DB = { 11 | serviceKeys: ServiceKey[] 12 | mappings: Mapping[] 13 | availableDomains: Domain[] 14 | accessTokens: AccessToken[] 15 | } 16 | 17 | export { ServiceKey, DB } 18 | -------------------------------------------------------------------------------- /src/types/docker.ts: -------------------------------------------------------------------------------- 1 | type DockerError = { 2 | reason: string 3 | statusCode: number 4 | json: { 5 | message: string 6 | } 7 | } 8 | 9 | export { DockerError } 10 | -------------------------------------------------------------------------------- /src/types/general.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | type Mapping = { 3 | domain: string 4 | subDomain: string 5 | port: string 6 | ip: string 7 | id: string 8 | gitLink: string 9 | fullDomain: string 10 | } 11 | 12 | interface AuthenticatedRequest extends Request { 13 | user?: { 14 | isAdmin: boolean 15 | isPseudoAdmin: boolean 16 | } 17 | } 18 | 19 | type MappingById = { 20 | [mappingId: string]: Mapping 21 | } 22 | 23 | type AccessTokenById = { 24 | [id: string]: AccessToken 25 | } 26 | 27 | type Provider = { 28 | id: string 29 | service: string 30 | name: string 31 | keys: object 32 | domains: unknown 33 | } 34 | 35 | type Domain = { 36 | domain: string 37 | expiration: string 38 | provider: string 39 | } 40 | 41 | type ServiceResponse = { 42 | success: boolean 43 | message: string 44 | } 45 | 46 | type ProviderService = { 47 | getDomains: Function 48 | setRecord: Function 49 | } 50 | 51 | type ServiceConfig = { 52 | dns_provider: ProviderService 53 | } 54 | 55 | type ProxyMapping = { 56 | ip?: string 57 | port?: string 58 | } 59 | 60 | type NamecomDomain = { 61 | domainName: string 62 | locked: boolean 63 | autorenewEnabled: boolean 64 | expireDate: string 65 | createDate: string 66 | } 67 | 68 | type RequestForName = { 69 | domains: NamecomDomain[] | [] 70 | } 71 | 72 | type AccessToken = { 73 | name: string 74 | id: string 75 | } 76 | 77 | type ProviderInfo = { 78 | name: string 79 | dns: string 80 | keys: string[] 81 | service: string 82 | path: string 83 | } 84 | 85 | export { 86 | Mapping, 87 | MappingById, 88 | Provider, 89 | Domain, 90 | ServiceResponse, 91 | ServiceConfig, 92 | ProviderService, 93 | ProxyMapping, 94 | NamecomDomain, 95 | RequestForName, 96 | AccessToken, 97 | AccessTokenById, 98 | AuthenticatedRequest, 99 | ProviderInfo 100 | } 101 | -------------------------------------------------------------------------------- /src/types/tests.ts: -------------------------------------------------------------------------------- 1 | type Headers = { 2 | authorization: string 3 | 'Content-Type': string 4 | } 5 | 6 | type Options = { 7 | headers: Headers 8 | method: string 9 | body?: string 10 | } 11 | 12 | export { Options, Headers } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | "outDir": "./build", /* Redirect output structure to the directory. */ 16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | // "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /views/admin/accessTokens.ejs: -------------------------------------------------------------------------------- 1 | <%- include('../layout/head.ejs') %> 2 | 3 |
    4 |
    5 | 6 |
    7 |

    AccessTokens

    8 | 9 | 32 |
    33 | 34 |
    35 |
    36 | 37 | Name: 38 | 39 |
    40 | 41 | 42 | 43 | 48 | SUBMIT 49 | 50 |
    51 | 52 |
    53 | 54 |
    55 |
    56 |
      57 |
      58 |
      59 | 60 | <%- include('../layout/footer.ejs') %> 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /views/admin/providers.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%- include('../layout/head.ejs') %> 3 | 4 |
      5 | 31 |
      32 |
      33 |
      34 |
        35 |
      36 |
      37 |
      38 |
      39 |
      40 | 41 | <%- include('../layout/footer.ejs') %> 42 | 43 | -------------------------------------------------------------------------------- /views/client.ejs: -------------------------------------------------------------------------------- 1 | <%- include('layout/head.ejs') %> 2 | 3 |
      4 |
      5 |

      My Proxy

      6 | 15 |
      16 |
      17 |
      18 | 24 |
      25 | 34 | 35 |
      36 |
      37 |
      38 |
      39 | 40 | Port 41 | 42 |
      43 | 51 |
      52 |
      53 |
      54 | 55 | IP 56 | 57 |
      58 | 64 |
      65 | 72 |
      73 | Port Number is not required. If you must, please make it > 3001 76 |
      77 |
      78 |
      79 |
        80 |
        81 |
        82 |
        83 |
        84 |
        85 |

        Sample Server Code

        86 |
        
         87 | const express = require('express');
         88 | const app = express();
         89 | app.use(express.static('public'));
         90 | 
         91 | app.get('/', (req, res) => {
         92 |   res.send('hello');
         93 | });
         94 | 
         95 | app.listen(process.env.PORT || 8123);
         96 |         
        97 |
        98 |
        99 |
        100 | 101 | <%- include('layout/footer.ejs') %> 102 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/garageScript/myProxy/8de527ca39c3f2c85cf5a766faa6d6195850fcc4/views/error.ejs -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 |

        <%= message %>

        2 | -------------------------------------------------------------------------------- /views/layout/footer.ejs: -------------------------------------------------------------------------------- 1 | 6 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /views/layout/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MyProxy 6 | 12 | 18 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 | <%- include('layout/head.ejs') %> 2 | 3 |
        4 |
        5 |
        6 |

        Please login

        7 |
        8 |
        9 |
        10 | <% if(error.length > 0){ %> 11 | 12 | <% } %> 13 | 14 | 20 |
        21 | 22 |
        23 |
        24 |
        25 |
        26 |
        27 | 28 | <%- include('layout/footer.ejs') %> 29 | -------------------------------------------------------------------------------- /views/manageDomain.ejs: -------------------------------------------------------------------------------- 1 | <%- include('layout/head.ejs') %> 2 | 3 |
        4 | 18 |
        19 | Environment Variables 20 | 27 | 34 |
        35 |
        36 |
          37 |
        38 |
        39 |
        40 |
        41 |
        42 | 43 | <%- include('layout/footer.ejs') %> 44 | -------------------------------------------------------------------------------- /views/sshKeys.ejs: -------------------------------------------------------------------------------- 1 | <%- include('layout/head.ejs') %> 2 | 3 |
        4 | 30 |
        31 |
        32 |
        33 | 34 | Title 35 | 36 |
        37 | 45 |
        46 |
        47 |
        48 | 49 | SSH Key 50 | 51 |
        52 | 57 |
        58 | 65 |
        66 |
        67 |
        68 |
        69 |
          70 |
          71 |
          72 |
          73 |
          74 |
          75 | 76 | 77 | 78 | <%- include('layout/footer.ejs') %> 79 | --------------------------------------------------------------------------------