This commit is contained in:
2020-04-09 22:47:08 -04:00
22 changed files with 762 additions and 3298 deletions

View File

@ -1,9 +0,0 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,156 +0,0 @@
# proxy
## API docs
[API docs](api.md)
## Server set up
The server requires:
* NodeJS 8.x
* open ssh server(any modern version will do)
* inbound Internet access
* OpenResty
* redis
* lua rocks
This has been tested on ubuntu 16.04, but should work on any modern Linux distro. It used the Linux users for its user management, so this will **ONLY** work on Linux, no macOS, BSD or Windows.
The steps below are for a new ubuntu server, they should be mostly the same for other distros, but the paths and availability of packages may vary. A dedicated server is highly recommended (since it will make ever user a system user), a VPS like Digital Ocean will do just fine.
* Install other
These packages are needed for the PAM node package
```bash
apt install libpam0g-dev build-essential
```
* Install open ssh server
```bash
apt install ssh
```
* Install openresty
[OpenResty® Linux Packages](https://openresty.org/en/linux-packages.html)
* Install redis
```bash
apt install redis-server
```
* install lua plugin
```bash
apt install luarocks
sudo luarocks install lua-resty-auto-ssl
```
* Configure sshd for tunneling
* openresty config
Set up fail back SSL certs
```bash
mkdir /etc/ssl/
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -subj '/CN=sni-support-required-for-valid-ssl' -keyout /etc/ssl/resty-auto-ssl-fallback.key -out /etc/ssl/resty-auto-ssl-fallback.crt
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -subj '/CN=sni-support-required-for-valid-ssl' -keyout /etc/ssl/resty-auto-ssl-fallback.key -out /etc/ssl/resty-auto-ssl-fallback.crt
openssl dhparam -out /etc/nginx/dhparam.pem 4096
```
change the `/etc/openresty/nginx.conf to have this config`
```
#user nobody;
worker_processes 4;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
client_max_body_size 4g;
lua_shared_dict auto_ssl 100m;
lua_shared_dict auto_ssl_settings 64k;
resolver 8.8.4.4 8.8.8.8;
init_by_lua_block {
auto_ssl = (require "resty.auto-ssl").new()
auto_ssl:set("storage_adapter", "resty.auto-ssl.storage_adapters.redis")
auto_ssl:set("allow_domain", function(domain)
return true
end)
auto_ssl:init()
}
init_worker_by_lua_block {
auto_ssl:init_worker()
}
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
server {
listen 127.0.0.1:8999;
# Increase the body buffer size, to ensure the internal POSTs can always
# parse the full POST contents into memory.
client_body_buffer_size 128k;
client_max_body_size 128k;
location / {
content_by_lua_block {
auto_ssl:hook_server()
}
}
}
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
include sites-enabled/*;
}
```
add the SSL config file `/etc/openresty/autossl.conf`, contents from here https://github.com/theta42/t42-common/blob/master/templates/openresty/autossl.conf.erb
Add the proxy config `/etc/openresty/sites-enabled/000-proxy` contents from here https://github.com/theta42/t42-common/blob/master/templates/openresty/010-proxy.conf.erb
## ref
https://blog.trackets.com/2014/05/17/ssh-tunnel-local-and-remote-port-forwarding-explained-with-examples.html
https://github.com/GUI/lua-resty-auto-ssl

View File

@ -1,29 +1,29 @@
## get host info
**get** `/api/<HOST>`
**GET** `/api/hosts<HOST>`
```bash
curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://admin.rubyisforpussys.com/api/mine.com
curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://proxy-host.com/api/hostsmine.com
```
* 200 {"host":"yours.com","results":{"ip":"127.0.0.1:4000","updated":"1518595297563","username":"test10","forceSSL": false, "targetSSL": true, "targetPort": "443"}}
* 404 {"host":"mine.comf","results":null}
* 404 {"name": "HostNotFound", "message": "Host does not exists"}
## view all hosts
**get** `/api/`
**GET** `/api/hosts`
```bash
curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://admin.rubyisforpussys.com/api/
curl -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" https://proxy-host.com/api/hosts
```
* 200 {"hosts":["mine.com","mine2.com"]}
## add/edit host
## Add host
**post** `/api/`
**POST** `/api/hosts`
Params
* **host** -- Required, The domain name for the new record.
@ -37,22 +37,37 @@ the proxy. The default is false and this is HIGHLY recommended.
* **
```bash
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://admin.rubyisforpussys.com/api/
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://proxy-host.com/api/hosts
```
* 200 {"message":"Host yours.com Added"}
* 400 {"message":"Missing fields: ip"}
* 200 {"message":"Host yours.com added."}
* 409 {"name":"HostNameUsed", "message":"Host already exists"}
* 422 {"name":"ObjectValidateError","message":[{"key":"ip","message":"ip is required."}]} Missing or incorrect keys/values. Returns a list with a message per key error.
## Edit
**PUT** `/api/hosts<host>`
Takes the same params as add, but none are required
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d '{"host": "test.vm42.com", "ip": "192.168.1.21", "targetSSL": false, "targetPort": "443", "forceSSL": true} https://proxy-host.com/api/hosts
* 200 {"message":"Host yours.com updated."}
* 404 {"name": "HostNotFound", "message": "Host does not exists"}
* 409 {"name":"HostNameUsed", "message":"Host already exists"}
* 422 {"name":"ObjectValidateError","message":[{"key":"ip","message":"ip is required."}]} Missing or incorrect keys/values. Returns a list with a message per key error.
## delete host
**delete** /`api`
**DELETE** /`api/hosts/<host>`
```bash
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X DELETE -d "{\"host\": \"yours.com\"}" https://admin.rubyisforpussys.com/api/
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X DELETE https://proxy-host.com/api/hosts
```
* 200 {"message":"Host yours.com deleted"}
* 404 {"name": "HostNotFound", "message": "Host does not exists"}
## create invite token
@ -60,7 +75,7 @@ curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd
**post** `/users/invite`
```bash
curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f-b8df55431cd1" -X POST https://admin.rubyisforpussys.com/users/invite
curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f-b8df55431cd1" -X POST https://proxy-host.com/users/invite
```
* 200 {"token":"5caf94d2-2c91-4010-8df7-968d10802b9d"}
@ -71,7 +86,7 @@ curl -H "Content-Type: application/json" -H "auth-token: 0b06eb2e-4ca4-4881-9a0f
**post** `/auth/invite/<INVITE TOKEN>`
```bash
curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \"password\": \"palm7\"}" https://admin.rubyisforpussys.com/auth/invite/b33d8819-ec64-4cf4-a6ec-77562d738fa4
curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \"password\": \"palm7\"}" https://proxy-host.com/auth/invite/b33d8819-ec64-4cf4-a6ec-77562d738fa4
```
@ -86,7 +101,7 @@ curl -H "Content-Type: application/json" -X POST -d "{\"username\": \"test9\", \
**post** `/auth/login`
```bash
curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "password": "mypassword"}' https://admin.rubyisforpussys.com/auth/login
curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "password": "mypassword"}' https://proxy-host.com/auth/login
```
* 200 {"login":true,"token":"027d3964-7d81-4462-a6f9-2c1f9b40b4be"}
@ -98,7 +113,7 @@ curl -H "Content-Type: application/json" -X POST -d '{"username": "test8", "pass
**post** `/auth/verifykey`
```bash
curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9vboz5YGgESsrR2e4JOeP2qtmQo2S8BjI+Y/VxPQ6WbNFzAkXxDniHcnPCrhkeX36SKINvMjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.com\"}" https://admin.rubyisforpussys.com/auth/verifykey
curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9vboz5YGgESsrR2e4JOeP2qtmQo2S8BjI+Y/VxPQ6WbNFzAkXxDniHcnPCrhkeX36SKINvMjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.com\"}" https://proxy-host.com/auth/verifykey
```
* 200 {"info":"4096 SHA256:dfdCYzt0atMBXVZTJzUxsu99IjXXFXpocSox5q+jOs8 wmantly@gmail.com (RSA)\n"}
@ -110,7 +125,7 @@ curl -H "Content-Type: application/json" -X POST -d "{\"key\":\"ssh-rsa AAAAB3Nz
**post** `/users/key`
```bash
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d "{\"key\": \"ssh-rsa AAAAB3NzaC1yc2EAAjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.co\"}" https://admin.rubyisforpussys.com/users/key
curl -H "Content-Type: application/json" -H "auth-token: 8eff4f16-086d-40fd-acbd-7634b9a36117" -X POST -d "{\"key\": \"ssh-rsa AAAAB3NzaC1yc2EAAjWnt4XOK2S+X+1tCoXJzqtcKKyK0gx8ijBxcWVPxsMWjMYTGSVSKiKnt6CyQzrbVGJMh3iAQ8Yv1JwH+6SAtMgT8it7iLyntNFJCesh4I/znEG58A5VBbdUle1Ztz9afjj1CZns17jk7KPm9ig5DmuvdvnMEfhFjfKv1Rp6S5nxacMoTP4tJNSEUh55IicoWk94ii5GwUVLYgyMmzdlA32TqVLFpU2yAvdA9WSnBaI/ZyktlfI7YAmK2wFBsagr9Pq1TcUAY6rZ/GTMjDxExgdYn/FxlufcuqeNJsJXs2A+0xDS/9mv/yGQzNZrL8DrVhY2OKKLoH4Q7enDbhSgEFmJUJMqPxuPEgLEvKfzcURSvIwRj1iCEw6S4dhdaLJl2RRBb1ZWBQbE5ogIbvAl7GFJUAhj3pqYJnd30VENv1MkK+IoCS7EEP0caqL9RNAId0Plud7q2XElHqzkYUE+z+Q/LvGgclXK1ZmZejNaMnV53wfhAevfwVyNGK9i5gbwc1P2lplIa5laXCcVWezqELEkTpdjp4AeKmMuCr8rY8EnLKIcKWEOsX5UumztCow6e1E55v3VeHvRZLpw4DZP7EE0Q8B/jPFWqbCw== wmantly@gmail.co\"}" https://proxy-host.com/users/key
```
* 200 {"message":true}

View File

@ -2,41 +2,55 @@
const path = require('path');
const ejs = require('ejs')
const express = require('express');
const app = express();
const middleware = require('./middleware/auth');
// Set up the express app.
const app = express();
// load the JSON parser middleware. Express will parse JSON into native objects
// for any request that has JSON in its content type.
app.use(express.json());
// Set up the templating engine to build HTML for the front end.
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Have express server static content( images, CSS, browser JS) from the public
// local folder.
app.use('/static', express.static(path.join(__dirname, 'public')))
// Routes for front end content.
app.use('/', require('./routes/index'));
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', middleware.auth, require('./routes/users'));
app.use('/api/hosts', middleware.auth, require('./routes/hosts'));
// catch 404 and forward to error handler
// API routes for authentication.
app.use('/api/auth', require('./routes/auth'));
// API routes for working with users. All endpoints need to be have valid user.
app.use('/api/user', middleware.auth, require('./routes/user'));
// API routes for working with hosts. All endpoints need to be have valid user.
app.use('/api/host', middleware.auth, require('./routes/host'));
// Catch 404 and forward to error handler. If none of the above routes are
// used, this is what will be called.
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.message = 'Page not found'
err.status = 404;
next(err);
});
// error handler
// Error handler. This is where `next()` will go on error
app.use(function(err, req, res, next) {
// set locals, only providing error in development
console.error(err.status || res.status, err.name, req.method, req.url);
console.error(err.message);
console.error(err.stack);
console.error('=========================================');
console.error(err.status || res.status, req.url, err);
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.json({message: 'error!'});
res.json({name: err.name, message: err.message});
});
module.exports = app;
// Allow the express app to be exported into other files.
module.exports = app;

View File

@ -1,19 +1,17 @@
'use strict';
const Users = require('../models/users');
const {Auth} = require('../models/auth');
async function auth(req, res, next){
if(req.header('auth-token')){
let user = await Users.checkToken({token: req.header('auth-token')});
try{
let user = await Auth.checkToken({token: req.header('auth-token')});
if(user.username){
req.user = user;
return next();
}
}catch(error){
next(error);
}
return res.status(401).json({
message: 'Login failed'
});
}
module.exports = {auth};

View File

@ -0,0 +1,55 @@
const {promisify} = require('util');
const pam = require('authenticate-pam');
const authenticate = promisify(pam.authenticate);
const {User} = require('./user');
const {Token, AuthToken} = require('./token');
Auth = {}
Auth.errors = {}
Auth.errors.login = function(){
let error = new Error('PamLoginFailed');
error.name = 'PamLoginFailed';
error.message = `Invalid Credentials, login failed.`;
error.status = 401;
return error;
}
Auth.login = async function(data){
try{
let auth = await authenticate(data.username, data.password);
let user = await User.get(data);
let token = await AuthToken.add(user);
return {user, token}
}catch(error){
if (error == 'Authentication failure'){
throw this.errors.login()
}
throw error;
}
};
Auth.checkToken = async function(data){
try{
let token = await AuthToken.get(data);
if(token.is_valid){
return await User.get(token.created_by);
}
}catch(error){
throw this.errors.login();
}
};
Auth.logOut = async function(data){
try{
let token = await AuthToken.get(data);
await token.remove();
}catch(error){
throw error;
}
}
module.exports = {Auth, AuthToken};

19
nodejs/models/host.js Executable file
View File

@ -0,0 +1,19 @@
'use strict';
const Host = require('../utils/redis_model')({
_name: 'host',
_key: 'host',
_keyMap: {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}},
'updated_by': {default:"__NONE__", isRequired: false, type: 'string',},
'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'host': {isRequired: true, type: 'string', min: 3, max: 500},
'ip': {isRequired: true, type: 'string', min: 3, max: 500},
'targetport': {isRequired: true, type: 'number', min:0, max:65535},
'forcessl': {isRequired: false, default: true, type: 'boolean'},
'targetssl': {isRequired: false, default: false, type: 'boolean'},
}
});
module.exports = {Host};

View File

@ -1,72 +0,0 @@
'use strict';
const {promisify} = require('util');
const client = require('../redis');
async function getInfo(data){
let info = await client.HGETALL('host_' + data.host);
return info
}
async function listAll(){
try{
let hosts = await client.SMEMBERS('hosts');
return hosts;
}catch(error){
return new Error(error);
}
}
async function listAllDetail(){
try{
let out = [];
let hosts = await listAll();
for(let host of hosts){
out.push(await getInfo({host}));
}
return out
}catch(error){
return new Error(error);
}
}
async function add(data){
try{
await client.SADD('hosts', data.host);
await client.HSET('host_' + data.host, 'ip', data.ip);
await client.HSET('host_' + data.host, 'updated', (new Date).getTime());
await client.HSET('host_' + data.host, 'username', data.username);
await client.HSET('host_' + data.host, 'targetPort', data.targetPort);
if(data.forceSSL !== undefined){
await client.HSET('host_' + data.host, 'forcessl', !!data.forceSSL);
}
if(data.targetSSL !== undefined){
await client.HSET('host_' + data.host, 'targetssl', !!data.targetSSL);
}
} catch (error){
return new Error(error);
}
}
async function remove(data){
try{
await client.SREM('hosts', data.host);
let count = await client.DEL('host_' + data.host);
return count;
} catch(error) {
return new Error(error);
}
}
module.exports = {getInfo, listAll, listAllDetail, add, remove};

60
nodejs/models/token.js Normal file
View File

@ -0,0 +1,60 @@
'use strict';
const redis_model = require('../utils/redis_model')
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
const Token = function(data){
return redis_model({
_name: `token_${data.name}`,
_key: 'token',
_keyMap: Object.assign({}, {
'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
'created_on': {default: function(){return (new Date).getTime()}},
'updated_on': {default: function(){return (new Date).getTime()}, always: true},
'token': {default: UUID, type: 'string', min: 36, max: 36},
'is_valid': {default: true, type: 'boolean'}
}, data.keyMap || {})
});
};
Token.check = async function(data){
try{
return this.is_valid;
}catch(error){
return false
}
}
var InviteToken = Object.create(Token({
name: 'invite_test1',
keyMap:{
claimed_by: {default:"__NONE__", isRequired: false, type: 'string',}
}
}));
InviteToken.consume = async function(data){
try{
if(this.is_valid){
data['is_valid'] = false;
await this.update(data);
return true;
}
return false;
}catch(error){
throw error;
}
}
var AuthToken = Object.create(Token({
name: 'auth_test1',
}));
AuthToken.add = async function(data){
data.created_by = data.username;
return AuthToken.__proto__.add(data);
};
module.exports = {Token, InviteToken, AuthToken}

138
nodejs/models/user.js Executable file
View File

@ -0,0 +1,138 @@
'use strict';
const linuxUser = require('linux-sys-user').promise();
const objValidate = require('../utils/object_validate');
const {Token, InviteToken} = require('./token');
var User = {}
User.keyMap = {
'username': {isRequired: true, type: 'string', min: 3, max: 500},
'password': {isRequired: true, type: 'string', min: 3, max: 500},
}
User.list = async function(){
try{
let users = await linuxUser.getUsers();
for(let user of users){
delete user.password
}
return users;
}catch(error){
throw error;
}
};
User.get = async function(data){
try{
if(typeof data !== 'object'){
let username = data;
data = {};
data.username = username;
}
let user = await linuxUser.getUserInfo(data.username);
if(user){
let obj = Object.create(this);
Object.assign(obj, user);
return obj;
}else{
let error = new Error('UserNotFound');
error.name = 'UserNotFound';
error.message = `PAM:${data.username} does not exists`;
error.status = 404;
throw error;
}
}catch(error){
throw error;
}
};
User.exists = async function(data){
// Return true or false if the requested entry exists ignoring error's.
try{
await this.get(data);
return true
}catch(error){
return false;
}
};
User.add = async function(data) {
try{
data = objValidate.processKeys(this.keyMap, data);
let systemUser = await linuxUser.addUser(data.username);
await require('util').promisify(setTimeout)(500)
let systemUserPassword = await linuxUser.setPassword(data.username, data.password);
return this.get(data.username);
}catch(error){
if(error.message.includes('exists')){
let error = new Error('UserNameUsed');
error.name = 'UserNameUsed';
error.message = `PAM:${data.username} already exists`;
error.status = 409;
throw error;
}
throw error;
}
};
User.addByInvite = async function(data){
try{
let token = await InviteToken.get(data.token);
if(!token.is_valid){
let error = new Error('Token Invalid');
error.name = 'Token Invalid';
error.message = `Token is not valid or as allready been used. ${data.token}`;
error.status = 401;
throw error;
}
let user = await this.add(data);
if(user){
await token.consume({claimed_by: user.username});
return user;
}
}catch(error){
throw error;
}
};
User.setPassword = async function(data){
try{
if(!data.password1 || data.password1 !== data.password2){
throw new Error('PasswordMismatch');
}
await linuxUser.setPassword(this.username, data.password1);
return this;
}catch(error){
throw error;
}
};
User.invite = async function(){
try{
let token = await InviteToken.add({created_by: this.username});
return token;
}catch(error){
throw error;
}
};
module.exports = {User};

View File

@ -1,107 +0,0 @@
'use strict';
const {promisify} = require('util');
const client = require('../redis');
const linuxUser = require('linux-sys-user');
const pam = require('authenticate-pam');
const UUID = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};
const authenticate = promisify(pam.authenticate);
const addSSHtoUser = promisify(linuxUser.addSSHtoUser)
const getUserGroups = promisify(linuxUser.getUserGroups);
const verifySSHKey = promisify(linuxUser.verifySSHKey);
const addUser = promisify(linuxUser.addUser);
const setPassword = promisify(linuxUser.setPassword);
/*
Invite
*/
async function makeInviteToken(data){
let token = UUID();
await client.HSET('users_tokens', token, JSON.stringify({
created_by: data.username,
isAdmin: data.isAdmin,
invited: false
}));
return token;
}
async function checkInvite(data){
let token = await client.HGET('users_tokens', data.token);
return JSON.parse(token);
}
async function consumeInvite(data){
let invite = await checkInvite(data);
invite.invited = data.username;
await client.HSET('users_tokens', data.token, JSON.stringify(invite));
}
/*
Auth/ Auth token
*/
async function login(data){
try{
await authenticate(data.username, data.password);
return await getUserGroups(data.username);
}catch(error){
return false;
}
}
async function addToken(data){
let token = UUID();
await client.HSET('users_tokens', token, data.username);
return token;
}
async function checkToken(data){
let user = await client.HGET('users_tokens', data.token);
return {
username: user,
groups: (user && await getUserGroups(user))
}
}
async function addSSHkey(data){
try{
let user = await addSSHtoUser(data.username, data.key);
return true;
} catch (error) {
return error;
}
}
/*
Users
*/
async function add(data) {
let systemUser = await addUser(data.username);
let systemUserPassword = await setPassword(data.username, data.password);
}
async function verifyKey(data){
return await verifySSHKey(key)
}
async function ifUserExists(data){
const getUserInfo = promisify(linuxUser.getUserInfo);
return await getUserInfo(data.username);
}
module.exports = {login, add, addToken, checkToken, ifUserExists,
makeInviteToken, checkInvite, consumeInvite, addSSHkey, verifyKey};

2895
nodejs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,9 +15,7 @@
"authenticate-pam": "github:WeiAnAn/node-authenticate-pam",
"ejs": "^3.0.1",
"express": "~4.16.1",
"forever": "^1.0.0",
"linux-sys-user": "github:wmantly/linux-user",
"linux-user": "github:wmantly/linux-user",
"linux-sys-user": "^1.1.0",
"redis": "^2.8.0"
},
"license": "MIT",

View File

@ -1,90 +1,55 @@
'use strict';
const router = require('express').Router();
const Users = require('../models/users');
const {User} = require('../models/user');
const {Auth, AuthToken} = require('../models/auth');
/*
Password login
*/
router.post('/login', async function(req, res){
let username = req.body.username;
let password = req.body.password;
let groups = await Users.login({username, password});
if(groups){
router.post('/login', async function(req, res, next){
try{
let auth = await Auth.login(req.body);
return res.json({
login: true,
token: await Users.addToken({username}),
groups: groups,
});
}else{
return res.status(401).json({
login: false
});
}
});
/*
verify public ssh key
*/
router.post('/verifykey', async function(req, res){
let key = req.body.key;
try{
return res.json({
info: await Users.verifyKey(key)
token: auth.token.token,
});
}catch(error){
return res.status(400).json({
message: 'Key is not a public key file!'
});
next(error);
}
});
router.post('/invite/:token', async function(req, res, next) {
let username = req.body.username;
let password = req.body.password;
let token = req.params.token;
try{
req.body.token = req.params.token;
let user = await User.addByInvite(req.body);
let token = await AuthToken.add(user);
// make sure invite token is valid
let invite = await Users.checkInvite({token});
if(!invite || invite.invited){
return res.status(401).json({
message: 'Token not valid'
return res.json({
user: user.username,
token: token.token
});
}catch(error){
next(error);
}
// make sure requires fields are in
if(!username || !password){
return res.status(400).json({
message: 'Missing fields'
});
}
// make sure the requested user name can be used
if(await Users.ifUserExists({username})){
return res.status(409).json({
message: 'Username taken'
});
}
// create the new user
await Users.add({username, password, isAdmin: invite.isAdmin});
// consume the invite token
await Users.consumeInvite({token, username});
// send back API token for the new user
return res.json({
user: username,
token: await Users.addToken({username})
});
});
module.exports = router;
/*
verify public ssh key
*/
// router.post('/verifykey', async function(req, res){
// let key = req.body.key;
// try{
// return res.json({
// info: await Users.verifyKey(key)
// });
// }catch(error){
// return res.status(400).json({
// message: 'Key is not a public key file!'
// });
// }
// });

72
nodejs/routes/host.js Executable file
View File

@ -0,0 +1,72 @@
'use strict';
const router = require('express').Router();
const {Host} = require('../models/host');
router.get('/:host', async function(req, res, next){
try{
return res.json({
host: req.params.host,
results: await Host.get({host: req.params.host})
});
}catch(error){
return next(error);
}
});
router.get('/', async function(req, res, next){
try{
return res.json({
hosts: await Host[req.query.detail ? "listDetail" : "list"]()
});
}catch(error){
next(error)
}
});
router.put('/:host', async function(req, res, next){
try{
req.body.updated_by = req.user.username;
await Host.update(req.body, req.params.host);
return res.json({
message: `Host "${req.params.host}" updated.`
});
}catch(error){
return next(error);
}
});
router.post('/', async function(req, res, next){
try{
req.body.created_by = req.user.username;
await Host.add(req.body);
return res.json({
message: `Host "${req.body.host}" added.`
});
} catch (error){
next(error);
}
});
router.delete('/:host', async function(req, res, next){
try{
let host = await Host.get(req.params);
let count = await host.remove(host);
return res.json({
message: `Host ${req.params.host} deleted`,
});
}catch(error){
next(error);
}
});
module.exports = router;

View File

@ -1,76 +0,0 @@
'use strict';
const router = require('express').Router();
const Host = require('../models/hosts');
router.get('/:host', async function(req, res){
let host = req.params.host;
let info = await Host.getInfo({host});
return res.status(info ? 200 : 404).json({
host: req.params.host,
results: info
});
});
router.get('/', async function(req, res){
try{
return res.json({
hosts: req.query.detail ? await host.listAllDetail() : await Host.listAll()
});
}catch(error){
return res.status(500).json({message: `ERROR ${error}`});
}
});
router.post('/', async function(req, res){
let ip = req.body.ip;
let host = req.body.host;
let targetPort = req.body.targetPort;
if(!host || !ip || !targetPort ){
return res.status(400).json({
message: `Missing fields: ${!host ? 'host' : ''} ${!ip ? 'ip' : ''} ${!targetPort ? 'targetPort' : ''}`
});
}
try{
await Host.add({
host, ip, targetPort,
username: req.user.username,
forceSSL: req.body.forceSSL,
targetSSL: req.body.targetSSL,
});
return res.json({
message: `Host ${host} Added`
});
} catch (error){
return res.status(500).json({
message: `ERROR: ${error}`
});
}
});
router.delete('/:host', async function(req, res, next){
let host = req.params.host;
try{
let count = await Host.remove({host});
return res.json({
message: `Host ${host} deleted`,
});
}catch(error){
return res.status(500).json({
message: `ERROR: ${error}`
});
}
});
module.exports = router;

42
nodejs/routes/user.js Executable file
View File

@ -0,0 +1,42 @@
'use strict';
const router = require('express').Router();
const {User} = require('../models/user');
router.get('/me', async function(req, res){
try{
return res.json({username: req.user.username});
}catch(error){
next(error);
}
});
router.post('/invite', async function(req, res, next){
try{
let token = await req.user.invite();
return res.json({token: token.token});
}catch(error){
next(error);
}
});
router.post('/key', async function(req, res, next){
try{
let added = await User.addSSHkey({
username: req.user.username,
key: req.body.key
});
return res.status(added === true ? 200 : 400).json({
message: added
});
}catch(error){
next(error);
}
});
module.exports = router;

View File

@ -1,30 +0,0 @@
'use strict';
const router = require('express').Router();
const Users = require('../models/users');
router.get('/me', async function(req, res){
return res.json({username: req.user.username});
});
router.post('/invite', async function(req, res){
let token = await Users.makeInviteToken({
username: res.user
});
return res.json({token:token});
});
router.post('/key', async function(req, res){
let added = await Users.addSSHkey({
username: req.user.username,
key: req.body.key
});
return res.status(added === true ? 200 : 400).json({
message: added
});
});
module.exports = router;

View File

View File

@ -0,0 +1,80 @@
'use strict';
const process_type = {
number: function(key, value){
if(key.min && value < key.min) return `is to small, min ${key.min}.`
if(key.max && value > key.max) return `is to large, max ${key.max}.`
},
string: function(key, value){
if(key.min && value.length < key.min) return `is too short, min ${key.min}.`
if(key.max && value.length > key.max) return `is too short, max ${key.max}.`
}
}
function returnOrCall(value){
return typeof(value) === 'function' ? value() : value;
}
function processKeys(map, data, partial){
let errors = [];
let out = {};
for(let key of Object.keys(map)){
if(!map[key].always && partial && !data.hasOwnProperty(key)) continue;
if(!partial && map[key].isRequired && !data.hasOwnProperty(key)){
errors.push({key, message:`${key} is required.`});
continue;
}
if(data.hasOwnProperty(key) && typeof(data[key]) !== map[key].type){
errors.push({key, message:`${key} is not ${map[key].type} type.`});
continue;
}
out[key] = data.hasOwnProperty(key) && data[key] !== undefined ? data[key] : returnOrCall(map[key].default);
if(data.hasOwnProperty(key) && process_type[map[key].type]){
let typeError = process_type[map[key].type](map[key], data[key]);
if(typeError){
errors.push({key, message:`${key} ${typeError}`});
continue;
}
}
}
if(errors.length !== 0){
throw new ObjectValidateError(errors);
return {__errors__: errors};
}
return out;
}
function parseFromString(map, data){
let types = {
boolean: function(value){ return value === 'false' ? false : true },
number: Number,
string: String,
};
for(let key of Object.keys(data)){
if(map[key] && map[key].type){
data[key] = types[map[key].type](data[key]);
}
}
return data;
}
function ObjectValidateError(message) {
this.name = 'ObjectValidateError';
this.message = (message || {});
this.status = 422;
}
ObjectValidateError.prototype = Error.prototype;
module.exports = {processKeys, parseFromString, ObjectValidateError};

View File

@ -14,6 +14,7 @@ const _client = client();
module.exports = {
client: client,
HGET: promisify(_client.HGET).bind(_client),
HDEL: promisify(_client.HDEL).bind(_client),
SADD: promisify(_client.SADD).bind(_client),
SREM: promisify(_client.SREM).bind(_client),
DEL: promisify(_client.DEL).bind(_client),

191
nodejs/utils/redis_model.js Normal file
View File

@ -0,0 +1,191 @@
'use strict';
const client = require('../utils/redis');
const objValidate = require('../utils/object_validate');
let table = {};
table.get = async function(data){
try{
// if the data argument was passed as the index key value, make a data
// object and add the index key to it.
if(typeof data !== 'object'){
let key = data;
data = {};
data[this._key] = key;
}
// Get all the hash keys for the passed index key.
let res = await client.HGETALL(`${this._name}_${data[this._key]}`);
// If the redis query resolved to something, prepare the data.
if(res){
// Redis always returns strings, use the keyMap schema to turn them
// back to native values.
res = objValidate.parseFromString(this._keyMap, res);
// Make sure the index key in in the returned object.
res[this._key] = data[this._key];
// Create a instance for this redis entry.
var entry = Object.create(this);
// Insert the redis response into the instance.
Object.assign(entry, res);
// Return the instance to the caller.
return entry;
}
}catch(error){
throw error
}
let error = new Error('EntryNotFound');
error.name = 'EntryNotFound';
error.message = `${this._name}:${data[this._key]} does not exists`;
error.status = 404;
throw error;
};
table.exists = async function(data){
// Return true or false if the requested entry exists ignoring error's.
try{
await this.get(data);
return true
}catch(error){
return false;
}
};
table.list = async function(){
// return a list of all the index keys for this table.
try{
return await client.SMEMBERS(this._name);
}catch(error){
throw error;
}
};
table.listDetail = async function(){
// Return a list of the entries as instances.
let out = [];
for(let entry of await this.list()){
out.push(await this.get(entry));
}
return out
};
table.add = async function(data){
// Add a entry to this redis table.
try{
// Validate the passed data by the keyMap schema.
data = objValidate.processKeys(this._keyMap, data);
// Do not allow the caller to overwrite an existing index key,
if(data[this._key] && await this.exists(data)){
let error = new Error('EntryNameUsed');
error.name = 'EntryNameUsed';
error.message = `${this._name}:${data[this._key]} already exists`;
error.status = 409;
throw error;
}
// Add the key to the members for this redis table
await client.SADD(this._name, data[this._key]);
// Add the values for this entry.
for(let key of Object.keys(data)){
await client.HSET(`${this._name}_${data[this._key]}`, key, data[key]);
}
// return the created redis entry as entry instance.
return await this.get(data[this._key]);
} catch(error){
throw error;
}
};
table.update = async function(data, key){
// Update an existing entry.
try{
// If an index key is passed, we assume is passed, assume we are not
// part of an entry instance. Make one and recall this from from a entry
// instance,
if(key) return await (await this.get(key)).update(data);
// Check to see if entry name changed.
if(data[this._key] && data[this._key] !== this[this._key]){
// Merge the current data into with the updated data
let newData = Object.assign({}, this, data);
// Remove the updated failed so it doesnt keep it
delete newData.updated;
// Create a new record for the updated entry. If that succeeds,
// delete the old recored
if(await this.add(newData)) await this.remove();
}else{
// Update what ever fields that where passed.
// Validate the passed data, ignoring required fields.
data = objValidate.processKeys(this._keyMap, data, true);
// Loop over the data fields and apply them to redis
for(let key of Object.keys(data)){
this[key] = data[key];
await client.HSET(`${this._name}_${this[this._key]}`, key, data[key]);
}
}
return this;
} catch(error){
// Pass any error to the calling function
throw error;
}
};
table.remove = async function(data){
// Remove an entry from this table.
data = data || this;
try{
// Remove the index key from the tables members list.
await client.SREM(this._name, data[this._key]);
// Remove the entries hash values.
let count = await client.DEL(`${this._name}_${data[this._key]}`);
// Return the number of removed values to the caller.
return count;
} catch(error) {
throw error;
}
};
function Table(data){
// Create a table instance.
let obj = Object.create(table);
// Insert the user assigned options
Object.assign(obj, data);
// Return the table instance to the caller.
return obj;
};
module.exports = Table;