巡風xunfeng----巡風源碼閱讀與分析html
巡風是一款適用於企業內網的漏洞快速應急、巡航掃描系統,經過搜索功能可清晰的瞭解內部網絡資產分佈狀況,而且可指定漏洞插件對搜索結果進行快速漏洞檢測並輸出結果報表。python
環境:git
巡風是基於python的flask框架寫的,數據庫爲mongodb。github
可安裝在Windows OSX Linux Dockerweb
Python2.7 pip mongodbmongodb
安裝:數據庫
我安裝在window,用於簡單閱讀代碼和調試。json
https://github.com/ysrc/xunfeng flask
下載後跟着官網的window安裝教程便可。網絡
而後運行 Run.bat (得使用管理員運行,否則沒反應。。)
安裝成功。
閱讀:
使用的ide爲pycharm
Run.bat
mogod.exe 用於啓動mongodb
Run.py 啓動web網站
Aider.py # 輔助驗證腳本
VulScan.py # 漏洞檢測引擎
NAScan.py # 網絡資產信息抓取引擎
Run.py
from views.View import app if __name__ == '__main__': #app.debug = True app.run(threaded=True, port=8888,host='')
去到views/View.py
一共有24個方法。
一個方法一個方法來看。
1.Search()
# 搜索頁 @app.route('/filter') @logincheck def Search(): return render_template('search.html')
@logincheck 使用了裝飾器函數。跟過去查看views/lib/Login.py
# 登陸狀態檢查 def logincheck(f): @wraps(f) def wrapper(*args, **kwargs): try: if session.has_key('login'): if session['login'] == 'loginsuccess': return f(*args, **kwargs) else: return redirect(url_for('Login')) else: return redirect(url_for('Login')) except Exception, e: print e return redirect(url_for('Error')) return wrapper
若是seesion中的login等於loginsuccess 就繼續執行view.py下的函數。不然跳轉掉Error模板。就是檢測是否有登陸。
回到Search() 加載search.html模板
2.Deleteall()
# 刪除全部 @app.route('/deleteall', methods=['post']) @logincheck @anticsrf def Deleteall(): Mongo.coll['Task'].remove({}) return 'success'
先判斷了登陸狀態,多了一個@anticsrf 裝飾器函數。跟過去查看views/lib/AntiCSRF.py
# 檢查referer def anticsrf(f): @wraps(f) def wrapper(*args, **kwargs): try: if request.referrer and request.referrer.replace('http://', '').split('/')[0] == request.host: return f(*args, **kwargs) else: return redirect(url_for('NotFound')) except Exception, e: print e return redirect(url_for('Error')) return wrapper
判斷是否有referrer頭,並且將http://替換成空 再分割取第一部分,也就是取出網站的host,而後與本站host相比較。看是否一致。不同的話,跳轉404頁面。不然就繼續執行。
@anticsrf 就是防止CSRF漏洞的。
回到Deleteall()
Mongo 跟過去發現是鏈接mongoDB。選擇Task這個數據表,移除全部數據。
就是將任務總數所有刪除。
3.Main()
# 搜索結果頁 @app.route('/') @logincheck def Main(): q = request.args.get('q', '') page = int(request.args.get('page', '1')) plugin = Mongo.coll['Plugin'].find() # 插件列表 plugin_type = plugin.distinct('type') # 插件類型列表 if q: # 基於搜索條件顯示結果 result = q.strip().split(';') query = querylogic(result) cursor = Mongo.coll['Info'].find(query).sort('time', -1).limit(page_size).skip((page - 1) * page_size) return render_template('main.html', item=cursor, plugin=plugin, itemcount=cursor.count(), plugin_type=plugin_type, query=q) else: # 自定義,無任何結果,用戶手工添加 return render_template('main.html', item=[], plugin=plugin, itemcount=0, plugin_type=plugin_type)
判斷是否登陸(下同)
先獲取傳入的q 和page 。
plugin = Mongo.coll['Plugin'].find() #鏈接數據庫,列出Plugin中全部清單。 plugin_type = plugin.distinct('type') #從查詢的全部清單裏面獲取名字是 type的數據。
而後將q進行分割「;」主要是分割相似這種的 q= 127.0.0.1;127.8.8.1
分紅列表傳入querylogic()函數。 跟過去看看views/lib/QueryLogic.py(詳細:http://www.javashuo.com/article/p-xzajjajg-es.html)
將搜索的值q轉成mongoDB能查詢的語句。
cursor = Mongo.coll['Info'].find(query).sort('time', -1).limit(page_size).skip((page - 1) * page_size)
在info表裏把條件代入查詢sort()排序 limit()分頁
最後傳給視圖
4.Getplugin()
# 獲取插件信息異步 @app.route('/getplugin', methods=['get', 'post']) @logincheck def Getplugin(): type = request.form.get('type', '') risk = request.form.get('risk', '') search = request.form.get('search', '') query = {} if type: query['type'] = type if risk: query['level'] = risk if search: search = unquote(search) query['name'] = {"$regex": search, '$options': 'i'} cursor = Mongo.coll['Plugin'].find(query) rsp = [] for i in cursor: result = {'name': i['name'], 'info': i['info']} rsp.append(result) return json.dumps(rsp)
獲取了type risk search 是否有值
沒有的話,就所有查詢。有的話 在Plugin表代入條件查詢。而後將插件名字和信息轉json格式返回。
5.Addtask()
# 新增任務異步 @app.route('/addtask', methods=['get', 'post']) @logincheck @anticsrf def Addtask(): title = request.form.get('title', '') plugin = request.form.get('plugin', '') condition = unquote(request.form.get('condition', '')) plan = request.form.get('plan', 0) ids = request.form.get('ids', '') isupdate = request.form.get('isupdate', '0') resultcheck = request.form.get('resultcheck', '0') result = 'fail' if plugin: targets = [] if resultcheck == 'true': # 結果集全選 list = condition.strip().split(';') query = querylogic(list) cursor = Mongo.coll['Info'].find(query) for i in cursor: tar = [i['ip'], i['port']] targets.append(tar) else: # 當前頁結果選擇 for i in ids.split(','): tar = [i.split(':')[0], int(i.split(':')[1])] targets.append(tar) temp_result = True for p in plugin.split(','): query = querylogic(condition.strip().split(';')) item = {'status': 0, 'title': title, 'plugin': p, 'condition': condition, 'time': datetime.now(), 'target': targets, 'plan': int(plan), 'isupdate': int(isupdate), 'query': dumps(query)} insert_reuslt = Mongo.coll['Task'].insert(item) if not insert_reuslt: temp_result = False if temp_result: result = 'success' return result
先獲取了頁面傳了的值 先默認result爲fail
沒有plugin的話直接返回fail
有的話,先判斷結果集是否全選,將結果集的ip和port都加入列表,不然將當前頁的ip將入列表。 而後執行插入。成功返回success
6.Task()
# 任務列表頁面 @app.route('/task') @logincheck def Task(): page = int(request.args.get('page', '1')) cursor = Mongo.coll['Task'].find().sort('time', -1).limit(page_size).skip((page - 1) * page_size) return render_template('task.html', item=cursor)
查詢出任務信息,展現。
7.Recheck()
# 複測任務異步 @app.route('/taskrecheck') @logincheck @anticsrf def Recheck(): tid = request.args.get('taskid', '') task = Mongo.coll['Task'].find_one({'_id': ObjectId(tid)}) result = 'fail' if task and task['plan'] == 0 and task['status'] == 2: # 一次性任務,而且已經掃描完成 result = Mongo.coll['Task'].update({'_id': ObjectId(tid)}, {'$set': {'status': 0}}) if result: result = 'success' return result
找到任務後,判斷掃描完成後,更新數據庫。返回success。
8.TaskDetail()
# 任務詳情頁面 @app.route('/taskdetail') @logincheck def TaskDetail(): id = request.args.get('taskid', '') page = int(request.args.get('page', '1')) taskdate = request.args.get('taskdate', "") plugin_name = '' task_info = Mongo.coll['Task'].find_one({'_id': ObjectId(id)}) if task_info: plugin_name = task_info['plugin'] vulcount = 0 lastscan = Mongo.coll["Result"].distinct('task_date', {'task_id': ObjectId(id)}) result_list = [] if len(lastscan) > 0: lastscan.sort(reverse=True) if taskdate: # 根據掃描批次查看結果 cursor = Mongo.coll['Result'].find( {'task_id': ObjectId(id), 'task_date': datetime.strptime(taskdate, "%Y-%m-%d %H:%M:%S.%f")}).sort( 'time', -1).limit(page_size).skip((page - 1) * page_size) else: # 查看最新批次結果 taskdate = lastscan[0].strftime("%Y-%m-%d %H:%M:%S.%f") cursor = Mongo.coll['Result'].find( {'task_id': ObjectId(id), 'task_date': lastscan[0]}).sort('time', -1).limit(page_size).skip( (page - 1) * page_size) vulcount = cursor.count() for _ in cursor: result_list.append( {'ip': _['ip'], 'port': _['port'], 'info': _['info'], 'vul_level': _['vul_info']['vul_level'], 'time': _['time']}) # 速度優化,數據量多采起不一樣的方式查詢 if len(result_list) > 100: ip_hostname = {} hostname = Mongo.coll['Info'].aggregate( [{'$match': {'hostname': {'$ne': None}}}, {'$project': {'_id': 0, 'ip': 1, 'hostname': 1}}]) for _ in hostname: if 'hostname' in hostname: ip_hostname[_["ip"]] = _["hostname"] for _ in result_list: if 'ip' in ip_hostname: _['hostname'] = ip_hostname[_["ip"]] else: _['hostname'] = '' else: for _ in result_list: hostname = Mongo.coll['Info'].find_one({'ip': _['ip']}) if hostname and 'hostname' in hostname: _['hostname'] = hostname['hostname'] else: _['hostname'] = '' return render_template('detail.html', item=result_list, count=vulcount, id=id, taskdate=taskdate, plugin_name=plugin_name, scanlist=lastscan)
經過id找到任務詳情,而後將詳情展現出來。有taskdate就是能夠查詢指定的日期。沒有這個參數就是查詢最新日期。當結果大於100,使用優化的查詢語句。
9.DeleteTask()
# 刪除任務異步 @app.route('/deletetask', methods=['get', 'post']) @logincheck @anticsrf def DeleteTask(): oid = request.form.get('oid', '') if oid: result = Mongo.coll['Task'].delete_one({'_id': ObjectId(oid)}) if result.deleted_count > 0: result = Mongo.coll['Result'].delete_many({'task_id': ObjectId(oid)}) if result: return 'success' return 'fail'
刪除任務操做
10.Downloadxls()
# 下載excel報表異步 @app.route('/downloadxls', methods=['get', 'post']) @logincheck @anticsrf def DownloadXls(): tid = request.args.get('taskid', '') taskdate = request.args.get('taskdate', '') result_list = [] if tid: # 有任務id if taskdate: # 從任務中拉取指定批次掃描結果 taskdate = datetime.strptime(taskdate, "%Y-%m-%d %H:%M:%S.%f") cursor = Mongo.coll['Result'].find({'task_id': ObjectId(tid), 'task_date': taskdate}).sort( 'time', -1) else: # 從任務中直接取該任務最新一次掃描結果 lastscan = Mongo.coll["Result"].distinct('task_date', {'task_id': ObjectId(tid)}) if len(lastscan) == 0: cursor = [] taskdate = datetime.now() else: lastscan.sort(reverse=True) taskdate = lastscan[0] cursor = Mongo.coll['Result'].find({'task_id': ObjectId(tid), 'task_date': taskdate}).sort( 'time', -1) title = Mongo.coll['Task'].find_one({'_id': ObjectId(tid)})['title'] for _ in cursor: hostname = '' result = Mongo.coll['Info'].find_one({'ip': _['ip']}) if result and 'hostname' in result: hostname = result['hostname'] result_list.append( {'ip': _['ip'], 'port': _['port'], 'info': _['info'], 'vul_level': _['vul_info']['vul_level'], 'time': _['time'], 'vul_name': _['vul_info']['vul_name'], 'lastscan': taskdate, 'title': title, 'hostname': hostname}) response = make_response(CreateTable(result_list, taskdate.strftime("%Y%m%d-%H%M%S"))) if taskdate == '': response.headers["Content-Disposition"] = "attachment; filename=nodata.xls;" else: response.headers["Content-Disposition"] = "attachment; filename=" + quote( title.encode('utf-8')) + taskdate.strftime( "%Y-%m-%d-%H-%M-%S") + ".xls;" else: # 下載綜合報表 tasks = Mongo.coll['Task'].find({}) t_list = [] for t in tasks: name = t['title'] lastscan = Mongo.coll["Result"].distinct('task_date', {'task_id': t['_id']}) if len(lastscan) == 0: cursor = Mongo.coll['Result'].find({'task_id': t['_id']}) taskdate = None else: lastscan.sort(reverse=True) taskdate = lastscan[0] cursor = Mongo.coll['Result'].find({'task_id': t['_id'], 'task_date': taskdate}) for _ in cursor: # 單任務詳情 hostname = Mongo.coll['Info'].find_one({'ip': _['ip']}) if hostname: _['hostname'] = hostname['hostname'] else: _['hostname'] = None _['title'] = name _['vul_level'] = _['vul_info']['vul_level'] _['vul_name'] = _['vul_info']['vul_name'] _['lastscan'] = taskdate t_list.append(_) response = make_response(CreateTable(t_list, 'all_data')) response.headers["Content-Disposition"] = "attachment; filename=all_data.xls;" response.headers["Content-Type"] = "application/x-xls" return response
216-243行 將掃描結果查詢出來後加到result_list
response = make_response(CreateTable(result_list, taskdate.strftime("%Y%m%d-%H%M%S")))
跟CreateTable()函數 View/lib/CreateExcel.py
def CreateTable(cursor, id): item = [] item.append(['IP', '端口', '主機名', '風險等級', '漏洞描述', '插件類型', '任務名稱', '時間', '掃描批次']) for i in cursor: if i['lastscan']: _ = [i['ip'], i['port'], i['hostname'], i['vul_level'], i['info'], i['vul_name'], i['title'], i['time'].strftime('%Y-%m-%d %H:%M:%S'), i['lastscan'].strftime('%Y-%m-%d %H:%M:%S')] else: _ = [i['ip'], i['port'], i['hostname'], i['vul_level'], i['info'], i['vul_name'], i['title'], i['time'].strftime('%Y-%m-%d %H:%M:%S'), ''] item.append(_) file = write_data(item, id) return file.getvalue()
建立個列表,將數據加入列表和描述對應起來。write_data()函數
def write_data(data, tname): file = xlwt.Workbook(encoding='utf-8') table = file.add_sheet(tname, cell_overwrite_ok=True) l = 0 for line in data: c = 0 for _ in line: table.write(l, c, line[c]) c += 1 l += 1 sio = StringIO.StringIO() file.save(sio) return sio
經過xlwt包,將數據一行行寫到文件裏, 而後保存,文件名爲時間格式。
回到view/view.py
make_response()返回文件名。 245-250行設置了http頭和下載文件名字。後面返回下載。
251-277行同上。
11.search_result_xls()
# 搜索結果報表下載接口 @app.route('/searchxls', methods=['get']) @logincheck @anticsrf def search_result_xls(): query = request.args.get('query', '') if query: result = query.strip().split(';') filter_ = querylogic(result) cursor = Mongo.coll['Info'].find(filter_).sort('time', -1) title_tup = ('IP', '端口號', '主機名', '服務類型') xls = [title_tup, ] for info in cursor: item = ( info.get('ip'), info.get('port'), info.get('hostname'), info.get('server') ) xls.append(item) file = write_data(xls, 'search_result') resp = make_response(file.getvalue()) resp.headers["Content-Disposition"] = "attachment; filename=search_result.xls;" resp.headers["Content-Type"] = "application/x-xls" resp.headers["X-Content-Type-Options"] = "nosniff" return resp else: redirect(url_for('NotFound'))
搜索結果有個話,寫入文件下載。沒有的話NotFound
12.Plugin()
# 插件列表頁 @app.route('/plugin') @logincheck def Plugin(): page = int(request.args.get('page', '1')) cursor = Mongo.coll['Plugin'].find().limit(page_size).skip((page - 1) * page_size) return render_template('plugin.html', cursor=cursor, vultype=cursor.distinct('type'), count=cursor.count())
查詢-展現
13.AddPlugin()
單獨分析 (http://www.javashuo.com/article/p-aogwousg-em.html)
14.DeletePlugin()
# 刪除插件異步 @app.route('/deleteplugin', methods=['get', 'post']) @logincheck @anticsrf def DeletePlugin(): oid = request.form.get('oid', '') if oid: result = Mongo.coll['Plugin'].find_one_and_delete({'_id': ObjectId(oid)}, remove=True) if not result['filename'].find('.') > -1: result['filename'] = result['filename'] + '.py' if os.path.exists(file_path + result['filename']): os.remove(file_path + result['filename']) return 'success' return 'fail'
刪除插件,從數據庫中刪除而且刪除文件
15.Analysis
# 統計頁面 @app.route('/analysis') @logincheck def Analysis(): ip = len(Mongo.coll['Info'].distinct('ip')) record = Mongo.coll['Info'].find().count() task = Mongo.coll['Task'].find().count() vul = int(Mongo.coll['Plugin'].group([], {}, {'count': 0},'function(doc,prev){prev.count = prev.count + doc.count}')[0]['count']) plugin = Mongo.coll['Plugin'].find().count() vultype = Mongo.coll['Plugin'].group(['type'], {"count":{"$ne":0}}, {'count': 0},'function(doc,prev){prev.count = prev.count + doc.count}') cur = Mongo.coll['Statistics'].find().sort('date', -1).limit(30) trend = [] for i in cur: trend.append( {'time': i['date'], 'add': i['info']['add'], 'update': i['info']['update'], 'delete': i['info']['delete']}) vulbeat = Mongo.coll['Heartbeat'].find_one({'name': 'load'}) scanbeat = Mongo.coll['Heartbeat'].find_one({'name': 'heartbeat'}) if vulbeat == None or scanbeat == None: taskpercent = 0 taskalive = False scanalive = False else: taskpercent = vulbeat['value'] * 100 taskalive = (datetime.now() - vulbeat['up_time']).seconds scanalive = (datetime.now() - scanbeat['up_time']).seconds taskalive = True if taskalive < 120 else False scanalive = True if scanalive < 120 else False server_type = Mongo.coll['Info'].aggregate( [{'$group': {'_id': '$server', 'count': {'$sum': 1}}}, {'$sort': {'count': -1}}]) web_type = Mongo.coll['Info'].aggregate([{'$match': {'server': 'web'}}, {'$unwind': '$webinfo.tag'}, {'$group': {'_id': '$webinfo.tag', 'count': {'$sum': 1}}}, {'$sort': {'count': -1}}]) return render_template('analysis.html', ip=ip, record=record, task=task, vul=vul, plugin=plugin, vultype=vultype, trend=sorted(trend, key=lambda x: x['time']), taskpercent=taskpercent, taskalive=taskalive, scanalive=scanalive, server_type=server_type, web_type=web_type)
看了頁面回來看代碼,這個方法就是將數據庫中的值查詢出來而後顯示,不具體分析語句。
16.Config()
# 配置頁面 @app.route('/config') @logincheck def Config(): val = [] table = request.args.get('config', '') if table in ("vulscan", "nascan"): dict = Mongo.coll['Config'].find_one({'type': table}) if dict and 'config' in dict: dict = dict['config'] for _ in dict: if _.find('_') > 0: item_type = "list" else: item_type = "word" val.append({"show": item_type, "type": _, "info": dict[_]["info"], "help": dict[_]["help"], "value": dict[_]["value"]}) val = sorted(val, key=lambda x: x["show"], reverse=True) return render_template('config.html', values=val)
判斷是爬蟲引擎仍是掃描引擎,而後分別查詢出數據。
17.UpdateConfig()
# 配置更新異步 @app.route('/updateconfig', methods=['get', 'post']) @logincheck @anticsrf def UpdateConfig(): rsp = 'fail' name = request.form.get('name', 'default') value = request.form.get('value', '') conftype = request.form.get('conftype', '') if name and value and conftype: if name == 'Masscan' or name == 'Port_list': origin_value = Mongo.coll['Config'].find_one({'type': 'nascan'})["config"][name]["value"] value = origin_value.split('|')[0] + '|' + value elif name == 'Port_list_Flag': name = 'Port_list' origin_value = Mongo.coll['Config'].find_one({'type': 'nascan'})["config"]['Port_list']["value"] value = value + '|' + origin_value.split('|')[1] elif name == 'Masscan_Flag': name = 'Masscan' path = Mongo.coll['Config'].find_one({'type': 'nascan'})["config"]["Masscan"]["value"] if len(path.split('|')) == 3: path = path.split('|')[1] + "|" + path.split('|')[2] else: path = path.split('|')[1] if value == '1': value = '1|' + path else: value = '0|' + path result = Mongo.coll['Config'].update({"type": conftype}, {'$set': {'config.' + name + '.value': value}}) if result: rsp = 'success' return rsp
先判斷是更新哪個配置。
根據name來判斷是哪一個配置,就從數據庫去取對應的值,而後把提交過來的value加上去更新。
18.PullUpdate()
19.checkupdate()
20.installplugin()
# 拉取線上最新插件異步 @app.route('/pullupdate') @logincheck @anticsrf def PullUpdate(): rsp = 'err' f = urlopen('https://sec.ly.com/xunfeng/getlist') j = f.read().strip() if j: try: remotelist = json.loads(j) #remotelist_temp = copy.deepcopy(remotelist) plugin = Mongo.coll['Plugin'].find({'source': 1}) for p in plugin: for remote in remotelist: if p['name'] == remote['name'] and remote['coverage'] == 0: remotelist.remove(remote) locallist = Mongo.coll['Update'].aggregate([{'$project': {'_id': 0, 'unicode': 1}}]) local = [] for i in locallist: local.append(i['unicode']) ret = [i for i in remotelist if i['unicode'] not in local] for i in ret: i['isInstall'] = 0 Mongo.coll['Update'].insert(i) rsp = 'true' except: pass return rsp # 檢查本地已知的線上插件列表異步 @app.route('/checkupdate') @logincheck @anticsrf def CheckUpdate(): json = [] notinstall = Mongo.coll['Update'].find({'isInstall': 0}).sort('unicode', -1) for _ in notinstall: json.append({'unicode': _['unicode'], 'name': _['name'], 'info': _['info'], 'time': _['pushtime'], 'author': _['author']}) return dumps(json) # 安裝/下載插件異步 @app.route('/installplugin') @logincheck @anticsrf def installplugin(): rsp = 'fail' unicode = request.args.get('unicode', '') item = Mongo.coll['Update'].find_one({'unicode': unicode}) json_string = {'add_time': datetime.now(), 'count': 0, 'source': 1} file_name = secure_filename(item['location'].split('/')[-1]) if os.path.exists(file_path + file_name): if ".py" in file_name: db_record = Mongo.coll['Plugin'].find_one({'filename': file_name.split('.')[0]}) else: db_record = Mongo.coll['Plugin'].find_one({'filename': file_name}) if not db_record or not db_record['source'] == 1: file_name = file_name.split('.')[0] + '_' + str(datetime.now().second) + '.' + \ file_name.split('.')[-1] else: db_record = Mongo.coll['Plugin'].delete_one({'filename': file_name.split('.')[0]}) if item['location'].find('/') == -1: urlretrieve('https://sec.ly.com/xunfeng/getplugin?name=' + item['location'], file_path + file_name) else: urlretrieve(item['location'], file_path + file_name) # 兼容舊的插件源 if os.path.exists(file_path + file_name): try: if file_name.split('.')[-1] == 'py': module = __import__(file_name.split('.')[0]) mark_json = module.get_plugin_info() json_string['filename'] = file_name.split('.')[0] else: json_text = open(file_path + file_name, 'r').read() mark_json = json.loads(json_text) json_string['filename'] = file_name mark_json.pop('plugin') json_string.update(mark_json) Mongo.coll['Plugin'].insert(json_string) Mongo.coll['Update'].update_one({'unicode': unicode}, {'$set': {'isInstall': 1}}) rsp = 'success' except: pass return rsp
均爲更新插件的,不細分析。
在https://sec.ly.com/xunfeng/getlist 查詢出最新插件,而後與數據庫比較。
查看是否本地有安裝。
https://sec.ly.com/xunfeng/getplugin?name= 在這裏實現下載。
21.Login()
22.Loginout()
# 登陸 @app.route('/login', methods=['get', 'post']) def Login(): if request.method == 'GET': return render_template('login.html') else: account = request.form.get('account') password = request.form.get('password') if account == app.config.get('ACCOUNT') and password == app.config.get('PASSWORD'): session['login'] = 'loginsuccess' return redirect(url_for('Search')) else: return redirect(url_for('Login')) # 登出異步 @app.route('/loginout') @logincheck def LoginOut(): session['login'] = '' return redirect(url_for('Login'))
一個登錄一個登出。
23.NotFound()
24.Error()
@app.route('/404') def NotFound(): return render_template('404.html') @app.route('/500') def Error(): return render_template('500.html')
顯示404 或500
閱讀了view.py 裏的每一個方法具體都是幹嗎的,對巡風掃描器總體有一個大概瞭解。
感謝ysrc開源。