Z

ZingLix Blog

这里是 ZingLix 的个人博客,与你一起在计算机的世界探索~

Immich 反向地理编码原理和汉化思路

Immich 默认识别出来的照片位置都奇奇怪怪的,不仅仅是英文,还有一些不常见的名字,在照片分类搜索的时候非常麻烦。周末仔细研究了下 Immich 到底是怎么实现反向地理编码的,并想办法对其进行了汉化。 如果你到这里,是为了实现地名汉化的话,请直接前往 这个项目 Immich 反向地理编码工作原理 为了能够实现汉化的目标,首先我们得先明白 Immich 是怎么在本地实现反向地理编码的。 反向编码 以下以 v1.124.2 为例,Immich 的反向地理编码都实现在 reverseGeocode 这个函数中,传入的是一个 GeoPoint 对象,实际上就是经度和纬度。 之后,根据经纬度,进行了如下的 SQL 查询 1 2 3 4 5 6 7 8 9 10 11 SELECT * FROM geodata_places WHERE earth_box(ll_to_earth_public(${point.latitude}, ${point.longitude}), 25000) @> ll_to_earth_public(latitude, longitude) ORDER BY earth_distance( ll_to_earth_public(${point.latitude}, ${point.longitude}), ll_to_earth_public(latitude, longitude) ) LIMIT 1; 这其中 earth_box 创建一个以给定点为中心的球体范围 ll_to_earth_public 将地理坐标 (纬度和经度) 转换为三维球体上的点 WHERE 子句筛选出 距离输入的目标点 25,000 米(25 公里)范围内 的地理点,ORDER BY 子句根据距离从近到远排序。换句话说,就是找到了 geodata_places 库中,距离输入点最近的地理点。 找到了最近的点之后,取出这个点的 { countryCode, name: city, admin1Name },也就是 国家码、名称、一级行政区名称。整理一下顺序,将国家码转换成国家名,这就对应了我们在 Immich 中看到的照片位置中的 国、省、市 三级。至于这个表是如何构建的,后面我们再单独分析。 这里名称和一级行政区名称都是直接从数据库表中得到的,而国家名是从国家码转换得到的,这里用到了 node-i18n-iso-countries 这个库的 getName 方法。但在 Immich 中,调用时的代码是 getName(countryCode, 'en'),将语言用 'en' 写死了,所以只能是英文,并没有加上任何 i18n 的机制。 而如果上面没有找到的话,就会再进行一次 SQL 查询 1 2 3 4 SELECT * FROM naturalearth_countries WHERE coordinates @> point(:longitude, :latitude) LIMIT 1; 这段 SQL 就是在 naturalearth_countries 表中找到哪些记录的 coordinates 包含输入的坐标,也就是根据自然地球中国家的划分,确定坐标所在的国家。如果走到这一条,则不会再去确定更细粒度的省市两级划分。 简而言之,Immich 就是在数据库里事先准备好了大量地名,然后用照片的坐标去匹配数据库里最近的地名,之后就以该地名作为照片的地名。找不到的话,就退化到只用国家信息,根据国家的区划划分。 数据构建 接下来的一个大问题就是,数据库里的数据是从哪来的。 Immich 所有的反向地理编码数据都来的 GeoNames,放在了 /build/geodata 文件夹下,每次发版都会从 这里 获取最新的数据。 文件夹中有这么几个文件: admin1CodesASCII.txt:一级行政区划列表(id | name | name ascii | geoname id) admin2Codes.txt:二级行政区划列表(id | name | name ascii | geoname id) cities500.txt:所有人口大于 500 的城市列表 geodata-date.txt:数据更新时间 ne_10m_admin_0_countries.geojson:自然地球国家划分,详细介绍可以 看这 Immich 导入的入口在 init 函数中,这里会首先查看 system-metadata 中 key 为 reverse-geocoding-state 的值,里面记录了 lastUpdate 的时间,也就是上次导入数据的时间。会将这个时间与 geodata-date.txt 文件中的时间进行比较,如果文件中时间较新则说明有更新的数据则开始导入,否则就跳过避免重复导入。 具体导入的逻辑在 importGeodata 中,其中抛开建立表的逻辑,核心在于 loadCities500 函数。 cities500.txt 中格式类似 csv,以 \t 作为分隔,通过如下规则转换成数据库中的内容 1 2 3 4 5 6 7 8 9 10 11 id: Number.parseInt(lineSplit[0]), name: lineSplit[1], alternateNames: lineSplit[3], latitude: Number.parseFloat(lineSplit[4]), longitude: Number.parseFloat(lineSplit[5]), countryCode: lineSplit[8], admin1Code: lineSplit[10], admin2Code: lineSplit[11], modificationDate: lineSplit[18], admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`) ?? null, admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`) ?? null, 这其中 admin1Map 和 admin2Map 就是通过读取 admin1CodesASCII.txt 和 admin2Codes.txt 中 id 到 name 的映射关系得到的。 再结合前面提到的反向编码逻辑,就是根据 latitude 和 longitude 找到最近的点,然后拿到他的 countryCode、admin1Name 和 name,这一信息就作为了照片的地理位置信息。 没错,admin2Name 根本没用上,admin2Codes.txt 也没用 汉化思路 Immich 将照片的地理位置信息分为了 国、省、市 三级。再捋一遍文件的作用,也就是 从 cities500.txt 中找到最近的点,拿到他的名称作为 市 根据这个点的 admin1Code 信息,去 admin1CodesASCII.txt 文件中找到 省 级别的名称 根据这个点的 countryCode,用 node-i18n-iso-countries 转换成 国 级别名称 作用搞清楚了,接下来汉化的思路就好搞了 国 这一步骤主要依赖 node-i18n-iso-countries 这个库,而 代码 中把转换的目标语言写死为了 en,那么没有办法改目标语言,就只能从这个库的数据入手。 这个库的数据来源也是通过静态文件的形式实现的,具体文件内容可以看 这里。en.json 就是转换成 'en' 时候的数据来源,那我们只需要将其改写成中文即可,而中文的信息就在 zh.json 里,替换掉即可,就像 这样。 最后,将修改后的文件替换掉 Immich 镜像中的原始文件就可以了。 省 省的名称都在 admin1CodesASCII.txt 文件中,好在 GeoNames 提供了 alternateNamesV2.zip 这一文件,包含了许多地点的不同语言的名称,借助这一信息可以直接进行翻译,替换掉原来的名称即可。代码实现在 这里。 市 cities500.txt 这个文件主要的目标就是翻译 name 字段,但观察这个文件后可以发现,它的粒度非常细,不仅仅到市一级,还可能是区或者县,还是很古老的名字,非常不适合使用。 为了解决这个问题,可以通过地图提供商的逆向地理编码 API 对这些地方进行重新识别,获得标准的一级、二级行政区划名称,这里分别实现了适用于 国内采用高德的版本 和 国外使用 LocationIQ 的版本。 另外,默认的 cities500.txt 文件由于数据量有限,部分地区数据点较少,就会导致 Immich 在反向地理编码的时候出错。而实际上,GeoNames 还提供了不同国家的完整地理点信息,比如 CN.zip,可以作为补充添加进 cities500.txt 以提升效果,实现在 这里。但考虑到数据量庞大,所以只默认增加了直辖市,有需要的再增加。 总结 以上总结了 Immich 逆向地理编码的原理,以及分享了如何实现汉化的,代码都放在了这个 仓库 中,也有现成的东西可以用。

2025/1/23
articleCard.readMore

Go 在使用泛型时无法与 Pointer Receiver 共存的解决方法

问题描述 在使用 Go 的泛型时,如果泛型类型存在 constraint,而传入的类型在实现这个 constraint 时使用的是 pointer receiver,那么就会遇到 XXX does not satisfy XXX (method XXX has pointer receiver) 的报错,就比如下面这个例子希望用 Create 函数完成所有创建 Person 的操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type Person interface { SetID(id int) } type Student struct { ID int } func (p *Student) SetID(id int) { p.ID = id } func Create[T Person](id int) *T { var person T person.SetID(id) return &person } 这里 Student 用 (p *Student) 实现了 Person,然而如果用 Create[Student](id) 这种方式调用时,编译会遇到这个报错 1 Student does not satisfy Person (method SetID has pointer receiver) 问题解释 问题就在于这段代码中的 (p *Student) 1 2 3 func (p *Student) SetID(id int) { p.ID = id } 在 Go 中会认为是 *Student 实现了 SetID 方法,或者说实现了 Person interface,而不是 Student,因此提示 Student 并不满足 Person。 那一个办法是把实现 interface 传入的改成 value receiver 1 2 3 func (p Student) SetID(id int) { // 传入的 p 类型去掉了 * p.ID = id } 这样可以通过编译且正常运行,但问题是变成了值传递后,SetID 并不会作用于传入的那个变量,这个函数也形同虚设。 另一个解决方案是可以把调用函数时改成 Create[*Student](1),加上这个 *,报错也会随之消除。但问题就解决了吗? 再仔细看这个函数在传入类型后会变成什么样 1 2 3 4 5 6 7 // T -> *Student func Create[T Person](id int) *T { var person T // var person *Student person.SetID(id) return &person } 这里暂且不论原本的返回类型 *T 会变成 **Student 的问题,这个很容易通过调整返回值类型解决。 核心问题在于第二行我们声明了一个 *Student 类型的指针,但实例化在哪?我们创建了一个空指针,所以在运行时会遇到 runtime error: invalid memory address or nil pointer dereference。同时由于语言限制,我们手上的 T* 并不能转成 T 然后让我们完成实例化。 那么能不能传入 T,然后转成指针再调用 interface 的方法呢? 1 2 3 4 5 func Create[T Person](id int) *T { person := new(T) person.SetID(id) // 报错 return &person } 然而编译器又给了一个错误 person.SetID undefined (type *T is pointer to type parameter, not type parameter),这个问题在于 SetID 是定义给 Student 的,不是给 Student* 用的。 很遗憾,由于 Go 语言层面的缺陷,在仅使用 T 这一个参数时并不能完成我们想要的东西,如果有办法,请通过网页最下方的邮件告诉我,不甚感激。 解决方案 问题在于用 T 编译器不认 constraint,用 T* 又拿不到 T 进行实例化,那么只能去掉 T 的限制,同时再传入带有限制的 T*。思路如此,具体实现来说需要定义这么一个 interface 1 2 3 4 type PersonPtr[T any] interface { *T Person } 这个定义了一个指针 interface,第一行这里暂时先去掉了 constraint,允许传入任意类型 T,然后通过第二行使得这个 interface 允许的类型是且只能是 *T,让我们能从 T 拿到指针,再通过第三行去保证实现了 Person 这个 interface。 那我们就可以进一步修改函数,将传入的类型改为 PersonPtr 1 2 3 4 5 func Create[Ptr PersonPtr[T]](id int) *T { var ptr Ptr = new(T) ptr.SetID(id) return ptr } 但这仍然不够,编译器会提示 undefined: T,因为我们没有定义 T,所以必须在函数的泛型列表中加上 T,这个函数只能变为 1 2 3 4 5 func Create[T any, Ptr PersonPtr[T]](id int) *T { var ptr Ptr = new(T) ptr.SetID(id) return ptr } 调用时就变成了 1 stu := Create[Student, *Student](1) 这样调用真的很丑,但好在 Go 这回终于做了个人,通过类型的自动推导可以自动推导出第二个参数,所以调用时可以简化为 1 stu := Create[Student](1) 这样调用看起来就和谐了许多(虽然背后的实现需要用些难懂的 trick,但我们至少终于实现了 Go 中的泛型与 pointer receiver 的共存… 总结 珍爱生命,远离 Go 的泛型!

2024/1/24
articleCard.readMore

N5105 PVE 虚拟机随机死机/重启解决方案

N5105 运行虚拟机会随机死机/重启的问题很常见,之前我采取过如下办法 爱快降级至3.6.1 OpenWRT 换用 LXC 模式安装 关闭各种直通 只能说降低了死机概率,一般能撑到一天以上,所以我选择在半夜自动重启,勉强可以正常使用,但日常使用还是不可避免的会断网。 不过现在似乎有了一个终极解决方案,可以彻底解决 N5105 的死机问题,根据这个链接反馈,已经可以超过 10 天稳定运行,我目前也暂时未遇到死机问题。 UPDATE: 我已经几十天都没有死机过了 解决方案就是更新 microcode 至 0x24000024 版本。 1 2 3 4 5 6 # 安装 microcode apt update apt install intel-microcode reboot # 查看 microcode 版本 dmesg -T | grep microcode 重启完成后,microcode 应该就已经更新到不会死机的版本了,你应该可以看到 0x24000024 字样。 1 2 3 4 5 root@pve:~# dmesg -T | grep microcode [Wed Mar 22 22:23:26 2023] microcode: microcode updated early to revision 0x24000024, date = 2022-09-02 [Wed Mar 22 22:23:26 2023] SRBDS: Vulnerable: No microcode [Wed Mar 22 22:23:30 2023] microcode: sig=0x906c0, pf=0x1, revision=0x24000024 [Wed Mar 22 22:23:30 2023] microcode: Microcode Update Driver: v2.2. 或者 grep 'stepping\|model\|microcode' /proc/cpuinfo 查看 microcode 版本。 1 2 3 4 5 root@pve:~# grep 'stepping\|model\|microcode' /proc/cpuinfo model : 156 model name : Intel(R) Celeron(R) N5105 @ 2.00GHz stepping : 0 microcode : 0x24000024 但如果源版本比较老的话,更新的版本还是例如 0x24000023 的话,就请继续后续步骤 1 2 3 4 5 6 7 # 接下来继续更新 wget https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files/archive/main.zip unzip main.zip -d MCU cp -r /root/MCU/Intel-Linux-Processor-Microcode-Data-Files-main/intel-ucode/. /lib/firmware/intel-ucode/ update-initramfs -u reboot # 重启后应当可以更新至 0x24000024

2023/3/22
articleCard.readMore

PVE 下 LXC 启动 Docker 失败解决方案

PVE 下通过 LXC 安装的 Ubuntu 启动 Docker 镜像时候提示 1 2 3 4 docker: Error response from daemon: AppArmor enabled on system but the docker-default profile could not be loaded: running `/usr/sbin/apparmor_parser apparmor_parser -Kr /var/lib/docker/tmp/docker-default6944525` failed with output: apparmor_parser: Unable to replace "docker-default". Permission denied; attempted to load a profile while confined? 解决方式是在调整启动配置 PVE 设置中 选项-功能 中选中 嵌套 然后在宿主机中找到 /etc/pve/lxc/100.conf(注意把 100 替换成你的 LXC 容器 id),增加如下几句话,之后重启 1 2 3 lxc.apparmor.profile: unconfined lxc.cgroup.devices.allow: a lxc.cap.drop:

2023/3/9
articleCard.readMore

Encode Email

function encodeEmail(email, key) { // Hex encode the key var encodedKey = key.toString(16); // ensure it is two digits long var encodedString = make2DigitsLong(encodedKey); // loop through every character in the email for(var n=0; n Encode

2022/8/25
articleCard.readMore

快速查看显卡使用情况和占用用户

使用方法: python gpu.py 需要的依赖: xmltodict 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 import subprocess import xmltodict, pwd, json UID = 1 EUID = 2 def owner(pid): """Return username of UID of process pid""" for ln in open("/proc/{}/status".format(pid)): if ln.startswith("Uid:"): uid = int(ln.split()[UID]) return pwd.getpwuid(uid).pw_name def add_user(process): tmp = [] for p in process: p["user"] = owner(p["pid"]) tmp.append(p) return tmp def simplify(gpu): tmp = {} for k in gpu.keys(): if k in [ "@id", "product_name", "fan_speed", "fb_memory_usage", "utilization", "temperature", "processes", ]: tmp[k] = gpu[k] return tmp def get_gpu_info(): sp = subprocess.Popen( ["nvidia-smi", "-q", "-x"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) out_str = sp.communicate() out_str = out_str[0].decode("utf-8") o = xmltodict.parse(out_str)["nvidia_smi_log"] o = json.loads(json.dumps(o)) gpu_list = [] if not isinstance(o["gpu"], list): o["gpu"] = [o["gpu"]] for gpu in o["gpu"]: if gpu["processes"] is None: gpu["processes"] = {} gpu["processes"]["process_info"] = [] process = gpu["processes"]["process_info"] if not isinstance(process, list): process = [process] process = add_user(process) gpu["processes"]["process_info"] = process gpu = simplify(gpu) gpu_list.append(gpu) o["gpu"] = gpu_list return o gpu = get_gpu_info() print() print( " {: <13}\t{: <8}\t{: <20}\t{}".format( "user", "pid", "used_memory", "process_name" ) ) print( "---------------------------------------------------------" ) for i, g in enumerate(gpu["gpu"]): print( "{} {} ({}):".format( i, g["product_name"], g["utilization"]["gpu_util"], ) ) total = int(g["fb_memory_usage"]["total"].split(" ")[0]) for p in g["processes"]["process_info"]: used = int(p["used_memory"].split(" ")[0]) print( " {: <13}\t{: <8}\t{: <20}\t{}".format( p["user"], p["pid"], "{: <10} ({:5.2f}%)".format( p["used_memory"], 100 * used / total ), p["process_name"], ) ) print( "---------------------------------------------------------" ) print() 使用效果:

2021/11/17
articleCard.readMore

NGINX 配置避免 IP 访问时证书暴露域名

TL;DR 利用 NGINX 1.19.4 后的新特性 ssl_reject_handshake on;,将其置于默认访问时配置中,IP 访问时会终止 TLS 握手,也就不会暴露域名了。 细说 CDN 是建站时常用的工具,在自己的主机外面套一层 CDN 是常见操作,一般这样认为自己的主机就安全了,有人来攻击也会先到 CDN 服务器,攻击者根本无法获取到自己主机的 IP,但事实真的是这样吗? 我们先来看看一般配置后会出现什么问题。 1 2 3 4 5 6 7 8 9 10 11 12 13 server { listen 80 default_server; # Redirect all HTTP requests to HTTPS. return 301 https://$host$request_uri; } server { listen 443 default_server; server_name _; include conf.d/ssl.config; return 444; } 上面是一个很常用的 NGINX 配置,HTTP 访问全部重定向到 HTTPS 的 443 端口上,没有配置过的域名返回 444 终止连接。 好了,现在尝试用 IP 和 HTTPS 访问你的网站,你应该能够看到预想中访问失败、证书无效等连接失败的提示。 但是!注意下浏览器左上角提示的不安全,点开查看证书信息,你就会发现你的域名其实随着证书发送了过来。此时如果你是攻击者,那么其实就可以知道该域名背后的源主机 IP 就是这个。 上图即为用 IP 访问后,依旧能看到证书内容。这是因为返回 444 是 HTTP 层面的事情,意味着到达这一步下层的 TLS 握手已经完成。证书不被信任是一回事,但说明已经拿到了服务器的证书。 CDN 确实避免了直接 DNS 查询暴露 IP 的问题,但攻击者通过扫描全网 IP,用上述方式依旧可以知道每个 IP 对应的域名是什么,这也是为什么很多站长用了 CDN 后并且反复更换 IP 却依旧被攻击者迅速找到 IP 的原因。 Censys 就一直在干这件事,全网扫描 IP 并找到其对应的域名 那该怎么办呢? 问题根源出在 client 在 TLS 握手时发送了 ClientHello 后,NGINX 在 ServerHello 中带着含有域名的默认证书返回了,因为 NGINX 期望可以完成握手,这可能可以算是 NGINX 的一个缺陷。 如果你不熟悉 TLS 握手流程,那么可以看看 这篇文章 笨办法 既然 NGINX 默认提供了带有域名的证书,那么想不暴露也很简单,提供一个不含有正确域名的证书即可。 NGINX 设置中 HTTPS 访问如果没有设置证书,那么就会报错。但反正 IP 访问也不需要提供服务,那么直接自签一个 IP 证书,或者随便一个域名的证书都可。当然,如果能搞定合法的 IP 证书也不是不行。 搞定证书后,添加一个配置,让 IP 访问返回错误证书就完事了。 1 2 3 4 5 6 7 8 9 server { listen 443 ssl default_server; server_name your_ip; ssl_certificate xxxx.pem; // and more ssl config ... return 444; } 好方法 这种方法还得自己搞个证书,如果服务器多每个都得这么搞也挺麻烦的,好在这个问题 NGINX 这已经有了很完美的解决方案。 ClientHello 中是带着 SNI 的,所以其实握手阶段是可以知道访问的域名是否合法的,NGINX 1.19.4 中添加了一个新的配置项 ssl_reject_handshake 用于拒绝握手,也就不会提供证书。 使用方法也很简单,将原本默认配置中的 return 444 替换成 ssl_reject_handshake on 即可。 1 2 3 4 5 6 7 8 9 server { listen 443 default_server; server_name _; include conf.d/ssl.config; # 不用返回 444 了,直接拒绝握手 ssl_reject_handshake on; # return 444; } 配置后,再尝试 IP 访问,会发现浏览器报了 ERR_SSL_UNRECOGNIZED_NAME_ALERT 的错误,也看不到证书信息,目标达成! 其实还没完 上述方法是通过 ClientHello 中的 SNI 确定访问是否合法的,那如果 SNI 就是正确的域名呢? 这种场景发生于攻击者已经确定要攻击某个域名,那么他就可以将带着该域名的握手信息遍历所有 IP,握手成功就找到,这样访问其实与正常访问并无区别,唯一解决方法就是白名单只允许 CDN 服务器访问。 例如 hosts 直接将硬写 IP,将域名强行指向某个 IP 或者用这种方式 curl https://example.com --resolve 'example.com:443:172.17.54.18' 1 2 3 4 5 location / { allow 172.1.2.0/24; allow 1.2.3.4/32; deny all; } 上述 IP 段只能向 CDN 提供商询问,一般文档中都是有相关信息的。

2021/10/4
articleCard.readMore

NGINX 配置 HTTPS 最佳实践

1202 年了,不会还有网站不支持 HTTPS 吧?不过 HTTPS 的配置还是有很多讲究的。本文以 NGINX 的配置为例,嫌麻烦的可以直接跳到 最后 抄配置。 如果你不清楚 HTTPS 与 TLS 的工作原理,可以先阅读 这篇文章,可以帮助你理解下述配置。 获取证书 证书是实现 HTTPS 的基础,现在各个云服务商都提供了免费的证书申请,可以直接去申请。这里我以 acme.sh 为例说明下申请证书时的注意事项。 1 acme.sh --issue -d "*.zinglix.xyz" --keylength ec-256 --ocsp 上面是一个简单的用 acme 申请证书的命令,其中关键的是 keylength 和 ocsp 两个参数,OCSP 的作用我们 后面 再说,建议能开启则开启,先来谈谈密钥长度的问题。 证书加密的算法分为 RSA 和 ECDSA 两类,这对应到证书也就分为两类。acme 中 keylength 支持的参数有 2048, 3072, 4096, 8192 和 ec-256, ec-384, ec-521(参数支持,但暂不支持申请)。 ec- 开头的对应着 ECDSA 证书,其他的为 RSA 证书,长度越长安全性也就更高,但对性能的消耗也就越高。RSA 有更好的兼容性,ECDSA 可以提供更好的前向安全,具体差异可以看 这里。 根据 SSL Labs 的推荐,长于 2048 的 RSA 密钥和 256 bits 的 ECDSA 密钥对于 CPU 性能是一种浪费,从中获得安全性的提升有限,导致过度加密。 因此推荐密钥长度为 2048 与 ec-256! 那么两种类型证书选择哪一个呢?那当然是全都要啦~ NGINX 支持同时使用两个证书,只需要都写上就行了 1 2 3 4 5 6 # RSA 证书 ssl_certificate /cert/*.zinglix.xyz/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz/*.zinglix.xyz.key; # ECDSA 证书 ssl_certificate /cert/*.zinglix.xyz_ecc/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz_ecc/*.zinglix.xyz.key; 这里 ssl_certificate 最好是使用完整的证书链,如果没有提供必要的中间证书可能会导致证书链不可信。 HTTP/2 与会话恢复 HTTP/2 可以有效提升对网络的利用效率,会话恢复可以复用曾经协商过的数据,两者都可以帮助减少 RTT,所以建议开启,可以有效减少建立连接时的耗时。 1 2 3 4 5 listen 443 ssl http2; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; ssl_session_tickets off; 加密协议与套件 SSL 已经是不安全的了,绝不要使用。TLSv1.0 与 TLSv1.1 虽然没有被证明不安全,但作为老旧的协议即将过时,除非你的客户真的需要,也不要开启。 TLSv1.2 可以说是目前被最广泛使用的协议,应当被开启。TLSv1.3 作为最新的协议,在性能和安全性上都有提升,支持的话也应当开启。 如果为了极致的安全,只开启 TLSv1.3 也是没有问题的,现代的浏览器都已经支持 TLSv1.3,只要你相信你的客户不会使用略微老旧的软件 至于加密套件,RC4、DES 等等都不安全,但说那么多套件头也晕了,下面已经整理了一份支持绝大多数客户端且安全的配置 1 2 3 4 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers 用于指定服务器是否有推荐的套件,为了能够根据服务器配置用上更安全的套件,防止 BEAST 攻击,建议开启。 HSTS HSTS (HTTP Strict Transport Security) 能够告诉浏览器,该网站只应该通过 HTTPS 访问,避免使用 HTTP。开启方式如下,只需要添加一个 HTTP 头 1 add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always; 其中三个参数 max-age=<expire-time>:指明 HSTS 的有效期,最佳实践是一年时间即 31536000。注意,开启后如果关闭 HTTPS 将在有效期内导致网站无法访问。 includeSubDomains:是一个可选参数,指明是否同时适用于子域名。 preload:可选参数,指明是否为预加载。 HSTS 虽然可以实现强制浏览器使用 HTTPS,但是第一次访问时依旧不知道目标网站是否采用了 HSTS。 为了解决这一问题,Google 维护了一个名单,里面是采用了 HSTS 的网站列表,名单会随着浏览器分发。这样浏览器第一次访问时,会先查看网站是否位于该名单里,从而决定是否采用 HTTPS。这一机制即为 预加载机制。 可以在 该网站 上提交申请,注意必须加上与上述配置相同或更严格的配置,同时重定向所有 HTTP 请求。 DHE 参数 密钥交换时常采用 Diffie-Hellman 密钥交换,低强度(768 及以下)的参数容易被破解,以及一些常见的 1024 位参数。 出于性能与安全性的考虑,2048 位即可,如果你不担心性能可以选择更高的。 可以用 openssl dhparam -out dhparams.pem 2048 生成一个(通常很慢)或者获取一个现成的,例如 2048位 和 4096位。 1 ssl_dhparam /path/to/dhparam.pem; OCSP OCSP 是一种从证书颁发者处验证证书是否被撤回的机制,可以让浏览器验证证书有效性。 最佳实践是证书中强制要求 OCSP,但客户到颁发者处的连接质量可能并不好,例如国内访问 Let’s Encrypt,这会导致网站访问速度下降。 OCSP Stapling 是一种让服务器在握手过程中同时传递 OCSP 相应的技术,向颁发者验证的过程由服务器代劳,避免用户直接去访问。开启也很简单,如下 1 2 ssl_stapling on; ssl_stapling_verify on; 0-RTT 0-RTT 是 TLSv1.3 很棒的一个特性,有效消除握手时间,但会导致重放攻击,且放弃了前向安全性。 在足够了解后果的前提下可以开启,但如果不确定那就算了,建议是 GET 等无副作用的操作可以开启。 1 ssl_early_data on; 其他一些细节 网站所有资源开启 HTTPS,例如一些公用 CSS、JS 非法域名(尤其是 IP 访问)禁止握手,避免暴露证书 重定向所有 HTTP 请求至 HTTPS 1 2 3 4 5 6 server { listen 80 default_server; listen [::]:80 default_server; return 301 https://$host$request_uri; } 完整配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # HTTP/2 listen 443 ssl http2; server_name your_domain; # RSA 证书(推荐2048位) ssl_certificate /cert/*.zinglix.xyz/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz/*.zinglix.xyz.key; # ECDSA 证书(推荐256位) ssl_certificate /cert/*.zinglix.xyz_ecc/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz_ecc/*.zinglix.xyz.key; # 会话恢复 ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; ssl_session_tickets off; # 加密协议与套件 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers on; # HSTS add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always; # DHE 参数(推荐2048位) ssl_dhparam /cert/ffdhe2048.txt; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; 验证 配置后,可以验证自己的配置是否正确,下面推荐两个网站 SSL Labs:除了配置外,可以检查与各类客户端的兼容性 ImmuniWeb:有更完整的配置测试 上述配置后应该都能取得 A+ 的成绩。 最后提一嘴关于 SSL Labs 中有两项 90 分,虽然没有必要,但可以继续冲击 100。 关于 Key Exchange,其要求 DH 参数和证书密钥长度均大于等于 4096,替换这两个文件即可。当然之前提到过这会导致过度加密,带来的性能损失并不能带来足够的安全性提升。 而 Cipher Strength,其要求所有加密套件均大于等于 256 bits,而这里存在一个 bug 是 TLSv1.3 有一个算法为 TLS13-AES-128-GCM-SHA256,虽然 128 位但其实配上 TLSv1.3 足够安全,但这导致了开启 TLSv1.3 就无法得到 100 分。 不过除了关闭 TLSv1.3 之外,还是有解决方法的,那就是移除 TLS13-AES-128-GCM-SHA256 套件,NGINX 1.19.4 开始支持调整 TLSv1.3 的加密套件,只需加上下述配置,但会导致 0-RTT 失效 1 2 ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; ssl_ecdh_curve secp384r1;

2021/10/3
articleCard.readMore

「论文笔记」A Unified Generative Framework for Various NER Subtasks

NER 任务分为三类,目前来说,各个子任务目前都有方法可以一定程度上解决,但缺少一个方法可以同时解决三个任务。下图分别介绍了三个子任务的常见解决方案 Flat NER(扁平实体抽取):通常采用序列标注的方法 Nested NER(嵌套实体抽取):将输入文本采用类似 n-gram 的方法进行拆分,拆成一个个小的 span 后进行分类 Discontinous NER(非连续实体抽取):用一个堆栈,为每个 token 选择移入移出规约等操作,类似于编译器生成 AST 的过程,详见另一篇论文 这篇文章提出使用 seq2seq 的框架以生成的方式来同时完成三个任务,也就是标题中的那个可用于各种 NER 子任务的通用框架。输出形式则为上图中的 (d),通过生成的方式输出实体的内容,最后以一个指示实体类型的特殊标签作为该实体的结束,并以同样的方式生成下一个实体。 模型结构 模型整体结构较为简单,为了让模型更好的完成生成任务,文中采用预训练过的 seq2seq 模型 BART 作为基础。输入为一段文本(首尾加上了特殊 token),输出时采用指针网络的方式,输出在输入中的位置。 具体来说,模型沿用了 BART 的 encoder-decoder 的架构(下半部分),主要工作在如何生成字符(上半部分)。 模型通过 encoder 和 decoder 后,分别拿到了两个隐状态 \(H^e\) 和 \(h_t^d\)。 实际中,index 并不能直接输入 decoder,所以解码器中还进行了一步 index2token,以 token 的方式输入 之后作者将 \(H^e\) 通过一层 MLP 后与输入的 Token Embedding 加权相加(公式 7)得到 \(\overline{H}^e\)。 然后模型会计算一个 Pointer Distribution 和 Tag Distribution,即文中每个位置(指针)的概率分布和标签的概率分布,两者合在一起就是图中的 Final Prediction Prob.,从中选择概率最高的也就是这一步输出的结果。 实际上,就是将 \(\overline{H}^e\) 和 tag 的 embedding 后的输出 \(G^d\) 分别与 decoder 的输出 \(h_t^d\) 相乘后拼接(公式 9),通过 softmax 找到概率最大的,作为输出。 然后就以自回归的方式不断重复上述步骤,依次输出下一个 token,从而得到最后的结果。 结果 实验结果基本上都处于 SOTA 的水平,不过文中有许多分析的部分,例如实体表示方式之类的。 实验表明长度更短、更类似 BPE 序列的实体会有更好的效果。此外,文中出现越靠后的实体,在 Flat NER 和 Discontinous NER 上召回率都会随之增高,而嵌套实体并没有,作者认为是因为实体间有关联,导致错误传播。 文章最大的贡献点在于提出了一种方式,能够用生成的方式完成各类 NER 任务。

2021/6/16
articleCard.readMore

Linux 更换时区

timedatectl 指令可以查看当前的时间信息,如下 1 2 3 4 5 6 7 Local time: Sun 2021-05-09 23:00:17 CST Universal time: Sun 2021-05-09 15:00:17 UTC RTC time: Sun 2021-05-09 15:00:17 Time zone: Asia/Shanghai (CST, +0800) Network time on: yes NTP synchronized: yes RTC in local TZ: no 如果时区不对,可以用 sudo timedatectl set-timezone Asia/Shanghai 这条指令切换到国内的时区,最后一个参数就是你要切换的时区,这条指令需要 sudo 权限。 具体的时区列表可以用 timedatectl list-timezones 获得或者去 /usr/share/zoneinfo 路径下查看。

2021/5/9
articleCard.readMore