Commit 45df5811 authored by ibuler's avatar ibuler

Merge branch 'connect'

parents 6856fad0 a04d7725
......@@ -16,3 +16,4 @@ db.sqlite3
config.py
migrations/
*.log
host_rsa_key
......@@ -30,11 +30,17 @@ def get_object_or_none(model, **kwargs):
def encrypt(*args, **kwargs):
return signing.dumps(*args, **kwargs)
try:
return signing.dumps(*args, **kwargs)
except signing.BadSignature:
return ''
def decrypt(*args, **kwargs):
return signing.loads(*args, **kwargs)
try:
return signing.loads(*args, **kwargs)
except signing.BadSignature:
return ''
def date_expired_default():
......
......@@ -54,7 +54,7 @@ INSTALLED_APPS = [
'users.apps.UsersConfig',
'assets.apps.AssetsConfig',
'perms.apps.PermsConfig',
'webterminal.apps.WebterminalConfig',
# 'terminal.apps.TerminalConfig',
'ops.apps.OpsConfig',
'audits.apps.AuditsConfig',
'common.apps.CommonConfig',
......@@ -274,36 +274,36 @@ REST_FRAMEWORK = {
}
# This setting is required to override the Django's main loop, when running in
# development mode, such as ./manage runserver
WSGI_APPLICATION = 'ws4redis.django_runserver.application'
# WSGI_APPLICATION = 'ws4redis.django_runserver.application'
# URL that distinguishes websocket connections from normal requests
WEBSOCKET_URL = '/ws/'
# WEBSOCKET_URL = '/ws/'
# WebSocket Redis
WS4REDIS_CONNECTION = {
'host': CONFIG.REDIS_HOST or '127.0.0.1',
'port': CONFIG.REDIS_PORT or 6379,
'db': 2,
}
# WS4REDIS_CONNECTION = {
# 'host': CONFIG.REDIS_HOST or '127.0.0.1',
# 'port': CONFIG.REDIS_PORT or 6379,
# 'db': 2,
# }
# Set the number of seconds each message shall persisted
WS4REDIS_EXPIRE = 3600
# WS4REDIS_EXPIRE = 3600
WS4REDIS_HEARTBEAT = 'love you'
# WS4REDIS_HEARTBEAT = 'love you'
WS4REDIS_PREFIX = 'demo'
# WS4REDIS_PREFIX = 'demo'
SESSION_ENGINE = 'redis_sessions.session'
# SESSION_ENGINE = 'redis_sessions.session'
SESSION_REDIS_PREFIX = 'session'
# SESSION_REDIS_PREFIX = 'session'
SESSION_REDIS_HOST = CONFIG.REDIS_HOST
# SESSION_REDIS_HOST = CONFIG.REDIS_HOST
SESSION_REDIS_PORT = CONFIG.REDIS_PORT
# SESSION_REDIS_PORT = CONFIG.REDIS_PORT
SESSION_REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
# SESSION_REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
SESSION_REDIS_DB = CONFIG.REDIS_DB
# SESSION_REDIS_DB = CONFIG.REDIS_DB
# Custom User Auth model
......
......@@ -25,7 +25,6 @@ urlpatterns = [
url(r'^(api/)?users/', include('users.urls')),
url(r'^assets/', include('assets.urls')),
url(r'^perms/', include('perms.urls')),
url(r'^terminal/', include('webterminal.urls')),
]
......
......@@ -112,6 +112,12 @@ class User(AbstractUser):
else:
return True
@property
def is_valid(self):
if self.is_active and not self.is_expired:
return True
return False
@property
def private_key(self):
return decrypt(self._private_key)
......
......@@ -12,7 +12,8 @@ from django.utils.translation import ugettext as _
from paramiko.rsakey import RSAKey
from common.tasks import send_mail_async
from common.utils import reverse
from common.utils import reverse, get_object_or_none
from .models import User
try:
......@@ -147,3 +148,75 @@ def send_reset_ssh_key_mail(user):
logger.debug(message)
send_mail_async.delay(subject, message, recipient_list, html_message=message)
def validate_ssh_pk(text):
"""
Expects a SSH private key as string.
Returns a boolean and a error message.
If the text is parsed as private key successfully,
(True,'') is returned. Otherwise,
(False, <message describing the error>) is returned.
from https://github.com/githubnemo/SSH-private-key-validator/blob/master/validate.py
"""
if not text:
return False, 'No text given'
startPattern = re.compile("^-----BEGIN [A-Z]+ PRIVATE KEY-----")
optionPattern = re.compile("^.+: .+")
contentPattern = re.compile("^([a-zA-Z0-9+/]{64}|[a-zA-Z0-9+/]{1,64}[=]{0,2})$")
endPattern = re.compile("^-----END [A-Z]+ PRIVATE KEY-----")
def contentState(text):
for i in range(0, len(text)):
line = text[i]
if endPattern.match(line):
if i == len(text) - 1 or len(text[i + 1]) == 0:
return True, ''
else:
return False, 'At end but content coming'
elif not contentPattern.match(line):
return False, 'Wrong string in content section'
return False, 'No content or missing end line'
def optionState(text):
for i in range(0, len(text)):
line = text[i]
if line[-1:] == '\\':
return optionState(text[i + 2:])
if not optionPattern.match(line):
return contentState(text[i + 1:])
return False, 'Expected option, found nothing'
def startState(text):
if len(text) == 0 or not startPattern.match(text[0]):
return False, 'Header is wrong'
return optionState(text[1:])
return startState([n.strip() for n in text.splitlines()])
def check_user_is_valid(**kwargs):
password = kwargs.pop('password', None)
public_key = kwargs.pop('public_key', None)
user = get_object_or_none(User, **kwargs)
if password and not user.check_password(password):
user = None
if public_key and not user.public_key == public_key:
user = None
if user and user.is_valid:
return user
return None
from django.contrib import admin
# Register your models here.
from __future__ import unicode_literals
from django.apps import AppConfig
class WebterminalConfig(AppConfig):
name = 'webterminal'
from __future__ import unicode_literals
from django.db import models
# Create your models here.
{% extends 'base.html' %}
{% block content %}
<div class="container">
<div id="term">
</div>
</div>
<div class="termChangBar">
<input type="number" min="100" value="100" placeholder="col" id="term-col"/>
<input type="number" min="35" value="35" placeholder="row" id="term-row"/>
<button id="col-row">修改窗口大小</button>
</div>
{% endblock %}
{% block custom_foot_js %}
<script type="application/javascript" src="/static/js/jquery-2.1.1.js"></script>
<script type="application/javascript" src="/static/js/term.js"></script>
<script>/**
* Created by liuzheng on 3/3/16.
*/
var rowHeight = 1;
var colWidth = 1;
function WSSHClient() {
}
WSSHClient.prototype._generateEndpoint = function (options) {
console.log(options);
if (window.location.protocol == 'https:') {
var protocol = 'wss://';
} else {
var protocol = 'ws://';
}
var endpoint = protocol + document.URL.match(RegExp('//(.*?)/'))[1] + '/ws/foobar?subscribe-broadcast&publish-broadcast&echo';
return endpoint;
};
WSSHClient.prototype.connect = function (options) {
var endpoint = this._generateEndpoint(options);
if (window.WebSocket) {
this._connection = new WebSocket(endpoint);
}
else if (window.MozWebSocket) {
this._connection = MozWebSocket(endpoint);
}
else {
options.onError('WebSocket Not Supported');
return;
}
this._connection.onopen = function () {
options.onConnect();
};
this._connection.onmessage = function (evt) {
try {
options.onData(evt.data);
} catch (e) {
var data = JSON.parse(evt.data.toString());
options.onError(data.error);
}
};
this._connection.onclose = function (evt) {
options.onClose();
};
};
WSSHClient.prototype.send = function (data) {
this._connection.send(JSON.stringify({'data': data}));
};
function openTerminal(options) {
var client = new WSSHClient();
var rowHeight, colWidth;
try {
rowHeight = localStorage.getItem('term-row');
colWidth = localStorage.getItem('term-col');
} catch (err) {
rowHeight = 35;
colWidth = 100
}
if (rowHeight) {
} else {
rowHeight = 35
}
if (colWidth) {
} else {
colWidth = 100
}
var term = new Terminal({
rows: rowHeight,
cols: colWidth,
useStyle: true,
screenKeys: true
});
term.open();
term.on('data', function (data) {
client.send(data)
});
$('.terminal').detach().appendTo('#term');
//term.resize(colWidth, rowHeight);
term.write('Connecting...');
client.connect($.extend(options, {
onError: function (error) {
term.write('Error: ' + error + '\r\n');
},
onConnect: function () {
// Erase our connecting message
client.send({'resize': {'rows': rowHeight, 'cols': colWidth}});
term.write('\r');
},
onClose: function () {
term.write('Connection Reset By Peer');
},
onData: function (data) {
if (data == "love you")
console.log(data);
else
term.write(data);
}
}));
//rowHeight = 0.0 + 1.00 * $('.terminal').height() / 24;
//colWidth = 0.0 + 1.00 * $('.terminal').width() / 80;
return {'term': term, 'client': client};
}
//function resize() {
// $('.terminal').css('width', window.innerWidth - 25);
// console.log(window.innerWidth);
// console.log(window.innerWidth - 10);
// var rows = Math.floor(window.innerHeight / rowHeight) - 2;
// var cols = Math.floor(window.innerWidth / colWidth) - 1;
//
// return {rows: rows, cols: cols};
//}
$(document).ready(function () {
var options = {};
$('#ssh').show();
var term_client = openTerminal(options);
console.log(rowHeight);
// by liuzheng712 because it will bring record bug
//window.onresize = function () {
// var geom = resize();
// console.log(geom);
// term_client.term.resize(geom.cols, geom.rows);
// term_client.client.send({'resize': {'rows': geom.rows, 'cols': geom.cols}});
// $('#ssh').show();
//}
try {
$('#term-row')[0].value = localStorage.getItem('term-row');
$('#term-col')[0].value = localStorage.getItem('term-col');
} catch (err) {
$('#term-row')[0].value = 35;
$('#term-col')[0].value = 100;
}
$('#col-row').click(function () {
var col = $('#term-col').val();
var row = $('#term-row').val();
localStorage.setItem('term-col', col);
localStorage.setItem('term-row', row);
term_client.term.resize(col, row);
term_client.client.send({'resize': {'rows': row, 'cols': col}});
$('#ssh').show();
});
$(".terminal").mouseleave(function () {
$(".termChangBar").slideDown();
});
$(".terminal").mouseenter(function () {
$(".termChangBar").slideUp();
})
});</script>
{% endblock %}
from django.test import TestCase
# Create your tests here.
# coding:utf-8
from django.conf.urls import url
from .views import *
from django.contrib import admin
admin.autodiscover()
app_name = 'webterminal'
urlpatterns = [
url(r'^$', TerminalView.as_view(), name='webterminal'),
]
\ No newline at end of file
from django.shortcuts import render
from django.urls import reverse_lazy
from django.db.models import Q
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.detail import DetailView
from django.views.generic.base import TemplateView
from django.views import View
from django.http import HttpResponse
from ws4redis.redis_store import RedisMessage
from ws4redis.publisher import RedisPublisher
from django.conf import settings
# Create your views here.
class TerminalView(TemplateView):
template_name = 'main.html'
def get(self, request, *args, **kwargs):
welcome = RedisMessage('Hello everybody') # create a welcome message to be sent to everybody
RedisPublisher(facility='foobar', broadcast=True).publish_message(welcome)
return super(TerminalView, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
redis_publisher = RedisPublisher(facility='foobar', groups=[request.POST.get('group')])
message = RedisMessage(request.POST.get('message'))
redis_publisher.publish_message(message)
return HttpResponse('OK')
......@@ -70,6 +70,10 @@ class Config:
# EMAIL_USE_TLS = False # If port is 587, set True
# EMAIL_SUBJECT_PREFIX = '[Jumpserver] '
# SSH use password or public key for auth
SSH_PASSWORD_AUTH = False
SSH_PUBLIC_KEY_AUTH = True
def __init__(self):
pass
......
......@@ -21,3 +21,6 @@ django-simple-captcha==0.5.2
django-formtools==1.0
sshpubkeys==2.2.0
djangorestframework-bulk==0.2.1
python-gssapi==0.6.4
tornado==4.4.2
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
if __name__ == '__main__':
pass
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import logging
import os
BASE_DIR = os.path.dirname(os.path.abspath(__name__))
class Config:
SSH_HOST = ''
SSH_PORT = 2200
LOG_LEVEL = 'INFO'
LOG_DIR = os.path.join(BASE_DIR, 'logs')
LOG_FILENAME = 'ssh_server.log'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'main': {
'datefmt': '%Y-%m-%d %H:%M:%S',
'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s',
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'null': {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'main',
'stream': 'ext://sys.stdout',
},
'file': {
'level': 'DEBUG',
'class': 'logging.handlers.TimedRotatingFileHandler',
'formatter': 'main',
'filename': os.path.join(LOG_DIR, LOG_FILENAME),
'when': 'D',
'backupCount': 10,
},
},
'loggers': {
'jumpserver': {
'handlers': ['console', 'file'],
# 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info')
'level': LOG_LEVEL,
'propagate': True,
},
'jumpserver.web_ssh_server': {
'handlers': ['console', 'file'],
# 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info')
'level': LOG_LEVEL,
'propagate': True,
},
'jumpserver.ssh_server': {
'handlers': ['console', 'file'],
# 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info')
'level': LOG_LEVEL,
'propagate': True,
}
}
}
def __init__(self):
pass
def __getattr__(self, item):
return None
class DevelopmentConfig(Config):
pass
class ProductionConfig(Config):
pass
class TestingConfig(Config):
pass
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig,
}
env = 'default'
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import logging
import os
BASE_DIR = os.path.dirname(os.path.abspath(__name__))
class Config:
LOG_LEVEL = 'INFO'
LOG_DIR = os.path.join(BASE_DIR, 'logs')
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'main': {
'datefmt': '%Y-%m-%d %H:%M:%S',
'format': '%(asctime)s [%(module)s %(levelname)s] %(message)s',
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'null': {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'main'
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'formatter': 'main',
'filename': LOG_DIR,
},
},
'loggers': {
'jumpserver': {
'handlers': ['console', 'file'],
# 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info')
'level': LOG_LEVEL,
},
'jumpserver.web_ssh_server': {
'handlers': ['console', 'file'],
# 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info')
'level': LOG_LEVEL,
},
'jumpserver.ssh_server': {
'handlers': ['console', 'file'],
# 'level': LOG_LEVEL_CHOICES.get(LOG_LEVEL, None) or LOG_LEVEL_CHOICES.get('info')
'level': LOG_LEVEL,
}
}
}
def __init__(self):
pass
def __getattr__(self, item):
return None
class DevelopmentConfig(Config):
pass
class ProductionConfig(Config):
pass
class TestingConfig(Config):
pass
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'testing': TestingConfig,
'default': DevelopmentConfig,
}
env = 'default'
if __name__ == '__main__':
pass
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
__version__ = '0.3.3'
import sys
import os
import base64
import time
from binascii import hexlify
import sys
import threading
from multiprocessing.process import Process
import traceback
import tty
import termios
import struct
import fcntl
import signal
import socket
import select
import errno
import paramiko
import django
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
APP_DIR = os.path.join(os.path.dirname(BASE_DIR), 'apps')
sys.path.append(APP_DIR)
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
try:
django.setup()
except IndexError:
pass
from django.conf import settings
from users.utils import ssh_key_gen, check_user_is_valid
from utils import get_logger, SSHServerException, control_char
logger = get_logger(__name__)
paramiko.util.log_to_file(os.path.join(BASE_DIR, 'logs', 'paramiko.log'))
class SSHServer(paramiko.ServerInterface):
host_key_path = os.path.join(BASE_DIR, 'host_rsa_key')
channel_pools = []
def __init__(self, client, addr):
self.event = threading.Event()
self.change_window_size_event = threading.Event()
self.client = client
self.addr = addr
self.username = None
self.user = None
self.channel_width = None
self.channel_height = None
@classmethod
def host_key(cls):
return cls.get_host_key()
@classmethod
def get_host_key(cls):
logger.debug("Get ssh server host key")
if not os.path.isfile(cls.host_key_path):
cls.host_key_gen()
return paramiko.RSAKey(filename=cls.host_key_path)
@classmethod
def host_key_gen(cls):
logger.debug("Generate ssh server host key")
ssh_key, ssh_pub_key = ssh_key_gen()
with open(cls.host_key_path, 'w') as f:
f.write(ssh_key)
def check_channel_request(self, kind, chanid):
if kind == 'session':
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_auth_password(self, username, password):
self.user = user = check_user_is_valid(username=username, password=password)
if self.user:
self.username = username = user.username
logger.info('Accepted password for %(username)s from %(host)s port %(port)s ' % {
'username': username,
'host': self.addr[0],
'port': self.addr[1],
})
return paramiko.AUTH_SUCCESSFUL
else:
logger.info('Authentication password failed for %(username)s from %(host)s port %(port)s ' % {
'username': username,
'host': self.addr[0],
'port': self.addr[1],
})
return paramiko.AUTH_FAILED
def check_auth_publickey(self, username, public_key):
self.user = user = check_user_is_valid(username=username, public_key=public_key)
if self.user:
self.username = username = user.username
logger.info('Accepted public key for %(username)s from %(host)s port %(port)s ' % {
'username': username,
'host': self.addr[0],
'port': self.addr[1],
})
return paramiko.AUTH_SUCCESSFUL
else:
logger.info('Authentication public key failed for %(username)s from %(host)s port %(port)s ' % {
'username': username,
'host': self.addr[0],
'port': self.addr[1],
})
return paramiko.AUTH_FAILED
def get_allowed_auths(self, username):
auth_method_list = []
if settings.CONFIG.SSH_PASSWORD_AUTH:
auth_method_list.append('password')
if settings.CONFIG.SSH_PUBLICK_KEY_AUTH:
auth_method_list.append('publickey')
return ','.join(auth_method_list)
def check_channel_shell_request(self, channel):
self.event.set()
self.__class__.channel_pools.append(channel)
channel.username = self.username
channel.addr = self.addr
return True
def check_channel_pty_request(self, channel, term, width, height, pixelwidth,
pixelheight, modes):
channel.change_window_size_event = threading.Event()
channel.width = width
channel.height = height
return True
def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight):
channel.change_window_size_event.set()
channel.width = width
channel.height = height
return True
class BackendServer:
def __init__(self, host, port, username):
self.host = host
self.port = port
self.username = username
self.ssh = None
self.channel = None
def connect(self, term='xterm', width=80, height=24, timeout=10):
self.ssh = ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
ssh.connect(hostname=self.host, port=self.port, username=self.username, password=self.host_password,
pkey=self.host_private_key, look_for_keys=False, allow_agent=True, compress=True, timeout=timeout)
except Exception:
logger.warning('Connect backend server %s failed' % self.host)
return None
self.channel = channel = ssh.invoke_shell(term=term, width=width, height=height)
logger.info('Connect backend server %(username)s@%(host)s:%(port)s successfully' % {
'username': self.username,
'host': self.host,
'port': self.port,
})
channel.settimeout(100)
channel.host = self.host
channel.port = self.port
channel.username = self.username
return channel
@property
def host_password(self):
return 'redhat'
@property
def host_private_key(self):
return None
class Navigation:
def __init__(self, username, client_channel):
self.username = username
self.client_channel = client_channel
def display_banner(self):
client_channel = self.client_channel
client_channel.send(control_char.clear)
client_channel.send('\r\n\r\n\t\tWelcome to use Jumpserver open source system !\r\n\r\n')
client_channel.send('If you find some bug please contact us <ibuler@qq.com>\r\n')
client_channel.send('See more at https://www.jumpserver.org\r\n')
# client_channel.send(self.username)
def display(self):
self.display_banner()
def return_to_connect(self):
pass
class ProxyChannel:
ENTER_CHAR = ['\r', '\n', '\r\n']
input_data = []
output_data = []
def __init__(self, client_channel, backend_channel, client_addr):
self.client_channel = client_channel
self.backend_channel = backend_channel
self.client_addr = client_addr
self.in_input_mode = True
def stream_flow(self, input_=None, output_=None):
if input_:
self.in_input_mode = True
if input_ in ['\r', '\n', '\r\n']:
self.in_input_mode = False
if output_:
print(''.join(self.__class__.output_data))
if not self.in_input_mode:
command = ''.join(self.__class__.output_data)
del self.__class__.output_data
self.__class__.output_data = []
self.__class__.output_data.append(output_)
def proxy(self):
client_channel = self.client_channel
backend_channel = self.backend_channel
client_addr = self.client_addr
while True:
r, w, x = select.select([client_channel, backend_channel], [], [])
if client_channel.change_window_size_event.is_set():
backend_channel.resize_pty(width=client_channel.width, height=client_channel.height)
if client_channel in r:
self.in_input_mode = True
client_data = client_channel.recv(1024)
if client_data in self.__class__.ENTER_CHAR:
self.in_input_mode = False
command = ''.join(self.__class__.output_data)
print('########### command ##########')
print(command)
print('########### end command ##########')
del self.__class__.output_data
self.__class__.output_data = []
backend_channel.send(client_data)
output = ''.join(self.__class__.output_data)
print('>>>>>>>>>>> output <<<<<<<<<<')
print(output)
print('>>>>>>>>>>> end output <<<<<<<<<<')
continue
if len(client_data) == 0:
logger.info('Logout from ssh server %(host)s: %(username)s' % {
'host': client_addr[0],
'username': client_channel.username,
})
break
backend_channel.send(client_data)
if backend_channel in r:
backend_data = backend_channel.recv(1024)
if len(backend_data) == 0:
client_channel.send('Disconnect from %s \r\n' % backend_channel.host)
client_channel.close()
logger.info('Logout from backend server %(host)s: %(username)s' % {
'host': backend_channel.host,
'username': backend_channel.username,
})
break
self.__class__.output_data.append(backend_data)
client_channel.send(backend_data)
class JumpServer:
backend_server_pools = []
backend_channel_pools = []
client_channel_pools = []
CONTROL_CHAR = {
'clear': ''
}
def __init__(self):
self.listen_host = '0.0.0.0'
self.listen_port = 2222
def display_navigation(self, username, client_channel):
nav = Navigation(username, client_channel)
nav.display()
return 'j', 22, 'root'
def get_client_channel(self, client, addr):
transport = paramiko.Transport(client, gss_kex=False)
transport.set_gss_host(socket.getfqdn(""))
try:
transport.load_server_moduli()
except:
logger.warning('Failed to load moduli -- gex will be unsupported.')
raise
transport.add_server_key(SSHServer.get_host_key())
ssh_server = SSHServer(client, addr)
try:
transport.start_server(server=ssh_server)
except paramiko.SSHException:
logger.warning('SSH negotiation failed.')
client_channel = transport.accept(20)
if client_channel is None:
logger.warning('No ssh channel get.')
return None
self.__class__.client_channel_pools.append(client_channel)
if not ssh_server.event.is_set():
logger.warning('Client never asked for a shell.')
return client_channel
def get_backend_channel(self, host, port, username, term='xterm', width=80, height=24):
backend_server = BackendServer(host, port, username)
backend_channel = backend_server.connect(term=term, width=width, height=height)
if backend_channel is None:
logger.warning('Connect %(username)s@%(host)s:%(port)s failed' % {
'username': username,
'host': host,
'port': port,
})
return None
self.__class__.backend_server_pools.append(backend_server)
self.__class__.backend_channel_pools.append(backend_channel)
return backend_channel
def command_flow(self, input_=None, output_=None):
pass
def handle_ssh_request(self, client, addr):
logger.info("Get ssh request from %(host)s:%(port)s" % {
'host': addr[0],
'port': addr[1],
})
try:
client_channel = self.get_client_channel(client, addr)
if client_channel is None:
client.close()
return
host, port, username = self.display_navigation('root', client_channel)
backend_channel = self.get_backend_channel(host, port, username,
width=client_channel.width,
height=client_channel.height)
if backend_channel is None:
client.shutdown()
client.close()
client.send('Close')
return
proxy_channel = ProxyChannel(client_channel, backend_channel, addr)
proxy_channel.proxy()
# Todo: catch other exception
except IndexError:
logger.info('Close with server %s from %s' % (addr[0], addr[1]))
sys.exit(100)
def listen(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((self.listen_host, self.listen_port))
sock.listen(5)
print(time.ctime())
print('Jumpserver version %s, more see https://www.jumpserver.org' % __version__)
print('Starting ssh server at %(host)s:%(port)s' % {'host': self.listen_host, 'port': self.listen_port})
print('Quit the server with CONTROL-C.')
while True:
try:
client, addr = sock.accept()
thread = threading.Thread(target=self.handle_ssh_request, args=(client, addr))
thread.daemon = True
thread.start()
except Exception as e:
logger.error('Bind failed: ' + str(e))
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
server = JumpServer()
try:
server.listen()
except KeyboardInterrupt:
sys.exit(1)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
import logging
from logging.config import dictConfig
from ssh_config import config, env
CONFIG_SSH_SERVER = config.get(env)
def get_logger(name):
dictConfig(CONFIG_SSH_SERVER.LOGGING)
return logging.getLogger('jumpserver.%s' % name)
class ControlChar:
CHARS = {
'clear': '\x1b[H\x1b[2J',
}
def __init__(self):
pass
def __getattr__(self, item):
return self.__class__.CHARS.get(item, '')
class SSHServerException(Exception):
pass
control_char = ControlChar()
# -*- coding: utf-8 -*-
#
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment