U improve websocket security

pull/220/head
vapao 2020-10-10 18:01:39 +08:00
parent 643b83894c
commit 6b6bbf16b1
14 changed files with 39 additions and 45 deletions

View File

@ -3,11 +3,9 @@
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from django_redis import get_redis_connection from django_redis import get_redis_connection
from apps.account.models import User
from apps.host.models import Host from apps.host.models import Host
from threading import Thread from threading import Thread
import json import json
import time
class ExecConsumer(WebsocketConsumer): class ExecConsumer(WebsocketConsumer):
@ -38,9 +36,8 @@ class ExecConsumer(WebsocketConsumer):
class SSHConsumer(WebsocketConsumer): class SSHConsumer(WebsocketConsumer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
kwargs = self.scope['url_route']['kwargs'] self.user = self.scope['user']
self.token = kwargs['token'] self.id = self.scope['url_route']['kwargs']['id']
self.id = kwargs['id']
self.chan = None self.chan = None
self.ssh = None self.ssh = None
@ -70,8 +67,7 @@ class SSHConsumer(WebsocketConsumer):
# print('Connection close') # print('Connection close')
def connect(self): def connect(self):
user = User.objects.filter(access_token=self.token).first() if self.user.has_host_perm(self.id):
if user and user.token_expired >= time.time() and user.is_active and user.has_host_perm(self.id):
self.accept() self.accept()
self._init() self._init()
else: else:

View File

@ -2,9 +2,13 @@
# Copyright: (c) <spug.dev@gmail.com> # Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from django.urls import path from django.urls import path
from .consumers import * from channels.routing import URLRouter
from consumer.middleware import AuthMiddleware
from consumer.consumers import *
websocket_urlpatterns = [ ws_router = AuthMiddleware(
path('ws/exec/<str:token>/', ExecConsumer), URLRouter([
path('ws/ssh/<str:token>/<int:id>/', SSHConsumer), path('ws/exec/<str:token>/', ExecConsumer),
] path('ws/ssh/<int:id>/', SSHConsumer),
])
)

View File

@ -105,10 +105,7 @@ def generate_random_str(length: int = 4, is_digits: bool = True) -> str:
def get_request_real_ip(headers: dict): def get_request_real_ip(headers: dict):
x_real_ip = headers.get('x-real-ip') x_real_ip = headers.get('x-forwarded-for')
if not x_real_ip: if not x_real_ip:
x_forwarded_for = headers.get('x-forwarded-for') x_real_ip = headers.get('x-real-ip', '')
if not x_forwarded_for: return x_real_ip.split(',')[0]
return ''
x_real_ip = x_forwarded_for.split(',')[0]
return x_real_ip

View File

@ -1,14 +1,12 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com> # Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from channels.routing import ProtocolTypeRouter, ChannelNameRouter, URLRouter from channels.routing import ProtocolTypeRouter, ChannelNameRouter
from consumer import routing, executors from consumer import routing, executors
application = ProtocolTypeRouter({ application = ProtocolTypeRouter({
'channel': ChannelNameRouter({ 'channel': ChannelNameRouter({
'ssh_exec': executors.SSHExecutor, 'ssh_exec': executors.SSHExecutor,
}), }),
'websocket': URLRouter( 'websocket': routing.ws_router
routing.websocket_urlpatterns
)
}) })

View File

@ -9,7 +9,10 @@ let Permission = {
permissions: [] permissions: []
}; };
export let X_TOKEN;
export function updatePermissions() { export function updatePermissions() {
X_TOKEN = localStorage.getItem('token');
Permission.isSuper = localStorage.getItem('is_supper') === 'true'; Permission.isSuper = localStorage.getItem('is_supper') === 'true';
Permission.hostPerms = JSON.parse(localStorage.getItem('host_perms') || '[]'); Permission.hostPerms = JSON.parse(localStorage.getItem('host_perms') || '[]');
Permission.permissions = JSON.parse(localStorage.getItem('permissions') || '[]'); Permission.permissions = JSON.parse(localStorage.getItem('permissions') || '[]');

View File

@ -5,7 +5,8 @@
*/ */
import http from 'axios' import http from 'axios'
import history from './history' import history from './history'
import {message} from 'antd'; import { X_TOKEN } from './functools';
import { message } from 'antd';
// response处理 // response处理
function handleResponse(response) { function handleResponse(response) {
@ -40,7 +41,7 @@ function handleResponse(response) {
http.interceptors.request.use(request => { http.interceptors.request.use(request => {
request.isInternal = request.url.startsWith('/api/'); request.isInternal = request.url.startsWith('/api/');
if (request.isInternal) { if (request.isInternal) {
request.headers['X-Token'] = localStorage.getItem('token') request.headers['X-Token'] = X_TOKEN
} }
request.timeout = request.timeout || 30000; request.timeout = request.timeout || 30000;
return request; return request;

View File

@ -6,10 +6,9 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Steps, Collapse, PageHeader, Spin, Tag, Button, Icon } from 'antd'; import { Steps, Collapse, PageHeader, Spin, Tag, Button, Icon } from 'antd';
import http from 'libs/http'; import { http, history, X_TOKEN } from 'libs';
import { AuthDiv } from 'components'; import { AuthDiv } from 'components';
import OutView from './OutView'; import OutView from './OutView';
import history from 'libs/history';
import styles from './index.module.css'; import styles from './index.module.css';
import store from './store'; import store from './store';
import lds from 'lodash'; import lds from 'lodash';
@ -79,7 +78,7 @@ class Ext1Index extends React.Component {
store.request.status = '2'; store.request.status = '2';
store.outputs = outputs; store.outputs = outputs;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/`); this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/?x-token=${X_TOKEN}`);
this.socket.onopen = () => { this.socket.onopen = () => {
this.socket.send('ok'); this.socket.send('ok');
}; };

View File

@ -6,10 +6,9 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Steps, Collapse, PageHeader, Spin, Tag, Button, Icon } from 'antd'; import { Steps, Collapse, PageHeader, Spin, Tag, Button, Icon } from 'antd';
import http from 'libs/http'; import { http, history, X_TOKEN } from 'libs';
import { AuthDiv } from 'components'; import { AuthDiv } from 'components';
import OutView from './OutView'; import OutView from './OutView';
import history from 'libs/history';
import styles from './index.module.css'; import styles from './index.module.css';
import store from './store'; import store from './store';
import lds from 'lodash'; import lds from 'lodash';
@ -80,7 +79,7 @@ class Ext1Index extends React.Component {
store.request.status = '2'; store.request.status = '2';
store.outputs = outputs; store.outputs = outputs;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/`); this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/?x-token=${X_TOKEN}`);
this.socket.onopen = () => { this.socket.onopen = () => {
this.socket.send('ok'); this.socket.send('ok');
}; };

View File

@ -7,7 +7,7 @@ import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Form, Input, Tag, Upload, message, Button } from 'antd'; import { Modal, Form, Input, Tag, Upload, message, Button } from 'antd';
import hostStore from 'pages/host/store'; import hostStore from 'pages/host/store';
import http from 'libs/http'; import { http, X_TOKEN } from 'libs';
import store from './store'; import store from './store';
import lds from 'lodash'; import lds from 'lodash';
@ -15,7 +15,6 @@ import lds from 'lodash';
class Ext2Form extends React.Component { class Ext2Form extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.token = localStorage.getItem('token');
this.state = { this.state = {
loading: false, loading: false,
uploading: false, uploading: false,
@ -112,7 +111,7 @@ class Ext2Form extends React.Component {
)} )}
</Form.Item> </Form.Item>
<Form.Item label="上传数据" help="通过数据传输动作来使用上传的文件。"> <Form.Item label="上传数据" help="通过数据传输动作来使用上传的文件。">
<Upload name="file" fileList={fileList} headers={{'X-Token': this.token}} beforeUpload={this.handleUpload} <Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={this.handleUpload}
data={{deploy_id: info.deploy_id}} onChange={this.handleUploadChange}> data={{deploy_id: info.deploy_id}} onChange={this.handleUploadChange}>
{fileList.length === 0 ? <Button loading={uploading} icon="upload">点击上传</Button> : null} {fileList.length === 0 ? <Button loading={uploading} icon="upload">点击上传</Button> : null}
</Upload> </Upload>

View File

@ -6,6 +6,7 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Collapse, Tooltip, Icon } from 'antd'; import { Modal, Collapse, Tooltip, Icon } from 'antd';
import { X_TOKEN } from 'libs';
import OutView from './OutView'; import OutView from './OutView';
import styles from './index.module.css'; import styles from './index.module.css';
import store from './store'; import store from './store';
@ -24,7 +25,7 @@ class ExecConsole extends React.Component {
componentDidMount() { componentDidMount() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${store.token}/`); this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${store.token}/?x-token=${X_TOKEN}`);
this.socket.onopen = () => { this.socket.onopen = () => {
this.socket.send('ok'); this.socket.send('ok');
for (let item of Object.values(store.outputs)) { for (let item of Object.values(store.outputs)) {

View File

@ -6,14 +6,13 @@
import React from 'react'; import React from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Modal, Form, Input, Select, Col, Button, Upload, message } from 'antd'; import { Modal, Form, Input, Select, Col, Button, Upload, message } from 'antd';
import http from 'libs/http'; import { http, X_TOKEN } from 'libs';
import store from './store'; import store from './store';
@observer @observer
class ComForm extends React.Component { class ComForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.token = localStorage.getItem('token');
this.state = { this.state = {
loading: false, loading: false,
uploading: false, uploading: false,
@ -200,7 +199,7 @@ class ComForm extends React.Component {
</Form.Item> </Form.Item>
</Form.Item> </Form.Item>
<Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥则优先使用该密钥。"> <Form.Item label="独立密钥" extra="默认使用全局密钥,如果上传了独立密钥则优先使用该密钥。">
<Upload name="file" fileList={fileList} headers={{'X-Token': this.token}} beforeUpload={this.handleUpload} <Upload name="file" fileList={fileList} headers={{'X-Token': X_TOKEN}} beforeUpload={this.handleUpload}
onChange={this.handleUploadChange}> onChange={this.handleUploadChange}>
{fileList.length === 0 ? <Button loading={uploading} icon="upload">点击上传</Button> : null} {fileList.length === 0 ? <Button loading={uploading} icon="upload">点击上传</Button> : null}
</Upload> </Upload>

View File

@ -5,7 +5,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Drawer, Breadcrumb, Table, Icon, Divider, Switch, Button, Progress, Modal, message } from 'antd'; import { Drawer, Breadcrumb, Table, Icon, Divider, Switch, Button, Progress, Modal, message } from 'antd';
import { http, uniqueId } from 'libs'; import { http, uniqueId, X_TOKEN } from 'libs';
import lds from 'lodash'; import lds from 'lodash';
import styles from './index.module.css' import styles from './index.module.css'
@ -129,7 +129,7 @@ class FileManager extends React.Component {
_updatePercent = token => { _updatePercent = token => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/`); this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/exec/${token}/?x-token=${X_TOKEN}`);
this.socket.onopen = () => this.socket.send('ok'); this.socket.onopen = () => this.socket.send('ok');
this.socket.onmessage = e => { this.socket.onmessage = e => {
if (e.data === 'pong') { if (e.data === 'pong') {
@ -145,9 +145,8 @@ class FileManager extends React.Component {
handleDownload = (name) => { handleDownload = (name) => {
const file = `/${this.state.pwd.join('/')}/${name}`; const file = `/${this.state.pwd.join('/')}/${name}`;
const token = localStorage.getItem('token');
const link = document.createElement('a'); const link = document.createElement('a');
link.href = `/api/file/object/?id=${this.id}&file=${file}&x-token=${token}`; link.href = `/api/file/object/?id=${this.id}&file=${file}&x-token=${X_TOKEN}`;
document.body.appendChild(link); document.body.appendChild(link);
const evt = document.createEvent("MouseEvents"); const evt = document.createEvent("MouseEvents");
evt.initEvent("click", false, false); evt.initEvent("click", false, false);

View File

@ -9,7 +9,7 @@ import { AuthDiv } from 'components';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'; import { FitAddon } from 'xterm-addon-fit';
import FileManager from './FileManager'; import FileManager from './FileManager';
import { http } from 'libs'; import { http, X_TOKEN } from 'libs';
import 'xterm/css/xterm.css'; import 'xterm/css/xterm.css';
import styles from './index.module.css'; import styles from './index.module.css';
@ -18,7 +18,6 @@ class WebSSH extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.id = props.match.params.id; this.id = props.match.params.id;
this.token = localStorage.getItem('token');
this.socket = null; this.socket = null;
this.term = new Terminal(); this.term = new Terminal();
this.container = null; this.container = null;
@ -37,7 +36,7 @@ class WebSSH extends React.Component {
const fitPlugin = new FitAddon(); const fitPlugin = new FitAddon();
this.term.loadAddon(fitPlugin); this.term.loadAddon(fitPlugin);
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/ssh/${this.token}/${this.id}/`); this.socket = new WebSocket(`${protocol}//${window.location.host}/api/ws/ssh/${this.id}/?x-token=${X_TOKEN}`);
this.socket.onmessage = e => this._read_as_text(e.data); this.socket.onmessage = e => this._read_as_text(e.data);
this.socket.onopen = () => { this.socket.onopen = () => {
this.term.open(this.container); this.term.open(this.container);

View File

@ -10,7 +10,7 @@ module.exports = function (app) {
target: 'http://127.0.0.1:8000', target: 'http://127.0.0.1:8000',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
headers: {'X-Real-IP': '127.0.0.1'}, headers: {'X-Real-IP': '1.1.1.1'},
pathRewrite: { pathRewrite: {
'^/api': '' '^/api': ''
} }