2016年6月30日星期四

HTTP Client新宠Retrofit和ReactiveX结婚之后,新的挑战来了

其实也不算很新,Retrofit,名称和很多新宠一样操蛋。但是,他的开发者必然是最精益求精的人,设计和实现代码之精致,令人发指。但是一旦和另一个古怪精灵ReactiveX结合之后,一定会有不少人就更“不好”了。例如想加个共通的重试功能,就不是那么好搞了。

我后知后觉,就不多细细说Retrofit了,比较好的介绍在用 Retrofit 2 简化 HTTP 请求
大致说起来,只要用纯Java代码结合Annotation定义一个漂亮干净的API接口,例如
    public static interface SomeService {
        @GET("xxx/yyy")
        Observable<String> someApi(@Query("qqq") String qqq);
    }
然后就可以让Retrofit生成一个符合这个接口的Proxy一样的东西(的确,内部用的技术是Java7内置的Class Proxy技术,就是任何method调用都会掉到一个共通的方法里,里面能够知道method名,参数),
        SomeService serviceInvoker = new Retrofit.Builder()
                .baseUrl("http://some_web_site.com")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build()
                .create(SomeService.class);
拿这这个东西(例子里的serviceInvoker),就可以像普通API调用一样使用了,不用写什么HttpClient关联的代码了,数据类型也自动搞定。
        Observable<String> rx = serviceInvoker.someApi("oooops");
        rx.subscribe(
                result -> println(result),
                error -> println("error: " + error));
除了Observable<...>影响心情外,其它的都感觉良好。
这个Observable就是Retrofit的小老婆ReactiveX了,又称为RxJava什么的。关于这个我以前写过RxJava的一些大白话。简而言之,ReactiveX不是那么直观,想要干什么总要委婉一点优雅一点才能通的过。
RxJava的作者估计也是个追求完美的设计家,硬生生地从各种看似分散的事儿里理出了这么一套模式还打出了一片天地,能够异步回调,能够像stream一样变形串起来操作,再加上一些暧昧的名词,看起来显得高大上,但是所以理解起来就得发挥十分的想象力,怎么爽就怎么想才行。
烦恼往往就出在小老婆ReactiveX身上 (大老婆是那个Call<?>的)。
这次的挑战是,需要在rx.subscribe(...)之前,加个retryWhen定义,告诉他当出什么样的错误才重试
                rx.retryWhen(errors -> errors.flatMap(error -> {
                    boolean needRetry = false;
                    if (restRetryCount >= 1) {
                        if (error instanceof IOException) {
                            needRetry = true;
                        } ...
                    }

                    if (needRetry) {
                        restRetryCount--;
                        return Observable.just(null); //means need retry
                    } else {
                        return Observable.error(error); //means finish with error
                    }
                }))
                .subscribe(...)
代码不是那么简洁可以忍着,但就算一行代码,也肯定不想在每个使用serviceInvoker的地方都这么干的。于是想写在共通里,
可是。。。似乎没地方插手,因为在共通代码.create(SomeService.class)附近没有出现这个rx的身影。
这就是令人为难的地方了,不管三七二十一,先把事儿解决了再说,我就先上一个方法,思路就是找到创建出rx的地方,把代码插在那儿。经过跟踪,发现是RxJavaCallAdapterFactory.create()里,于是换成一个新的newCallAdaptorFactory来包着他,一旦得到了rx就加上.retryWhen
        RxJavaCallAdapterFactory originCallAdaptorFactory = RxJavaCallAdapterFactory.create();

        CallAdapter.Factory newCallAdaptorFactory = new CallAdapter.Factory() {
            @Override
            public CallAdapter<?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {

                CallAdapter<?> ca = originCallAdaptorFactory.get(returnType, annotations, retrofit);

                return new CallAdapter<Observable<?>>() {

                    @Override
                    public Type responseType() {
                        return ca.responseType();
                    }

                    int restRetryCount = 3;

                    @Override
                    public <R> Observable<?> adapt(Call<R> call) {
                        Observable<?> rx = (Observable<?>) ca.adapt(call);

                        return rx.retryWhen(errors -> errors.flatMap(error -> {
                            boolean needRetry = false;
                            if (restRetryCount >= 1) {
                                if (error instanceof IOException) {
                                    needRetry = true;
                                } else if (error instanceof HttpException) {
                                    if (((HttpException) error).code() > 400) { //please modify this condition
                                        needRetry = true;
                                    }
                                }
                            }

                            if (needRetry) {
                                println("******* retry *******");
                                restRetryCount--;
                                return Observable.just(null);
                            } else {
                                return Observable.error(error);
                            }
                        }));
                    }
                };
            }
        };
看看运行结果,的确重试了3次了。至于最后一个错误纯粹表示最终的错误。
20:00:39.453 @main ******* retry *******
20:00:39.464 @main ******* retry *******
20:00:39.464 @main ******* retry *******
20:00:39.497 @main error: java.net.UnknownHostException: some_web_site.com
很怀疑Retrofit的作者看见这个代码会大怒,说还有这个那个更好的方法为什么不用?哎,谁让你做的那么深奥呢。
在stackoverflow网站上android - How to retry HTTP requests with OkHttp/Retrofit? - Stack Overflow,有人回答用OkHttpClient的Interceptor来做,可是网络压根连不上的时候那个方法就没用。从方法论上说,就应该和那种http client无关。

2016年6月29日星期三

TCP古董TIME_WAIT:为什么被动关闭的一方不需要TIME_WAIT傻等呢,似乎人人都了然了


突然想起了这个问题,原因自然可以猜测得到。可是奇怪的是,实在没找到有人问同样的问题,更没有看到什么地方说明原因。真...孤独。总觉得世界上一定有人想起过这个问题,只不过没人愿意提罢了,于是做个备忘录。

TIME_WAIT,是网络高手们的必经之路,有很多文章可参考,随便给一个我觉得写得很有经验的文章:你所不知道的TIME_WAIT和CLOSE_WAIT - 老男孩教育博客
用自己的话说,大致就是,
主动关闭连接的那一方呢,虽然也收到了对方的分手信号,可以说是顺利的完成了分手手续了,可是,它但还是担心对方有什么迟延数据等会儿会乱来,所以等几十几百秒。
  • 有道理啊,但是很傻,特别是对于Web Server这样的被动服务来说是很傻。

反正现状是,几乎所有的Web Server在创建时socket server时都指定了SO_REUSEADDR标记了,部分原因就是为了克服这个破TIME_WAIT时不让侦听的毛病。否则铁定被人投诉。同理,NodeJS之类的工具里也是这样处理了。
如果没有这个标记,那经常在Debug时手动停止或者不小心死了(反正都会引发这个TIME_WAIT)后,再次启动Web Server时,就会爆“地址有人在用了”,那谁能忍受啊。
Web Server:想开点,对自己好点,别扣着破标准了,我主动关闭了连接后,既然也收到了对方的结束信号了,那后面来的什么迟延数据关我啥事儿啊,发现乱数据我就回个RST,就算是出事儿也是对方那边出事儿。
所以呢,这么多年过来,也没人说Web Server和NodeJS这么有什么问题,那么多高手都盯着呢。
当然,这些大白话都不是什么严谨的话,扣字眼就没完没了了。
  • 而对于客户端,路由器,这些东西上面的TIME_WAIT还是不能简单无视的,这在上面的链接文章里有例子。


  • 我好奇的是,为什么被动关闭连接的那一方,当顺利的完成了分手手续后,不进入那该死的几十几百秒等待状态呢?

为什么文档上不明确添加个说明呢,光给出个状态图硬说:就是这样的状态迁移,你爱接受不接受。
随便给个链接:CLOSE_WAIT
输入图片说明
猜测一下,那是因为TCP是排序处理数据的,既然都收到了主动方发出的FIN信号(就是主动方发了一堆数据后单独发了个FIN表明完了),那就说明主动方的所有数据都已经接收到了,不存在什么迟到的数据的事儿了。之后一旦收到了主动方的ACK,那就OK了,干干净净分手。
猜测是猜测,可这东西又不好做实验,不找个100%肯定的文章我是不放心的,这是强迫症。
例如,如果主动方发来的FIN比之前的数据还早来,那分手之后,岂不是还是有可能对方的数据迟迟而来?不过幸好,TCP是检查顺序的,来早的FIN应该会被cache起来,并不立刻当真处理的。
这就是我要确认的内容,理论上显然是没错的,可实际上谁知道呢,我需要一个有经验的人的话才安心。
硬翻那协议说明的话,总是隔靴挠痒,不贴心,我就想要我的问题的答案。
还好,找到了有人明确这样说了,我就罢休了。 TCP数据流稳定性--TCP分片,重组及乱序 - 晓风残梦 - 博客园
TCP协议规定,对于收到的乱序报文并不丢弃,而是缓存下来(这样做是为了减少更多的重传),立即发送希望接受的报文确认。例如:发送端A发送了以下几个包:第一个:1001-1100,第二个1101-1200,第三个FIN包(序列号是1201,一个虚字节)。
1)第二个包在传输的过程中丢失了,接收端收到第三个包后并不丢弃,而是缓存下来,然后,立即发送一个ACK,确认号是1101(这样做的目的是不必等到发送端A的第二个包超时后重传,发送端A收到3个同样的ACK后立即重传,这是快速重传的概念)。


2016年6月27日星期一

迟到的发现,可以从VirtualBox里用ADB访问USB接口连着的Android了

几年前还是不行的,有一天我要进行一个危险的研究,必须得用虚拟机,而且要从里面通过ADB访问USB接口连着的Android,感觉VirtualBox经过几年功夫应该已经对应了这个常见的需求了,结果真的有了,谢谢你VirtualBox。

“不必多说,你心里知道,你我到底都要做些什么”,就复制几个图吧。
我是用Mac OS X为主机,在VirtualBox里用Windows 10的,但是忘了截图,就
贴个别人的,贴的图是用Windwos 7为主机,在VirtualBox里用Ubuntu的
都管用。


结束

正规表达式的备忘录(吝啬匹配)

* 和 + 是贪婪的匹配,尽量长的匹配,有个不为人周知的修饰法,*或者+后面再加上?, 就是表示尽量短的匹配方式。

有时候想要尽量短的匹配,就需要用这个?修饰。
找了半天才找到这个方法,不容易啊。
例如
开始特征
  <span>我要找的内容</span
结束特征
开始特征
  无关的内容
结束特征
用JavaScript来做实验,如果用正规表达式
/开始特征([^]*)结束特征/
注:[^]表示任意字符,包括回车换行。()表示抓包。
来找,结果会连"无关的内容"那段也包含进去了。
但是如果在*后面加个?就如我所愿了。
/开始特征([^]*?)结束特征/


2016年6月24日星期五

体验了一下Docker的root用户映射

2016/07/01:要想限制容器里用户的权限,有两种方法:

  1. 让你在容器里做个凡人(这个方法对于有些需要root的容器是不适合的)
具体的就是,指定容器里用户的uid:gid,使得容器里压根不存在root用户。
$ docker run -it -u 1000:1000 ubuntu
groups: cannot find name for group ID 1000   #这个错误没关系
I have no name!@fcaadb40ddd0:/$ id     #执行id命令看看结果。
uid=1000 gid=1000 groups=1000
(这里的uid:gid和主机的不一样,具体的怎么关联的不太清楚。一般来说这个就足够了,但是也许会有什么应用需要root权限,这时就需要下一步要说的方法来限制权限了)
  1. 让你在容器里做玉皇大帝, 但是这个玉皇大帝和所有凡人,都只是被映射到茫茫宇宙中一片卑微甚至虚空的身份上
就算容器里有个孙悟空突破了限制做了玉皇大帝,那也翻不出如来佛的手掌。 具体的就是,把容器里的root等用户映射成主机那边的指定的一片uid:gid。

2017/01/20:忽然想起来看看,容器里以root运行的进程,在外面看来到底是什么?是一个进程。那是什么用户身份呢?也是root,只是capabilities受到很多限制,理论上依然危险。

在docker里以root身份运行sleep 12345
docker@somehost:~$ docker run -it busybox
/ # id
uid=0(root) gid=0(root) groups=10(wheel)
/ # sleep 12345
在容器外面查看这个sleep是什么进程,什么用户身份?也是root。
docker@somehost:~$ ps -ef |grep 12345
root      1997  1980  0 09:44 pts/1    00:00:00 sleep 12345
查看该进程的权限信息,就能够发现capabilities是受限制的,但是理论上还是可以钻漏洞影响到容器外的。
docker@somehost:~$ cat /proc/1997/status |grep Cap
...
CapEff:    00000000a80425fb
...
用capsh工具查看00000000a80425fb就明白意思。这个root收到了限制。
最近用的docker-machine版本是0.8.2,docker 1.12.5。发现docker-machine里面已经默认创建好了dockremap这个用户了,连/etc/subuid /etc/subgid里都准备好了。结果,只要修改活着创建/etc/docker/daemon.json,加上{"userns-remap":"default"}就可以了,重启动docker-machine就可以生效。
确认
docker@somehost:~$ cat /etc/subuid 
dockremap:165536:65536
docker@somehost:~$ cat /etc/subgid 
dockremap:165536:65536
准备
docker@somehost:~$ cat /etc/docker/daemon.json
{
  "userns-remap": "default"
}
如果还不存在dockremap用户,那么就运行这个命令生成用户dockremap:
sudo adduser --system dockremap
重启动
docker-machine restart somehost
docker-machine ssh somehost
在docker里以root身份运行sleep 12345
docker@somehost:~$ docker run -it busybox
/ # id
uid=0(root) gid=0(root) groups=10(wheel)
/ # sleep 12345
在容器外面查看这个sleep是什么进程,什么用户身份
docker@somehost:~$ ps -ef |grep 12345
165536    2360  2344  0 09:36 ?        00:00:00 sleep 12345
就是这个165536,说明生效了,用户身份不是以前的0(root)了。

2016/?/?: 我要体验的就是第2种功能,刚出来半年,因为uid映射只能手动做,而且是针对docker daemon设定的,所以默认是不开启的,也无法从普通的docker run命令里指定。

主要是docker daemon加个选项 --userns-remap=test_user:test_group and add /etc/subuid /etc/subgid
如果按照docker官方文档说的--userns-remap=default(就是让docker daemon创建dockremap用户),也会报错说找不到用户,据有的用户反映是tiny core linux里的adduser和普通的linux发行版不一样,我没仔细查。
$ docker-machine ssh rethink   # rethink is my vm name, please change it to yours.
...
docker@rethink:~$ sudo vi /var/lib/boot2docker/profile 
...
EXTRA_ARGS='
--label provider=virtualbox
--userns-remap=test_user:test_group            #####this is what i add#####
'
...
docker@rethink:~$ exit
then restart by docker-machine restart rethink, cause same error Maximum number of retries (10) exceeded(看/var/lib/boot2docker/docker.log能看到错误是说用户找不到), docker daemon is not started,
then add user/group,
docker@rethink:~$ sudo addgroup -g 20 test_group
docker@rethink:~$ sudo adduser -G test_group -SDHu 501 test_user
then prepare uid/gid range for container
docker@rethink:~$ sudo -c "echo test_user:100000:65536 > /etc/subuid"
docker@rethink:~$ sudo  -c "echo test_group:200000:65536 > /etc/subgid"
then start docker daemon
docker@rethink:~$ sudo /usr/local/bin/docker daemon -D -g "/var/lib/docker" -H unix:// -H tcp://0.0.0.0:2376 --label provider=virtualbox \
--userns-remap=test_user:test_group --tlsverify --tlscacert=/var/lib/boot2docker/ca.pem --tlscert=/var/lib/boot2docker/server.pem --tlskey=/var/lib/boot2docker/server-key.pem -s aufs

...
INFO[0000] User namespaces: ID ranges will be mapped to subuid/subgid ranges of: test_user:test_group 
DEBU[0000] Creating user namespaced daemon root: /mnt/sda1/var/lib/docker/100000.200000 
...
Then i run docker container. There are strange thing here: previously used docker image will be treated as "not exist", get downloaded again.
$ docker run -it -v /bin:/xxx busybox
Unable to find image 'busybox:latest' locally
latest: Pulling from library/busybox
8ddc19f16526: Pull complete 
Digest: sha256:a59906e33509d14c036c8678d687bd4eec81ed7c4b8ce907b888c607f6a1e0e6
Status: Downloaded newer image for busybox:latest
/ # id
uid=0(root) gid=0(root) groups=10(wheel)
/ # ls /xxx
VBoxClient     cat            dd             echo           gunzip         ln             mount          printenv       ...
...
/ # echo hi > /xxx/a
sh: can't create /xxx/a: Permission denied
很好, container里的root只能老老实实的以host os里的test_user身份运行了。
不过一旦重启动就泡汤了(还是老错误说用户找不到), 出现问题后,请进去把/var/lib/boot2docker/profile里的改动给恢复就行了。
重启动导致的问题很好理解,八成是因为docker-machine的VM是从关盘(boot2docker.iso)里启动的,我做的修改大部分都是在iso的内存盘里做的,并没保存到iso里,这是docker-machine的一个问题,有人提了建议了。

2016年6月23日星期四

原来fastboot boot custom.img可以无需刷机就以启动定制系统(以root)

知道的太晚了,对于解锁过的android机子,无需刷机就可以启动一个定制系统,同时可以用adb shell以root身份做任意操作。


至少这个有两个好处: 一是 刷机之前启动一下看看行不行,不行就不刷。 二是 有时仅仅为了临时以root做点事儿用这个就很方便, 启动后运行adb shell时就会发现已经是root身份了。不然呢, 即使刷了recovery分区,那么以后官方的更新来的时候会无法更新成功(因为官方更新会把查分文件交给recovery系统来做,需要官方的recovery系统)

这一切的前提是机子已经结果锁了(就是允许对bootloader进行一些操作,具体的方法查查各自的厂商是怎么搞的,有的很诡异,需要拨打一个奇怪的号码)。

2016年6月21日星期二

哈,"broker"这个词害死人,和“分解者”毫无关系,实际是和HUB差不多的意思

汗。。。 不过一定也有人以为和break + er有关系。 例如,Message Broker, RuntimeBroker.exe, ImeBroker.exe, Connection Broker, AtBroker.exe, 每次都很纳闷, 误以为是分解者,为啥需要分解呢。

有一次偶然看到ncat的Connection Brokering功能,还自我宣称是它最强大的功能,

就忍不住看了一下,发现哈哈误会啊,原来是那种转发,代理,广播,集线器差不多的意思。

2016年6月20日星期一

试了一个获取Android系统权限的工具Kingroot,运气不错,取得root了。

看起来做得很精致干净,谢谢!试验的机子是一款没有名气的牌子。2016/07/14补:后来发现有个更干净更管用的工具,名字多了个o,叫KingoRoot。

kingroot 这个公司看来搜罗了很多Android的漏洞,挨个试着破解,运气好的时候,就不用启动就成功了。也有人运气背的,不停的崩溃重启动。这东西谁能保证呢。
安装:显然不能从Google 的Play store里来,只能手动到那个网站下载app,我是下载手机版的,然后adb install ......apk。
然后运行KingRoot里的Root Auth功能,需要联网,它似乎会查询代码库,然后自动的进展,过几分钟,他说好了。
这好了的证据,一个是,我发现这个app无法像一般app那样拖拽到垃圾箱卸载了,app信息里的卸载按钮压根不存在。
然后我adb shell进去一看,/system/xbin/su新生成了!执行su一下就会有Kingroot画面弹出来问允不允许,就和SuperSU那个差不多。
然后随便干什么都行了,只是有一点, /system目录依然是mount成只读的,所以大概需要重新mount一下,我还没试,想来和以往的经验没什么不同。不管怎样,至少id成了0了,/data/data下可以乱搞了,各种不顺眼的process都可以kill了。
追加,试了,可以把/system搞成可写的了。方法google一下到处都有,摘要:
#   mount -o rw,remount /system



2016/06/28:
这个工具毕竟是特殊工具,用时要做好遇难的思想准备。KingRoot有Windows版(通过ADB连接手机进行破解),我在虚拟机里用了(感谢VirtualBox可以安装一个为了ADB的USB扩展: http://forum.xda-developers.com/showthread.php?t=570452),
KingRoot最终报错,但实际上/system/xbin/*su* 已经生成了,其实已经被root了,但是一执行就会爆“no ...ui ...”之类错误,这是手动把KingRoot的apk安装上去(不用执行)就可以了,他就会和su配成对来弹出一个对话框确认来自adb的root请求。
进一步发现,这个工具很可能受到腾讯安全公司的资助,因为它内部包含了Tencent的ADB Server和一个QQ关联的东西,通过工具看到这个ADB Server会往腾讯安全公司发数据。可能存在后门。
手机版的就没确认又无后门。

总之,用这个工具,要在虚拟机里用,并且手机要禁止网络,成功之后在把KingRoot的su换成标准的SuperSU之类的(可笑的是,这东西也不是开源的)。
2016/07/14补:后来发现有个更干净更管用的工具,名字多了个o,叫KingoRoot。这个工具没在Windows上安装什么乱七八糟的(安装时不要同意赞助软件就行了),运行时我观察了,没有起额外的进程,出了自带的adb。也顺利的root成功了,而且手机上安装的东西没有别的功能,只是管理root同意名单,叫做SuperUser,可能是基于一个开源的SuperUser改的。