知乎“分享”功能可能正危及你的隐私

如果你和我一样是知乎和 QQ 的深度用户,你可能对这样的消息格式很熟悉:

一条知乎的分享消息

这是从知乎安卓客户端(因为我是安卓用户,所以本文只讨论安卓客户端的问题,iOS 不清楚,可能一样可能不一样)分享到 QQ 的文案格式。让我们仔细看看这个链接:

https://www.zhihu.com/question/61427877/answer/361977341?utm_source=qq&utm_medium=social&utm_member=YzA1N2VkNTNiYTMyMmMwZDdiODYxYmI0NDRiOWZlYTY%3D%0A

一个分享链接

紫红色的部分就是打开这个页面所需要的链接。里面有两个数字,分别代表问题、回答,这样,这个链接就会打开一个回答的页面。这很好,完全没有什么问题。

接下来,绿色的部分:utm_source=qq。到这里,事情变得很微妙。这表示这个链接是通过 QQ 分享出来的。蓝色的部分:utm_medium=social,这表示这个链接是通过社交平台分享出来的。你可以试试把这两个参数删掉,这并不影响你打开这个回答页面。

最重要的问题在黄色的部分:utm_member=Yz.....%3D%0A。你会发现这个部分很长,而且看上去似乎没有什么意义。真的如此吗?我将简单的演示如何通过这个字符串来获取你的知乎个人页地址。

做一些转换

把这串乱码做 URL 解码,再做 base64 解码。此时,它变成了一串 16 进制字符串——好的,还是乱码。但这串乱码已经有用了,打开 https://zhihu.com/people/c057ed53ba322c0d7b861bb444b9fea6 ,你会看到,它跳转到了分享人的个人页。

感谢兮嘉的出演

这样就完成了一个“从分享链接找到分享人的知乎帐号”的过程。这其实很严重——如果你把链接分享到一个群里,那么当群友打开这个链接时,知乎就知道了你和他们是群友;群友也知道了你的知乎帐号。这是十分明显的隐私信息泄漏。

为了讨论隐私泄漏的问题,我们先来聊一聊追踪是如何实现的。如果你已经了解了 utm 追踪,你可以跳到第二节“这有什么问题?”继续阅读。

追踪如何实现?

链接

万维网用户对“链接”都非常熟悉。简而言之,万维网通过链接联系在一起,通过链接来在不同网站、不同应用之间导航。

一个典型的链接,如同一开始我们提到的那个,是由协议、主机、路径、参数和一些其它部分构成。其中,协议和主机用来确定请求该如何、发送到哪个服务器,而路径和参数则是直接发往服务器,由服务器用于决定展示什么内容。

在分享时,知乎,或者其它别的什么 App,大致都会将当前页面的内容生成一个链接,然后再将链接分享到其它 App 中。

utm 参数

上文我们提到了,参数是直接发往服务器的,而分享的时候可以自定义参数,意味着我们可以通过在链接中加入一些特殊的参数。这些特殊的参数静静的躺在链接里,等待别人点击——而一旦被别人(比如你分享的好友)点击,这些参数就将发往服务器,服务器就知道:哦,这个用户的请求有这些参数,意味着他是从分享链接点击过来的。

而 utm (Urchin Tracking Module)参数是业界通用的、用于处理追踪的参数。通常而言,这些参数并不影响你打开的页面是什么样的(对用户无感知),只是用来统计和追踪。这些参数通常以 utm_ 开头。(扩展阅读:https://en.wikipedia.org/wiki/UTM_parameters

就像知乎做的那样,分享的时候增加了 utm_source=qq 这个参数,其它用户点击的时候,这个参数会被发送到知乎服务器上。这样,知乎的服务器就知道了,你的好友是通过 qq 分享的链接访问的知乎。

追踪的积极意义

如果你理解了前面我所说的内容,你很容易想到为什么大家会热衷于使用追踪技术。产品经理想了解从哪个渠道的分享到达率最高、哪个渠道的转化最厉害,或者可能根据来源渠道给予各种奇奇怪怪的针对性优化(或者负优化……)。甚至说,有一定规模并建立起一定的监控机制之后,如果某个渠道出现了问题,也可以迅速发现。

这有什么问题?

如果说追踪对用户没有感知,并且又有那么多好处的话,为什么我要说知乎正在危及你的隐私呢?事实上,由于链接参数的可见性(对第三方平台、浏览器等工具是可见的),这些参数可能被泄漏给第三方。此外,对第一方(应用开发者,比如知乎)而言,许多所有用户操作都属于用户的个人隐私,需要对用户显式说明。

我将在这里简单的说明几个较为明显、容易想到的问题。这些问题并不一定实际存在——我也不知道他们是不是存在,我只在此提出:这些技术是完全可行的,并且知乎也可以简单的避免这些问题。

跨平台的用户追踪

对于每个用户而言,不管你分享多少次,只要你不退出或者更换帐号,utm_member 是不会变化的。这可以被唯一标识用户,我们称之为“知乎用户标识”——当我们看到这个用户标识,就知道它是你。虽然这个链接是 https 的(这样运营商和中间服务器不会看到链接),但除了你和知乎,还有别人可以获取这个链接。

比如你分享到 QQ 上,那么 QQ 就可以知道你的知乎用户标识;你把链接复制到微博,那么微博也知道了你的知乎用户标识;你用浏览器打开,浏览器也知道了你的知乎用户标识。第三方平台完全可以把你在他们平台上的帐号和你的知乎用户标识关联起来,实现跨平台追踪。

可被任何人识别的用户标识符

文章的开篇部分我简要展示了如何从分享链接中获取你在知乎的用户信息。这事实上没有任何技术门槛,任何人都能进行尝试(如果你对技术一无所知,你可以搜索“在线 base64 解码”、“在线 URL 解码”,能搜索到很多在线工具代劳)。假设你在知乎上是超级大V,但身边的人都不知道、你也不想让他们知道。但现在,只要你通过安卓知乎客户端从 QQ 上分享任何内容给你的朋友,大家都知道你就是知乎大V了……

应该怎么办?

相信你已经理解了问题是如何存在的(如果没有理解那就算了……)。那么如何避免这种隐私问题呢?这里我也从用户和运营方(知乎)的两个方面来思考应当如何处理。

用户怎么办

对用户而言,移动端事实上缺乏这类隐私防范的工具。就当前这个事件来看,我发现通过“复制链接”的方法可以获得相对干净的链接,从而摆脱 utm 的追踪。

在桌面浏览器上,我推荐有些隐私洁癖的朋友使用 Neat URL 这个扩展。它可以移除很多无用的 URL 参数,并且有丰富的自定义功能。(利益相关:我是一个 Firefox 用户,我也推荐每个人使用 Firefox,所以不知道 Chrome 下应该怎么办~)

知乎怎么办

知乎应当从整体考虑放弃追踪特定用户的分享行为。如果你非要那么做,至少请:

  • utm_member 字段每次分享都不一样(避免追踪特定用户)
  • utm_member 字段无法被第三方轻易反解出用户唯一标识

举个我认为比较好的做法:在客户端随机生成一个唯一字符串,将该字符串发送到服务器上,让用户与其关联。每隔一段时间(比如 4 小时)重新生成该字符串。

总结和反思

事实上在发现这个问题之后,我有和一些朋友描述我的发现,并且也试图向知乎反馈该事件,但并未得到什么响应。我想,大概从知乎到用户,所有人都不太理解这为什么对隐私有所伤害。

当然,知乎和国内大部分 App 一样,在隐私方面的问题远不止我提及的这一点,只是这一点非常容易验证而又十分明显,才这样公开的提出来。知乎在国内的移动市场上算是比较有情怀的(起码以前这样……),也希望知乎能够有足够的内部力量推动关注这些事情。

国内在隐私方面的关注实在是太少,而又很难有合适的渠道来了解、学习这些和自己生活息息相关的事情。这篇文章或许有不少片面或者理解不到位的地方,也请各位指正。

Safari 下同页面创建的视频超过 16 个则无法播放

最近在做一些视频相关的东西。在 Safari 下发现这样一个问题:<video> 标签创建多了,会导致同一个页面内后续的视频都无法播放。不管是 iOS 的 Safari,还是 macOS 下的,都存在同样的问题。尤其是 iOS 下,超过 16 个的视频几乎必定无法播放;效果就是显示一个播放按钮并且有一条斜线。

无法播放的视频截图

通过 video.error来检查最后的错误,得到的是 MEDIA_ERR_DECODE;同时,video.play() 方法会 reject 一个 AbortError。在 Google 检索了很久也没有得到有用的信息。

为了演示这个 bug,一个简单的示例的源码被我放在了 gist 上。你可以通过在 Safari 上打开 RawGit 提供的链接 来测试这个 bug。你只需要点击“测试”,并在视频开始播放后再次点击“测试”,如此循环 16 次,便能得到和我上一张图片一样的结果。

示例的代码如此简单,以至于我认为这个 bug 是几乎不可能发生的;它仅仅是调整了一个 DOM 的 innerHTML 来创建一个 video 元素(事实上无论通过何种方法创建,都会有这个问题;playsinline 属性似乎是必要的),然后再删除它,循环 16 次而已。

hello.innerHTML = '';
var src = 'https://cdn.rawgit.com/mediaelement/mediaelement-files/4d21a042/big_buck_bunny.mp4';
hello.innerHTML = '<video id="video" webkit-playsinline playsinline controls src="' + src + '">';
setTimeout(function () {
    document.getElementById('video').play();
}, 500);

目前,我没有发现任何能够解决这个 bug 的方法。可能唯一的办法就是,播放 15 个视频之后,刷新一下整个页面吧。

为熊猫 TV 直播生成带弹幕的录播视频

最近在沉迷谜之声直播。由于有时候下班比较晚,赶不上开播,又偶尔对直播游戏感兴趣的,就会跑去找录播来看。这个主播十分好心,会在贴吧里上传自己的录播视频。然而由于主播自己的录播视频采用 obs 串流的同时录制,对弹幕肯定是无能为力的,于是就想着能不能通过什么办法把弹幕加上去。所幸,办法还是有的。

现在熊猫直播全站 https 后,弹幕数据都是走的 websocket 了,通过 Fiddler 之类的抓包工具可以轻松抓到。不过,知乎上有一篇详细的回答说明老版本弹幕格式,也有一些现成的项目用来解析弹幕,所以这里就不关注弹幕数据格式了。总而言之,我把这些东西封装成了一个 npm 库 pandan,可以通过 node.js 简单的获取一个房间的实时弹幕了。比如:

let room = new (require('pandan').Room)(426346)
room.on('room-danmaku', function ({data}) {
  console.log(`${data.from.nickName}: ${data.content}`)
})
room.join()

有了弹幕数据自然就好办多了。从抓到的数据包来看,有主播开播、下播事件,送礼、送竹子等事件,完全足够我生成弹幕了。通过一段小脚本,可以把弹幕格式转换成下面这个样子的 xml:

...
<d p="1830.6300048828,1,25,16777215,1415241251,0,Da8b0a0d,668464499">再见了我的青春</d>
<d p="1477.6800537109,1,25,52479,1415241254,0,35b92ad0,668464527">虐啊</d>
...

脚本比较丑,就不贴了,相关参数的解析可以看这篇文章。这里有个小问题,由于直播通常有几秒钟的 buffer,所以弹幕整个时效性会稍微延后一些。这个问题在你观看直播的时候不容易发现(因为大家都有延迟),但生成录播之后弹幕会很明显的延后。当然,解决起来也十分简单,将弹幕时间轴往前平移几秒钟就可以了。

然后祭出大杀器:danmaku2ass,一个能将弹幕转换成 ass 字幕的工具。执行类似这样的命令:

python3 danmaku2ass/danmaku2ass.py -o 2017-07-17-1942-09.ass -s 1152x648 -fn "Microsoft YaHei" -fs 30 -a 0.8 -dm 7 2017-07-17-1942-09.xml

就能生成 ass 字幕了。使用任何带字幕功能的播放器,都可以播放啦~看个例子:

example image

当然,并不是所有主播都会自己上传录播视频。如果你刚好也有这种需求,可以尝试在连接弹幕服务器之后,获取开播事件,并在开播之后通过 you-get 来获取直播录像。

User Mode Linux – 一个用户态的 Linux 内核

User Mode Linux 可以在用户态启动一个 Linux。这使我们能在类似 OpenVZ 虚拟化技术的系统上,使用最新的 Linux 内核;甚至可以在非 root 用户下启动。

但有些 OpenVZ VPS 的 TOS 可能不允许你这么做;另外,这样可能会有一定的性能损失。

准备 rootfs 镜像

我们找一台 Ubuntu Server 来准备 rootfs。这个时候需要有 root 权限,因为你需要 mount 一个 loop 文件系统,chroot 改点东西之类的。

# 下载镜像
wget http://uk.images.linuxcontainers.org/images/alpine/3.1/amd64/default/20170305_17:50/rootfs.tar.xz
# 安装解压软件
apt install xz-utils
# 创建镜像,32 M 对于 alpine 是够用的
dd if=/dev/zero of=rootfs.img bs=1M count=32
mkfs.ext4 rootfs.img
# 把镜像挂到 loop 上
mkdir rootfs
sudo mount -o loop rootfs.img rootfs
sudo tar xvf rootfs.tar.xz -C rootfs
# 给 root 设个密码
sudo chroot rootfs /bin/sh
  passwd
  exit
# 清理
sudo umount rootfs
rmdir rootfs

此时 rootfs.img 已经做好了。

编译内核

我们继续用 ubuntu server 来编译 UML 内核。

sudo apt install build-essential kernel-package fakeroot libncurses5-dev libssl-dev ccache
# 抓内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.10.1.tar.xz
tar xvf linux-4.10.1.tar.xz
cd linux-4.10.1
make defconfig ARCH=um
make menuconfig ARCH=um
# 这里会出来一个界面让你配置内核,有茫茫多的参数可以选
# 大概找一些 linux 内核编译的文章可以参考吧
# 主要是 User Mode Linux 有一些特殊选项可以注意下,比如强制静态编译什么的
# 然后就开始正式编译啦
make ARCH=um
strip linux # 移除调试符号

编译过程可能比较慢。有时候会有一些库找不到啥的,apt 装一下就好。

获取 slirp

slirp 是用来在没有 root 的情况下联网的。

apt source slirp

这里获取到的 slirp 放到目标机器上编译,不需要在 ubuntu server 上搞了。

cd slirp-1.0.17/src
./configure
make CFLAGS="-I. -DUSE_PPP -DUSE_MS_DNS -DFULL_BOLT -fno-strict-aliasing -Wno-unused -std=gnu89" clean all

启动 UML

现在万事俱备,只欠东风了。

# 在 UML Host 上
# 把一些二进制文件放好
slirp=/home/oott123/uml/slirp/slirp-1.0.17/bin/slirp
uml=/home/oott123/uml/kernel/linux-4.10.1/linux
rootfs=/home/oott123/uml/alpine/rootfs.img
cow=/home/oott123/uml/machine/demo/data.cow # 放 copy-on-write 数据的地方
export TMPDIR=/tmp
$uml rw ubda=$cow,$rootfs mem=128M eth0=slirp,,$slirp

配置网络

启动之后你可能会发现没有网络。不用担心,改改网络配置:

# 在 UML 里
vi /etc/network/interfaces
#####
auto eth0
iface eth0 inet static
address 10.0.2.15
gateway 10.0.2.2
netmask 255.255.255.0
dns-nameservers 10.0.2.3
hostname $(hostname)
#####
/etc/init.d/networking restart
echo 'nameserver 114.114.114.114' > /etc/resolv.conf
# 测试网络
wget -O- myip.ipip.net
# 安装软件
apk update
apk add curl
curl myip.ipip.net
# 关闭 UML
halt

与主机交互

或许你需要将 UML 内的端口转发到主机上。

# 在主机上
vi ~/.slirprc
redir 2222 22

或许你需要访问主机的文件系统。

# 在 UML 上,把 UML 内的 /mnt/home 映射到 /home
# 注意权限问题、注意两边不要同时读写
mkdir /mnt/home
mount none /mnt/home -t hostfs -o /home

2016: 我的浏览器 A – Z

A: https://aoishi.[REMOVED] 自己服务器的管理面板
B: http://bbs.i-cassell-you.com/forum.php 还是卡院
C: http://civitas.soobb.com/ 多少年没上了……
D: https://www.dnspod.cn/ DNSPod 有几个域名在上面
E: http://easy-admin.[REMOVED] ???已经是我记不起来的东西了
F: http://fiddle.jshell.net/ 呃……其实我用得不是很多
G: https://gitlab.com/ Ainou 的代码托管在那,所以经常去
H: http://hicivitas.com/ 以前兼职公司的官网
I: http://www.ip138.com/ …………大概是经常看自己 ip
J: http://jd.com/ 已经不怎么上淘宝了,狗东比较舒服
K: http://kuler.adobe.com/ 一个配色工具,可能是因为在收藏夹吧,平时不怎么用
L: http://logcg.com/ 这是谁的博客啊……完全不记得访问过了
M: https://mp.weixin.qq.com/ 微信公众平台……嗯最近可能是上得比较多
N: http://notes.jetienne.com/ 这又是谁的博客了……不记得了
O: http://openshift.redhat.com/ 我感觉今年我应该没上过
P: http://piao-tech.blogspot.com/ 怎么越来越离谱了
Q: http://qzone.qq.com/ ……啥。。
R: http://redis.io/ 嗯……倒是经常去查文档
S: https://suiyu.xyz/ 偶尔会写日记
T: http://tower.im/ 好用的团队协作工具呢……不过现在也不怎么用了
U: http://upyun.com/ 偶尔会上去配一下 CDN
V: https://v2ex.com/ 毫无悬念
W: http://weibo.com/ 其实我最近比较少上了……
X: http://xiumima.com/ 呃……社工库么
Y: http://yeah.net/ 偶尔会上网易邮箱看看
Z: https://zhihu.com/ 知乎倒是常去看的

总之,通过浏览器,可以看出一个趋势:我使用浏览器浏览不同的 web 网站的时间越来越少了——大部分网站我根本想不起来。而更多的时间,可能花在了刷手机上。移动互联网近年来确实是势不可挡,我也不得不沦陷了。

唯一不变的,大概是 Web 的信仰——以及,今年我还在坚持使用 Firefox 浏览大部分网页。

清空 Mac QQ 的最近表情列表

更新 Mac QQ 之后,最近发送过的表情变成了一大片惊讶,如图:

惊讶的 Mac QQ

不管发什么表情这些顺序都不会变,简直坑爹。去 QQ 的存储目录找了一下,发现删掉以下文件后就会好:

HistoryFacesStack_V52.archive
HistoryFacesList_V52.archive

这些文件位于 ~/Library/Application Support/QQ/(QQ号)/CustomFaceConfig.db(非沙盒)或 ~/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/(QQ号)/CustomFaceConfig.db(沙盒)下。

删了以后,重启两次 QQ,果断好了:

不再惊讶的 Mac QQ

Unmanaged Exports 在中文系统下报错 syntax error at token ‘{‘ 问题

UnmanagedExports 是一个可以在 C# 下生成类 c api 的 dll 的工具。

不过这玩意儿在中文系统下编译,可能会产生如下报错:

C:\Users\Administrator\AppData\Local\Temp\tmp6C70\adddll.il(60) : error : syntax error at token '{' in:   { AddDll

解决方法很简单,换成英文系统即可。

至于啥原理,我也不知道。写下来避免后人踩坑……如果有哪位同仁知道怎么回事,也请告诉我~

记 VestaCP 的 Let’s Encrypt 自助签发配置

主要原料是 https://github.com/interbrite/letsencrypt-vesta 这个脚本。

然后写一个 python 脚本用来给用户自助签发

#!/bin/env python
import sys
import re
import os
import subprocess

args = ["/usr/local/bin/letsencrypt-vesta", "-u", os.environ['SUDO_USER']]
for i in sys.argv[1:]:
    if re.match('^[a-z0-9][a-z0-9.-]+$', i):
        args.append(i)
subprocess.call(args)

给用户 sudo 权限

%users ALL=NOPASSWD:/usr/local/bin/sign.py

然后让用户执行 sudo /usr/local/bin/sign.py 域名 即可。

重新分享我的 RSS 文章订阅

由于最近犯懒严重,所以这玩意儿也很久没更新了……

之前我曾在博客分享过我的 Star 文章订阅,坚持了一段时间之后,就开始用 kindle 读 RSS 了,所以就没法去 Tiny Tiny RSS 给喜欢的文章加 Star。不过最近我发现,用 kindle 读 RSS 的效率实在是比不上用手机 App 读,于是又切换回了 Tiny Tiny RSS。

这次把 Tiny Tiny RSS 重新配置了一遍,然后改用“Publish”功能取代之前用的“Star”功能,并且加了一些 Article Nots,这样订阅就会变得更加像“分享”一些。

RSS 地址:http://share.best33.com/feed/people/oott123.rss

我是如何搭建一个低成本的 Minecraft 小型服务器的

注目:本人不接受任何的免费的技术咨询指导,提问请先发红包!

自己有个小型的 Minecraft 服务器,为了节约成本以及管理方便,做了一整套自动化的管理方案。整套方案代码量近万行,同时很多东西都写死在了代码里,想开源但是很多东西目前还不完善,根本没法拿出手……所以写了个思路整理,若有需要的朋友可以参考着搭一个。

一、 服务端的选用

没啥好说的,由于需要 mod ,用的 KCauldron-1.7.10, mod 都是自己打的, Forge + LiteModLoader ,大概十几个 mod 。

插件用了一个非正版验证插件,配合宏与按键绑定(Marcos and key bind)进行自动登录。

二、 ISP 的选择

ISP 使用青云。由于服务器在线人数常年在 5 人以下,故选用青云可以随时开关实例,节约运行成本。

青云关机的话,公网 IP 和硬盘也收费,故使用 API 将 ip 释放,仅保留硬盘扣费。

三、 如何开启服务器

由于公网 IP 变动和服务器常年关闭,在另外一台服务器上搭建了一个 HTTP 服务,并自己写了个启动器。

当启动器启动的时候调用那个服务去开启服务器、下发公网 IP 并写入 servers.dat,避免动态域名解析被缓存的问题。

开启服务器后,利用 Telegram bot API ,在群里进行通知服务器的新 IP。

利用 mc 协议检查十分钟没人登录后,服务器自行关闭。

四、 如何管理 mc 服务

使用 supervisord 进行自启动。

需要控制台时,利用 supervisorctl fg minecraft 来调用控制台。