Python – 折腾(存档) https://blog.sorz.org 已停止更新和维护。该页面为2018年3月创建的存档,其内容可能已过于陈旧、与现状不符,仅作为历史存档用作参考。 Sun, 14 Jan 2018 11:08:20 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.9.4 简单的 SSTP 服务器 /p/sstp-server/ /p/sstp-server/#comments Wed, 01 Oct 2014 10:48:23 +0000 https://sorz.org/?p=347963 SSTP (Secure Socket Tunneling Protocol) 是微软开发的一种 VPN 协议,用 SSL/TLS 加密 PPP 流量。参见 使用 SSTP 协议的 VPN 有什么优势和缺陷?

本来没怎么关注这个东西,支持少非主流,IP over TCP,也是因为一些奇怪的事突然有了这方面需求了,才开始了解它。

SoftEther

目前在 Linux,服务器似乎只有 SoftEther 这个实现。这软件也是挺拼的,把各种流行的非主流的 VPN 协议都给实现了一遍,跨平台,GPL。还提供傻瓜化的 GUI 管理,中日英三语界面,简直就是部署 VPN 的大杀器。只是一来太重,二来对 RADIUS 支持不全,不太符合需求。

DIY

因为内部走的是 PPP,如果只是调用 pppd 并转发数据,由 pppd 负责用户认证等麻烦事,自己实现一个目测也不是太复杂。加上微软的官方文档甚详,近来又一直想学一下 Twisted 但又不知如何入手,决定来写一个试试。

托 Twisted 的简单易用,挺顺利地初步完成了这个 SSTP 服务器。

代码放在 GitHub 和 PyPI 上了,sorz/sstp-server
PyPI

安装使用

供参考,详见sstpd --help

sudo apt-get install python-dev python-pip python-twisted
sudo pip install sstp-server
sudo sstpd -c cert.pem --local 10.0.0.1 --remote 10.0.0.0/24

关于证书,请参考 HTTPS 证书相关教程。
别忘了建/etc/ppp/options.sstpd,一个例子:

name sstpd
require-mschap-v2
nologfd
nodefaultroute
ms-dns 8.8.8.8
ms-dns 8.8.4.4

需要 IPv6 支持的,可加参数--listen ::

麻烦

折腾过程中遇到的主要障碍还是关于 PPP 的。简单地转发是不行的,因为每个 SSTP 包中只允许放入一个 PPP frame,需要自行分离出每个 frame。

PPP 这个古老的协议比预想中的要复杂。Windows 在 SSTP 中使用的其实是这个 HDLC-like Framing。找了一份 SSTP 客户端的代码,它是自行对两种格式进行了转换。好在后来发现 pppd 其实自身就支持 HDLC(sync 参数),于是成功偷懒

但后来发现 HDLC 似乎需要 Linux 内核支持(CONFIG_PPP_SYNC_TTY = yes)。

更新:
果然偷懒失败了,用 HDLC 取巧的方法是不行的,偶尔会有 frame 被截断,后面就全乱了。于是还是老老实实地照着 RFC 1662 来做 framing,自行 (un)escape 一些字符。但是这样性能变得很糟糕,试着把这部分用 C 扩展重写了一遍。想来这还是我第一次出于解决问题的需要写 C 呢……

感谢 @deba12 指出了这个问题,并协助测试、改善性能。

安全性

目前这个实现其实是不完整的,没有实现 Crypto binding 部分,导致其可能遭受中间人攻击。

使用了 SSL 还会遭受中间人攻击?微软在文档末尾提供了一个这种攻击的情境,挺有意思的。

攻击者建立一个假 Wi-Fi AP,然后诱骗用户连接。
Wi-Fi 使用 802.11 EAP 进行认证,用户以为他是在登录 Wi-Fi,但实际上,攻击者将这个认证请求转发给了 SSTP(PPP) 服务器!用户确实在和真的认证服务器在对话,只不过认证的不是 Wi-Fi 而是 SSTP 服务。

Crypto binding 可以防止这种攻击,想详细了解请参见微软文档。
但实现这个有些复杂,我这边的使用情境暂时没有这个需求,就先放一放了。
(懒你就直说 _(:з」∠)_

]]>
/p/sstp-server/feed/ 73
乱发数据包 /p/illegal-send/ /p/illegal-send/#comments Sun, 23 Mar 2014 10:43:58 +0000 https://sorz.org/?p=347865 这是去年写的一个用于干扰教学的工具。

Computer Networks 这门课,需要用到 Wireshark 这个抓包软件查看网络流量。
写了这玩意,通过广播一些特制的数据,可以在其他人的机子上刷出点有趣(?)的东西:在 Wireshark 上刷奇怪的东西

如图,一眼就能看出不正常.. 其实就是乱发 HTTP Response,并把原来显示状态码(200、404 …)的部分,改成了其他文字…

由于需要发,用了 WinPcap,它提供了一些 API 能够比较方便地跳过系统的协议栈,让网卡发送完全自定义的数据。这里使用了它的一个 Python 绑定 winpcapy

最后需要发送的是以太网帧,也就是说,要自己从 HTTP、TCP、IP 一路构建下来。为了简化工作使用了 dpkt ,里面提供了丰富的协议支持,能很方便地构建数据包。

一点小细节:Wireshark 会分析 TCP 协议,并把不正常的包用红色标出。为了避免这样,每个包的目的端口都是随机选取的,让 Wireshark 认为每个包都属于不同的 TCP 会话。当然换成随机的 IP 也没问题。

代码贴 Gist 上了 Gist 9721330

玩法:

  1. 下载安装 Python 2.7 和 WinPcap
  2. 下载打包的好的工具;
  3. 运行net_test.py得到网卡列表和对应的地址(e.g rpcap://\Device\NPF_{x-x-x-x-x});
  4. 运行net_test.py rpcap://\Device\NPF_{x-x-x-x-x}发送。
    完整用法是net_test.py <interface> [repeat_times [message]],可选重复次数与消息内容(默认是 5 次、发送随机表情)。

注意需要管理员权限,系统如有开 IP Forwarding 功能需关闭

代码不复杂,想玩更多花样可以自己改..
注意这样不按协议标准发数据的事.. 还是少做得好.. 玩玩就好,切勿滥用..

]]>
/p/illegal-send/feed/ 4
用 OpenVPN 实现双方 NAT 内 VPN 连接的尝试 /p/openvpn-traversal/ /p/openvpn-traversal/#comments Sun, 29 Sep 2013 05:37:41 +0000 https://sorz.org/?p=347754 学校位于墙外,宿舍宽带上下 100Mbps,如此优势不拿来架代理简直浪费.. 问题在于宿舍网络位于 NAT 后面没公网 IP,也没有 UPnP 这类方便的东西。不仅架代理麻烦,开 MC / TR 服务器什么的也很麻烦.. 尤其是在双方都是内网的情况下。

困难

于是做了不少尝试,比如宿舍内通过 VPN 连接自家路由器,然后将数据包通过自家路由器转发到宿舍路由器上,但这样做怎加了额外的延时,且受家里带宽限制。再比如通过 UDP 打洞 来实现双方直连..
但是如此一来,要用 UDP 来传送 TCP 内容,就需要自己实现 TCP 的各种功能,还要追踪连接,太过复杂大大超出自己能力水平(太弱了。所以就想着用各种办法偷懒。

绕开难点

上学期做的一个尝试是,用 tun 虚拟网卡,直接转发IP包。如此一来,TCP 全由系统实现,避开了这些复杂的事… (参见《OpenWrt 上使用 Python 操作 TAP/TUN》)最后止步于放假回家,和 Windows 下修改路由表的一些问题(很想具体喷一下但是这就扯远太了)。

前段时间忽然想到,OpenVPN 可使用 UDP 建立连接。如此一来,只要自己先(用少量代码)完成 UDP 打洞,而后的操作就可以全权交由(相当完善的) OpenVPN 处理,工作量、复杂度急剧下降。

具体实现

先简单重复一下 UDP 打洞的具体过程。

  1. 在 NAT 后的双方,向一台外网设备发送 UDP 包,让该设备得到双方的 外网 IP 和 UDP 端口号;
  2. 该外网设备通知双方对方的 外网 IP 和 端口号;
  3. 双方互相向对方的 外网端口 发送 UDP 包,连接建立;
  4. 完成,通过该连接通讯。

实际使用时可能会受到一些限制,宿舍网络不存在此问题不再累述。

 OpenVPN 服务器

首先,在宿舍内架 OpenVPN。我在 Cubieboard 上的 Ubuntu 架的,就按通常设置就行。
UDP 打洞要求在客户端连接前,服务器发送一个 UDP 包到客户端,且必须从 OpenVPN 监听端口上发出。为了在发送时不中断 openvpnd,我用了 pylibnet 跳过系统 socket API 直接发送该包。代码如下:

#!/usr/bin/env python
#encoding: utf-8
import libnet
from libnet.constants import RAW4, RESOLVE, IPV4_H, UDP_H, IPPROTO_UDP


IFACE = 'wlan2'  # Sending via the interface.

def sendto(sport, address):
    l = libnet.context(RAW4, IFACE)
    dest_ip = l.name2addr4(address[0], RESOLVE)
    l.build_udp(sp=sport, dp=address[1],
            payload='\x00hehe'

    l.autobuild_ipv4(len=(IPV4_H + UDP_H),
            prot=IPPROTO_UDP, dst=dest_ip)

    l.write()


if __name__ == '__main__':
    sendto(6000, ('vpn.sorz.org', 6001))

这段代码(ovpn-snatd.py)用于告知 VPS,OpenVPN 服务器的地址端口。同时接收由 VPS 转发的客户端地址端口信息:

#!/usr/bin/env python
import socket

import sendudp


SNAT_BIND_PORT = 6001
SNAT_SERVER = ('sorz.org', 6002)
OPENVPN_BIND_PORT = 1194

def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('', SNAT_BIND_PORT))
    sock.settimeout(20)

    while True:
        sock.sendto('\x00', SNAT_SERVER)
        try:
            sock.recv(1024)  # Ignore ping response
            data = sock.recv(1024)  # Receiving users' connection request.
        except socket.timeout:
            continue
        if data[0] != '\x03':
            continue

        print('a new connection from ' + data[1:])
        client = data[1:].split(':')
        sendudp.sendto(OPENVPN_BIND_PORT, (client[0], int(client[1])))


if __name__ == '__main__':
    main()

(由于宿舍外网端口号实际上不改变,所以偷懒直接写代码里了,下同)

VPS 转发数据协助建立连接

然后,让 VPS 帮助传递端口双方外网端口地址,总共四段代码:

#!/usr/bin/python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', 6000))


while True:
    data, addr = s.recvfrom(1024)
    s.sendto(str(addr[1]), addr)

↑ 让用户得到自己的 OpenVPN 客户端对应的外网端口。

# (...)

SNAT_SERVER_PORT = 6002

@app.route('/ovpn/connect')
def movpn_openvpn():
    server = get_memcache().get('movpn.openvpn.server')
    if not server:
        return 'Server is not running.', 404
    addr = request.remote_addr
    if addr.startswith('::ffff:'):
        addr = addr[7:]
    port = request.args.get('port', 1194)

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.sendto('\x02%s:%s' % (addr, port), ('localhost', SNAT_SERVER_PORT))

    return server

# (...)

↑ 这是 flask 的代码片段。通过 HTTP,将用户的外网地址端口发给下面的ovpn-snatd.py,同时返回给用户 OpenVPN 服务器的外网地址端口(为了方便,通过 memcache 读取)。

#!/usr/bin/env python
import memcache

SNAT_SERVER_PORT = 6002  # local listening
OPENVPN_BIND_PORT = 1194

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('0.0.0.0', SNAT_SERVER_PORT))

mc = memcache.Client(['127.0.0.1:11211'])
server = mc.get('movpn.openvpn.server')
if server:
    server = (server.split(':')[0], OPENVPN_BIND_PORT)

while True:
    data, addr = s.recvfrom(1024)
    if data[0] == '\x00':  # From openvpn server
        if addr != server:
            server = addr
            mc.set('movpn.openvpn.server', '%s:%s' % (server[0], OPENVPN_BIND_PORT))
        s.sendto('\x01', addr)

    elif data[0] == '\x02':  # From local web server (user's conn request)
        if addr[0] != '127.0.0.1':
            print('\x02 != localhost')
            continue
        print('new connect from ' + data[1:])
        s.sendto('\x03%s' % data[1:], server)

    else:
        print('unknown')

↑ 将 flask 发来的用户地址端口信息,继续转发给 OpenVPN 服务器。也将后者的地址端口信息,储存至 memcache,方便 flask 读取。

OpenVPN 客户端请求连接

用户启动 OpenVPN 前,先随机生成一个 UDP 端口,通过getported.py取得该端口对应的外网端口。然后通过 HTTP 将端口号传递给view.py,同时取得服务器地址。最后释放该 UDP 端口,让给 OpenVPN ,由它建立连接。代码如下:

#!/usr/bin/env python
import random
import logging
import subprocess
import time
import socket

import requests


NAT_REQUEST_URL = 'https://vpn.sorz.org/ovpn/connect?port=%s'
SNAT_SERVER = ('vpn.sorz.org', 6000)


def get_default_param():
    return ['bin\openvpn.exe',
            '--client',
            '--bind',
            '--local', '0.0.0.0',
            '--proto', 'udp',
            '--dev', 'tun',
            '--resolv-retry', 'infinite',
            '--persist-key',
            '--persist-tun',
            '--ca', 'ca.crt',
            '--cert', 'testclient.crt',
            '--key', 'testclient.key',
            '--ns-cert-type', 'server',
            '--keepalive', '20', '60',
            '--comp-lzo',
            '--verb', '3',
            '--mute', '20',
            '--script-security', '2', 'system'
            ]


def main():
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)s %(levelname)-8s %(message)s',
                        datefmt='%Y-%m-%d %H:%M:%S', filemode='a+')

    logging.info('Version 0.2a1')
    port = random.randint(8192, 65535)
    logging.info('Use random port %s.' % port)
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('0.0.0.0', port))
    sock.settimeout(3)
    nat_port = None
    for i in range(5):
        try:
            sock.sendto('orz', SNAT_SERVER)
            nat_port = int(str(sock.recv(256)))
        except socket.timeout:
            logging.warn('Timeout, retry getting NAT port.')
            continue
        except ValueError:
            logging.warn('Illegal value, retry getting NAT port.')
            continue
    sock.shutdown(socket.SHUT_RDWR)
    sock.close()
    if nat_port is None:
        logging.error("Can't get NAT port. Using local bind port.")
        nat_port = port
    logging.info('NAT Port is %s.' % nat_port)

    r = requests.get(NAT_REQUEST_URL % nat_port)
    if r.status_code == 404:
        logging.error('Server is offline.')
        return
    server = r.text.strip()
    logging.info('Server address is %s.' % server)

    logging.info('Waiting 2 seconds...')
    time.sleep(2)
    openvpn = get_default_param()
    openvpn.extend(['--remote'] + server.split(':'))
    openvpn.extend(('--lport', str(port)))
    logging.info('Calling openvpn')
    subprocess.call(openvpn)


if __name__ == '__main__':
    main()

用户使用起来还是很方便的。给 Windows 用的话用可用 py2exe 打包,然后将 OpenVPN 客户端放在bin\ 。连接前只要安装 Tun/tap 驱动就行了。Linux 下把bin\openvpn.exe改成openvpn即可。在 Windows 7 和 Ubuntu 下测试过没问题。

问题

  • 受 NAT 的具体实现方式限制,部分网络无法使用( 在拜托 @ipchihin同学用宿舍网络测试时,就遇到了这种情况);
  • 某墙对 OpenVPN 的政策有时十分严格,甚至有报告因此被封 IP 的(1, 2, 3)。很难想像整个宿舍网络从此无法与国内联系的情形;
  • 连接不便。Windows 需安装 Tun/tap 驱动,Android 和 iOS 需要 root / 越狱。在连接层实现,提供 Socks 5 / HTTP 代理才是王道啊!

另外,因为只是一次尝试并未正式投入实用,所以通信时并未做认证和加密。为避免安全隐患,代码发布前经过少量修改,但修改后未经实际测试。如有疏漏请留言,谢谢。

当时做得就挺乱的,现在讲得更乱,还请求轻喷。

]]>
/p/openvpn-traversal/feed/ 5
利用上网卡转发短信至邮箱,来电提醒 /p/sms2email/ /p/sms2email/#comments Tue, 18 Jun 2013 14:11:04 +0000 http://sorz.org/?p=347689 想法
SMS to Email 效果

SMS to Email 效果

唔,最早的想法是:学校离家较远所以有两张SIM卡,而且由于资费原因只能分别在两地使用,想通过互联网代理,使得可以在一地同时使用两张卡..

最早想到的是使用闲置低端安卓机,后来发现手头闲置的一张华为数据卡(EC1261)可以利用,遂试着捣鼓出一个这玩意..

本文实现:

  • 将所收短信转发至指定 E-mail
  • 挂断来电并回复短信,同时发送 E-mail 提醒

虽然还不能接打电话,但至少接收各种验证短信不再麻烦了呢 🙂

设备

闲置 华为数据卡 EC1261 一张
装 ubuntu 的 Cubieboard 一块

这张上网卡除了上网,收发短信也是可以的:流量提醒就是用短信实现的(所以总是一条条地接收);带了个 2.5mm 耳机插口,应该是用来打电话的,虽然电信给的软件不带此功能。

其他型号的华为上网卡应该类似

安装

Windows: 插入装好驱动就成,华为上网卡用串口和 PC 通讯,插入后设备管理器中可见串口号(COMXX)。

Linux: 以前在 ubuntu 10.10 试过插上就能用。在刷了官网上下的 ubuntu 镜像的 Cubieboard 上,貌似装了 usb_modeswitch 才正常。若自行编译内核,需确保勾选usb_wwanoption模块。会看见类似/dev/ttyUSB0的设备出现。

脚本设计运行于 Linux,依赖 Python 2 和 pySerial,发邮件需装 sendmail 。

apt-get install python-serial sendmail usb-modeswitch

通讯

关于如何“操作”上网卡,这个是关键呢,感谢这篇文章给了我线索,搜索关键词 HUAWEI CDMA Datacard Modem AT Command Interface Specification 即可,PDF 里有详细说明。

根据说明写了段程序,完成标题所述功能:hw_smsd.py
(注意下读写串口可能需要 root 权限)

#!/usr/bin/env python
#encoding: utf-8

# Copyright (C) 2013 @XiErCh
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import time
import serial
import smtplib
from datetime import datetime


LOGFILE_PATH  = 'mail.log'
TTY_DEV_PATH  = '/dev/ttyUSB0'

SENDER_EMAIL  = 'hw-smsd@somehost'
NOTICE_EMAIL  = 'your@email'
CALL_SMSREPLY = u'抱歉,本号码暂时停用。请拨打 13800138000 联系我,谢谢。'

class ATSerial(serial.Serial):
    '''Send a AT command and return the response or raise a error.
    '''
    # Please see HUAWEI CDMA Datacard Modem AT Command Interface Specification
    # for detial.
    def at(self, cmd):
        self.write('AT%s\r\n' % cmd)
        self.flush()
        self.readline()
        rsp = ''
        while True:
            l = self.readline().strip()
            if not l:
                continue
            elif l == 'OK':
                return rsp
            elif l == 'ERROR':
                raise IOError('AT: ERROR')
            elif l == 'COMMAND NOT SUPPORT':
                raise IOError('AT: COMMAND NOT SUPPORT')
            elif l == 'TOO MANY PARAMETERS':
                raise IOError('AT: TOO MANY PARAMETERS')
            rsp += '%s\n' % l


def get_serial():
    return ATSerial(TTY_DEV_PATH)


def sendmail(msg, subject='',fromaddr=SENDER_EMAIL, toaddr=NOTICE_EMAIL):
    s = smtplib.SMTP('localhost')
    msg = u"From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s" \
            % (fromaddr, toaddr, subject, msg)
    s.sendmail(fromaddr, toaddr, msg.encode('utf-8'))
    logfile.write(msg.encode('utf-8') + '\n\n')
    logfile.flush()
    s.quit()


def sendsms(s, number, text, number_type=0):
    logfile.write('SMS to %s:\n%s\n\n' % (number, text))
    logfile.flush()
    s.write('AT^HCMGS="%s",%s\r' % (number, number_type))
    s.flush()
    time.sleep(0.1)
    s.write(text.encode('utf-16-be') + '\x00\x1a')
    while True:
        l = s.readline().strip()
        if l == 'OK':
            return
        elif l.startswith('+CMS ERROR:'):
            raise IOError(l)


def e_new_sms(s, opt):
    # opt[0]     callerID
    #    [1:7]   yyyy,mm,dd,hh,mm,ss
    #    [7]     lang
    #    [8]     format
    #    [9]     length
    #    [10-13] ...
    text = s.read(int(opt[9])).decode('utf-16-be')
    s.read(3) # Ignore ^Z\r\n
    send_time = "%s-%s-%s %s:%s:%s" % tuple(opt[1:7])
    recv_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    mailsubject = u'来自 %s 的短信' % opt[0]
    mailtext = u'%s\r\n\r\n----\r\n发送号码:%s\r\n发送日期:%s\r\n接收日期:%s' \
            % (text, opt[0], send_time, recv_time)
    sendmail(mailtext, mailsubject)
    print("SMS => E-mail")
    s.at('+CNMA') # ack


def e_income_call(s, opt):
    # opt[0] number
    #    [1] type
    #    [5] CLI validity
    if not opt[0]:
        if opt[5] == '1':
            opt[0] = u'隐藏号码'
        else:
            opt[0] = u'未知号码'
    mailsubject = u'来自 %s 的来电' % opt[0]
    mailtext = u'来电者:%s\r\n来电时间:%s' \
            % (opt[0], datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
    sendmail(mailtext, mailsubject)
    print("Call => E-mail")
    s.at('+CHV') # reject
    if opt[0].startswith('1'):
        sendsms(s, opt[0], CALL_SMSREPLY, opt[1])
        print("Call <= SMS")


def loop(s):
    line = s.readline().strip()
    if not line:
        return
    elif line == 'RING':
        return
    try:
        cmd, rsp = line.split(':', 1)
        rsp = rsp.strip()
        opt = rsp.split(',')
    except ValueError:
        print('unknow [%s]' % line)
        return

    if cmd == '^HCMT':
        e_new_sms(s, opt)
    elif cmd == '+CLIP':
        e_income_call(s, opt)

    else:
        print('unknow - %s:' % cmd)
        print(opt)


def main():
    s = get_serial()

    # SMS Parameter Selection:
    # ^HSMSSS=<ack>,<prt>,<fmt>,<prv>
    #   <fm>  6: Unicode
    s.at("^HSMSSS=0,0,6,0")

    # New SMS Notification Setting:
    # +CNMI=[<mode>[,<mt>[,<bm>[,<ds>[,<bfr>]]]]]
    s.at("+CNMI=1,2")

    #sendmail('test', 'sendmail test')
    while True:
        loop(s)

if __name__ == '__main__':
    logfile = open(LOGFILE_PATH, 'a')
    main()

还比较简陋,但运行了几天没问题,就发上来~怎么说也算留个思路吧。

改进

记得刚才说的,这张上网卡有带一个 2.5mm 插口么,
和 Cubieboard 的 Line-In, 耳机口 相连,再装个 Asterisk 配置一下,一个自己的 SIP 网络电话就架成了~

不过目前来说,我对这功能需求并不迫切呢..

]]>
/p/sms2email/feed/ 14
GFWList 兼容 Squid /p/squid-blacklist/ /p/squid-blacklist/#comments Tue, 14 May 2013 16:39:25 +0000 http://sorz.org/?p=347666 这两天在玩 Squid,一个功能颇为丰富的网络代理(缓存)软件。
GFWList 是一个 AutoProxy 维护的一个列表,顾名思义。

Squid 通过 ACL 为每个访问分类,为每个请求分类,控制行为。与目的网站相关的有:dstdomain(目标域名)、dstdom_regex(目标域名,正则表达式) 和dst_as(目标 AS 号)等,具体用法见 官方文档

GFWList 使用的是 Adblock 一样的格式,给它全转成正则表达式,然后使用dstdom_regex匹配就好了。写了个转换脚本(gfwlist2regex.py,运行后自动下载转换生成黑白名单,方便日后更新。

#!/usr/bin/env python
#encoding: utf-8
import urllib2
from base64 import b64decode


LIST_URL   = 'https://autoproxy-gfwlist.googlecode.com/svn/trunk/gfwlist.txt'
BLACK_FILE = 'gfw.url_regex.lst'
WHITE_FILE = 'cn.url_regex.lst'

def convert_line(line):
    if line[0] == '/' and line[-1] == '/':
        return line[1:-1]
        
    line = line.replace('*', '.+')
    line = line.replace('(', r'\(').replace(')', r'\)')
    if line.startswith('||'):
        return '^https?:\/\/%s.*' % line[2:]  
    elif line.startswith('|'):
        return '^%s.*' % line[1:]
    elif line[-1] == '|':
        return '.*%s$' % line
    else:
        return '.*%s.*' % line

        
def convert(gfwlist):
    black = open(BLACK_FILE, 'w')
    white = open(WHITE_FILE, 'w')
    
    for l in gfwlist.split('\n'):
        l = l[:-1]
        if not l or l[0] == '!' or l[0] == '[':
            continue
            
        if l.startswith('@@'):
            white.write(convert_line(l[2:]) + '\n')
        else:
            black.write(convert_line(l) + '\n')

            
def main():
    src = urllib2.urlopen(LIST_URL).read()
    src = b64decode(src)
    convert(src)
             
if __name__ == '__main__':
    main()

修改 Squid 设置,例如这样:

# ...
acl cn    dstdom_regex 'cn.url_regex.lst'
acl gfwed dstdom_regex 'gfw.url_regex.lst'

prefer_direct on
  # 默认直连
never_direct  allow gfwed
never_direct  deny  cn
  # 白名单直连失败将尝试使用代理;如使用
  # always_direct allow cn 将禁止白名单使用代理。

cache_peer localhost parent 1234 0 name=s1 weight=5
cache_peer localhost parent 4321 0 name=s2 weight=10
# ...

黑名单里的必须走上级代理(cache_peer);其余先尝试直接访问,如果被重置,将会转而尝试上级代理。挺智能的。

]]>
/p/squid-blacklist/feed/ 2
MMA7455L + Python + Cubieboard /p/accel-py/ /p/accel-py/#respond Sat, 11 May 2013 17:11:22 +0000 http://sorz.org/?p=347656 MMA7455L (PDF) 三轴加速度传感器,可以使用 I2C 或者 SPI 总线连接。两者 Cubieboard 均有提供,虽然不懂有多大区别但是 SPI 要四根线好麻烦所以就用 I2C 了..(

与 Cubieboard 的连接参照 WikiPB18 (TWI1-SCK)PB19 (TWI1-SDA) 两根接在模块的 SCLSDI 口上,接好电源(3.3V)和地线,模块的 CS 也接在电源上(以使能 I2C)。官网 ubuntu 镜像默认启用 I2C-1,若未启用则需修改 Fex 文件:twi1_used = 1

可以使用 i2c-tools 确认连接正确。装好后输入i2cdetect -y 1,如果出现1d(MMA7455L 的地址),说明连接无误可以通讯。

在 Cubieboard 上使用 Python 读取加速度数值,代码在这:MMA7455L.py,目前实现功能有:

  • 读取 8 或 10 位的 X, Y, Z 轴加速度数值(-127 ~ 128 / -511 ~ 512);
  • 切换 ±2g, ±4g 和 ±8g(set_range());
  • 自动校准(set_offset()clear_offset())。

#!/usr/bin/env python
import smbus
import time


class Accel():
    def __init__(self, dev=1, g=2):
        self.bus = smbus.SMBus(dev)
        self.set_range(g)


    def write(self, register, data):
        self.bus.write_byte_data(0x1D, register, data)


    def read(self, data):
        return self.bus.read_byte_data(0x1D, data)


    def set_range(self, g):
        if g == 8:
            d = 0b01000001
        elif g == 4:
            d = 0b01001001
        elif g == 2:
            d = 0b01000101
        else:
            raise ValueError("Only 2, 4 or 8.")
        self.write(0x16, d)
        self.g = g


    def _get(self, addr):
        l = self.read(addr)
        h = self.read(addr + 1)
        d = h << 8 | l
        if d > 511:
            d -= 1024
        return d


    def get_x(self):
        return self._get(0x00)


    def get_y(self):
        return self._get(0x02)


    def get_z(self):
        return self._get(0x04)


    def clear_offset(self):
        self.write(0x10, 0)
        self.write(0x11, 0)
        self.write(0x12, 0)
        self.write(0x13, 0)
        self.write(0x14, 0)
        self.write(0x15, 0)
        time.sleep(0.1)


    def _set_offset(self, addr, zero):
        d = self._get(addr - 0x10)
        d = zero - d
        d = int(d * 2.5)
        d += 65536
        self.write(addr, d & 0xff) # Low
        self.write(addr + 1, d >> 8 & 0xff) # High


    def set_offset(self):
        self.clear_offset()
        self._set_offset(0x10, 0)
        self._set_offset(0x12, 0)
        if self.g == 4:
            zero = 32
        else:
            zero = 64
        self._set_offset(0x14, zero)
        time.sleep(0.1)


def main():
    mma = Accel(1)
    mma.set_offset()
    while True:
        x = mma.get_x()
        y = mma.get_y()
        z = mma.get_z()
        print("x =%4d ,y =%4d, z =%4d" % (x, y, z))
        time.sleep(0.1)


if __name__ == '__main__':
    main()

这块IC功能挺多的,先把好玩的给写了,以后有需要再改脚本。

P.s. 写校准时被坑了两次,MMA7455L 的 PDF 里不给计算方法,得下 AN3745 看。里面有两句话:

(…) values calculated in Step 2 must be multiplied by two.
Note that there will still be a bit of offset shift, and you may need to multiply by a bit more than two to exactly subtract the offset.

果断只看到前一句就乘 2 了,实际上反复测试要乘 2.5 才完美…
另外虽然 PDF 里写数据只有 10 位,但负数取补码时得按 16 位取,发 16 位才行…

]]>
/p/accel-py/feed/ 0
OpenWrt 上使用 Python 操作 TAP/TUN /p/openwrt-py-tun/ /p/openwrt-py-tun/#comments Sat, 27 Apr 2013 18:58:40 +0000 http://sorz.org/?p=347635 这两天折腾 tun,之前完全没接触驱动这么底层的东西,全靠 GlacJAY 大大的这篇文章入门了。

一句话,参照此 gist,同时将TUNSETIFF = 0x400454ca改为TUNSETIFF = -2147199798 即可。

那篇文章的代码在 Windows 和 Ubuntu 下都正常运行,但今天在路由器上运行时却报错了:

Traceback (most recent call last):
  ……
  File "/root/movpn/tun.py", line 218, in _open_tun
    fcntl.ioctl(tun, TUNSETIFF, ifr)
IOError: [Errno 81] File descriptor in bad state

因为 vtun 运行正常,所以 tun 本身应该是没问题的。又测试了一下,这个错误在 ioctl 的request不正确时出现。遂铺天盖地地寻找此值,发现不少相同遭遇,如这里还有这,但没找到解决方案。

先去解了 ioctl,得知第二个参数是由好几个部分组成的,从 if_tun.h 找到了 ioctl.h,最后卡在一点C语言也不会,琢磨半天也没弄明白 TUNSETIFF 取值多少..

无奈,只好试着自己写段代码把 TUNSETIFF 打出来了..
之前只听说过 交叉编译 这个词,现在就要动手了好鸡冻,好在 OpenWrt 的相关资料很丰富:参照 wiki 以及 这篇帖子 外加一晚上时间终于完成了。

找了一个 C Hello Word 改了一下:

#include 
#include <linux/if_tun.h>
#include <sys/ioctl.h>

main() {
    printf("TUNSETNOCSUM  = %d\n", TUNSETNOCSUM);
    printf("TUNSETDEBUG   = %d\n", TUNSETDEBUG);
    printf("TUNSETIFF     = %d\n", TUNSETIFF);
    printf("TUNSETPERSIST = %d\n", TUNSETPERSIST);
    printf("TUNSETOWNER   = %d\n", TUNSETOWNER);
    printf("TUNSETLINK    = %d\n", TUNSETLINK);
    printf("TUNSETGROUP   = %d\n", TUNSETGROUP);
}

交叉编译:

xx@xx:~/openwrt/staging_dir/toolchain-mips_r2_gcc-4.6-linaro_uClibc-0.9.33.2$ \
    bin/mips-openwrt-linux-gcc test.c

上传路由器,执行:

TUNSETNOCSUM  = -2147199800
TUNSETDEBUG   = -2147199799
TUNSETIFF     = -2147199798
TUNSETPERSIST = -2147199797
TUNSETOWNER   = -2147199796
TUNSETLINK    = -2147199795
TUNSETGROUP   = -2147199794

OK,全出来了,话说为啥都是负数 = = 和 CPU 位宽整数长度啥的有关?换一个路由器是否还是这个值?
不知道,管用就行,Python 里TUNSETIFF = -2147199798就好了。

P.s. 嘛其实回想起来也不复杂,就是编译 OpenWrt 略显蛋疼:经常在各种下载时卡住,挂VPN、断VPN,反复试。 有个 git clone 就是卡在 84% 死活过不去,最后把 git:// 改成 https:// 才好..

]]>
/p/openwrt-py-tun/feed/ 6
为 Shadowsocks 客户端加入 HTTP 代理支持 /p/ss-http/ /p/ss-http/#comments Fri, 11 Jan 2013 12:24:57 +0000 http://ouno.tk/?p=347540 shadowsocks你懂的 软件,SSH -D替代品。

SOCKS5 代理自己用着是没问题的,但普及以及携带使用的时候,HTTP 代理会更方便些(无 DNS 污染问题)…

代码在这,基于 0.9.2。
先放 pastebin 上了.. git 用不清楚就先不 fork 了..  – –
(虽然改得比较丑,但使用起来没问题啦

]]>
/p/ss-http/feed/ 2
ASS 字幕在线转换为 SRT /p/web-ass2srt/ /p/web-ass2srt/#respond Fri, 26 Aug 2011 08:34:11 +0000 http://xierch.tk/?p=347234 >> ass to srt 在线字幕转换:
http://lab.sorz.org/tools/asstosrt/

这就是之前写的那个asstosrt2.py脚本的GAE移植版
具体说明、源码请见:《[Python] ass字幕批量转srt – asstosrt2.py》

这个GAE移植版…可以说…相当简陋(丑陋)…

 暂不支持批量转换…所以只能供手头没工具时临时使用吧…
需要批量转换可以直接下载 asstosrt2.py 脚本…

#UPDATE111218:
支持上传ZIP压缩包批量转换了!如需离线使用仍可下载 asstosrt2.py 脚本。

#UPDATE120303:
已由GAE迁至VPS的Django上,更灵活些..

]]>
/p/web-ass2srt/feed/ 0
[Python] ASS 字幕批量转 SRT – asstosrt2.py /p/py-ass2srt/ /p/py-ass2srt/#comments Tue, 19 Jul 2011 17:01:33 +0000 http://xierch.tk/?p=347213 #UPDATE120423: 查看新版。本文所述版本已不再更新。
#UPDATA111218:
不想看以下大堆文字说明,请直接猛戳此处使用在线版本直接批量转换

电视、手机 都不支持 ASS 字幕 ,之前用 SrtEdit 转。
但后来遇到个字幕组,把 OP&ED 的字幕放在最前面,字幕顺序颠倒了,手机就不认了…
于是写了这个 Python 脚本

为了使用方便,只需要将ass文件全部选中,拖到 asstosrt2.py 上就好了
#UPDATE110903: 如果无法拖拽,可直接运行,将自动转换同目录下所有.ass文件。
再没有其他操作了…
如果没有错误,转换完窗口会直接关闭,在ass文件同目录下会生成srt文件

当然你需要先安装 Python 2.x 才行

下载 asstosrt2.py:
最新: GitHub | 历史:  SkyDrive | Google Docs

下载 Python 2.7:
Python.org


#UPDATA110903: 请确保 atslib.zip 与 asstosrt2.py 在同一目录下。

功能特点:

× 默认输出srt文件编码为 UTF-16 (Unicode / UCS-2 Little Endian)
    我电视手机刚好都支持这个编码
× 自动删除 {} 内的特效代码
    srt 不支持嘛
× 默认对字幕按时间进行重新排序
    我手机只要顺序一乱就罢工了,电视还没试过
× 默认删除带过渡特效的字幕
    例如顶部滚动显示“仅供研究,请在24小时内删除……”等,容易覆盖对白字幕
× 繁简体转换(默认禁用)
调用了部分维基百科的翻译规则,使用了部分 pyswim 项目代码
× 只显示第一行字幕(默认禁用)
    一些双语字幕将中文放置在第一行,启用后可简单删除 \N  后的内容

由于比较懒 🙂 ,如需修改默认设置,请直接编辑代码 48~52 行。

程序很简单,或者说很简陋,本来就是自己方便着用的东西嘛…200行..
Python 2.7, Windows 7 下调试无误,GPLv3
初学 Python 没几天,糟糕的代码,见笑了
P.s 不要吐槽排序部分代码  -_-||

#UPDATE 120306:
(Ver 0.4.6) 支持换行显示

#UPDATE 110923:
(Ver 0.4.5) 解决某些设备(如某些三星电视)完全无法识别字幕的问题。

#UPDATE 110903:
(Ver 0.4.4) 忽略文件编码错误,解决少量字幕转换失败的问题。
(Ver 0.4.3) 非拖拽模式增加转换.ssa文件。
(Ver 0.4.2) 增加繁简体转换功能;
直接运行将转换通目录下所有.ass文件。

#UPDATE 110723:
(Ver 0.3.2) 自动识别ASS文件编码,解决无法转换非 UTF-8 / ASCII 文件的问题;
增加了一点转换失败判断;加了个简短的说明文件并打包。

]]>
/p/py-ass2srt/feed/ 24