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 逆向地理编码的原理,以及分享了如何实现汉化的,代码都放在了这个 仓库 中,也有现成的东西可以用。

1/23/2025
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 的泛型!

1/24/2024
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

3/22/2023
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:

3/9/2023
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

8/25/2022
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() 使用效果:

11/17/2021
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 提供商询问,一般文档中都是有相关信息的。

10/4/2021
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;

10/3/2021
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 任务。

6/16/2021
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 路径下查看。

5/9/2021
articleCard.readMore
订阅 ZingLix Blog | 柑橘 RSS 阅读器