换回OpenWRT路由系统

嗯,虽然使用Debian做路由好长一段时间,但没有个界面始终是不太方便,这虽然在使用电脑时没什么问题,但在平板上输入命令真的是痛苦,于是乎还是用OpenWRT吧,反正还是在Linux圈圈里打转转。

由于没有硬路由,干脆KVM吧,下载系统镜像https://downloads.openwrt.org/releases/19.07.4/targets/x86/64/openwrt-19.07.4-x86-64-combined-ext4.img.gz

转换为qcow2文件: 

qemu-img convert -f raw openwrt-19.07.4-x86-64-combined-ext4.img -O qcow2 openwrt.qcow2

另外虚拟硬盘的空间小了点,加! 

qemu-img resize t.qcow2 +1G

好了,创建虚拟机。

<domain type='kvm'>
  <name>OpenWRT</name>
  <uuid>590d9131-9951-4282-9e81-24bd7e5a1fac</uuid>
  <memory unit='KiB'>1048576</memory>
  <currentMemory unit='KiB'>1048576</currentMemory>
  <vcpu placement='static'>1</vcpu>
  <os>
    <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type>
    <boot dev='hd'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <pae/>
  </features>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>restart</on_crash>
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='disk'>
      <driver name='qemu' type='qcow2'/>
      <source file='/home/jeff/openwrt/openwrt.qcow2'/>
      <target dev='hda' bus='ide'/>
      <address type='drive' controller='0' bus='0' target='0' unit='0'/>
    </disk>
    <controller type='usb' index='0' model='piix3-uhci'>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/>
    </controller>
    <controller type='pci' index='0' model='pci-root'/>
    <controller type='pci' index='1' model='pci-bridge'>
      <model name='pci-bridge'/>
      <target chassisNr='1'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
    </controller>
    <controller type='pci' index='2' model='pci-bridge'>
      <model name='pci-bridge'/>
      <target chassisNr='2'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
    </controller>
    <controller type='ide' index='0'>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/>
    </controller>
    <interface type='bridge'>
      <mac address='52:54:00:78:f9:59'/>
      <source bridge='lan'/>
      <model type='virtio'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x01' function='0x0'/>
    </interface>
    <interface type='bridge'>
      <mac address='52:54:00:78:f9:60'/>
      <source bridge='wan'/>
      <model type='virtio'/>
      <address type='pci' domain='0x0000' bus='0x02' slot='0x01' function='0x0'/>
    </interface>
    <input type='mouse' bus='ps2'/>
    <input type='keyboard' bus='ps2'/>
    <graphics type='vnc' port='5901' autoport='no' listen='0.0.0.0'>
      <listen type='address' address='0.0.0.0'/>
    </graphics>
    <video>
      <model type='cirrus' vram='16384' heads='1' primary='yes'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
    </video>
    <memballoon model='virtio'>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x06' function='0x0'/>
    </memballoon>
  </devices>
</domain>

启动完成!

抓取视频信息的函数

因为写了一个相册系统,所以发现这个问题,这年头谁还只是傻傻的照相啊,肯定是外带拍N多的视频啦。好吧,代码再改改,添加上传视频的功能。

只是说起来简单,做起来啰嗦。这视频不如照片信息的格式统一,不过还好,基本上应付市面上机器拍摄的视频信息提取函数也大概写出来了。

注意:这是基于FFMpeg的,所以不管是Windows还是Linux都是安装FFMpeg才行。

function getVideoInfo($path){
    $fileNameArray = explode('.',$path);
    $ext = strtolower($fileNameArray[count($fileNameArray)-1]);

    ob_start();
    passthru(sprintf('ffmpeg -i "%s" 2>&1',$path));
    $info = ob_get_contents();
    ob_end_clean();
    $data = [];
    switch ($ext) {
        case 'mov':
            if(preg_match("/com.apple.quicktime.make:(.*)/",$info,$res)) $data['Make'] = $res[1];
            if(preg_match("/creation_time   :(.*)/",$info,$res)){
                $date = strtotime(str_replace('/',' ',trim($res[1])));
                $data['date'] = date('Y-m-d H:i:s',$date);
            }
            // if(preg_match("/com.apple.quicktime.creationdate:(.*)/",$info,$res)) $data['Date'] = date('Y-m-d H:i:s',strtotime($res[1]));
            if(preg_match("/com.apple.quicktime.model:(.*)/",$info,$res)) $data['Model'] = $res[1];
            if(preg_match("/com.apple.quicktime.location.ISO6709:(.*)/",$info,$res)){
                $local = explode('+',$res[1]);
                $data['latitude'] = $local[2];
                $data['longitude'] = $local[1];
            }
            if (preg_match("/Duration: (.*?), start: (.*?), bitrate: (\d*) kb\/s/", $info, $res)){
                $data['duration'] = $res[1];
                $data['bitrate'] = $res[3].'kb/s';
            }
            break;
        case 'avi':
            if(preg_match("/encoder         :(.*)/",$info,$res)){
                $data['make'] = explode(' ',trim($res[1]))[0];
                $data['model'] = explode(' ',trim($res[1]))[1];
            }
            if(preg_match("/creation_time   :(.*)/",$info,$res)){
                $date = strtotime(str_replace('/',' ',trim($res[1])));
                $data['date'] = date('Y-m-d H:i:s',$date);
            }
            if (preg_match("/Duration: (.*?), start: (.*?), bitrate: (\d*) kb\/s/", $info, $res)){
                $data['duration'] = $res[1];
                $data['bitrate'] = $res[3].'kb/s';
            }
            break;
        case 'mp4':
            if(preg_match("/creation_time   :(.*)/",$info,$res)){
                $date = strtotime(str_replace('/',' ',trim($res[1])));
                $data['date'] = date('Y-m-d H:i:s',$date);
            }
            if(preg_match("/location        :(.*)/",$info,$res)){
                $local = explode('+',$res[1]);
                $data['latitude'] = $local[2];
                $data['longitude'] = $local[1];
            }
            if (preg_match("/Duration: (.*?), start: (.*?), bitrate: (\d*) kb\/s/", $info, $res)){
                $data['duration'] = $res[1];
                $data['bitrate'] = $res[3].'kb/s';
            }
        case '3gp':
            if(preg_match("/creation_time   :(.*)/",$info,$res)){
                $date = strtotime(str_replace('/',' ',trim($res[1])));
                $data['date'] = date('Y-m-d H:i:s',$date);
            }
            if(preg_match("/location        :(.*)/",$info,$res)){
                $local = explode('+',$res[1]);
                $data['latitude'] = $local[2];
                $data['longitude'] = $local[1];
            }
            if (preg_match("/Duration: (.*?), start: (.*?), bitrate: (\d*) kb\/s/", $info, $res)){
                $data['duration'] = $res[1];
                $data['bitrate'] = $res[3].'kb/s';
            }
            break;
        default:
            if(preg_match("/creation_time   :(.*)/",$info,$res)){
                $date = strtotime(str_replace('/',' ',trim($res[1])));
                $data['date'] = date('Y-m-d H:i:s',$date);
            }
            if(preg_match("/location        :(.*)/",$info,$res)){
                $local = explode('+',$res[1]);
                $data['latitude'] = $local[2];
                $data['longitude'] = $local[1];
            }
            if (preg_match("/Duration: (.*?), start: (.*?), bitrate: (\d*) kb\/s/", $info, $res)){
                $data['duration'] = $res[1];
                $data['bitrate'] = $res[3].'kb/s';
            }
            break;
    }
    return $data;


}

郁闷之中写了一个基于NAS的个人相册系统

前段时间以来都挺郁闷的,包括了现在。

在陷于郁闷当中的时候,突然看到了一条关于网易相册关闭的新闻。当时也没留意,关了就关了吧,反正我的照片都用本地硬盘装着,平时都是不通电搁在柜子里。想来网易也有一点以前传过的照片,好像在零六零七年的样子,那只是极少的部分。后来嫌麻烦,也就没传了,只是家里人要走了我的账号。

直到有一天,姐夫说起了不能传照片了,这才直到他们一直都在往里面放照片。于是乎,赶紧告诉他们网易相册要关门的消息,另外帮忙把所有的照片都打包了下来。还好打包及时,所有照片都Down下来了,只是发现好多原始照片都遗失,这是因为家人在上传的时候因为不懂选择了缩小图片,导致上面的照片都是几百乘几百像素的,并且丢失了照片原始时间地址等信息。

望着一个优盘,应该给谁呢?里面装的都是十多年来家人共同往上传的照片,再买优盘分配可最后分享也还是不方便。

在家人的埋怨网易中,是时候发挥码农的功用了,不就是个相册吗?对于一个码农来说,那也是个事?So Easy!

首先规划整个系统,存放照片,而且放的都是原文件,空间需求是极大的,为此干脆把空间安置在了家里的NAS里,有四个4T硬盘做了Raid5阵列,一定程度保障数据的安全,基本不用担心。为了随时随地访问,做了一个基于12V的UPS,用的是摩托车电瓶,电路自己焊,基于这里是讲软件系统就不描述了。对外访问有点麻烦,因为没有对外服务的端口,这个大家都懂,所以得改,我换成了8端口。不想麻烦也可以使用反向代理,但那得有还有个开放80端口的服务器,代理软件可以用frp。反正我是决定麻烦一下算了,因为代理也代表着数据得中转,这是影响速度的事情。另外简单了,就是还得有一个域名,如果有服务器,可以是国际域名,没有服务器也可以使用免费的二级域名。

然后,就是码代码了。拿的轮子有:ThinkPHP、Bootstrap、jQuery、FileInput、PhotoSwipe、Masonry、BootstrapValidator,前端两个页面,一个相册列表页一个相册页也就是照片列表页,数据库一共六个表,相册表、相片表、星星表、标签表、关联表、用户表,后台方法也简单,基本上就是增删改查。

好了,一顿乱炒,新鲜出炉!下载地址:magic

关于Git服务器公钥的问题

最开始的时候一直都是用着github作为代码仓库,本来也是简单方便的,但后来有段时间github访问奇慢无比,更改了几次hosts文件都没用。

那个郁闷啊,干脆建个仓库吧。自建到也简单,用的是华硕N-18U路由器,支持USB2和USB3两个口子,插上一个32G的U盘,又装了一个Entware-ng,这个路由是armv7的CPU,系统是Tomato内核是2.6的,所以选择armv7sf-k2.6版本。

地址是:https://entware.net/  http://bin.entware.net/  http://pkg.entware.net/binaries/ 

命令是:wget -O – http://bin.entware.net/armv7sf-k2.6/installer/generic.sh | /bin/sh

装好之后就可以使用opkg install xxx命令来安装软件了。

首先安装git,目前版本是2.18.0,到这里就已经可以初步使用自己的服务器了。用git在指定的目录创建空的仓库:git init –bare xxx.git,xxx为你的项目名称。在本地则用git remote add origin root@你的域名:/git/xxx.git添加远程仓库地址。然后直接git push origin master来提交。

哦,等等,要输入密码!

github添加为远程仓库后可不用每次都输入密码的,这多麻烦!

备份PostgreSQL时的共享内存问题

从某方面来说PostgreSQL真不如MySQL方便,最近数据库越来越大,想冷备一下,于是就pg_dump一下,结果提示Warning Out of Shared Memory,忽略,但备出来的文件显然不是全部数据库,大小差别太大了。于是乎百度,结果只发现CSDN有一个帖子讲了这个事情,然后就没有然后了,后来在官网找到了这个问题,在PostgreSQL的配置文件postgresql.conf里有个max_locks_per_transaction配置项,在Windows版本里是被注释掉了,并且值为64,取消注释值改为1024重启服务,完美解决。

Easy-rsa生成密钥证书

本来一直都是用PPTP的,简单方便,但最近移动的宽带不知道换了什么路由还是怎么回事,居然一直无法拨通,只有换成OpenVPN了,但是换成OpnVPN麻烦的是需要证书和密钥什么的,比较啰嗦。因为密钥经常需要更新或者需要新增,所以在这里记下来。

首先拷贝一份配置样本,如果路径不对那么用find找吧。

cp -r /usr/share/easy-rsa/ /etc/openvpn/

切换目录,执行:

cd  /etc/openvpn/easy-rsa
source ./vars
./clean-all
./build-ca

生成服务器密钥和客户端密钥

./build-key-server server
./build-key client

生成过程中会提示输入一些信息,直接回车默认,选择[y/n]的都选y,客户端如果需要可以把client改成你需要的名字。

生成Diffie Hellman参数

./build-dh

在OpenVPN的配置文件server.conf中注意文件的路径问题,然后重启载入既可以了。

对照通达信一些指标的Python实现

行情软件里习惯了通达信的简介,虽然很多时候还是要依赖大智慧,但平时看图形基本都是用通达信。因此在有时候做数据分析的时候,不可避免的需要再次的实现一些指标功能,所以在Python里整理了一下,写了部分的指标工具。

# 威廉指标
def williams(df, n, column='williams'):
# 100*(10日内最高价的最高值-收盘价)/(10日内最高价的最高值-10日内最低价的最低值)
for i in range(len(df)):
if i < n-1: continue
df.ix[i, column] = 100 * (df.high.values[i-n+1:i+1].max()-df.close.values[i])/(
df.high.values[i-n+1:i+1].max()-df.low.values[i-n+1:i+1].min())
return df

# 布林指标
def bollinger(df,n):
for i in range(len(df)):
if i < n-1: continue
df.ix[i, 'BOLL'] = df.close.values[i-n+1:i+1].mean()
df.ix[i, 'UB'] = df.ix[i, 'BOLL'] + 2 * numpy.std(df.close.values[i-n+1:i+1], ddof=1)
df.ix[i, 'LB'] = df.ix[i, 'BOLL'] - 2 * numpy.std(df.close.values[i-n+1:i+1], ddof=1)
return df

# 轨道线
def ene(df,n,m1,m2):
for i in range(len(df)):
if i < n-1: continue
df.ix[i, 'UPPER'] = (1+m1/100)*df.close.values[i-n+1:i+1].mean()
df.ix[i, 'LOWER'] = (1-m2/100)*df.close.values[i-n+1:i+1].mean()
df.ix[i, 'ENE'] = (df.ix[i, 'UPPER'] + df.ix[i, 'LOWER'])/2
return df

def kdj(df,n,m1,m2):
for i in range(len(df)):
if i < n-1: continue
df.ix[i, 'rsv'] = (df.close.values[i]-df.low.values[i-n+1:i+1].min()) / (df.high.values[i-n+1:i+1].max()-df.low.values[i-n+1:i+1].min())*100
df = getSMA(df,m1,1,'rsv','K')
df = getSMA(df,m2,1,'K','D')
for i in range(len(df)):
df.ix[i, 'J'] = 3*df.K.values[i] - 2*df.D.values[i]
return df

所有数据都是Dataframe类型,以时间为Index顺序排列。关于getSMA这个函数可以在本博另一帖子《

行情软件里的平均函数以及Python的实现》中可以找到。

利用动态IP绑定一级域名做Web服务器

其实用动态IP绑定一级域名倒不是难题,问问度娘或者谷哥都可以找到答案。真正的问题是营运商封锁了80端口,这才是巧妇难为无米之炊的关键。前段时间想了一条思路,需要一个独立的服务器,这个想法实验了半年,证实是不行的,但是折腾的结果是做站还是可以的。

好了,不故弄玄虚了,其实我也没办法解决掉80端口被封的问题,只是换了个思路利用443端口做https的网站。本文所需记录的关键也就是这里了,因为https需要证书的,付费的证书咱没考虑,而选择的Let's Encrypt免费证书有时间限制的,期限已到就需要重新申请。所以有两样需要计划任务,一个是重拨号之后IP地址的更新,一个是证书到期后证书的重新申请。

关于IP解析,目前国内好用的有Dnspod的和阿里的,并且这两家都提供有解析更新API,所以很是方便,我这次用的是Dnspod的解析,他的开发文档地址是:https://www.dnspod.cn/docs/index.html,按照他的手册来并不复杂,就不多说了。重点来说说Let's Encrypt免费证书的申请,Let's Encrypt对证书的时效是90天,就是说90天后证书就会时效,但这个可以用计划任务来处理,脚本写好后90天运行一次就可以了。但它还有另外一个问题,就是Let's Encrypt需要你向它证明这个域名是属于你的,这有两种方式,第一是在你服务器里建立一个文件,Let's Encrypt会通过访问传统http来访问到这个文件就可以了,对于这个方式那么问题就来了,80端口已经被封了如何能访问得到呢?所以此路是不通的。第二种方式就是在dns服务器发送一个记录类型为txt的自定义解析,记录值就是Let's Encrypt计算得出的字符串,由Let's Encrypt去访问,能访问得到并且一样就OK通过了,这个没有问题,并且动态IP解析本来就要做一样的工作。

申请的代码如下:

import os
import sys
import json
import copy
import dns.resolver
from subprocess import Popen, PIPE
try:
    from urllib.request import urlopen  # Python 3
except ImportError:
    from urllib2 import urlopen  # Python 2

import platform
import re
import binascii
import base64
import hashlib
import shutil
import time
import textwrap
import logging

import dnspod  # 域名解析模块

CA = "https://acme-v01.api.letsencrypt.org"
system = platform.system()
sourcePath = './source/'
certsPath = './certs/'

log = logging.getLogger(__name__)
log.addHandler(logging.StreamHandler())
log.setLevel(logging.INFO)

class GetCerts(object):
    """docstring for GetCerts"""
    def __init__(self):
        super(GetCerts, self).__init__()
        self.config = self.__loadConfig()
        self.domain = self.__loadDomain()
        self.__cearteDir()

        self.accountkey = 'account.key'
        self.ops = self.config['ops'] if system == 'Windows' else 'openssl'

        self.cearteAccountKey()
        self.cearteDomainSource()
        self.loadAccountKey()
        self.regAccount()

        for i in self.domain:
            self.__loadPrivateKey(i)

    @staticmethod
    def __b64(b):
        return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "")

    @staticmethod
    def __waitChallengeDeployed(domain):
        count = 0
        is_deployed = GetCerts.__checkDomain(domain)
        while count < 240 and not is_deployed:
            time.sleep(10)
            count += 1
            is_deployed = GetCerts.__checkDomain(domain)
        return is_deployed

    @staticmethod
    def __checkDomain(domain):
        try:
            answers = dns.resolver.query('_acme-challenge.{0}'.format(domain), 'TXT')
            if len(answers) > 0:
                return True
        except dns.exception.DNSException as e:
            log.debug("TXT not found: %s", e)
        except:
            log.error("Unexpected error: %s", sys.exc_info()[0])
            raise
        return False

    # 建立账号证书文件夹
    def __cearteDir(self):
        if not os.path.exists(certsPath):
            os.makedirs(certsPath)
        if not os.path.exists(sourcePath):
            os.makedirs(sourcePath)

    # 载入程序设置文件
    def __loadConfig(self):
        try:
            file = open('config.json', 'r')
        except:
            log.debug("缺少配置文件,请配置基本参数")
            return False
        else:
            return json.loads(file.read())

    # 载入域名列表文件
    def __loadDomain(self):
        try:
            file = open('domain.json', 'r')
        except:
            log.debug("缺少域名文件,请配置域名参数")
            return False
        else:
            return json.loads(file.read())

    # 建立账号证书文件
    def cearteAccountKey(self):
        self.regKey = False
        if os.path.isfile(self.accountkey):
            command = [self.ops,'rsa','-in',self.accountkey,'-check']
            proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
            proc.communicate()
            if proc.returncode != 0:
                command = [self.ops,'genrsa', '-out',self.accountkey,'4096']
                proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
                proc.communicate()
                self.regKey = True
        else:
            command = [self.ops,'genrsa', '-out',self.accountkey,'4096']
            proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
            proc.communicate()
            self.regKey = True
      
    # 建立域名私钥和请求证书  
    def cearteDomainSource(self):

        for i in self.domain:
            csrFile = '%s%s.csr' % (sourcePath, i['domain'])
            keyFile = '%s%s.key' % (sourcePath, i['domain'])

            if os.path.isfile(keyFile):
                command = [self.ops, 'rsa', '-in', keyFile, '-check']
                proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
                proc.communicate()
                if proc.returncode != 0:
                    command = [self.ops,'genrsa', '-out', keyFile, '4096']
                    proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
                    proc.communicate()
            else:
                command = [self.ops, 'genrsa', '-out', keyFile, '4096']
                proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
                proc.communicate()

            domains = str()
            if len(i['record']) > 1:
                for r in i['record']:
                    domains += 'DNS:%s,' % i['domain'] if r == '@' else 'DNS:%s.%s,' % (r, i['domain'])
            else:
                domains = i['domain'] if  i['record'][0] == '@' else '%s.%s' % (i['record'][0], i['domain'])
            if not os.path.isfile(csrFile):
                if system == 'Windows':
                    command = [self.ops, "req", "-new", "-sha256", "-nodes", "-keyout", keyFile,"-subj", "/CN={0}".format(domains), "-out", csrFile, '-config', self.config['ops_cnf']]
                else:
                    command = [self.ops, "req", "-new", "-sha256", "-nodes", "-keyout", keyFile,"-subj", "/CN={0}".format(domains), "-out", csrFile]
                proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
                proc.communicate()

    def loadAccountKey(self):
        command = [self.ops, "rsa", "-in", self.accountkey, "-noout", "-text"]
        proc = Popen(command,stdin=PIPE, stdout=PIPE, stderr=PIPE)
        out, err = proc.communicate()
        if proc.returncode != 0:
            raise IOError("OpenSSL Error: {0}".format(err))
        pub_hex, pub_exp = re.search(
            r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
            out.decode('utf8'), re.MULTILINE | re.DOTALL).groups()
        pub_exp = "{0:x}".format(int(pub_exp))
        pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
        self.header = {
            "alg": "RS256",
            "jwk": {
                "e": GetCerts.__b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
                "kty": "RSA",
                "n": GetCerts.__b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
            },
        }
        account_key_json = json.dumps(self.header['jwk'], sort_keys=True, separators=(',', ':'))
        self.thumbprint = GetCerts.__b64(hashlib.sha256(account_key_json.encode('utf8')).digest())

    def __loadPrivateKey(self, domain):

        self.dns = dnspod if domain['dns'] == 'dnspod' else alidns

        self.dns.ID = domain['id']
        self.dns.TOKEN = domain['token']

        csrFile = '%s%s.csr' % (sourcePath, domain['domain'])

        command = [self.ops, "req", "-in", csrFile, "-noout", "-text"]
        proc = Popen(command, stdout = PIPE, stderr = PIPE)
        out, err = proc.communicate()
        if proc.returncode != 0:
            raise IOError("Error loading {0}: {1}".format(csrFile, err))
        domains = set([])
        common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8'))
        if common_name is not None:
            domains.add(common_name.group(1))
        subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
        if subject_alt_names is not None:
            for san in subject_alt_names.group(1).split(", "):
                if san.startswith("DNS:"):
                    domains.add(san[4:])
        print('domains',domains)

        for i in domains:
            print(i)
            code, result = self.__sendSignedRequest(CA + "/acme/new-authz", {
                "resource": "new-authz",
                "identifier": {"type": "dns", "value": i},
            })
            log.debug("Requesting challenges: {0} {1}".format(code, result))
            if code != 201:
                raise ValueError("Error requesting challenges: {0} {1}".format(code, result))
            challenge = [c for c in json.loads(result.decode('utf8'))['challenges'] if c['type'] == "dns-01"][0]
            token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
            keyauthorization = "{0}.{1}".format(token, self.thumbprint)
            dnstoken = GetCerts.__b64(hashlib.sha256(keyauthorization.encode('utf8')).digest())

            ndd = i.split(".")
            if len(ndd) == 2:
                subdomain = "_acme-challenge"
                basedomain = ndd[0] + "." + ndd[1]
            else:
                subdomain = "_acme-challenge." + ndd[0]
                basedomain = ndd[1] + "." + ndd[2]

            self.dns.updateRecord(basedomain, subdomain, dnstoken, 'TXT')

            try:
                is_deployed = GetCerts.__waitChallengeDeployed(i)
                if is_deployed:
                    code, result = self.__sendSignedRequest(challenge['uri'], {
                        "resource": "challenge",
                        "keyAuthorization": keyauthorization,
                    })
                    # print('code:',code,'result:',result)
                    if code != 202:
                        raise ValueError("Error triggering challenge: {0} {1}".format(code, result))
                    while True:
                        try:
                            resp = urlopen(challenge['uri'])
                            challenge_status = json.loads(resp.read().decode('utf8'))
                            # print('challenge_status:',challenge_status)
                            log.debug(challenge_status)
                        except IOError as e:
                            raise ValueError("Error checking challenge: {0} {1}".format(
                                e.code, json.loads(e.read().decode('utf8'))))
                        if challenge_status['status'] == "pending":
                            log.debug("Pending")
                            time.sleep(1)
                        elif challenge_status['status'] == "valid":
                            log.debug("{0} verified!".format(i))
                            break
                        else:
                            raise ValueError("{0} challenge did not pass: {1}".format(
                                i, challenge_status))
            finally:
                pass
        log.info("Signing certificate...")
        command = [self.ops, "req", "-in", csrFile, "-outform", "DER"]
        proc = Popen(command, stdout=PIPE, stderr=PIPE)
        csr_der, err = proc.communicate()
        code, result = self.__sendSignedRequest(CA + "/acme/new-cert", {
            "resource": "new-cert",
            "csr": self.__b64(csr_der),
        })
        if code != 201:
            raise ValueError("Error signing certificate: {0} {1}".format(code, result))

        sign_cert = """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
            "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64)))

        sign_cert_file = open(certsPath+domain['domain']+'.crt', 'w')
        sign_cert_file.write(sign_cert)
        sign_cert_file.close()
        log.info("Certificate signed %s", domain['domain']+'.crt')

        shutil.copy(certsPath+domain['domain']+'.crt', certsPath+domain['domain']+'.pem')

        chain_cert = urlopen("https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem")
        with open(certsPath+domain['domain']+'.pem', 'ab') as output:
            output.write(chain_cert.read())

        log.info("Certificate chain signed %s", domain['domain']+'.chained.pem')


    def __sendSignedRequest(self, url, payload):
        payload64 = GetCerts.__b64(json.dumps(payload).encode('utf8'))
        protected = copy.deepcopy(self.header)
        protected["nonce"] = urlopen(CA + "/directory").headers['Replay-Nonce']
        protected64 = GetCerts.__b64(json.dumps(protected).encode('utf8'))
        command = [self.ops, "dgst", "-sha256", "-sign", self.accountkey]
        proc = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE)
        out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8'))
        if proc.returncode != 0:
            raise IOError("OpenSSL Error: {0}".format(err))
        data = json.dumps({
            "header": self.header, "protected": protected64,
            "payload": payload64, "signature": GetCerts.__b64(out),
        })
        try:
            resp = urlopen(url, data.encode('utf8'))
            return resp.getcode(), resp.read()
        except IOError as e:
            return getattr(e, "code", None), getattr(e, "read", e.__str__)()

    def regAccount(self):
        log.debug("Registering account...")
        code, result = self.__sendSignedRequest(CA + "/acme/new-reg", {
            "resource": "new-reg",
            "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"
        })
        if code == 201:
            log.info("Account registered!")
        elif code == 409:
            log.debug("Already registered!")
        else:
            raise ValueError("Error registering: {0} {1}".format(code, result))

    def sign(self):
        pass

if __name__ == '__main__':
    gc=GetCerts()

还有参数文件的格式如下:

{
"ops":"d:/Wamp/Apache24/bin/openssl.exe",
"ops_cnf":"d:/Wamp/Apache24/conf/openssl.cnf",
"CA":"https://acme-v01.api.letsencrypt.org/directory",
"IP_VERSION":"",
"CONTACT_EMAIL":"",
"RENEW_DAYS":"30",
"KEY_ALGO":"rsa",
"KEYSIZE":"4096",
"LICENSE":"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"
}

域名的配置文件:

[{"dns": "dnspod", "id": "你的id", "token": "你的token", "domain": "你的域名(不要W)", "record": ["www"]}]

代码保存下来,剩下的工作就是制定执行运行任务了,当然可以直接把输出目录直接定义到Apache的证书目录去,每次自动更新后Apache再次载入就可以了,这时候的网站就出现了一把绿色的钥匙而不是大大红色的叉了。

关于Kodi字幕插件的开发

最近把家里的HDPC终于改成Debian了,起初一直在Windows 10下,但经过长时间的使用,装着E5200芯的老机器终于拉不动了,启动都要卡半天。思索了半天,干脆换到了Debian下。没去试现在的安卓X86版本和凤凰系统,是因为不想过于折腾了,虽说Debian并不是个好的娱乐平台,但好在安装Kodi倒是很方便对这个系统也比较熟悉,而我也仅仅用它就可以了。

安装的是Debian 9.4版本, Kodi是17.6的。设置什么的轻车熟路,电影、电视、直播啥的都基本没问题,但找遍了网上现有的中文字幕插件,就没一个可以用的,有的说是给15的版本可以用,但我没试而且也不想换版本。没办法了,只有自己改吧。

在这里真要吐槽一下Kodi的官方资料,英文的不说,而且是积极的简单,什么函数基本上就是一句话解释,连示例都没有一个,不过还好,综合了很多现成的源码,基本上了解了它字幕插件的思路和一些函数方法。代码是基于网络现有的插件基础上改的,字幕来源于字幕库。

首先是搜索函数,一番查验这个到是简单,原先的代码找不到内容是因为网站页面div的class改了,所以把bs的查找规则调整就ok了。

def Search( item ):
    subtitles_list = []

    log( __name__ ,"Search for [%s] by name" % (os.path.basename( item['file_original_path'] ),))
    if item['mansearch']:
        url = ZIMUKU_API % (item['mansearchstr'])
    else:
        url = ZIMUKU_API % (item['title'])
    try:
        socket = urllib.urlopen(url)
        data = socket.read()
        socket.close()
        soup = BeautifulSoup(data,'html.parser')
    except:
        return
    results = soup.find_all("div", class_="item prel clearfix")
    for it in results:
        moviename = it.find("div", class_="title").a.text.encode('utf-8')
        iurl = it.find("div", class_="title").a.get('href').encode('utf-8')
        movieurl = '%s%s' % (ZIMUKU_BASE, iurl)
        try:
            socket = urllib.urlopen(movieurl)
            data = socket.read()
            socket.close()
            soup = BeautifulSoup(data,'html.parser').find("table", class_="table")
            soup = soup.find("tbody")
        except:
            return
        subs = soup.find_all("tr")
        for sub in subs:
            name = sub.a.text.encode('utf-8')
            if name.split('.')[-1] not in ['zip','Zip','ZIP']: continue
            flag = sub.img.get('src').split('/')[-1].split('.')[0].encode('utf-8')
            lang = FLAG_DICT.get(flag,'unkonw')
            link = '%s%s' % (ZIMUKU_BASE, sub.a.get('href').encode('utf-8'))

            if lang == '英':
                subtitles_list.append({"language_name":"English", "filename":name, "link":link, "language_flag":'en', "rating":"0", "lang":lang})
            else:
                subtitles_list.append({"language_name":"Chinese", "filename":name, "link":link, "language_flag":'zh', "rating":"0", "lang":lang})

而下载函数就麻烦了一点,看原来代码是获取下载地址后直接下载,而实际上我在网站下载时发现网站使用了301重定向,并且在访问下载地址时,网站做了防爬虫处理,需要在头里带上Referer,否则系统是不会给你正确的定向地址的,经过改动的代码如下:

def Download(url,lang):
    try: shutil.rmtree(__temp__)
    except: pass
    try: os.makedirs(__temp__)
    except: pass

    exts = [".srt", ".sub", ".smi", ".ssa", ".ass" ]
    try:
        socket = urllib.urlopen( url )
        data = socket.read()
        soup = BeautifulSoup(data,'html.parser')
        url = soup.find("li", class_="li dlsub").a.get('href').encode('utf-8')
        socket = urllib.urlopen(url)

        socket = urllib.urlopen(url)
        data = socket.read()
        soup = BeautifulSoup(data,'html.parser')
        div = soup.find('div',class_='down clearfix')
        li = div.find('li')
        headers['Referer'] = url

        req = urllib2.Request(li.a.get('href'),headers=headers)
        resp = urllib2.urlopen(req)
        socket = urllib.urlopen(resp.geturl())
        data = socket.read()
        socket.close()
    except:
        return []
    if len(data) < 1024: return []
    tempfile = os.path.join(__temp__, "subtitles.zip")
    xbmc.log(tempfile)
    with open(tempfile, "wb") as subFile: subFile.write(data)
    subFile.close()
    xbmc.sleep(500)
    lists = unZip(tempfile)
    lists = [i for i in lists if os.path.splitext(i)[1] in exts]

    if len(lists) == 1:
        return lists[0]
    else:
        index = [i.split('/')[-1] for i in lists]
        sel = xbmcgui.Dialog().select('请选择压缩包中的字幕', index)
        if sel == -1: sel = 0
        return lists[sel]

经过调整,大的问题没有了,不过些小的地方,原代码里貌似是只能在Windows下用的,因为好像它是用Winrar来解压的,而我在Debian下使用,所以只能考虑Zip文件了,至于能不能用rar包还有单独的srt等文件,以后折腾吧。

def getFileList(path):
    fileslist = []
    for d in os.listdir(path):
        if os.path.isdir(path+d):
            fileslist.extend(getFileList(path+d+'/'))
        if os.path.isfile(path+d):
            fileslist.append(path+d)
    return fileslist

def unZip(filepath):
    zip_file = zipfile.ZipFile(filepath,'r')
    for names in zip_file.namelist():
        if type(names) == str and names[-1] != '/':
            utf8name = names.decode('gbk')
            data = zip_file.read(names)
            fo = open(os.path.join(__temp__, utf8name), "w")
            fo.write(data)
            fo.close()
        else:
            zip_file.extract(names,__temp__)
    return getFileList(__temp__+'/')

总结一下啊,用惯了Python3后用2真的不习惯,并且在Kodi里测试也是非常的不方便,改动一下需要重新进入才行,经过测试在Linux和Windows下都没问题,版本从17到最新的18,至于更低的版本则没测试,接下来要把各种字幕扩展名的文件下载功能补全,等到某一天有心情折腾了再说吧。附地址:https://pan.baidu.com/s/1GxAod4Xdll-3pvM_OOru8A

更新:由于上次改的有太多的局限性,所以继续抽时间改了改,现在这个版本继续支持Linux和Windows,并且文件格式由单纯的zip扩展到rar的压缩,还包括了各种未压缩的字幕格式,由于网上并没有发现7z的字幕压缩包,所以就没有考虑这个格式了。但现在由于格式的增多,就带来了一些问题,为了这个插件的正常使用,在Windows下必须要安装WinRAR并且目录还得是C:\Program Files\WinRAR,在Linux下需要安装unrar,否则的话会下载失败,其他没有要求,注意这点即可。

贴上代码:

def getFileList(path):
    fileslist = []
    for d in os.listdir(path):
        if os.path.isdir(path+d):
            fileslist.extend(getFileList(path+d+'/'))
        if os.path.isfile(path+d):
            fileslist.append(path+d)
    return fileslist

def extractCompress(file):
    path  = __temp__ + '/subtitles/'
    if os.path.isdir(path): shutil.rmtree(path)
    if not os.path.isdir(path): os.mkdir(path)

    if file.lower().endswith('zip'):
        zipFile = zipfile.ZipFile(file,'r')
        for names in zipFile.namelist():
            if type(names) == str and names[-1] != '/':
                utf8name = names.decode('gbk')
                data = zipFile.read(names)
                with open(path+utf8name, 'wb') as f: f.write(data)
            else:
                zipFile.extract(names,path)
        return getFileList(path)

    if file.lower().endswith('rar'):
        if platform.system() == 'Windows':
            rarPath = 'C:\Program Files\WinRAR'
            sysPath = os.getenv('Path')
            if 'winrar' not in sysPath.lower(): os.environ["Path"] = sysPath+';'+rarPath
            command = "winrar x -ibck %s %s" % (file, path)
        if platform.system() == 'Linux':
            command = 'unrar x %s %s' % (file, path)
        res = os.system(command)
        if res == 0: return getFileList(path)

def Search( item ):
    subtitles_list = []

    log(__name__ ,"Search for [%s] by name" % os.path.basename(item['file_original_path']))
    if item['mansearch']:
        url = ZIMUKU_API % (item['mansearchstr'])
    else:
        url = ZIMUKU_API % (item['title'])
    try:
        socket = urllib.urlopen(url)
        data = socket.read()
        socket.close()
        soup = BeautifulSoup(data,'html.parser')
    except:
        return
    results = soup.find_all("div", class_="item prel clearfix")
    for it in results:
        moviename = it.find("div", class_="title").a.text.encode('utf-8')
        iurl = it.find("div", class_="title").a.get('href').encode('utf-8')
        movieurl = '%s%s' % (ZIMUKU_BASE, iurl)
        try:
            socket = urllib.urlopen(movieurl)
            data = socket.read()
            socket.close()
            soup = BeautifulSoup(data,'html.parser').find("table", class_="table")
            soup = soup.find("tbody")
        except:
            return
        subs = soup.find_all("tr")
        for sub in subs:
            name = sub.a.text.encode('utf-8')
            flag = sub.img.get('src').split('/')[-1].split('.')[0].encode('utf-8')
           .get(flag,'unkonw')
            link = '%s%s' % (ZIMUKU_BASE, sub.a.get('href').encode('utf-8'))

            if lang == '英':
                subtitles_list.append({"language_name":"English", "filename":name, "link":link, "language_flag":'en', "rating":"0", "lang":lang})
            else:
                subtitles_list.append({"language_name":"Chinese", "filename":name, "link":link, "language_flag":'zh', "rating":"0", "lang":lang})

    if subtitles_list:
        for it in subtitles_list:
            listitem = xbmcgui.ListItem(label=it["language_name"],
                                  label2=it["filename"],
                                  iconImage=it["rating"],
                                  thumbnailImage=it["language_flag"]
                                  )

            listitem.setProperty( "sync", "false" )
            listitem.setProperty( "hearing_imp", "false" )

            url = "plugin://%s/?action=download&link=%s&lang=%s" % (__scriptid__,
                                                                        it["link"],
                                                                        it["lang"]
                                                                        )
            xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]),url=url,listitem=listitem,isFolder=False)

def Download(url,lang):
    try: shutil.rmtree(__temp__)
    except: pass
    try: os.makedirs(__temp__)
    except: pass

    exts = [".srt", ".sub", ".smi", ".ssa", ".ass" ]
    try:
        socket = urllib.urlopen( url )
        data = socket.read()
        soup = BeautifulSoup(data,'html.parser')
        url = soup.find("li", class_="li dlsub").a.get('href').encode('utf-8')
        socket = urllib.urlopen(url)

        socket = urllib.urlopen(url)
        data = socket.read()
        soup = BeautifulSoup(data,'html.parser')
        div = soup.find('div',class_='down clearfix')
        li = div.find('li')
        headers['Referer'] = url

        req = urllib2.Request(li.a.get('href'),headers=headers)
        resp = urllib2.urlopen(req)
        fileName = resp.headers['Content-Disposition'].replace('"','').split('=')[1]

        socket = urllib.urlopen(resp.geturl())
        data = socket.read()
        socket.close()
    except:
        return []
    if len(data) < 1024: return []
    tempfile = os.path.join(__temp__, "subtitles.%s" % fileName.split('.')[-1])
    xbmc.log(tempfile)
    with open(tempfile, "wb") as subFile: subFile.write(data)
    xbmc.sleep(100)
    if fileName.split('.')[-1].lower() in ('zip','rar'):
        lists = extractCompress(tempfile)
    else:
        lists = [tempfile]

    lists = [i for i in lists if os.path.splitext(i)[1] in exts]

    if len(lists) == 1:
        return lists[0]
    else:
        index = [i.split('/')[-1] for i in lists]
        sel = xbmcgui.Dialog().select('请选择压缩包中的字幕', index)
        if sel == -1: sel = 0
        return lists[sel]

贴上下载地址:点击下载

关于SubLimeText的一些记录

软件使用个人习惯设置:

"bold_folder_labels": true,
"color_scheme": "Packages/Color Scheme - Default/Monokai.tmTheme",
"font_size": 13,
"highlight_line": true,
"ignored_packages":["Vintage"],
"save_on_focus_lost": true,
"scroll_past_end": true,
"show_encoding": true,
"show_full_path": true,
"spell_check": false,
"tab_size": 4,
"theme": "Adaptive.sublime-theme",
"translate_tabs_to_spaces": true,
"trim_trailing_white_space_on_save": true,
"unused": "vars",
"word_wrap": true

安装包管理:CTRL+` ,出现控制台,粘贴以下代码至控制台。

import urllib.request,os; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); open(os.path.join(ipp, pf), 'wb').write(urllib.request.urlopen( 'http://sublime.wbond.net/' + pf.replace(' ','%20')).read())

3175的注册码

https://download.sublimetext.com/sublime-text_build-3175_amd64.deb

—– BEGIN LICENSE —–
sgbteam
Single User License
EA7E-1153259
8891CBB9 F1513E4F 1A3405C1 A865D53F
115F202E 7B91AB2D 0D2A40ED 352B269B
76E84F0B CD69BFC7 59F2DFEF E267328F
215652A3 E88F9D8F 4C38E3BA 5B2DAAE4
969624E7 DC9CD4D5 717FB40C 1B9738CF
20B3C4F1 E917B5B3 87C38D9C ACCE7DD8
5F7EF854 86B9743C FADC04AA FB0DA5C0
F913BE58 42FEA319 F954EFDD AE881E0B
—— END LICENSE ——

记得要把主机地址给改了,不然老是失效

127.0.0.1       www.sublimetext.com
127.0.0.1       license.sublimehq.com

关于一些好用的插件

Emmet

功能:编码快捷键,前端必备

简介:Emmet作为zen coding的升级版,对于前端来说,可是必备插件,如果你对它还不太熟悉,可以在其官网(http://docs.emmet.io/)上看下具体的演示视频。

JSFormat

功能:Javascript的代码格式化插件

简介:很多网站的JS代码都进行了压缩,一行式的甚至混淆压缩,这让我们看起来很吃力。而这个插件能帮我们把原始代码进行格式的整理,包括换行和缩进等等,是代码一目了然,更快读懂~

使用:在已压缩的JS文件中,右键选择jsFormat或者使用默认快捷键(Ctrl+Alt+F)

LESS

功能:LESS高亮插件

简介:用LESS的同学都知道,sublime没有支持less的语法高亮,所以这个插件可以帮上我们

使用:打开.less文件或者设置为less格式

Less2CSS

功能:编译Less

简介:监测到文件改动时,编译保存为.css文件

使用:打开.less文件,编写代码保存即可看到同时生成.css的文件,如果没有则需要安装node。不推荐用这种方法编译,要么用koala,要么就用grunt编译。

Alignment

功能:”=”号对齐

简介:变量定义太多,长短不一,可一键对齐

使用:默认快捷键Ctrl+Alt+A和QQ截屏冲突,可设置其他快捷键如:Ctrl+Shift+Alt+A;先选择要对齐的文本

sublime-autoprefixer

功能:CSS添加私有前缀

简介:CSS还未标准化,所以要给各大浏览器一个前缀以解决兼容问题

使用:Ctrl+Shift+P,选择autoprefixer即可。需要安装node.js。

Clipboard History

功能:粘贴板历史记录

简介:方便使用复制/剪切的内容

使用:

  • Ctrl+alt+v:显示历史记录

  • Ctrl+alt+d:清空历史记录

  • Ctrl+shift+v:粘贴上一条记录(最旧)

  • Ctrl+shift+alt+v:粘贴下一条记录(最新)

Bracket Highlighter

功能:代码匹配

简介:可匹配[], (), {}, “”, ”, <tag></tag>,高亮标记,便于查看起始和结束标记

使用:点击对应代码即可

Git & SublimeGit

功能:git管理

简介:插件基本上实现了git的所有功能

jQuery

功能:jQ函数提示

简介:快捷输入jQ函数,是偷懒的好方法

DocBlockr

功能:生成优美注释

简介:标准的注释,包括函数名、参数、返回值等,并以多行显示,手动写比较麻烦

使用:输入/*、/**然后回车,还有很多用法,请参照

ColorPicker

功能:调色板

简介:需要输入颜色时,可直接选取颜色

使用:快捷键Windows: ctrl+shift+c

ConvertToUTF8

功能:文件转码成utf-8

简介:通过本插件,您可以编辑并保存目前编码不被 Sublime Text 支持的文件,特别是中日韩用户使用的 GB2312,GBK,BIG5,EUC-KR,EUC-JP ,ANSI等。ConvertToUTF8 同时支持 Sublime Text 2 和 3。

使用:安装插件后自动转换为utf-8格式

AutoFileName

功能:快捷输入文件名

简介:自动完成文件名的输入,如图片选取

使用:输入”/”即可看到相对于本项目文件夹的其他文件

Nodejs

功能:node代码提示

教程:https://sublime.wbond.net/packages/Nodejs

IMESupport

功能:sublime中文输入法

简介:还在纠结 Sublime Text 中文输入法不能跟随光标吗?试试「IMESupport 」这个插件吧!目前只支持 Windows,在搜索等界面不能很好的跟随光标。

使用:Ctrl + Shift + P →输入pci →输入IMESupport →回车

Trailing spaces

功能:检测并一键去除代码中多余的空格

简介:还在纠结代码中有多余的空格而显得代码不规范?或是有处女座情节?次插件帮你实现发现多余空格、一键删除空格、保存时自动删除多余空格,让你的代码规范清爽起来

使用:安装插件并重启,即可自动提示多余空格。一键删除多余空格:CTRL+SHITF+T(需配置),更多配置请点击标题。快捷键配置:在Preferences / Key Bindings – User加上代码(数组内)

FileDiffs

功能:强大的比较代码不同工具

简介:比较当前文件与选中的代码、剪切板中代码、另一文件、未保存文件之间的差别。可配置为显示差别在外部比较工具,精确到行。

使用:右键标签页,出现FileDiffs Menu或者Diff with Tab…选择对应文件比较即可

GBK Encoding Support

功能:中文识别

简介:Sublime Text 2可识别UTF-8格式的中文,不识别GBK和ANSI,因此打开很多含中文的文档都会出现乱码。可以通过安装插件GBK Support,来识别GBK和ANSI。

使用:

  • Open a GBK File

  • Save file with GBK encoding

  • Change file encoding from utf8 to GBK or GBK to utf8