# coding: utf-8
# +-------------------------------------------------------------------
# | 宝塔Windows面板
# +-------------------------------------------------------------------
# | Copyright (c) 2015-2019 宝塔软件(http://bt.cn) All rights reserved.
# +-------------------------------------------------------------------
# | Author: 沐落 <cjx@bt.cn>
# +-------------------------------------------------------------------

import public,os,sys,db,time,json,re
from BTPanel import session,cache,json_header,admin_path
from flask import request,redirect,g

class userlogin:

    def sms_error_num(self):
        """
        短信防爆破
        """
        address = public.GetClientIp()
        nKey = '{}_panelSmsNum'.format(address)
        num = cache.get(nKey)
        if not num: num = 0
        num = num + 1
        cache.set(nKey,num,600)

        return num


    def __check_sms(self,post):
        """
        @发送短信前置检查
        """
        if not hasattr(post, 'phone'):
            return public.returnMsg(False,'发送失败，手机号码不能为空.')

        if self.limit_address('?') < 1: return public.returnMsg(False,'LOGIN_ERR_LIMIT')

        userInfo = public.get_user_info()
        if not userInfo: return public.returnMsg(False,'未绑定宝塔帐号，不支持短信登录.')

        err_count = self.sms_error_num()
        if err_count >= 6: return public.returnMsg(False,'错误次数达到上限，请10分钟后重试.')

        username = post.phone.strip()
        if username != public.md5(userInfo['username']):
            return public.returnMsg(False,'面板绑定帐号不匹配，剩余尝试次数 {} 次.'.format( 6 - err_count))

        sms = public.init_msg("sms")
        if not sms: return public.returnMsg(False,'未安装短信模块，不支持短信登录.')

        config = sms.get_config(None)
        if 'login' in config and config['login']: return public.returnMsg(True,'前置验证成功.')

        return public.returnMsg(False,'未开启短信登录，不支持短信登录.')

    def send_sms(self,post):
        """
        @username 登录手机号码
        """
        result =  self.__check_sms(post)
        if not result['status']: return result,json_header

        smsObj = public.init_msg("sms")
        try:
            config = smsObj.get_config(None)
            if config['count'] <= 0: return public.returnMsg(False,'短信条数不足，请联系客服购买.')
        except :
            return public.returnMsg(False,'连接官网失败，请稍后重新发送.')

        address = public.GetClientIp()
        import random
        sm_code = random.randint(100000,999999)

        ret = smsObj.send_msg(config['types']['login_panel'],{'sm_code':sm_code})
        if ret['status']:
            codeKey = "{}_{}".format(address,ret['sm_token'])
            session[codeKey] = "{}_{}".format(public.md5(str(sm_code)),int(time.time() + 600))

            public.writeFile('data/{}'.format(public.md5(address + "_sms")),"True")

        return ret

    def request_sms(self,post):
        """
        @post.phone  登录用户名
        @post.sm_code 登录密码（md5加密后）
        @post.sm_token 用户登录token
        @post.code 登录验证码（1次登录失败后显示）
        """

        logins = public.get_login_type()
        if not 'sms' in logins or not logins['sms']:
            return public.returnMsg(False,'未开启短信登录，请通过面板设置->登录保护->开启短信登录.')

        address = public.GetClientIp()
        tmpPath = 'data/{}'.format(public.md5(address + "_sms"))
        if not os.path.exists(tmpPath): return public.returnMsg(False,'登录失败，验证码已过期.')

        if not hasattr(post, 'sm_code') or not hasattr(post, 'sm_token'):
            return public.returnMsg(False,'短信登录失败，参数传递错误.')

        #验证前检查
        result =  self.__check_sms(post)
        if not result['status']: return result,json_header


        errKey = '{}_err_count'.format(address)
        try:
            sm_code = post.sm_code.strip()
            sm_token = post.sm_token.strip()

            errCount = cache.get(errKey)
            if errCount and errCount >= 6:
                os.remove(tmpPath)
                return public.returnMsg(False,'错误次数达到上限，请更换其他登录方式或5分钟后重试.')

            codeKey =  "{}_{}".format(address,sm_token)
            if not codeKey in session:
                os.remove(tmpPath)
                return public.returnMsg(False,'登录失败，验证码不存在或已过期1.')

            tmps = session[codeKey].split('_')
            if int(tmps[1]) < int(time.time()):
                os.remove(tmpPath)
                return public.returnMsg(False,'登录失败，验证码不存在或已过期2.')

            if tmps[0] == sm_code:

                loginUser = public.M('users').where("id=?",(1,)).field('id,username').find()
                session['login'] = True
                session['username'] = loginUser['username']
                session['uid'] = loginUser['id'];
                public.WriteLog('TYPE_LOGIN','LOGIN_SUCCESS',("短信登录(" + loginUser['username']+")",public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT'))))
                self.limit_address('-');
                cache.delete('panelNum')
                cache.delete('dologin')
                sess_input_path = 'data/session_last.pl'
                public.writeFile(sess_input_path,str(int(time.time())))

                self.set_request_token()
                self.login_token()
                cache.delete(errKey)
                os.remove(tmpPath)
                public.login_send_body("短信登录",loginUser['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT')))

                return public.returnMsg(True,'LOGIN_SUCCESS')
        except:
            os.remove(tmpPath)
            print(public.get_error_info())
        cache.inc(errKey,1)
        return public.returnMsg(False,'登录失败，验证码不存在或已过期3.'),json_header


    def request_post(self,post):
        """
        登录面板
        @post.username  登录用户名
        @post.password 登录密码（md5加密后）
        @post.code 登录验证码（1次登录失败后显示）
        """
        logins = public.get_login_type()
        if not 'pwd' in logins or not logins['pwd']:
            return public.returnMsg(False,'未开启密码登录，请通过面板设置->登录保护->开启密码登录.')

        if not hasattr(post, 'username') or not hasattr(post, 'password'):
            return public.returnJson(False,'LOGIN_USER_EMPTY'),json_header

        if self.check_crack(): return public.returnJson(False,'登录失败，请修复面板后重新登录.'),json_header
        post.username = post.username.strip()
        format_error = '参数格式错误'

        # 核验用户名密码格式
        post.username = public.rsa_decrypt(post.username)
        if len(post.username) != 32:
            return public.returnMsg(False,format_error),json_header
        post.password = public.rsa_decrypt(post.password)
        if len(post.password) != 32:
            return public.returnMsg(False,format_error),json_header

        if not re.match(r"^\w+$",post.username): return public.returnMsg(False,format_error),json_header
        if not re.match(r"^\w+$",post.password): return public.returnMsg(False,format_error),json_header

        last_login_token = session.get('last_login_token',None)
        if not last_login_token:
            public.WriteLog('TYPE_LOGIN','LOGIN_ERR_CODE',('****','****',public.GetClientIp()))
            return public.returnJson(False,"验证失败，请刷新页面重新登录!"),json_header

        self.error_num(False)
        if self.limit_address('?') < 1: return public.returnJson(False,'LOGIN_ERR_LIMIT'),json_header
        post.username = post.username.strip()

        sql = db.Sql()
        userInfo = None
        user_plugin_file = '{}/users_main.py'.format(public.get_plugin_path('users'))
        if os.path.exists(user_plugin_file):
            user_list = sql.table('users').field('id,username,password,salt').select()
            for u_info in user_list:
                if public.md5(public.md5(u_info['username'] + last_login_token)) == post.username:
                    userInfo = u_info
        else:
            userInfo = sql.table('users').where('id=?',1).field('id,username,password,salt').find()

        if 'code' in session:
            if session['code'] and not 'is_verify_password' in session:
                if not hasattr(post, 'code'): return public.returnJson(False,'验证码不能为空!'),json_header
                if not public.checkCode(post.code):
                    public.WriteLog('TYPE_LOGIN','LOGIN_ERR_CODE',('****','****',public.GetClientIp()))
                    return public.returnJson(False,'CODE_ERR'),json_header
        try:
            if not userInfo:
                public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp()))
                num = self.limit_address('+')
                if not num: return public.returnJson(False,'您多次登录失败,暂时禁止登录,请等待{}秒后重试!'.format(int(self.limit_expire_time - time.time()))),json_header
                return public.returnJson(False,'用户名或密码错误，<span style="color:red;">请刷新页面重试</span>，您还可以重试[{}]次'.format(num)),json_header

            if userInfo and not userInfo['salt']:
                public.chdck_salt()
                userInfo = sql.table('users').where('id=?',(userInfo['id'],)).field('id,username,password,salt').find()

            password = public.md5(post.password.strip() + userInfo['salt'])
            s_username = public.md5(public.md5(userInfo['username'] + last_login_token))
            if s_username != post.username or userInfo['password'] != password:

                public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp()))
                num = self.limit_address('+')
                return public.returnJson(False,'LOGIN_USER_ERR',(str(num),)),json_header
            _key_file = "data/two_step_auth.txt"
            public.run_thread(public.login_send_body,("账号密码",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT'))))

            if sys.path[0] != 'class/': sys.path.insert(0,'class/')
            if not public.password_expire_check():
                session['password_expire'] = True

            if hasattr(post,'vcode'):
                if self.limit_address('?',v="vcode") < 1: return public.returnJson(False,'您多次验证失败，禁止10分钟'),json_header
                import pyotp
                secret_key = public.readFile(_key_file)
                if not secret_key:
                    return public.returnJson(False, "没有找到key,请尝试在命令行关闭谷歌验证后在开启"),json_header
                t = pyotp.TOTP(secret_key)
                result = t.verify(post.vcode)
                if not result:
                    if public.sync_date(): result = t.verify(post.vcode)
                    if not result:
                        num = self.limit_address('++',v="vcode")
                        return public.returnJson(False, '验证失败，您还可以尝试[{}]次!'.format(num)), json_header
                now = int(time.time())
                public.writeFile("data/dont_vcode_ip.txt",json.dumps({"client_ip":public.GetClientIp(),"add_time":now}))
                self.limit_address('--',v="vcode")
                self.set_cdn_host(post)

                return self._set_login_session(userInfo)

            acc_client_ip = self.check_two_step_auth()

            if not os.path.exists(_key_file) or acc_client_ip:
                self.set_cdn_host(post)
                return self._set_login_session(userInfo)
            self.limit_address('-')
            session['is_verify_password'] = True
            return "1"
        except Exception as ex:
            print(public.get_error_info())
            stringEx = str(ex)
            if stringEx.find('unsupported') != -1 or stringEx.find('-1') != -1:

                public.ServiceReload()
                return public.returnJson(False,'USER_INODE_ERR'),json_header
            public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp()));
            num = self.limit_address('+');
            return public.returnJson(False,'LOGIN_USER_ERR',(str(num),)),json_header

    def request_temp(self,get):
        try:
            if not hasattr(get,'tmp_token'): return '错误的参数!'
            if len(get.tmp_token) != 48: return '错误的参数!'
            if not re.match(r"^\w+$",get.tmp_token):return '错误的参数!'
            skey = public.GetClientIp() + '_temp_login'
            if not public.get_error_num(skey,10): return '连续10次验证失败，禁止1小时'
            s_time = int(time.time())
            data = public.M('temp_login').where('state=? and expire>?',(0,s_time)).field('id,token,salt,expire').find()
            if not data:
                public.set_error_num(skey)
                return '验证失败1!'
            if not isinstance(data,dict):
                public.set_error_num(skey)
                return '验证失败2!'
            r_token = public.md5(get.tmp_token + data['salt'])
            if r_token != data['token']:
                public.set_error_num(skey)
                return '验证失败3!'
            public.set_error_num(skey,True)
            userInfo = public.M('users').where("id=?",(1,)).field('id,username').find()
            session['login'] = True
            session['username'] = '临时({})'.format(data['id'])
            session['tmp_login'] = True
            session['tmp_login_id'] = str(data['id'])
            session['tmp_login_expire'] = time.time() + 3600
            session['uid'] = data['id']
            sess_path = 'data/session'
            if not os.path.exists(sess_path):
                os.makedirs(sess_path,384)
            public.writeFile(sess_path + '/' + str(data['id']),'')
            login_addr = public.GetClientIp()+ ":" + str(request.environ.get('REMOTE_PORT'))
            public.WriteLog('TYPE_LOGIN','LOGIN_SUCCESS',(userInfo['username'],login_addr))
            public.M('temp_login').where('id=?',(data['id'],)).update({"login_time":s_time,'state':1,'login_addr':login_addr})
            self.limit_address('-')
            cache.delete('panelNum')
            cache.delete('dologin')
            sess_input_path = 'data/session_last.pl'
            public.writeFile(sess_input_path,str(int(time.time())))
            self.set_request_token()
            self.login_token()
            self.set_cdn_host(get)
            public.login_send_body("临时授权",userInfo['username'],public.GetClientIp(),str(request.environ.get('REMOTE_PORT')))
            return redirect('/')
        except:
            print(public.get_error_info())
            return '登录失败，登录过程发生错误'

    def request_tmp(self,get):
        """
        api登录接口
        @get.tmp_token 登录临时授权key(64位长度)
        """
        try:
            if not hasattr(get,'tmp_token'): return public.returnJson(False,'错误的参数1!'),json_header
            if len(get.tmp_token) == 48:
                return self.request_temp(get)
            if len(get.tmp_token) != 64: return public.returnJson(False,'错误的参数2!'),json_header
            if not re.match(r"^\w+$",get.tmp_token):return public.returnJson(False,'错误的参数3!'),json_header

            save_path = 'config/api.json'
            data = json.loads(public.ReadFile(save_path))
            if not 'tmp_token' in data or not 'tmp_time' in data: return public.returnJson(False,'验证失败!'),json_header
            if (time.time() - data['tmp_time']) > 120: return public.returnJson(False,'过期的Token'),json_header
            if get.tmp_token != data['tmp_token']: return public.returnJson(False,'错误的Token'),json_header
            userInfo = public.M('users').where("id=?",(1,)).field('id,username').find()
            session['login'] = True
            session['username'] = userInfo['username']
            session['uid'] = userInfo['id']
            session['tmp_login'] = True
            session['admin_auth'] = True
            public.WriteLog('TYPE_LOGIN','LOGIN_SUCCESS',(userInfo['username'],public.GetClientIp()));
            self.limit_address('-')
            cache.delete('panelNum')
            cache.delete('dologin')
            sess_input_path = 'data/session_last.pl'
            public.writeFile(sess_input_path,str(int(time.time())))
            del(data['tmp_token'])
            del(data['tmp_time'])
            public.writeFile(save_path,json.dumps(data))
            self.set_request_token()
            self.login_token()
            return redirect('/')
        except:
            return public.returnJson(False,'登录失败,' + public.get_error_info()),json_header


    def request_get(self,get):
        """
        登录跳转
        """
        if not 'title' in session: session['title'] = public.getMsg('NAME');

        ip = public.GetClientIp()
        if ip in ['127.0.0.1','localhost','::1']: return False

        domain = public.readFile('data/domain.conf')
        if domain:

            if(public.GetHost().lower() != domain.strip().lower()) and (public.GetHost().lower() != '127.0.0.1') and (public.GetHost().lower() != 'localhost'):
                errorStr = public.ReadFile('./BTPanel/templates/' + public.GetConfigValue('template') + '/error2.html')
                try:
                    errorStr = errorStr.format(public.getMsg('PAGE_ERR_TITLE'),public.getMsg('PAGE_ERR_DOMAIN_H1'),public.getMsg('PAGE_ERR_DOMAIN_P1'),public.getMsg('PAGE_ERR_DOMAIN_P2'),public.getMsg('PAGE_ERR_DOMAIN_P3'),public.getMsg('NAME'),public.getMsg('PAGE_ERR_HELP'))
                except IndexError:pass
                return errorStr
        if os.path.exists('data/limitip.conf'):
            iplist = public.readFile('data/limitip.conf')
            if iplist:

                iplist = iplist.strip()
                if not ip in iplist.split(','):
                    errorStr = public.ReadFile('./BTPanel/templates/' + public.GetConfigValue('template') + '/error2.html')
                    try:
                        errorStr = errorStr.format(public.getMsg('PAGE_ERR_TITLE'),public.getMsg('PAGE_ERR_IP_H1'),public.getMsg('PAGE_ERR_IP_P1',(public.GetClientIp(),)),public.getMsg('PAGE_ERR_IP_P2'),public.getMsg('PAGE_ERR_IP_P3'),public.getMsg('NAME'),public.getMsg('PAGE_ERR_HELP'))
                    except IndexError:pass
                    return errorStr

        sql = db.Sql()

        if 'login' in session:
            if session['login'] == True:
                return redirect('/')

        if not 'code' in session:
            session['code'] = False
        self.error_num(False)


    def _set_login_session(self,userInfo):
        """
        登录成功设置session
        @userInfo['id'] 用户id
        @userInfo['username'] 用户名

        """
        try:

            address = public.GetClientIp()
            port =  str(request.environ.get('REMOTE_PORT'))

            #if not public.is_login_area(address):
            #    return public.returnJson(False,'登录失败，归属地限制'),json_header

            session['login'] = True

            session['username'] = userInfo['username']
            session['uid'] = userInfo['id']
            public.WriteLog('TYPE_LOGIN','LOGIN_SUCCESS',(userInfo['username'],address+ ":" + port))
            self.limit_address('-')
            cache.delete('panelNum')
            cache.delete('dologin')
            if 'last_login_token' in session: del(session['last_login_token'])
            sess_input_path = 'data/session_last.pl'
            public.writeFile(sess_input_path,str(int(time.time())))

            self.set_request_token()
            self.login_token()

            login_address = '{}(未知)'.format(address,)
            #返回增加登录地区
            res = public.returnMsg(True,'LOGIN_SUCCESS')
            res['login_time'] = time.time()
            try:
                ip_info = public.get_free_ip_info(address)

                if 'city' in ip_info:
                    res['ip_info'] = ip_info
                login_address = '{}({})'.format(address,ip_info['info'])
            except:
                print(public.get_error_info())

            last_login = {}
            last_file = 'data/last_login.pl'
            try:
                last_login = json.loads(public.readFile(last_file))
            except:pass
            public.writeFile(last_file,json.dumps(res))

            res['last_login'] = last_login
            session['login_address'] = public.xsssec(login_address)

            print(res)
            return public.getJson(res),json_header
        except Exception as ex:
            stringEx = str(ex)
            if stringEx.find('unsupported') != -1 or stringEx.find('-1') != -1:

                public.ServiceReload()
                return public.returnJson(False,'USER_INODE_ERR'),json_header
            public.WriteLog('TYPE_LOGIN','LOGIN_ERR_PASS',('****','******',public.GetClientIp()));
            num = self.limit_address('+')
            return public.returnJson(False,'LOGIN_USER_ERR',(str(num),)),json_header


    def set_request_token(self):
        """
        生成request_token
        """
        html_token_key = public.get_csrf_html_token_key()
        session[html_token_key] = public.GetRandomString(48)
        session[html_token_key.replace("https_","")] = public.GetRandomString(48)

        session['request_token_head'] = public.GetRandomString(48)
        session['client_hash'] = public.get_client_hash()

    def login_token(self):
        """
        重置登录token
        """
        import config
        config.config().reload_session()



    def error_num(self,s = True):
        """
        面板登录防爆破
        """
        nKey = 'panelNum'
        num = cache.get(nKey)
        if not num:
            cache.set(nKey,1)
            num = 1
        if s: cache.inc(nKey,1)
        if num > 6: session['code'] = True

    def limit_address(self,type,v=""):
        """
        IP登录限制
        """
        import time
        clientIp = public.GetClientIp()
        numKey = 'limitIpNum_' + v + clientIp
        limit = 6
        outTime = 600
        try:
            #初始化
            num1 = cache.get(numKey)
            if not num1:
                cache.set(numKey,1,outTime)
                num1 = 1

            #计数
            if type == '+':
                cache.inc(numKey,1)
                self.error_num()
                session['code'] = True
                return limit - (num1+1)

            #计数验证器
            if type == '++':
                cache.inc(numKey,1)
                self.error_num()
                session['code'] = False
                return limit - (num1+1)

            #清空
            if type == '-':
                cache.delete(numKey)
                session['code'] = False
                return 1

            #清空验证器
            if type == '--':
                cache.delete(numKey)
                session['code'] = False
                return 1

            return limit - num1
        except:
            return limit

    def check_two_step_auth(self):
        """
        检查是否需要进行二次验证
        """
        dont_vcode_ip_info = public.readFile("data/dont_vcode_ip.txt")
        acc_client_ip = False
        if dont_vcode_ip_info:
            dont_vcode_ip_info = json.loads(dont_vcode_ip_info)
            ip = dont_vcode_ip_info["client_ip"] == public.GetClientIp()
            now = int(time.time())
            v_time = now - int(dont_vcode_ip_info["add_time"])
            if ip and v_time < 3600:
                acc_client_ip = True
        return acc_client_ip

    def set_cdn_host(self,get):
        """
        设置静态加速地址
        """
        try:
            if not 'cdn_url' in get: return True
            plugin_path = 'plugin/static_cdn'
            if not os.path.exists(plugin_path): return True
            cdn_url = public.get_cdn_url()
            if not cdn_url or cdn_url == get.cdn_url: return True
            public.set_cdn_url(get.cdn_url)
        except:
            return False


    def check_crack(self):

        return False