阅读视图

我的设备插件(WordPress 插件) — 就爱重复造轮子

✇obaby
作者obaby

这个假期事情有点多,也就没回老家。主要是自己最近也不想回,好处就是有点时间可以折腾自己喜欢的东西。

说的更直接点就是有时间瞎折腾,不过这第一次的效果并不是很好,例如录的视频,mic声音太小的,只能强行网上拉。录视频的时候发现原来的录屏工具注册失败了,变成了未授权。ScreenRecorderpro 7.0在双4k屏上尝试录制,直接崩溃了,升级到8.0发现注册没处理好,有的地方没搞。

新版变成了64位的,懒得去修改二进制文件了,直接修改hosts屏蔽掉服务器校验:

__int64 __fastcall CheckSNOnLine(unsigned int a1, unsigned int a2)
{
  __int64 v2; // rdx
  __int64 v3; // rbx
  const WCHAR *v4; // rax
  const WCHAR *v5; // rax
  __int64 v6; // rdx
  __int64 v7; // rax
  __int64 WebContent; // rax
  __int64 v9; // rsi
  __int64 v10; // rdx
  __int64 v11; // rdx
  unsigned int vars44; // [rsp+44h] [rbp+44h]
  __int64 vars48; // [rsp+48h] [rbp+48h] BYREF
  __int64 vars50; // [rsp+50h] [rbp+50h] BYREF
  __int64 vars58; // [rsp+58h] [rbp+58h] BYREF
  __int64 vars60; // [rsp+60h] [rbp+60h] BYREF
  __int64 vars68; // [rsp+68h] [rbp+68h] BYREF
  __int64 vars70; // [rsp+70h] [rbp+70h] BYREF
  __int64 vars78; // [rsp+78h] [rbp+78h] BYREF
  int vars8C; // [rsp+8Ch] [rbp+8Ch] BYREF
  __int64 vars90; // [rsp+90h] [rbp+90h] BYREF
  __int64 vars98; // [rsp+98h] [rbp+98h] BYREF
  __int64 varsA0; // [rsp+A0h] [rbp+A0h] BYREF
  __int64 varsA8; // [rsp+A8h] [rbp+A8h] BYREF
  __int64 varsB0; // [rsp+B0h] [rbp+B0h] BYREF
  __int64 varsB8; // [rsp+B8h] [rbp+B8h] BYREF

  vars48 = 0;
  vars50 = 0;
  vars58 = 0;
  vars60 = 0;
  vars68 = 0;
  vars70 = 0;
  vars78 = 0;
  varsB8 = 0;
  varsB0 = 0;
  varsA8 = 0;
  varsA0 = 0;
  vars98 = 0;
  vars90 = 0;
  sub_7CDDB0(&varsA0);
  sub_7CC140(&varsB8, a1, a2);
  LOBYTE(v2) = 1;
  v3 = sub_59E6D0(&qword_59D078, v2);
  sub_59E980(v3, -2147483646);
  *(_DWORD *)(v3 + 44) = 131097;
  if ( (unsigned __int8)sub_59EB90(v3, varsB8, 0) )
  {
    sub_59F9E0(v3, &varsA8, L"LastTime");
    sub_59F9E0(v3, &varsB0, L"SN");
  }
  sub_59E940(v3);
  sub_434690(&vars78, varsB0);
  if ( !vars78 )
  {
    sub_59E980(v3, -2147483647);
    *(_DWORD *)(v3 + 44) = 131097;
    if ( (unsigned __int8)sub_59EB90(v3, varsB8, 0) )
    {
      sub_59F9E0(v3, &varsA8, L"LastTime");
      sub_59F9E0(v3, &varsB0, L"SN");
    }
    sub_59E940(v3);
  }
  sub_410F80(&vars70, L"*************:", varsA8);
  v4 = (const WCHAR *)sub_410B40(vars70);
  OutputDebugStringW(v4);
  sub_410F80(&vars68, L"--------------:", varsB0);
  v5 = (const WCHAR *)sub_410B40(vars68);
  OutputDebugStringW(v5);
  if ( (unsigned int)sub_411190(varsA8, varsA0) )
  {
    sub_434690(&vars60, varsB0);
    sub_40FF60(&varsB0, vars60);
    if ( varsB0 )
    {
      if ( (unsigned __int8)sub_7CB9B0() )
      {
        sub_7CC030(&vars58);
        sub_4110B0(&vars98, 3, L"http://gilisoft.com/webtools/livecheck/IsValidKey.php?key=", varsB0, vars58);
        sub_40F8B0(&vars90);
        if ( (unsigned int)sub_7CB9E0() )
        {
          sub_4106C0(&vars50, vars98, 0);
          v7 = sub_4105E0(vars50);
          WebContent = CURL_GetWebContent(v7, &vars8C);
          v9 = WebContent;
          if ( vars8C > 0 )
          {
            sub_410BA0(&vars90, WebContent);
            CURL_FreeBuffer(v9);
            sub_434690(&vars48, vars90);
            sub_40FF60(&vars90, vars48);
          }
        }
        else
        {
          sub_7CBA00(&vars90, vars98);
        }
        if ( (unsigned int)sub_411190(vars90, L"expired") )
        {
          *(_DWORD *)(v3 + 44) = 131103;
          if ( (unsigned __int8)sub_59EB90(v3, varsB8, 0) )
            sub_59F940(v3, L"LastTime", varsA0);
          sub_59E940(v3);
          LOBYTE(v11) = 1;
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v3 - 32LL))(v3, v11);
          vars44 = -1;
        }
        else
        {
          *(_DWORD *)(v3 + 44) = 131103;
          if ( (unsigned __int8)sub_59EB90(v3, varsB8, 0) )
            sub_59F940(v3, L"SN", &dword_7CE3DC);
          sub_59E940(v3);
          LOBYTE(v10) = 1;
          (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v3 - 32LL))(v3, v10);
          vars44 = 0;
        }
      }
      else
      {
        vars44 = -1;
      }
    }
    else
    {
      LOBYTE(v6) = 1;
      (*(void (__fastcall **)(__int64, __int64))(*(_QWORD *)v3 - 32LL))(v3, v6);
      vars44 = 0;
    }
  }
  else
  {
    sub_40C4B0(v3);
    vars44 = -1;
  }
  sub_40F8B0(&vars48);
  sub_40F900(&vars50);
  sub_40F990(&vars58, 5);
  sub_40F990(&vars90, 6);
  return vars44;
}

反正,能用就ok拉:

今天上午,有点时间,有拿出上周搞的那个我的设备页面插件,这个东西其实最开始也是抄来的:

抄作业–我的设备

前端时间,看大家都更新了,开始基于post类型来做展示,每个设备都是个单页能回复评论,忘了在谁那里看的了。总觉得有些过去强大了,虽然也是个插件。

最终还是决定自己找个轮子,于是就有了这个东西,使用插件生成的页面:

https://h4ck.org.cn/my-devices

整体页面样式沿用原来的,也不用在主题自定义添加css中添加样式了。新建页面,插入short code即可:

# Baby Device Manager

一个功能强大的WordPress设备管理系统插件,支持设备分组管理、设备信息管理、自定义排序、状态跟踪等功能。

## 功能特点

- 设备分组管理
  - 创建和管理设备分组
  - 自定义分组排序
  - 分组描述信息
- 设备管理
  - 添加/编辑/删除设备
  - 设备状态管理(在售、停售、已售出、维修中、已报废)
  - 设备图片和产品链接
  - 自定义设备排序
  - 设备描述信息
- 前端展示
  - 响应式布局
  - 按分组分类显示
  - 支持多种排序方式
  - 美观的界面设计
  - 支持自定义每行显示设备数量(1-6个)
- 其他功能
  - 图片管理:支持设备图片上传和显示
  - 产品链接:支持添加产品详情页链接
  - 状态跟踪:支持多种设备状态管理
  - 自定义排序:支持设备分组和设备的自定义排序

## 安装要求

- WordPress 5.0 或更高版本
- PHP 7.2 或更高版本
- MySQL 5.6 或更高版本

## 安装方法

1. 下载插件压缩包
2. 在WordPress后台进入"插件 > 安装插件"页面
3. 点击"上传插件"按钮,选择下载的压缩包
4. 安装完成后点击"启用插件"

## 使用方法

### 管理界面

1. 设备分组管理
   - 进入"设备管理 > 设备分组"
   - 添加新分组:填写分组名称、描述和排序值
   - 编辑现有分组:修改分组信息或删除分组
   - 排序值越小,显示越靠前

2. 设备管理
   - 进入"设备管理 > 添加设备"
   - 填写设备信息:
     - 设备名称
     - 所属分组
     - 设备描述
     - 设备状态
     - 设备图片URL
     - 产品链接
     - 排序值

### 前端显示

使用 shortcode 在页面或文章中显示设备列表:

1. 基本用法
```
【baby_devices】
```

2. 按分组显示
```
【baby_devices group="分组名称"】
```

3. 按状态显示
```
【baby_devices status="在售"】
```

4. 自定义排序
```
【baby_devices orderby="sort_order" order="ASC"】
```

5. 组合使用
```
【baby_devices group="厨房电器" status="在售" orderby="sort_order" order="ASC"】
```

6. 自定义每行显示数量
```
【baby_devices per_row="4"】
```

### Shortcode 参数说明

- `group`:按分组名称筛选
- `status`:按设备状态筛选
- `orderby`:排序字段
  - `sort_order`:按自定义排序(默认)
  - `created_at`:按创建时间
- `order`:排序方向
  - `ASC`:升序(默认)
  - `DESC`:降序
- `per_row`:每行显示设备数量(1-6个,默认:3)

### 设置

1. 在WordPress后台菜单中找到"设备管理 > 设置"
2. 设置每行显示设备数量(1-6个)
3. 点击"保存设置"按钮保存

## 注意事项

1. 首次启用插件时会自动创建必要的数据表
2. 删除插件时不会自动删除数据表,需要手动删除
3. 建议定期备份数据库
4. 图片URL需要是可访问的完整地址

## 更新日志

### 1.0.4
- 修复状态按钮样式问题
- 优化状态类名生成逻辑

### 1.0.3
- 添加新的设备状态选项
- 优化数据库表结构

### 1.0.2
- 更新数据库表结构
- 优化设备状态显示

### 1.0.1
- 添加设备显示设置功能
- 支持自定义每行显示设备数量

### 1.0.0
- 初始版本发布
- 支持设备分组管理
- 支持设备信息管理
- 支持自定义排序
- 支持前端展示

## 技术支持

如有问题或建议,请访问:
- 官方网站:https://h4ck.org.cn
- 问题反馈:https://h4ck.org.cn/contact

## 作者

- obaby
- 网站:https://h4ck.org.cn

## 许可证

GPL v2 或更高版本

需要使用英文的括号替换中文的方括号【】

系统截图:

项目地址:

GitHub – obaby/Baby-Device-Manager: 一个功能强大的WordPress设备管理系统插件,支持设备分组管理、设备信息管理、自定义排序、状态跟踪等功能。

插件zip包下载:

https://www.123912.com/s/ucY7Vv-iwAAA?提取码:PjEH

https://www.123865.com/s/ucY7Vv-iwAAA?提取码:PjEH

 

The post 我的设备插件(WordPress 插件) — 就爱重复造轮子 appeared first on obaby@mars.

  •  

还是孩子的我们

✇obaby
作者obaby

平淡无奇的一个假期,竟然包含了端午和儿童节。长大了以后,似乎对所有的节日都失去了兴趣,也没了期待。没当节日来临,似乎也没什么不同,平平无奇的一天。甚至,连宝子报的兴趣班都不曾停课,俨然也仅仅是个周末而已。

周六报的小记者活动有个非遗传承体验的活动,为了参加活动,钢琴课就请假了。去过很多地方,看过很多东西之后,有时候忽然觉得,几乎所有地方都有非遗,似乎所有的地方的非遗竟然都有那么一丝丝的相似。只不过是换了个地方,换了个名字,仅此而已。

这几年,非遗传承以及旅游变得更加的热闹了。所有的地方都在振兴旅游业,也算是好事吧,有钱了,很多之前濒临灭绝的东西也就得以传承了。

这个吉祥物,我也不知道是个鸽鸽还是个海鸥,无处不在。

签到的时候,给了个序号42,说到时候按照序号体验。实际上还是天真了,很多地方直接已经开始有人在体验了。于是带着宝子去排队,这大姐漫不经心的样子,就那么几个孩子都能让别人给插队,最后实在忍不住了,说了她两句。

包个粽子,我让她站好,结果她就在边上看啊看,人家小朋友直接挤进去了,她也不吱声,真是让人恨的牙根痒痒。说她让她排好队的时候,还有家长看我。

其他的体验活动也基本都是先到先得,带着她到处跑,总算是体验了几项,剩下的她也不感兴趣了。

毛笔字感觉写的比我强点,毕竟之前没写过。

木版年画,就是刷漆,然后简单的印一下。

还有画脸谱,等过去的时候脸谱已经没了。月饼模子做月饼,等过去的时候也没了。最后就这样了,至于剪纸,他说不想剪直接没去。

最后还有小记者的才有表演,到时也有点意思

手机拍的照片非常不清楚,回来之后才发现手机壳的镜头那里花了。想着之前抛光玻璃,直接抛光一下,效果还是蛮不错的:手机壳-1

下午,还是照例的网球课。第二天带宝子去练球,发现之前去的网球馆关门了。

发球机什么的都搬走了,里面完全空掉了。大众点评找了个场地,匹克球场,买了体验券之后到了才发现那个匹克球是运动的名字,不是叫匹克的球场。

不过打电话的时候说他们今天休息,所以网球场也空着,就直接又转到了网球场。也算是歪打正着了。

下午从窗外传来广播声音:非洲老鼠,泰国人妖,……男变女,女变男,老太太瞬间返老还童……

这东西,有很神奇了,这几个东西是怎么扯到一起的?第一个没啥兴趣,后面两个在国内也玩不了啊,没尺度看不着,大尺度演不了。

晚上去公园,果然看到了之前的老相识,上次在公园表演的时候就是他们。最后卖什么纳米眼镜的时候我就走了。

不过表演的东西跟上次还是有区别的:

喷火,吹气球,硬气功弯钢筋,独轮车,蟒蛇等等。到时有些意思,不过这次没卖那些东西,让打赏,于是我打赏了30。后来又卖陀螺,宝子让买,又10块钱买了个陀螺,然后就陆续散场了。

其实,儿童节我也收到礼物了。

希望大家都能像孩子一样简单快乐,永葆童心。

而我,还是那个幼稚而又无聊的我。

The post 还是孩子的我们 appeared first on obaby@mars.

  •  

苹果Carplay 自定义壁纸实现逻辑分析

✇obaby
作者obaby

虽然在开车的时候,多数时间不会停留在carplay的桌面。然而,当第一次切进来的时候显示的那个桌面背景图片,着实不怎么喜欢。于是就想着能够换掉这个壁纸。

网上搜了一下,基本都是一年前的文章,或者说最新的文章都是一年前的。这就比较尴尬了。
而至于实现工具和方法,到处都是抄来抄去的文章,第一步基本都是安装巨魔助手,Troll app,https://trollstore.app
这是一个越狱的应用商店。通过这个越狱的商店安装AirWall,在air wall里面设置壁纸。
https://onejailbreak.com/blog/airwall/


这一切看起来似乎完美,但是,这个troll store app 最高支持到ios 17,我现在的18没有越狱,也不想越狱。那么又该怎么搞呢?


自然是自签名,目前爱思助手之类的貌似不支持普通的appstore账号签名安装了。
不过可以通过下面的工具,签名应用然后通过爱思助手安装。

Sideloadly Download iOS 18.3+ | Install IPA Without Revoke

Esign iPA download and install using Sideloaly App.
Download Esign IPA:
First, download the Esign IPA file for your PC.
Download Esign iPA file.
Install Sideloadly:
Sideloadly is the tool we’ll use to install Esign on your iOS device.
If you don’t have Sideloadly yet, download and install Sideloadly app on your PC(Windows or Mac).
Connect Your Device:
Connect your iPhone or iPad to your computer using a USB cable.
Open Sideloadly:
Launch the Sideloadly application on your computer.
Select the IPA File:
In Sideloadly, click on the IPA icon to select the Esign IPA file you downloaded earlier.
Sign the IPA File:
Enter your Apple ID and password when prompted. This step is necessary for signing the IPA file.
Start Installation:
Click the Start button in Sideloadly to begin the installation process.
Sideloadly will sign the Esign IPA file and install it on your iOS device.
Check Your Home Screen:
Once the process is complete, you’ll find the Esign app icon on your home screen.
Then go to Settings App → General → Profiles & Device Management → Find the Esign app profile and trust it.
You can now use Esign to sign and install other IPA files directly on your device.

然而,签名安装之后却发现了另外一个问题,那就是卡在了加载界面。一直在获取目录,后面就进行不下去了。

上网搜了一下,好无进展,都是说什么连接carplay之后在设置,然而,这就是句废话。连上了也没什么用。
不过,这个app体积不大,直接拉出来。扔到hopper里面看下实现逻辑,也并不复杂。
直接通过目录来获取的当前壁纸,同样替换壁纸也是直接写入文件实现的。


看F5之后的代码就更直观了。


导出代码,直接扔给cursor,让cursor根据f5代码拆分重构代码。

现在代码逻辑就明朗了,更换壁纸的方法主要在AirawWallpaper.m中:

#pragma mark - Helper Methods

- (void)checkingPath {
    // 检查壁纸路径
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *basePath = @"/var/mobile/Containers/Data/Application/";
    
    // 检查是否有权限访问
    if (![fileManager isWritableFileAtPath:basePath]) {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"权限错误"
                                                                      message:@"无法访问系统目录,请确保设备已越狱并授予了正确的权限。"
                                                               preferredStyle:UIAlertControllerStyleAlert];
        [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
        [self presentViewController:alert animated:YES completion:nil];
        return;
    }
    
    NSError *error = nil;
    NSArray *contents = [fileManager contentsOfDirectoryAtPath:basePath error:&error];
    if (error) {
        NSLog(@"Error reading directory: %@", error);
        return;
    }
    
    NSMutableArray *validPaths = [NSMutableArray array];
    for (NSString *path in contents) {
        if ([path containsString:@"com.apple.CarPlayApp.wallpaper-images"]) {
            NSString *fullPath = [basePath stringByAppendingPathComponent:path];
            BOOL isDirectory;
            if ([fileManager fileExistsAtPath:fullPath isDirectory:&isDirectory] && isDirectory) {
                [validPaths addObject:path];
            }
        }
    }
    
    if (validPaths.count > 0) {
        self.FullCache = [basePath stringByAppendingPathComponent:validPaths[0]];
        [self.tableView reloadData];
    } else {
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"错误"
                                                                      message:@"未找到 CarPlay 壁纸目录,请确保已正确安装 CarPlay 应用。"
                                                               preferredStyle:UIAlertControllerStyleAlert];
        [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
        [self presentViewController:alert animated:YES completion:nil];
    }
}

当然,上面这段代码的错误提示是我让cursor加上的。原来的并没有这段,这个是f5的代码:

int ___30-[AirawWallpaper checkingPath]_block_invoke(int arg0) {
    r31 = r31 - 0xc0;
    saved_fp = r29;
    stack[-8] = r30;
    var_80 = arg0;
    var_18 = [[NSFileManager defaultManager] retain];
    r0 = [var_18 enumeratorAtPath:@"/var/mobile/Containers/Data/Application/"];
    r29 = &saved_fp;
    var_20 = [r0 retain];
    var_28 = [@"" retain];
    do {
            r0 = [var_20 nextObject];
            r29 = r29;
            r0 = [r0 retain];
            r8 = var_28;
            var_28 = r0;
            [r8 release];
            if (r0 == 0x0) {
                break;
            }
            if ([var_28 rangeOfString:@"com.apple.CarPlayApp.wallpaper-images"] == 0x7fffffffffffffff) {
                continue;
            }
            r0 = [@"/var/mobile/Containers/Data/Application/" stringByAppendingPathComponent:var_28];
            r29 = r29;
            [var_18 fileExistsAtPath:[r0 retain] isDirectory:r29 - 0x29];
            if ((var_29 & 0x1) != 0x0) {
                    [*(var_80 + 0x20) addObject:var_28];
            }
            objc_storeStrong(r29 - 0x48, 0x0);
    } while (true);
    r11 = *(var_80 + 0x28);
    *(&var_78 + 0x10) = 0x100007ae0;
    *(&var_78 + 0x18) = 0x100014200;
    *(&var_78 + 0x20) = [*(var_80 + 0x30) retain];
    *(&var_78 + 0x28) = [*(var_80 + 0x20) retain];
    dispatch_async(r11, &var_78);
    objc_storeStrong(&var_78 + 0x28, 0x0);
    objc_storeStrong(&var_78 + 0x20, 0x0);
    objc_storeStrong(r29 - 0x28, 0x0);
    objc_storeStrong(r29 - 0x20, 0x0);
    r0 = objc_storeStrong(r29 - 0x18, 0x0);
    return r0;
}

不过现在,也能看出问题出在什么地方了。/var/mobile/Containers/Data/Application/这个目录,普通的app是没有足够的全项访问的。需要申请特殊的权限,直接让cursor创建权限申请的Entitlements:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- 基本权限 -->
    <key>application-identifier</key>
    <string>49U329UV5Q.by.oba.airwall</string>
    
    <key>com.apple.developer.team-identifier</key>
    <string>49U329UV5Q</string>
    
    <key>get-task-allow</key>
    <true/>
    
    <key>keychain-access-groups</key>
    <array>
        <string>49U329UV5Q.*</string>
        <string>com.apple.token</string>
    </array>
    
    <!-- 容器访问权限 (私有API) -->
    <key>com.apple.private.security.container-manager</key>
    <true/>
    
    <key>com.apple.private.security.disk-device-access</key>
    <true/>
    
    <key>com.apple.private.security.system-container</key>
    <true/>
    
    <!-- 文件系统完全访问 -->
    <key>com.apple.private.security.no-container</key>
    <true/>
    
    <key>com.apple.private.security.no-sandbox</key>
    <true/>
    
    <!-- TCC绕过权限 -->
    <key>com.apple.private.tcc.allow</key>
    <array>
        <string>kTCCServiceSystemPolicyAllFiles</string>
        <string>kTCCServiceSystemPolicyDesktopFolder</string>
        <string>kTCCServiceSystemPolicyDocumentsFolder</string>
        <string>kTCCServiceSystemPolicyDownloadsFolder</string>
        <string>kTCCServiceAppleEvents</string>
    </array>
    
    <!-- 根权限访问 -->
    <key>com.apple.private.security.storage.SystemPolicyAllFiles</key>
    <true/>
    
    <!-- 平台应用权限 -->
    <key>platform-application</key>
    <true/>
    
    <!-- 跳过验证 -->
    <key>com.apple.private.skip-library-validation</key>
    <true/>
    
    <!-- 任务访问权限 -->
    <key>task_for_pid-allow</key>
    <true/>
    
    <!-- 进程调试权限 -->
    <key>com.apple.system-task-ports</key>
    <true/>
    
    <!-- 网络权限 -->
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
</dict>
</plist>

既然添加了自定义的权限,那么使用这些签名工具就没办法写入权限文件了,需要自己来实现签名,具体的实现方法可以参考:
iOS 签名杂谈(一):https://h4ck.org.cn/2020/06/7112
iOS 签名杂谈(二): https://h4ck.org.cn/2020/06/7130

虽然之前做个一个签名的图形界面工具,但是引入了一个库不支持arm架构,也懒得再更新了。现在就只能创建签名脚本签名了,来回修改多次之后,让cursor创建了一个自签名脚本,sign_with_args.sh。通过这个东西就可以快速签名文件了。

签名脚本代码:

#!/bin/bash

# 变量声明(无默认值)
MOBILEPROVISION=""
APP_PATH=""
CERTIFICATE=""
BUNDLE_ID=""
TEAM_ID=""
DEVICE_UDID=""
ENTITLEMENTS_FILE=""  # 新增:自定义 Entitlements 文件路径

# 显示帮助信息
show_help() {
    echo "用法: $0 [选项]"
    echo "选项:"
    echo "  -p, --provision    mobileprovision文件路径 (必需)"
    echo "  -a, --app-path     应用路径 (必需)"
    echo "  -c, --certificate  证书名称 (必需)"
    echo "  -b, --bundle-id    Bundle ID (必需)"
    echo "  -t, --team-id      Team ID (必需)"
    echo "  -d, --device-udid  设备UDID (必需)"
    echo "  -e, --entitlements Entitlements文件路径 (可选,默认从mobileprovision提取)"
    echo "  -h, --help         显示此帮助信息"
    exit 0
}

# 检查必需参数
check_required_params() {
    local missing=0
    if [ -z "$MOBILEPROVISION" ]; then
        echo "错误: 缺少 mobileprovision 文件路径 (-p)"
        missing=1
    fi
    if [ -z "$APP_PATH" ]; then
        echo "错误: 缺少应用路径 (-a)"
        missing=1
    fi
    if [ -z "$CERTIFICATE" ]; then
        echo "错误: 缺少证书名称 (-c)"
        missing=1
    fi
    if [ -z "$BUNDLE_ID" ]; then
        echo "错误: 缺少 Bundle ID (-b)"
        missing=1
    fi
    if [ -z "$TEAM_ID" ]; then
        echo "错误: 缺少 Team ID (-t)"
        missing=1
    fi
    if [ -z "$DEVICE_UDID" ]; then
        echo "错误: 缺少设备 UDID (-d)"
        missing=1
    fi
    if [ $missing -eq 1 ]; then
        echo "请使用 -h 或 --help 查看帮助信息"
        exit 1
    fi
}

# 解析命令行参数
while [[ $# -gt 0 ]]; do
    case $1 in
        -p|--provision)
            MOBILEPROVISION="$2"
            shift 2
            ;;
        -a|--app-path)
            APP_PATH="$2"
            shift 2
            ;;
        -c|--certificate)
            CERTIFICATE="$2"
            shift 2
            ;;
        -b|--bundle-id)
            BUNDLE_ID="$2"
            shift 2
            ;;
        -t|--team-id)
            TEAM_ID="$2"
            shift 2
            ;;
        -d|--device-udid)
            DEVICE_UDID="$2"
            shift 2
            ;;
        -e|--entitlements)
            ENTITLEMENTS_FILE="$2"
            shift 2
            ;;
        -h|--help)
            show_help
            ;;
        *)
            echo "未知选项: $1"
            show_help
            ;;
    esac
done

# 检查必需参数
check_required_params

# 清理函数
cleanup() {
    echo "清理临时文件..."
    rm -f temp_entitlements.plist
}

# 错误处理
handle_error() {
    echo "错误: $1"
    cleanup
    exit 1
}

# 检查文件是否存在
if [ ! -f "$MOBILEPROVISION" ]; then
    handle_error "mobileprovision 文件不存在: $MOBILEPROVISION"
fi

# 检查目录是否存在
if [ ! -d "$APP_PATH" ]; then
    handle_error "应用目录不存在: $APP_PATH"
fi

# 如果提供了自定义 Entitlements 文件,检查其是否存在
if [ ! -z "$ENTITLEMENTS_FILE" ] && [ ! -f "$ENTITLEMENTS_FILE" ]; then
    handle_error "Entitlements 文件不存在: $ENTITLEMENTS_FILE"
fi

# 验证配置文件
echo "验证配置文件..."
PROFILE_INFO=$(security cms -D -i "$MOBILEPROVISION")
if [ $? -ne 0 ]; then
    handle_error "配置文件无效"
fi

# 提取信息
echo "正在从 mobileprovision 提取信息..."
PROFILE_TEAM_ID=$(echo "$PROFILE_INFO" | plutil -extract TeamIdentifier.0 raw -)
PROFILE_APP_ID=$(echo "$PROFILE_INFO" | plutil -extract Entitlements.application-identifier raw -)
PROFILE_EXPIRATION=$(echo "$PROFILE_INFO" | plutil -extract ExpirationDate raw -)

echo "Profile Team ID: $PROFILE_TEAM_ID"
echo "Profile Application ID: $PROFILE_APP_ID"
echo "Profile Expiration: $PROFILE_EXPIRATION"

# 验证 Team ID
if [ "$PROFILE_TEAM_ID" != "$TEAM_ID" ]; then
    echo "警告: Team ID 不匹配"
    echo "Profile中的: $PROFILE_TEAM_ID"
    echo "设置的: $TEAM_ID"
    handle_error "请确保使用正确的 mobileprovision 文件"
fi

# 验证设备 UDID
echo "验证设备 UDID..."
if ! security cms -D -i "$MOBILEPROVISION" | grep -A 20 ProvisionedDevices | grep -q "$DEVICE_UDID"; then
    echo "错误: 设备 UDID $DEVICE_UDID 未包含在 provisioning profile 中"
    echo "包含的设备:"
    security cms -D -i "$MOBILEPROVISION" | grep -A 20 ProvisionedDevices
    handle_error "设备未授权"
fi
echo "设备 UDID 验证通过: $DEVICE_UDID"

# 设置完整的 Bundle ID
FULL_BUNDLE_ID="$PROFILE_APP_ID"
echo "使用 Bundle ID: $FULL_BUNDLE_ID"

# 清理旧的签名
echo "清理旧的签名..."
rm -rf "$APP_PATH/_CodeSignature" 2>/dev/null

# 复制 mobileprovision 并设置权限
echo "复制 mobileprovision..."
cp "$MOBILEPROVISION" "$APP_PATH/embedded.mobileprovision"
chmod 644 "$APP_PATH/embedded.mobileprovision"

# 处理 Entitlements
if [ ! -z "$ENTITLEMENTS_FILE" ]; then
    echo "使用自定义 Entitlements 文件: $ENTITLEMENTS_FILE"
    cp "$ENTITLEMENTS_FILE" temp_entitlements.plist
else
    echo "从 mobileprovision 提取 Entitlements..."
    security cms -D -i "$MOBILEPROVISION" | plutil -extract Entitlements xml1 -o - - > temp_entitlements.plist
fi

# 显示 entitlements 内容
echo "Entitlements 内容:"
plutil -p temp_entitlements.plist

# 设置正确的文件权限
echo "设置文件权限..."
chmod -R 755 "$APP_PATH"
find "$APP_PATH" -type f -exec chmod 644 {} \;
find "$APP_PATH" -name "*.dylib" -exec chmod 755 {} \;
[ -f "$APP_PATH/AirWall" ] && chmod 755 "$APP_PATH/AirWall"
[ -f "$APP_PATH/AirWallHelper" ] && chmod 755 "$APP_PATH/AirWallHelper"
[ -f "$APP_PATH/trollstorehelper" ] && chmod 755 "$APP_PATH/trollstorehelper"

# 移除空文件(特别是0字节的PNG文件)
echo "移除空文件..."
find "$APP_PATH" -type f -size 0 -delete
echo "已移除空文件"

# 修正Info.plist中的Bundle ID
echo "修正Info.plist中的Bundle ID..."
plutil -replace CFBundleIdentifier -string "$PROFILE_APP_ID" "$APP_PATH/Info.plist"
echo "Bundle ID已设置为: $(plutil -extract CFBundleIdentifier raw "$APP_PATH/Info.plist")"

# 对可执行文件进行签名
echo "对可执行文件进行签名..."
if [ -f "$APP_PATH/AirWallHelper" ]; then
    echo "签名 AirWallHelper..."
    codesign -f -s "$CERTIFICATE" --entitlements temp_entitlements.plist "$APP_PATH/AirWallHelper" || handle_error "AirWallHelper 签名失败"
fi

if [ -f "$APP_PATH/trollstorehelper" ]; then
    echo "签名 trollstorehelper..."
    codesign -f -s "$CERTIFICATE" --entitlements temp_entitlements.plist "$APP_PATH/trollstorehelper" || handle_error "trollstorehelper 签名失败"
fi

# 对所有的 frameworks 和 dylibs 进行签名
echo "签名 frameworks 和 dylibs..."
find "$APP_PATH" -name "*.framework" -o -name "*.dylib" | while read -r file; do
    echo "签名: $file"
    codesign -f -s "$CERTIFICATE" "$file" || handle_error "Framework/dylib 签名失败: $file"
done

# 对整个应用进行签名
echo "对整个应用进行签名..."
codesign -f -s "$CERTIFICATE" --entitlements temp_entitlements.plist --deep "$APP_PATH" || handle_error "应用签名失败"

# 验证签名
echo "验证签名..."
codesign -vv -d "$APP_PATH"

# 检查_CodeSignature目录
echo "检查_CodeSignature目录..."
if [ -d "$APP_PATH/_CodeSignature" ]; then
    echo "✓ _CodeSignature 目录存在"
    ls -la "$APP_PATH/_CodeSignature/"
else
    echo "⚠  _CodeSignature 目录不存在,这可能导致安装失败"
fi

# 显示更多签名信息
echo "显示详细签名信息..."
codesign -d --entitlements :- "$APP_PATH"

# 验证所有签名
echo "验证所有签名..."
if codesign --verify --deep --strict --verbose=4 "$APP_PATH"; then
    echo "✓ 严格验证通过"
else
    echo "⚠  严格验证失败,但基本签名可能仍然有效"
fi

# 清理临时文件
cleanup

# 重新打包
echo "重新打包..."
rm -f signed_AirWall.ipa
zip -qry signed_AirWall.ipa Payload

echo "签名完成!"

 

然而,在测试的时候发现com.apple.private私有权限,签名之后安装全部被拒绝了。那么,也就是说目前是没办法在非越狱系统访问这个文件的,自然也就没法更改壁纸。

原来是想把代码开源的,结果上传之后把自己的证书也放进去了。所以就没发开园啦,主要原理也说了,结论就是,目前不越狱是没办法修改carplay的壁纸的,所以可以不用尝试了。

附视频链接:

The post 苹果Carplay 自定义壁纸实现逻辑分析 appeared first on obaby@mars.

  •  

小麻雀

✇obaby
作者obaby

昨天下班回家,停好车往回走的时候,看到在路上有一只小麻雀,就站在那里,左顾右盼。

看起来似乎应该是刚离家的小鸟,还不是很大,虽然羽毛已经比较丰富了,不过还是有那么一点点的稚嫩。

怕它在路上被车撵到,走过去想着把它赶到路边。走进了他却没有要飞走的意思,伸手去抓他,他顺势就直接走到了手心里。

一点都没有害怕的意思。

往前走了点,想把它放飞,最后终于鼓足勇气飞了起来,不过可能还是太小了吧,飞不远,也飞不高。就这么落到了路边,于是又走过去把它捡了起来。

就这么托着带回了家,想给他弄点吃的。泡了点米,弄了点水。最开始没想到放在那里,放到了一个空垃圾桶里,这个环境它明显不适应。没吃东西,也没喝水,对象说,你把它送回去吧,他不适应。

想着直接从床边放走,但是三楼的阳台对他来说可能是太高了,迟迟不肯飞走。只好带着他下楼,到了楼下后面的草地。放下之后,也不跑,就在那里一直叫。来回的折腾了一会儿,感觉就像找个安静的地方睡觉,钻草丛,还差点把自己卡住了。

感觉精神也不是很足,可能天快黑了,也是想睡觉了把。最后一头扎到了一根水管下面,再也不乱跑了。只是,这么呆着容易被野猫给抓走了。

于是,我又把它捧了起来,带回了家。找了个百花蛇草水的箱子,空空的箱子,他还是不大适应。鉴于在下面的表现,家里也没棉花,但是想到了卫生纸,可以用卫生纸啊,于是抽了数张纸巾,撕成长条,扔到箱子里。

小麻雀稍微迟疑,马上就钻进了纸条下面,也变得安静了,不再叽叽喳喳的叫。

怕它乱跑,在纸箱上打了一些孔,上面用保鲜膜稍微盖了一下。

晚上起来上厕所,听不到任何的叫声,太安静有时候真的怕它嘎了。不过,早上就证明自己的担心是多余的,五点多就开始叽叽喳喳叫个不停。

起床之后,看了下昨晚泡的小米,虽然还是有点硬,但是吃应该没问题了。喂了点小米,喂了点馒头。

搜了一下,貌似也没什么救助的地方,我说给动物园打电话,我对象说我是白费功夫。一想也是,网上搜了一下,也没什么好的办法。

最后还是决定给放走,毕竟,那才是他该生活的地方。

带着他到了昨天的地方,找个大点的空地。它还是喜欢呆在自己的手上,怎么都不下来。

伸着脖子叫的时候,还能看到脖子下的皮肤,毕竟,还是未成年的小鸟。嘴角还有嫩黄的装饰色。

最后把手斜在树枝边,他开始网上走,就终于到了小树枝上。它卖力的叫着,周围不断传来同伴的叫声。

终将,他还是会拥有自己的完美生活。毕竟世界那么大,这才是属于他的世界。

The post 小麻雀 appeared first on obaby@mars.

  •  

InfluxDB 诡异的时间窗口对齐(是我太傻逼)

✇obaby
作者obaby

之前的项目,将部分数据迁移到了InfluxDB v2 数据库。但是,在查询数据的时候发生了一件很诡异的事情,就是使用不同的时间间隔,返回的数据却完全不一样。

感谢 ymz316 帮我找到了 bug,还是数据处理逻辑的问题。我把 ai 给唬住了,他没分析代码,我也没分析代码。另外一个问题就是上报数据的时间间隔太长了,在时间为 1m 的时候表现出了诡异的行为,根本原因在于 1m 中采样的时候,后面四分钟都没数据(上报频率正好也是 5 分钟)于是采样到了 06 分钟的数据。就成了 01 06 11 的样子,这 tmd 把数据上报频率也给忽略了。

查询代码如下:

def query_data_with_5min_sampling(device_id, start_time, end_time, interval='05m'):
    """
    查询指定设备在时间范围内的数据,支持不同的采样间隔
    :param device_id: 设备ID
    :param start_time: 开始时间
    :param end_time: 结束时间
    :param interval: 采样间隔,支持 '10s', '30s', '01m', '05m', '10m', '30m', '01h',默认为 '05m'
    :return: 采样后的数据列表
    """
    if interval is None or interval == '':
        interval = '05m'
    if 'm' not in interval and 's' not in interval:
        interval = f"0{interval}m" if int(interval) < 10 else f"{interval}m"
    # 验证时间范围,如果大于一天,强制使用5分钟采样
    if end_time - start_time > timedelta(days=1):
        interval = '05m'
    # 如果小于等于一天且没有指定间隔,使用1分钟采样
    elif interval == '5m' and end_time - start_time <= timedelta(days=1):
        interval = '01m'

    query = f"""
    from(bucket: "ts")
        |> range(start: {datetime_to_tz_time_string(datetime_to_utc_time(start_time))}, 
                stop: {datetime_to_tz_time_string(datetime_to_utc_time(end_time))})
        |> filter(fn: (r) => r._measurement == "TSSourceData")
        |> filter(fn: (r) => r.device_id_string == "{device_id}")
        |> filter(fn: (r) => r._field =~ /^(temperature|humidity|health_level)$/)
        |> aggregateWindow(
            every: {interval},
            fn: mean,
            createEmpty: false
        )
        |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    """
    
    tables = client.query_api().query(org="power", query=query)
    
    lines = []
    for table in tables:
        for record in table.records:
            lines.append(record.values)
    return lines

数据支持: ’10s’, ’30s’, ’01m’, ’05m’, ’10m’, ’30m’, ’01h’,默认为 ’05m’

然而,当使用 5 分钟为间隔查询的时候,返回的第一条数据时间竟然是 01 分,不是整点,查询代码:

nt = current_time = get_rounded_time_before_time(8)
    # # print(nt)
    lines = query_data_with_5min_sampling('mddt6825050023_1', nt, datetime.now(), interval='5m')
    # print(lines)
    for data_point in lines:
        # print(data_point)   
        utc_time = data_point.get('_time')
        tz = pytz.timezone('Asia/Shanghai')
        local_time = utc_time.astimezone(tz)
        print(local_time.strftime('%Y-%m-%d %H:%M:%S'))

执行结果:

2025-05-27 01:01:00
2025-05-27 01:06:00
2025-05-27 01:11:00
2025-05-27 01:16:00
2025-05-27 01:21:00
2025-05-27 01:26:00
2025-05-27 01:31:00
2025-05-27 01:36:00
2025-05-27 01:41:00

然而,当时间改成 15 分钟或者其他时间,就完全是按照整点以及时间间隔来的:

lines = query_data_with_5min_sampling('mddt6825050023_1', nt, datetime.now(), interval='15m')

执行结果:

2025-05-27 01:15:00
2025-05-27 01:30:00
2025-05-27 01:45:00
2025-05-27 02:00:00
2025-05-27 02:15:00
2025-05-27 02:30:00
2025-05-27 02:45:00
2025-05-27 03:00:00
2025-05-27 03:15:00
2025-05-27 03:30:00
2025-05-27 03:45:00
2025-05-27 04:00:00
2025-05-27 04:15:00
2025-05-27 04:30:00
2025-05-27 04:45:00
2025-05-27 05:00:00
2025-05-27 05:15:00
2025-05-27 05:30:00
2025-05-27 05:45:00
2025-05-27 06:00:00
2025-05-27 06:15:00

我勒个豆,这么神奇吗?对于这种错误其实猜测可能是返回数据的对齐粒度问题,但是在尝试了使用 offset 等各种参数之后,对于 5 分钟的数据还是返回了 01。直接崩溃,让 cursor 来回改,最后代码改的面目全非了依然没达到效果。只能回滚代码。

这时候鬼使神差想到,这个参数既然是个字符串,那么传个 05m 呢?

lines = query_data_with_5min_sampling('mddt6825050023_1', nt, datetime.now(), interval='05m')

执行结果:

2025-05-27 01:05:00
2025-05-27 01:10:00
2025-05-27 01:15:00
2025-05-27 01:20:00
2025-05-27 01:25:00
2025-05-27 01:30:00
2025-05-27 01:35:00
2025-05-27 01:40:00
2025-05-27 01:45:00
2025-05-27 01:50:00
2025-05-27 01:55:00
2025-05-27 02:00:00
2025-05-27 02:05:00
2025-05-27 02:10:00
2025-05-27 02:15:00
2025-05-27 02:20:00

竟然神奇的治愈了,这尼玛不得不说竟然这么神奇。所以最开始的代码其实是修复之后的代码,对于没有 0 开头的分钟进行填充。

问了下 cursor,给出了下面的答复:

这特性,真是服了,问题是 cursor,为什么不是你发现了告诉我?而是我发现了告诉你呢?

果然是高级 quirk!

The post InfluxDB 诡异的时间窗口对齐(是我太傻逼) appeared first on obaby@mars.

  •  

数值的奴隶

✇obaby
作者obaby

以前没有智能设备的时候,感受不到多少数字的存在。很多年以前还是用功能机的时候,也没有那么多的数字需要关注,哪怕不带手机也没什么,只需要拿起手机看下未接来电,以及短信消息就可以了。

后来,身边的设备越来越智能,开始能接入各种系统,各种 app。设备上的消息也越来越多,反而之前最关注的两个电话和短信,现在竟然变得可有可无了。

甚至,很多的时候,明明有我的号码,但是却一定要打微信电话。这电话,似乎也没那么重要了。而短信,现在只剩了一个功能,就是接收验证码。

那些乱七八糟的 app 每天都在推送各种消息,各种无用的消息,然而,让人感觉比较恶心的是,在这些无用的消息中,偶尔会夹杂那么一两条有用的消息。例如各种银行的 app,好死不死的有时候还会发个信用卡还款通知之类的。

再后来,各种工作也开始用微信联系,一个普通的社交工具变成了让人讨厌的工作工具。当然公司也有自己的 oa 系统,app。但是,多数人还是在微信上进行工作沟通,于是,又有了各种乱七八糟的群。

就这样,这个微信也成了一个废号。朋友圈,之类的东西,自然也不再发,不再看。一个充满了班味,以及勾心斗角的地方,又有谁会喜欢呢。

更何况,现在它成了牛马的皮鞭,在里面唯一能听到的就是:“驾……”、“吁……”、“抬蹄,靠左,靠左……”

于是,为了解决这个问题,自己开始使用另外的私人号码,甚至为了拒绝这个问题。现在电话是两个,微信是两个,甚至很多的东西的确都想弄三个。不过,设备多了,自然这些骚扰的消息可能也会翻倍,毕竟,在不同的地方登录了相同的或者不同的号码。

说实话,安卓的这个 99+并不比 iOS 的 1501 让人能感觉开心一些,或者愉悦一些。看到的都一样,都是无尽的骚扰,和那些无用的消息。

这些数字,的确不怎么招人喜欢。

只是,人啊有时候又异常矛盾,在某些地方开始不断的去追求那些数值。最开始的是微信的步数,很多人为了那个步数,甚至买那种专门摇步数的工具,把手机放在上面,一摇一天,就为了在微信的步数排行榜能排第一。对于这种事情,其实我并不太理解,但是,对于这种个人追求,倒是也没啥好多的,毕竟,个人爱好而已。

五十步笑百步,虽然没在这些事情上浪费时间,但是在追求数值这件事情上,也会在别的地方沦陷。从之前的 apple watch,到现在的华为 watch。为的是为了运动健康的那个圆环,与其实说是为了运动,不如说是为了所谓的成就感,看着能合上三个圆环,这就是目的。运动的目标有点太高大了。

不过,这些设备,不管是华为还是苹果,总是有时会出些莫名其妙的问题。但你在追求一个每日都能达成目标的时候,总会在关键的时刻丢数据。30 分钟的运动,在最后统计的时候运动时间不达标。甚至更离谱的是步数,戴在手上,不如装在兜里。

这两天的运动时长明显是足够的。20 分钟的跳绳,锻炼时长 14 分钟,这数学不能说是体育老师教的,只能说纯粹是没啥数值计算能力。当然,程序员写点 bug 似乎也是很合理。毕竟,可能水平真的有限。这种限制,也的确让我无法合上三环,主要是一下午台球打完下来,的确也没那么轻松。

而至于步数,两个设备差了一倍。打了四个半小时台球,手表的步数一点都没加,可能我就跟木桩一样杵在那里打的把,毕竟高手走位,都能随心所欲。那需要移动呢?

今天早上,手表的步数统计似乎又正常了。这些电子产品有的时候就跟神经病一样。前端时间一直提示设备高温,一度想换个新的。只是感觉新的都好丑啊,等看看再扛一段时间吧。手表这种东西,说实话,现在对于自己来说唯一的用处就是记录每天的步数,记录每天跳绳,或者偶尔能获取那么一两个徽章。

然而,折腾了这么多,最后,能说明问题的就然还是这些数字。

只是,以后各种电子设备更智能了,或许,真的就成了设备的奴隶了。

也许哪天,我设置了跳绳一万个的目标,时间到了。机器人拿着皮鞭,一鞭子抽过来,厉声喝道:“赶紧起来跳绳了!”

The post 数值的奴隶 appeared first on obaby@mars.

  •  

稳字诀

✇obaby
作者obaby

小师妹灵儿眨巴着灵动的眼睛,脆生生地说道:“师傅说啦,练剑最重要的是手要稳,等我给师兄师姐们展示一下。”


众师兄师姐顿时来了兴致,交头接耳议论纷纷。急性子的大师兄挠着后脑勺:“莫不是在手腕上挂水桶练稳劲?”话音未落,就被细心的二师姐轻轻戳了下额头:“别乱说,灵儿师妹鬼灵精怪,定不会用那等老套法子,准保叫咱们大开眼界。”

在众人满是期待的目光中,小师妹灵儿素手轻抬,竟将长剑稳稳当当地举在半空,剑身平平如桌面。紧接着,她掏出手机轻轻放上,指尖在屏幕上飞速划动,来来回回连着打开八十多个App。

竟然没有任何app 打开淘宝或者京东!

The post 稳字诀 appeared first on obaby@mars.

  •  

认输

✇obaby
作者obaby

这几天在调整一个项目的 mqtt 上报的数据时发现一个诡异的问题,那就是同样的服务器,如果使用 mqtt 客户端连上去一切都是正常的,上报频率也确实是看起来跟客户说的一样一分钟 1 条。

然而,在代码里获取的时候就完全变了,有时候看起来一切正常有时候时间就变得异常不稳定。

[*] Time: 2025-05-20 15:09:13
[*] Time interval from last message: 240.51 seconds
[*] Topic: canteen/third/second/valve1
[*] Message: {"switch1":1,"switch2":0,"switch3":0,"switch4":0}
[A] Updated device status: canteen/third/second/valve1_switch1
[W] Device not found: canteen/third/second/valve1_switch3

甚至有时候时间能到十来分钟都没数据。这个就很诡异了。

输出错误日志会发现系统在一直尝试断线重连:

但是在不断重连之后又能间歇性 的收到消息,这就很神奇了。7: “Connection refused – not authorized (no credentials needed)”

在尝试调整 qos 以及优化连接代码之后,依然无果。没有任何的改进,不得已只能放弃原有的链接库paho,转投更先进的gmqtt。

gmqtt: Python async MQTT client implementation.

https://pypi.org/project/gmqtt/

看示例代码也比较简洁:

import asyncio
import os
import signal
import time

from gmqtt import Client as MQTTClient

# gmqtt also compatibility with uvloop  
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())


STOP = asyncio.Event()


def on_connect(client, flags, rc, properties):
    print('Connected')
    client.subscribe('TEST/#', qos=0)


def on_message(client, topic, payload, qos, properties):
    print('RECV MSG:', payload)


def on_disconnect(client, packet, exc=None):
    print('Disconnected')

def on_subscribe(client, mid, qos, properties):
    print('SUBSCRIBED')

def ask_exit(*args):
    STOP.set()

async def main(broker_host, token):
    client = MQTTClient("client-id")

    client.on_connect = on_connect
    client.on_message = on_message
    client.on_disconnect = on_disconnect
    client.on_subscribe = on_subscribe

    client.set_auth_credentials(token, None)
    await client.connect(broker_host)

    client.publish('TEST/TIME', str(time.time()), qos=1)

    await STOP.wait()
    await client.disconnect()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()

    host = 'mqtt.flespi.io'
    token = os.environ.get('FLESPI_TOKEN')

    loop.add_signal_handler(signal.SIGINT, ask_exit)
    loop.add_signal_handler(signal.SIGTERM, ask_exit)

    loop.run_until_complete(main(host, token))

其实,各种方式或者库对我来说没什么特殊的喜好,只要能解决自己的问题就好,作为一个实用主义住,该认输就认输,毕竟要解决这个异常问题可能得从框架本身入手了,这也非我所愿。有这点时间干点别的不好吗?

白天又又又又收到了整改通知,现在看到这种整改通知,的确是有点沮丧,改不完,根本改不完。

不过这次反馈的是功能问题,该修复还是要修复的。不过白天也确实没时间了,晚上还要带宝子去石老人看沙滩音乐会。

下班还是果断先带宝子出去玩啊:

舞台比较小,毕竟是海尔组织的小型活动,所以也没多大的舞台。据说主要目的还是为了今天的集体婚礼,宝子一直在边上的游乐设施玩,等玩够了却发现连舞台边都看不到,什么也看不着,好在无人机表演倒是不需要往前挤。

早上送宝子上学,宝子嚷嚷着要听收音机的 青紫堂的广告,非得听那个念电话号码的 57813377。不得不说,这个广告没白听,我都记住了。

学校外面看到有卖小樱桃的,问了下十三一斤,回家的路上买了点,说要两三节,结果一下子来了四斤多。

到家之后打开袋子发现是上当了,篮子底下的基本都是坏的。也就是说给我装的就没几个好的,连表面一层好的想找也找不出来了。

对象说,你洗洗看看吧,不行就不要了。

那和樱桃放到水盆里,倒上水,挑的时候的确是绝望了,不单软软的,还有很多烂的,挑了几个长了一下,也不好吃。最后放弃了,连袋子一起扔到了垃圾桶里。

这的确是上了老当了,被骗了,只能认输。

The post 认输 appeared first on obaby@mars.

  •  

赶海

✇obaby
作者obaby

周三的时候,三姐家的好大儿打电话说三姐和老太太要来青岛,问周六有没有时间,到家里来坐坐。能来自然是好的,周六一早带着宝子去上 钢琴课。发了个消息问了下行程,说十二点多才能到。

这个点才到,索性就先去吃饭了,提前到饭点点好菜,等待他们到来。吃完饭,孩子们去玩了,带他们回家。随便聊聊,一切看起来也挺好的。和谐而又温馨,鉴于第二天好大儿要回老家,正好可以再把老太太送回去,因为也就没准备让她在这里住,毕竟在来回折腾也挺费劲的。然而,就在送她上车之后她来了一句,“你看你,一点礼数也不懂,也不说留我住天。”

听到这话,瞬间心里就凉了半截,真是让人恨的压根痒痒,在来之前已经反复确认过行程。既然都已经定好了,又何必非得搞这些虚情假意?我是你的子女,连这都需要演?

有时候我真不想翻旧账,但是,每当这时候心里那股恨意总会直冲脑门。当初宝子刚出生没几天,说是来看孩子照顾月子,结果没几天就嚷嚷着要走。一家人忙的要死要活,她呆在家里帮不上任何的忙,还得有人专门照顾她,于是在她要求走的时候就果断让她走了。宝子现在 9 岁了,她一共看了一个月。

我上楼之后,收到三姐发的消息说,她就这样,你别生气。

就这样,谁跟她在一起不生气?那种挑理的行为,看到就让人上火。吃饭的时候三姐说,看你瘦了不少啊,肚子都小了。她又开始说,白(别)减肥,你们谁也白减肥。听到她这些自以为是的理论,也真的让人崩溃,我不想生活在她的世界了,只是我也不想听到这种世界观。总是说小瞎汉(算命的)说怎么着,怎么着。每次我都会说,他会算个 p 啊,净胡说八道。

然而啊,有时候却实在又下不了狠心怎么着,毕竟也老了。就由她去吧,连在一起吃顿饭,都不知道又那里没做到,都能莫名奇妙的生气。真的挺犯愁和她一起生活,好在也不用在一起生活。

生活,还是得找点自己喜欢的事情或者开心的事情做不是嘛,因为这些破事长期抑郁,那真是纯粹智商有问题了。

这风和日丽的天气,就适合去海边啊,不过其实说到去海边自己其实也挺犯愁的,毕竟近的沙滩都是人,远的沙滩自己又有点懒。

提了一嘴去赶海之后,宝子就忍不住了,问什么时候去赶海啊。周日退潮最低点是两点,所以要去赶海就得晒大太阳。上午自然也没时间,九点多爬起来去买早餐。

这次宝子早餐吃了不少,毕竟昨天晚上就没吃饭。为此还一直被宝子的姥姥叨叨,你们俩不吃,也不给孩子吃。问题是,孩子不饿啊,哈哈哈。

上午十一点十五分约的窝沟封闭,开车过去,停好车,简单核对信息之后,虽然还不到十一点,但是直接给做了。

宝子的牙齿还是很健康的,并且刚做了封闭两个小时内也不能吃东西,午饭自然就不用吃了。正好回家带上鞋套就可以去赶海了,给宝子换好拖鞋。

40 多分钟的路程,终点停车场感觉人还不是很多,赶海的人也不多,这样最好啦。

退潮之后的样子,就看到自己定位在海里啦。

滩涂上都是在挖蛤蜊或者找蛏子的。

一边远远的传来大喇叭的要喝:“挖 gala,找蛏子,卖工具,卖海盐,现场教学,包教包会,包学会”

自然,来这里赶海的,纯粹就是为了玩,也不会有人去找人学习啦,更何况很多人都自带工具啦。

挖gala,那个小耙子还是蛮好用的。说白了就是没啥技巧,全靠地毯式搜索,挖就完了。

挖到的圆圆小螃蟹,给放生啦。

宝子开始怎么也挖不到,说,我今天的梦想就是自己能挖到一个 gala。

当然,最后皇天不负有心人,最后还是挖到了,一旦开始挖到了,就会不断的挖到,虽然不多,但是三个人还是完了那么一点点。

回家的路上,宝子问,晚上吃什么啊?

“要不给你做海鲜噶啦面吧”我说。

“可以啊”

于是,到了晚上,这些小 gala 就进了锅里了。

怎么说呢,虽然不多,但是味道还算不错。

毕竟是自己挖的,不是吗。

The post 赶海 appeared first on obaby@mars.

  •  

Baby CDN Debugger

✇obaby
作者obaby

昨天晚上打完球往家走的时候,看了下手机,收到杜郎的消息说网站挂了。

这种情况一般就是cdn的问题了。因为自己手机能获取到v6的地址,晚上还回复了几条评论。现在看来基本可以确认还是cdn出问题了。

后来倦意也@说证书变成自签名了。

到家看了下果然是cdn的问题,昨天的时候cdn域名9offibrx.cnvip.akdns.top解析的地址有下面几个:

111.180.205.158
111.180.205.154
117.50.201.110
57.180.25.103
61.136.162.23

后来在群里也看到有人说站点挂了,挂在了同一个节点上111.180.205.158。

如果要解决cdn节点问题,最简单的办法就是自建解析,排除掉有问题的节点。

新建A记录,选几个可用的ip地址添加上:

新建cname记录,将www和@解析到上面的域名即可:

之后等解析生效就可以了,生效之后那些失效的节点就被排除掉了。

不过要排除到底是节点问题还是什么问题,其实最简单的就是直接通过postman之类的工具测试。在进行节点测试的时候需要再header中取消原来的host,添加新的host才能自定义host。

这个测试通过postman的确可以,但是如果要再手机上测试就麻烦了。于是,我自己做了一个工具http://cd.h4ck.org.cn,可以简单的认为是个postman网页版。

上面的错误是因为直接访问节点,服务器的证书是自签名证书,关闭证书校验就能获取数据了。如下:

预览页面可以看到具体页面信息:

证书信息:

可以看到是lecdn的自签名证书,那么现在该怎么通过这个节点测试博客呢?

直接添加自定义host:

这样一切就正常了是不是?所以,如果要测试cdn的某个节点就可以通过上面的方式测试了。

同样,对于ipv6的节点也是支持的,默认访问上面的域名会根据当前网络状态返回v4或者v6地址,如果没有切换到v6,可以通过访问https://cd6.h4ck.org.cn,直接访问v6的地址。

还是一博客的v6ip为例:

其实被waf系统拦截了:

需要注意v6的ip地址拼接采用后面的形式:https://[2408:8214:e10:7210:2e2:69ff:fe39:d706]:443 知名端口号无需添加,可以省略80 443,会自动根据协议处理。

添加域名之后就可以了:

对于cdn以及waf可以通过同样的方式测试,测试文件也是可以的。

查看资源缓存状态:

以及其他类型的错误排查也是可以的:

工具地址:

IPv4 & IPv6访问

http://cd.h4ck.org.cn

IPv6 访问Only

https://cd6.h4ck.org.cn

注意:cd地址不支持v6服务器的探测,cd6地址支持v4以及v6服务器探测。

The post Baby CDN Debugger appeared first on obaby@mars.

  •  

无知

✇obaby
作者obaby

一直以来的生活环境,或者生存方式,造就了自己的思考方式,或者说认知能力也在潜移默化中受到影响。

每当到一个新环境或者说看到新事物的时候总容易墨守成规的从已有的生活经验来理解这个新东西,毕竟那些从来没见过的东西靠想象力是难以想象出来的。

第一次在青岛听到脂渣这个东西的时候,总觉得应该跟自己小的时候吃的应该是差不多的东西。在记忆里,脂渣是使用五花肉甚至猪板油炼油之后,剩下的那些固体的渣渣。

有时候炼的太狠了甚至有那么一点点的糊味。

知道后来在超市看到他们卖的脂渣才发现,不是那种自己固认为的油渣。之前自己理解的脂渣其实叫做油渣更为合适,毕竟就是加工动物油剩下来的渣渣。而现在青岛卖的脂渣,是专门油炸加工出来的视频,甚至以后的就是纯瘦肉的,猪里脊炸出来的脂渣,拿到手里干干爽爽,上面没有任何的油渍或者水分。

虽然已经来青岛十多年了,但是开始吃脂渣却是近两年的事情,一些刻板印象觉得这个东西似乎也没有那么吸引人,或者准确点的说是自己特别想吃这个东西。当然还有一个原因就是这个东西的价格的确不便宜,纯瘦肉的一般 100 多一斤,肥瘦相间的七八十。

直到前年去十五大街附近给宝子拍写真,去那边的市场逛了一圈,看到了 在 qtv 上出现过的那个摊位,于是买了一些。后来加了微信,陆陆续续又买了不少。

最近开始服用药物之后,食欲或者说胃口又变了不少,感觉吃东西也吃不多了。

之前点外卖,总觉得有吃不饱,而现在点外卖其实对于量已经不在关注了。稍微吃点就足够了,为了能够稍微调整下饮食,于是又买了两包脂渣。主要是平时肉类也的确不怎么吃,或者说量没有那么大。

中午依然是蔬菜沙拉加一片牛排,刚好买的脂渣也送到了。可以稍微搭配一下。

吃药最大的好处,可能也在于能准确的控制热量摄入了。对于吃似乎没那么有追求了,至于好吃或者不好吃,自然就没那么关注了。

吃,自然是该尝试的还要尝试,不能那么无知,哪怕遇到不会吃的东西多吃几次也就了然了。

不过上网这件事情,的确无时不在刷新着自己的认知,学习速度是真的跟不上了。

之前只是知道“留子”是留学生,结果最近有看到个词“去父留子”。搜了一下才发现,“去父留子”也去了好几年了。这新词不理解我就认了,连记忆里的东西都变了,司马光砸瓮,一qi红尘妃子笑……脑子里也仅剩呵呵二字了。

现在是对于吃的一无所知;

对于技术毫无长进;

对于新词汇一窍不通;

甚至对于自己的记忆,都模棱两可。

The post 无知 appeared first on obaby@mars.

  •  

Hopper Disassembler for macOS 5.18.1

✇obaby
作者obaby

之前为了看文件依赖下载了一个Hopper 试用版,没想到现在5 都有破解版了。

长江后浪推前浪,前浪死在沙滩上。

截图:

下载地址:

https://www.123912.com/s/ucY7Vv-woAAA?提取码:jBYh

https://www.123865.com/s/ucY7Vv-woAAA?提取码:jBYh

The post Hopper Disassembler for macOS 5.18.1 appeared first on obaby@mars.

  •  

Ida Pro9

✇obaby
作者obaby

好长时间不曾登录那几个破解论坛了,前几天心血来潮登录了一下。看到 ida 更新了,已经到了 9,之前自己下载的上一个版本还是 8.3,中间 8.5 出现的时候竟然没下载过。

这曾经是自己赖以谋生的技能,也是自己的工作。只是后来一步一步的调整,竟然离逆向分析越来越远了,前段时间还有猎头找到自己,表示有广东的岗位,薪资待遇感觉还不错,不过查了一下是初创公司。

现在这个年龄似乎也承受不住太多的变化了,前途毕竟没有那么明亮。大公司不好过,小公司更不好过,而至于初创公司这就更难了。随便拉一个文件进去,选择解释器的界面变了,其他的感觉还是原来的样子。十几年过去了,似乎什么都变了,似乎有什么都没变。

曾经也曾在这样的代码里挣扎求生,只是,现在换了个地方挣扎。

版本号也到了 9.1,这么多年,自己竟然就用过一次正版授权。不是不想买,而是的确有些贵,并且现在也不是自己赖以生存的工具,所以就酱紫吧。

分享个磁力链接,需要自取:

magnet:?xt=urn:btih:f24cfadb8a66b343bf1ff4f0c1386a5f6991c818&dn=ida91

当然,也不一定非得从这里下,那些大的论坛都有下载地址,各种网盘的地址。

现在网盘越来越多了,反而越来也不喜欢网盘了。

毕竟不可能每个网盘都冲个会员,毕竟不充会员那 100k 的下载速度下载这 3G 多的文件得需要好几天。

切!

 

The post Ida Pro9 appeared first on obaby@mars.

  •  

母亲节,病,以及其他

✇obaby
作者obaby

今年感觉没怎么买过电子设备,主要的原因也在于确实是没什么需求。之前在《拼多多?》一文提到定位的问题的时候, zerunszeruns  提到可以买个华为 Tag 放到车里定位。

于是,在对比了一下 apple 的 air tag 和华为的产品之后,感觉华为的更便宜,于是一次性入手了四个,整体体验感觉也还可以。

自然,不管是华为 tag 还是苹果的air tag,整体的定位功能实现方式都差不多。所以自然周边没有华为或者苹果手机的时候,这个定位就延迟了。

好处自然也是比较明显的,在大白上放了一个之后,停车再也不用记录停车位置了。尤其是出去玩,露天停车场不一定原路返回的情况下。

有时候感觉总是有一些魔咒难以打破,有时候不买东西感觉一年半载都不会买,一旦开始买了之后就发现少了很多东西,还得继续买。

出去玩的时候还发现了另外一个问题,那就是给电子设备充电,目前需要充电的设备包括,自己的两块手机,华为 wathch gt(待机时间还行,7 天以内不需要带充电器),对象的两块手机,apple watch(每天一充),宝子的儿童手表。这么一算,至少需要五个充电口才能满足。所以每次出去,哪怕用三头的线也至少需要带三个充电器的头(p70 的快充头需要单独带),就很烦。

家里买了两个品胜的充电口,一个在书房,一个在客厅。为了以后不再带那么多的线,于是又买了个支持快充的品胜充电器。

mbp 更新系统失败,看了下发现磁盘空间满了,于是又买了块移动硬盘。

买这个东西的时候,最开始买错了,买了个 usb 口的,退了之后又换了这款。第二天鼠标又坏了,于是又买了块鼠标:

就这样乱七八糟又买了一大堆东西,现在的鼠标真的一言难尽,坏的还有点快,就这鼠标光申请售后已经申请了无数次了,不单是罗技的,雷柏的也一样。

杜郎dujun在之前的文章评论说我沉迷了。其实说不上沉迷,见到一个东西,或者接触一个东西的时候,我总是想知道他是怎么运行的,为什么这么操作不行,或者说为什么操作会失败,我可以不用,但是不能不行。当然,这比杜郎说的更严重,其实是一种病吧,那种撞了南墙不回头的精神,多少是有些魔怔了。而有时候对于一些东西也是一样的,我可以不用,但是不能没有。

虽然有时候它没有那么有用。

上周,眼看到母亲节了,对象说给宝子的姥姥买个新的手环,之前的手环已经不太好了。搜来搜去,为了保证周五能送到,又选了一款华为的手环。

为此,宝子还专门又写了一张贺卡。宝子的姥姥拿到之后自然异常开心,我帮忙把手环配对好之后,她说:“原来的还能用,干嘛要换新的呢?”

“新的都到了,就不要戴那个旧的了”我说,顺便就帮她把新的手环戴在了手上。不管怎么着,最起码也能开心几分钟把。

最近开始吃药之后,感觉饭量明显也降了很多,周日中午提议去吃铁锅炖,我说我就吃两口。结果吃饭的时候宝子开始叨叨:“你不说就吃两口吗,这都不止两口了”

“这是我的第二口”我一边说,一边去捞鱼头,“我的第一口是那个鱼尾巴”

其实中午也就吃了这两块,再加上一点点青菜。吃完饭回家的时候,对象去买水果了,我跟宝子先去车上,回到车上给老太太打电话,又是打了半天都没打通,最后实在没办法打了二姐的电话,才知道她们在吃饭,老太太的手机肯定是又调成静音了,上午打了无数个电话都没人接。

似乎姐姐们都回去了,老太太在喝酒,这个岁数了其实也管不上什么高血压不高血压了,偶尔喝点就喝点吧。说自己有时候经常失眠,也不知道想什么。

至于这个问题,自己没办法也解决不了。毕竟也没那么多的精力关注这么多的事情。周一有个项目要演示,周末其实除了折腾那个换脸工具,另外的时间就是改 bug,当然,还有莫名其妙新加出来的需求。

可能是最近由于用脑过渡吧,晚上有时候就特别困,其实下午就特别困,躺在床上一会儿就睡着了。身体状态可能也的确不行啊,这一堆的毛病。对象说她们体检和我们是一样的医院,结果她们办公室的一群人一块去体检,一辆车过去的,五个人血脂都有问题。到这里我就开始怀疑医院的设备有问题了,当然,这个还不能确定,所以,只能等体重再降低点,可能再减个十斤左右,去齐鲁医院做个系统的检查。

这些体检医院或者说医院的体检科,整体水平都一般,到医院去复诊的时候,难免还是得再开单子。我说,我准备去齐鲁的老年病科给我自己挂个号体检一下。毕竟老年科基本不需要排队,而很多病现在显示的都是中老年才有的症状,这,我还年纪轻轻就老年了?

50 岁以上才 30%,问题是我 tm 还没到 50 呢,就离谱。

周末的温度,竟然又降到了十度左右,这不单入春失败了,入夏也失败了,就离谱。但是这个有什么办法呢,似乎也没什么办法,于是在这个寂寥而又寒冷的夜里,我打开了空调的暖风。

 

The post 母亲节,病,以及其他 appeared first on obaby@mars.

  •  

FaceFusion 3.2.0 — 进阶体验(不要瑟瑟)

✇obaby
作者obaby

书接前文,如果要处理普通的视频资源,根据上一篇文章的内容完全就足够了。

但是,如果你想处理点多少有点暴露或者纯粹的瑟瑟内容,你是怎么都进行不下去滴。

启动脚本:补上一个快速启动face fusion的ps代码,保存为ps1,相关路径改成自己的,启动的时候直接拖到powershell里面执行即可。

conda deactivate
conda init
conda activate facefusion
cd  E:\facefusion3\facefusion
python facefusion.py run --open-browsers

现在来说下瑟瑟的问题,一般这时候会卡在分析完成的地方:

analysing:100%之后就没动静了,原因在于视频的分析完成之后发现你的视频有瑟瑟内容,而至于瑟瑟内容的检测是通过content_analyser.py中的detect_nsfw方法实现的,如下(这个是我改完的):

def detect_nsfw(vision_frame : VisionFrame) -> List[Score]:
    nsfw_scores = []
    model_size = get_model_options().get('size')
    temp_vision_frame = fit_frame(vision_frame, model_size)
    detect_vision_frame = prepare_detect_frame(temp_vision_frame)
    detection = forward(detect_vision_frame)
    detection = numpy.squeeze(detection).T
    nsfw_scores_raw = numpy.amax(detection[:, 4:], axis = 1)
    keep_indices = numpy.where(nsfw_scores_raw > 1.0)[0]

    if numpy.any(keep_indices):
        nsfw_scores_raw = nsfw_scores_raw[keep_indices]
        nsfw_scores = nsfw_scores_raw.ravel().tolist()

    return nsfw_scores

主要就是下面这一行,关于nsfw置信度的问题,原来是0.2 直接改到1.0就行了,毕竟,置信度不会超过1

keep_indices = numpy.where(nsfw_scores_raw > 1.0)[0]

重启进程,再次运行:

现在就会继续往下进行了。

视频资源文件导致的异常:最后来说下视频文件异常导致的崩溃,对于一些文件可能会出现下面的错误

Analysing:  95%|====================================================   | 3625/3800 [00:19<00:00, 189.67frame/s, rate=0]
Traceback (most recent call last):
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\queueing.py", line 625, in process_events
    response = await route_utils.call_process_api(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\route_utils.py", line 322, in call_process_api
    output = await app.get_blocks().process_api(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\blocks.py", line 2146, in process_api
    result = await self.call_function(
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\blocks.py", line 1664, in call_function
    prediction = await anyio.to_thread.run_sync(  # type: ignore
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\anyio\to_thread.py", line 56, in run_sync
    return await get_async_backend().run_sync_in_worker_thread(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\anyio\_backends\_asyncio.py", line 2470, in run_sync_in_worker_thread
    return await future
           ^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\anyio\_backends\_asyncio.py", line 967, in run
    result = context.run(func, *args)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\utils.py", line 884, in wrapper
    response = f(*args, **kwargs)
               ^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\uis\components\instant_runner.py", line 82, in run
    create_and_run_job(step_args)
  File "E:\facefusion3\facefusion\facefusion\uis\components\instant_runner.py", line 97, in create_and_run_job
    return job_manager.create_job(job_id) and job_manager.add_step(job_id, step_args) and job_manager.submit_job(job_id) and job_runner.run_job(job_id, process_step)
                                                                                                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\jobs\job_runner.py", line 11, in run_job
    if run_steps(job_id, process_step) and finalize_steps(job_id):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\jobs\job_runner.py", line 72, in run_steps
    if not run_step(job_id, index, step, process_step):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\jobs\job_runner.py", line 58, in run_step
    if job_manager.set_step_status(job_id, step_index, 'started') and process_step(job_id, step_index, step_args):
                                                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\core.py", line 323, in process_step
    error_code = conditional_process()
                 ^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\core.py", line 340, in conditional_process
    return process_video(start_time)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\core.py", line 418, in process_video
    if analyse_video(state_manager.get_item('target_path'), trim_frame_start, trim_frame_end):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\content_analyser.py", line 102, in analyse_video
    if analyse_frame(vision_frame):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\content_analyser.py", line 77, in analyse_frame
    nsfw_scores = detect_nsfw(vision_frame)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\content_analyser.py", line 115, in detect_nsfw
    temp_vision_frame = fit_frame(vision_frame, model_size)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\vision.py", line 243, in fit_frame
    height, width = vision_frame.shape[:2]
                    ^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'shape'

这些问题还是处在content_analyser.py 问题在于对vision_frame 为None的帧进行检测,导致检测进程崩了,这里提前判断下是否为空,当然,更直接的办法是直接全部返回False 禁用nsfw检测。

参考下面的方法修改代码即可。

def analyse_frame(vision_frame : VisionFrame) -> bool:
    if vision_frame is None:
        return False
    nsfw_scores = detect_nsfw(vision_frame)

    return len(nsfw_scores) > 0

好啦,最后来看看小视频吧:

如果用姐姐我的照片换脸视频了,换好的视频记得给我发一份,嘻嘻

The post FaceFusion 3.2.0 — 进阶体验(不要瑟瑟) appeared first on obaby@mars.

  •  

FaceFusion 3.2.0 — 免费AI换脸工具初体验

✇obaby
作者obaby

‌FaceFusion‌是一款功能强大的AI换脸软件,支持图片、视频和直播的换脸功能,其换脸效果真实、自然。FaceFusion不仅支持N卡处理程序(如Azure),还提供了CPU处理模式,适合各种硬件配置的用户使用‌。

功能特点:
  1. ‌多平台兼容‌:支持NVIDIA和AMD等主流显卡平台,满足不同用户的硬件需求‌。
  2. ‌多种处理模式‌:提供人脸替换、人脸高清修复和背景高清修复等多种策略,每种策略下包含多个模型可自由切换‌。
  3. ‌自定义设置‌:用户可以自定义执行线程、执行队列、最大内存和输出路径,电脑配置好的情况下可以适当调大这些参数‌。
  4. ‌预览功能‌:提供预览功能,可以自由选择换脸对象和多人换脸,单人换脸通过方位选择人脸、年龄选择人脸、以及性别选择人脸‌。
  5. ‌唇形同步‌:引入wave2lip处理器,同步口型动作,使视频更加自然‌。
  6. ‌面部对齐改进‌:通过68比5的地标变换,提高面部对齐的精确度‌。
  7. ‌新模型支持‌:增加uniface_256模型,提供更高质量的换脸选项;集成yoloface作为默认的人脸检测器模型,提升检测效率‌。

换脸这个东西,起之前也尝试过faceswap,然而,这个东西使用起来的确麻烦,需要提供的素材数量比较多,训练过程比较繁琐,并且最终的效果在样本数量不够大的时候就会发现实际效果一般:

让自己变成AV的主角【faceswap】

当然,图片换脸目前腾讯元宝提供了免费的传图换脸的功能,整体效果还是挺不错的。下面的是基于腾讯元宝来实现的:

不过,对于视频换脸的免费工具(手机上可用的),目前我还没发现,前端时间看到这个东西,于是尝试了一下。网上有个facefusion2.6.1压缩包版本,实际下载安装后,效果一般,由于缺少文件会导致无法运行,如果要使用的话,复制一个png文件,放到facefusion2.6.1\dependency\res目录下,命名为main.png即可。在运行过程中会显示这个图片。

之前还下载了一个DeepFaceLab_NVIDIA_RTX3000_series,这个东西要用比较麻烦的一点在于,只能靠命令一条一条的执行,看着目录下的文件就感觉头大了。

最终还是决定尝试下FaceFusion的最新版本,安装可以参考这个链接:Installation | FaceFusion

最简单方法,访问这个链接购买安装包:Windows Installer 20美元,解决了后面全部的问题。如果不想花20,那么继续往下看。

具体的步骤简单概述,以win11为准:

1.安装git conda ffmpeg

GIT
winget install -e --id Git.Git
Conda
winget install -e --id Anaconda.Miniconda3 --override "/AddToPath=1"
FFmpeg
winget install -e --id Gyan.FFmpeg

conda安装之后配置环境路径,添加到path下:

重启powershell继续后面的操作。

2.准备conda环境:

Initialize conda for your terminal:
conda init --all
Create the environment:
conda create --name facefusion python=3.12 pip=25.0
Activate the environment:
conda activate facefusion

3.安装gpu支持,我只有conda:

conda install conda-forge::cuda-runtime=12.8.1 conda-forge::cudnn=9.8.0.87

4.下载代码

git clone https://gitee.com/facefusion/facefusion
cd facefusion

5.安装(cuda)

python install.py --onnxruntime cuda

6.重新激活环境:

conda deactivate
conda activate facefusion

7.运行系统:

python facefusion.py run --open-browser

启动之后会自动下载各种特征文件,如果下载不动就多试几次,或者尝试挂个全局代理

现在就可以尝试换脸了:

按图选择模块进行换脸,此时就完成之后就可以下载了:

实际效果:

另外一个视频520AM:

实际效果就是这样的,不过安装的时候需要注意用户名路径不要存在中文。另外,这个东西貌似处理a片有问题,会报下面的错误,感觉还是视频文件问题,谁知道怎么解决的还望不吝赐教:

Traceback (most recent call last):
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\queueing.py", line 625, in process_events
    response = await route_utils.call_process_api(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\route_utils.py", line 322, in call_process_api
    output = await app.get_blocks().process_api(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\blocks.py", line 2146, in process_api
    result = await self.call_function(
             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\blocks.py", line 1664, in call_function
    prediction = await anyio.to_thread.run_sync(  # type: ignore
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\anyio\to_thread.py", line 56, in run_sync
    return await get_async_backend().run_sync_in_worker_thread(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\anyio\_backends\_asyncio.py", line 2470, in run_sync_in_worker_thread
    return await future
           ^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\anyio\_backends\_asyncio.py", line 967, in run
    result = context.run(func, *args)
             ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\obaby\.conda\envs\facefusion\Lib\site-packages\gradio\utils.py", line 884, in wrapper
    response = f(*args, **kwargs)
               ^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\uis\components\job_runner.py", line 91, in run
    if job_id and job_runner.run_job(job_id, process_step):
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\jobs\job_runner.py", line 11, in run_job
    if run_steps(job_id, process_step) and finalize_steps(job_id):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\jobs\job_runner.py", line 72, in run_steps
    if not run_step(job_id, index, step, process_step):
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\jobs\job_runner.py", line 58, in run_step
    if job_manager.set_step_status(job_id, step_index, 'started') and process_step(job_id, step_index, step_args):
                                                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\core.py", line 323, in process_step
    error_code = conditional_process()
                 ^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\core.py", line 340, in conditional_process
    return process_video(start_time)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\core.py", line 418, in process_video
    if analyse_video(state_manager.get_item('target_path'), trim_frame_start, trim_frame_end):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\content_analyser.py", line 102, in analyse_video
    if analyse_frame(vision_frame):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\content_analyser.py", line 77, in analyse_frame
    nsfw_scores = detect_nsfw(vision_frame)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\content_analyser.py", line 115, in detect_nsfw
    temp_vision_frame = fit_frame(vision_frame, model_size)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "E:\facefusion3\facefusion\facefusion\vision.py", line 243, in fit_frame
    height, width = vision_frame.shape[:2]
                    ^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'shape'

 

系统默认缓存目录为:

C:\Users\obaby\AppData\Local\Temp\facefusion

要修改这个路径,修改 facefusion.ini文件:

[paths]
temp_path = ./temp
jobs_path =
source_paths =
target_path =
output_path =

修改完重启即可:

2.6.1版本安装包:

https://www.123pan.com/s/5DsaTd-5YGc.html

链接:https://pan.quark.cn/s/11bffd2d5993
提取码:ankQ

The post FaceFusion 3.2.0 — 免费AI换脸工具初体验 appeared first on obaby@mars.

  •  

✇obaby
作者obaby

“包好!小栓——你不要这么咳。包好!”  — 《药》

五一假期,提前两天就回来了,想着也是如果宝子有落下的作业,再补补作业。不过,这一旦有了时间,宝子的姥姥就开始准备做吃的,主要也就是两种,一种是水饺,另外一种是馅饼。

听起来,似乎温馨充满了整个房间,到处都洋溢着幸福的味道。

其实,有时候我挺不喜欢他们这种所谓的自残式的的付出的,看着让人心惊胆颤。出去玩的这几天没怎么控制饮食,所以导致体重又上升了好几斤。回来之后感觉单纯的靠锻炼已经没有什么效果了,于是开始服用药物。

所以对于吃什么的,自己并没有太多的想法。想着只要宝子能吃好就行了,自己吃不吃倒是也无所谓。尽管如此,还是不想他们大费周章的去做什么。

不过宝子的姥姥姥爷既然已经决定动手了,自己也不好再说什么。姥姥给的理由是他们俩给宝子的小姨看孩子,平时不在这里,做什么吃的我们也吃不上,觉得不公平,对不起我们。宝子的小姨自己一个人在家带孩子,可以中午过来吃。

这一切听起来似乎也没什么问题,然而,在干了不到一半的时候。宝子的姥爷突然说,你妈肩膀疼,你们找时间去跟她检查检查。

“啥时候肩膀疼了?”我问了一句。

就这一句惹了祸了,姥姥瞬间提高了声调:“我肩膀疼和你们说了多少次了?!也没个人和我去看看”

听到这句话我瞬间懵了,这从来我都没听到过啊。对象说,可能是和双双(宝子的小姨)说的。

“恁妈这肩膀疼了很长时间了,……”宝子的姥爷开始解释。

有的时候真的挺佩服她的,自己肩膀疼,一方面说自己不想麻烦孩子,但是自己也不去看,就是懒得出门,一旦有点什么事情就不想露头,什么都想让被人代劳。然而,不明说,还要到处阴阳别人。本来一句话的事情,就说自己要去看看肩膀,但凡这么说了,请假都陪她去。然而,他这个疼也是天天吆喝,狼来了的故事一次次上映,也不知道她是真疼还是假疼。

终于水饺煮好了,让过去吃饭。我怕烫,都是等凉凉了才过去吃。宝子、小姨、宝子的弟弟先去吃。

吃饭的时候,看到姥姥去逗弟弟,问:“姥姥能不能挨着你啊?”

“不行”弟弟说道,一边说,一边往边上推她。

“恁这些白眼狼,都是白眼狼”看着宝子说道,“你是大狼”,又指着弟弟说道,“这是小狼!”说完就气呼呼的走了。

吃完饭,大家开始商量着挂号,检查的事情。放假有的科室不上班,想着等上班了去挂号检查。于是就这么简单的决定了。

下午吃完饭,他们都走了,姥姥姥爷跟着小姨带着孩子一块走了,毕竟还得给他们看孩子。

晚上宝子做作业,我在看电视,这时候大门开了。姥姥回来了,气鼓鼓的样子。问了下才知道,原来小姨下午回去之后也肩膀疼,然后就去拿药了。左等右等不见回来,终于过了一个多小时回来了,原来是在社区医院做了针灸。

这下算是捅了篓子了,自己肩膀疼没人管,自己肩膀疼就知道马上去医院看,去医院针灸拿药。这大公主哪受得了这刺激,直接气鼓鼓的跑回来了。看那个表情,也的确不想搭理她,爱咋咋地吧。

周二晚上,下班一起往回走的时候,宝子的小姨说:“老妈以为我今天是生日,说什么孩生日,娘苦日。逼着老爸给她煮了碗面条,还加了个荷包蛋吃了。但是问题是我是明天生日啊,以为我过生日,结果什么都没提。” 医院的预约也排上了,系统检查需要住院,大概得两三天,她说为了能让她好好水饺,预定的单间。呵,不得不说,这待遇真是牛逼到顶了。

昨天晚上,下班凑到一起,宝子的小姨又说了:“老妈今天又以我过生日的名义,让老爸出去给她买的鱼吃的。撒娇女人真好命”

这概括也的确准确啊,他们这些人,把养育子女当成了投资,所以一旦自己的孩子,少有不顺心,就开始各种 pua。白眼狼,白养了,各种话就开始絮叨不停。动不动就是为了你们怎么怎么着,问题是我不需要你为了我们。如果觉得自己的孩子是负担,那就别要孩子。那种苦难式的的付出,没困难制造困难也要表达他们的付出的,看着就累。也无法共情。

电视上,那些通情达理的父母果然都是别人的,而至于自己的父母,除了 pua 就是 cpu。哪有这么所谓的同理心,在他们看来孩子不过是自己的附属品,他们有绝对的权威。没有公主命,却得了公主病。带她出去玩也罢,出去吃饭也罢。都得不断的照顾她的喜好,这不吃,那不吃,这不行,那不行。即使满足了这一切,稍微哪天自己做的不到位,之前的种种新仇旧恨就一起翻出来,开始各种批斗。仿佛她们现在的这种境遇都是我们造成的。

即使是按照他们的投资逻辑来看自己的子女,他们的投入有多少?他们又给了什么?这一切都是我们奋斗来的,而不是他们给予的。说道给与,不过是给了条命,仅此而已。父母有恩吗?父母无恩,养育自己的子女那是义务,子女扶养父母,也是义务。是该做的,他们对我无恩,我对宝子也无恩,这是我的孩子,我对她好,只是因为我爱她,而不是我对她有什么所谓的恩情,也不希望她将来会为了报恩来委屈自己。

体检报告全部出来了,各种异常情况比之前又多了很多。建议还是减肥,低盐低脂饮食等等,这一切似乎也无药可医,至于他们的心病,我也无药可施。

昨天下午同事过来说,公司又要裁员了。现在的这些年轻人,之前是他们说的扶不起的 80 后,现在这些 80 后门承担的压力超过了之前他们所有的人,他们的压力和心病又有什么药能治疗?除了要承担工作,生活的压力,还要不断的应付父母,忍受他们的 pua,这又有什么药能治疗?

“包好”,这人血馒头治疗不了小栓的咳嗽,这世间的药,也解不了生活的疾苦。

The post appeared first on obaby@mars.

  •  

窦娥,冤

✇obaby
作者obaby

假期还是如约而至了,晚上本来想早点休息的,毕竟早上两点就要爬起来出发了。

但是,洗刷完,却怎么也睡不着了。终于十一点多 迷迷糊糊睡着了,夜里起来上一趟厕所,看了下时间,差几分钟不到一点。

256块钱的早餐

宝子的小姨凌晨多一点点在群里发了一条消息说到家了,她们带着孩子,晚上八点就走了,两百多公里的路程,走了四个多小时,也在意料之内。从晚上就不断的关注路况,生怕堵车,高速的拥堵状况直到睡觉前,还有很多地方是大段的红色。

醒来之后再也睡不着了,打开地图看下路况,仅剩下往京沪高速拐的地方有一点红色,等跑到那里应该也就差不多了。窗外传来雨滴打在玻璃上的声音,躺在床上刷了会儿手机,一点半把他们叫起来,穿好衣服就准备出门了。

提前下去把车开到楼下,这样一来可以不用拖着行李走太多的路程,第二也免得那么多人被雨打湿衣服。

一路上相对来说还算顺畅,不过在到了临沂之后,导航提示出高速更快,然而,出高速之后导航的长深。在出发之前已经做过功课,长身高速修路,整体限速80,所以段然是不可以走的。出了高速,重新导航,折返一段,最终还是走的京沪。比较幸运的是一路上基本没什么突发状况,除了有一辆车翻在路中间,黑乎乎的看不清楚,其他的还是比较畅通的。

出发早的好处,在此刻也就体现出来了,路上没什么人,并且服务区也没什么人。

最终,终于在三个小时之后,天亮了。此时对象可以开车了,换自己休息下。不过,这临沂境内的大雾,真的浓重。可见度异常的低,只能降低车速行驶。

到处弥漫着浓重的雾气,这些雾气,让远处的风景变得不可捉摸。

五个小时候,总算是到了淮安。

到目的地之后,第一件事情,自然是找地方吃早饭啦。大众点评,找个最近的饭店,路上经过一座塔,后来折返回来之后才发现这座塔叫做爱心塔。

在饭店门口停车的时候,开过来一辆宾利。

“哇哦,咱们跟开宾利的吃一样儿的餐厅哎”我说。

“会不会吃不起啊?”对象问。

“不至于,不至于。开宾利的都吃,可能真的不错”我说完,就带着他们往里走。

在前台点餐,给个定位器,给张单子,就可以进去随便找地方做了。虽然已经快八点,不过吃饭的人还真的不少。

当然,最贵的还是蟹黄汤包,38一个。

给了一根吸管,用来喝汤。整体来说味道还算不错,但是没有那么惊艳,可能是没有多少蟹黄的原因吧。

一桌子菜,对于早餐来说确实多了点,不过,这一顿吃完,下一顿饭可能就得很晚啦。

 

窦娥,冤

吃完饭去民宿,安置好,稍微休息一下。不过,这民宿也是第一次见到没有电视的民宿,只有个电视机,古老的panda品牌,没有机顶盒,也没什么其他的播放设备,就这么孤零零的一个电视。的确有点匪夷所思。

中午简单的游览下,最终选择了清晏园。一个不大的园林,周边有两个停车场。然而到达目的地之后,却发现都满了,围着院子转了一圈,终于在门口又等到了机会,有车出来才好歹停了进去。不大的园林,好处是人不多,适合溜达,自然也不需要导游讲解。

一天的时间,过得还是很快,毕竟这一天的时间,还有很多实在路上消耗的。

第二天的行程自然也简单,主要目标是周恩来纪念馆。驱车前往导航的停车场,快到目的地的时候发现前方异常拥堵,直接右拐,找了个停车场停了进去。步行围绕纪念馆前面的湖转半圈即可到达,入口处的喇叭在广播持身份证入场,不过在实际进入的时候也没有查验。

主楼仅有雕塑能看,其他的地方都不开放。

沿着边上继续参观的牌子前进,竟然直接出来了。往后绕直仿照总理之前办公场所一比一复刻的办公室。

纪念馆人潮涌动,被裹挟着一路向前。从纪念馆出来之后,刚好可以从另外一侧的出口绕出去,沿着河岸继续前进,走不远就到了淮安府署,另外一个景点。

而至于,淮安府署。门票60一张,的确不便宜,然而,这个地方确满足了自己对于古代那些刑具的好奇心,与我而言的确是物超所值。淮安府署属于国内唯二保存完整的府衙结构之一,在刑狱一块的完整程度的确比较高。在其他地方不曾见过这些东西,之前的了解也仅限于书上,或者影视作品,或者之前看《世界酷刑简史》里面的一些图片。

而窦娥,也是在这个地方被斩首处死的。《窦娥冤》的故事大家应该都听过,讲的是:

是元代戏曲家 关汉卿 创作的杂剧,刊行于明万历十年(公元1582年),故事原型来自《列女传》中的《东海孝妇》。 全剧四折一楔子,写弱小寡妇 窦娥,在无赖 张驴儿 陷害、昏官 桃杌 毒打下,屈打成招,成为杀人凶手,被判斩首示众。 临刑前,满腔悲愤的窦娥许下三桩誓愿:血溅 白练,六月飞雪,大旱三年。 果然,窦娥冤屈感天动地,三桩誓愿一一实现。

这是语文课本中的内容,其实,后面还有,那就是:

刽子手行刑后。窦娥的鲜血竟然一滴都没有落在地上,全部飞溅在了高挂的白布上。当时围观的百姓暗自称奇。紧接着天地变色,狂风大作,天空飘起鹅毛大雪,密密地覆盖在窦娥的身上。那时候正是六月夏天,每一个在场的人都惊呼:“这窦娥真是冤枉的!”
接下来,楚州果真大旱了三年。所有人都相信窦娥的冤屈,为窦娥抱不平,直到窦娥的父亲窦天章在京城做官返乡,窦娥的冤案才得到昭雪,杀人凶手张驴儿被处以死刑,贪官知府也得到了应有的惩罚。

而至于窦娥冤,这罪魁祸首,自然是她的父亲,为了凑盘缠考取功名,把自己的女儿卖了!

刑科外面现在还矗立着腰斩台,用来实行腰斩的钺依然高悬。

只是不知道这死在腰斩台台上的人,有多少罪大恶极,又有多少含冤负恨。用来控制钺升起的绞绳已经被锁死,自然无法再拉上去,或者放下来。另外一侧没有锁死,可以自由的转动,但是不会有任何实质性的变化。

本来,想着这淮安府署也不过如此,自己坐在院子里耍手机,对象带着宝子去溜达,过了回来对象回来很神秘的跟我说:“你该去看看刑科的东西,应该是你感兴趣的”

听到这句话,我蹭的一声蹦起来,这自然是要去看的。腰斩台边上叫做皮场庙,这个庙自然是用来剥皮的。

既然是针对贪官剥皮,在古代自然都是男性,不得不说,这里面的还原度是真的不错,连阴茎的皮都给呈现了出来,自然,这种活在古代也是一种手艺。剥皮自然要完整,不过这个东西无法申请非遗,毕竟现在申请了,那些贪官也不能拿来真给剥皮。

初次之外,看到了另外一个刑具,那就是木驴。

这个刑具,其实并不是单纯给女性设计的。男性一样适用,毕竟男人下面也有个洞不是吗?看着这粗大的棍子,就该猜到插到下体里面是什么感觉了。《窦娥冤》最后一折,就有关于张驴儿骑木驴的判罚:

你这一行人,听我下断:张驴儿毒杀亲爷,谋占寡妇,合拟凌迟,押付市曹中,钉上木驴,剐一百二十刀处死。

钉上木驴,自然就是汤张驴儿直接骑到这个东西上面。之后就是凌迟处死,不过这个凌迟的判罚并不算重,一百二十刀而已,如果真的判千刀万剐,一千刀,那自然是要割完一千刀才能死。对于这种行刑,自然也是需要深厚功力,申请非遗恐怕也找不到师傅了。这凌迟要从身体最不重要的部位开始,比如乳头,耳朵,鼻子,嘴唇,屁股,大腿等等。一步一步的向中间靠近,据传最厉害的刽子手,在快一千刀割完的时候,胸部只剩下一层薄膜,透过肋骨能看到跳动的心脏。

而这最后一刀就是捅进心脏里。

而至于这其他的刑法,不管是世界酷刑描述的多么厉害,其实与中国古代的酷刑相比,并没多先进,人性使然,所以折磨人的方法也自然大同小异。

当然,也有专门区分性别的刑法,例如“乳夹”,这个要夹男人的话可能就有些难度了。

不过有个屋子做了各种行刑的动画,当然,我最想看的木驴和凌迟没有,有点遗憾。

只是,这窦娥,的确死的冤!

 

金湖水上森林,我比窦娥还冤

第三天,选了个稍微远点的地方,金湖水上森林,这个地方在大众点评上的评价两极分化。不过来都来了,去看看也行,驱车前往一百公里,的确有点远,不过好在路上还是比较畅通的。

买票的时候本来想就买个门票,剩下的就算了,对象表示买套票,最后买了110的套票。进门就遇到了第一个障碍,等摆渡车的人排队都要排到大门外了。

只好先去做竹排,沿着最美步道,一路前行。步道的美景真的不错。

那种原始森林的感觉,让人心旷神怡。然而在步行了差不多一公里到了竹排的地方,发现排队的都要排到大路上了。

带拐弯的队伍,并且队伍又粗又长,看到这种队伍就真的让人挺反感的。主要问题还是工作人员的不作为,警戒线就在拐弯的地方有一点点。终于到了拐弯的地方,还出现一堆插队的,跟维持秩序的工作人员说,我们是一起的,于是一下子塞进来五六个人。工作人员装死,啥都没看到,自己也没必要说什么了。更神奇的是,塞进来的那些都没买船票,问题船票那里买的,这就尼玛离谱。

又让我想到了昨天中午吃饭排队等号的情景,看所有的餐厅都在排队,唯有一家浦园中桌和大桌没有排队的,于是选了他们。到了之后,取号也很方便,给了个b09。说现在叫号的是06,需要等三桌。然而,等了半个小时了,一个号都没叫,问什么情况,说要等。

终于,快一个小时了,再问说现在是按照等待时间叫号,跟小卓一起的,中桌是加的凳子,真是心里一万个草泥马奔过。

但是a都从a40叫到a60了,实在忍不住去看叫号机。这时候刚要叫a61,实在忍不住了问服务员:“刚才就说下一桌,现在下一桌怎么又要61了?!”

“再下一桌就是你们了”服务员说答到。

“你们不是按照等待时长吗?我们等了69分钟了,a61只有60分钟,凭什么先叫61?”我继续问。

“这……”服务员吞吞吐吐,“你们先来吧”

就这菜算吃上饭,而此时已经都两点了,等号的人就还我们几个。吃饭的都吃完走了,这体验真的只能说是垃圾!

现在又来这么一出,我也不管队伍粗不粗了,拉着宝子往前挤,我可去你妈的吧。就这样,我一个人开路,拉着四个人往里挤,把那几个给挤到了后面。

半个小时一个项目,等下一个项目小火车,这次要好很多,因为有围栏,只能一个一个往前走。排进去,等了十几分钟不动,这时候有人说小火车半小时一趟。我数了数前面的人,大约得最少一个小时。这时候有人等不了了,直接跳出去或者逆行出去了,就这样等了半小时,等没了二十几个人。不过好处是,感觉等一个小时就能坐上了。

鉴于这种秩序,我要然还是拉着宝子尽量往前走。小火车拉着人走的时候我说:“这也太慢了,蛆guyong的都比他快!”

“别瞎说,比蛆快”我对象说,“等你上车了就不嫌他慢了,慢悠悠多好”

这体验,不说要求有多高,最起码把秩序维护好吧。

下了小火车,宝子的姥姥姥爷死活不往回走了。于是带着宝子去萌宠乐园等其他的地方玩,说实话,真的啥都没有。风筝广场,就他们自己放的三四个风筝。

只看到尼玛跟葫芦娃一起上天了。

玻璃栈道上都是人,下滑的速度太慢了,本来还想跟宝子去的,后来看这个速度直接放弃了。

这景区,感觉真的不值这个价格,如果评价的话,五星能得一星,其中0.9星是给那个最美步行道的。

而至于这最美味卫生间(我给他起的名字,其实叫最美卫生间),真的不知道有啥卵用。

出园的时候发现小火车排满了,而这么多人,没四五个小时根本排不上。来这地方,果然是妥妥大冤种。

 

接着奏乐,接着舞

晚上的夜市,感觉更有意思。

古城,国内的古城都大同小异,看不出太多的区别。

第二天大学城的夜市人头攒动。

天还没黑就已经很多人了。

逛了会儿,发现卖香水的,于是69买了一只香水。

然后买了半只兔兔吃掉了。

最后一晚,来都来了,怎么也得去最热闹的地方看看。九点多出门,自己住的地方一如往日的宁静,路上也没什么车。然而,到了里运河文化长廊才发现,人可能都在这里吧。路上行人如织,走都走不动。终于找了个路边把车停下。往河边走各种拍照打卡的,卖各种吃的小贩,占满了整个街道。

而既然来了,那就酱紫吧。接着奏乐,接着舞。

The post 窦娥,冤 appeared first on obaby@mars.

  •  

今天中午吃啥子,罗非罗非罗非鱼

✇obaby
作者obaby

自从体检完之后,身体状况感觉每况愈下,小腿酸胀。体检报告出来的数据越来越多,随之而来的毛病也越来越多,现在感觉能正常活着都不容易了。导致另外一个问题,就是做啥都提不起兴趣,前几天刚说要放弃投降。昨天下午改了下闺蜜圈,把 ai 功能屏蔽了,又提交了华为的应用市场,今天早上通知审核通过了。

至于后续的打算,华为商店的就不再更新了。等节后回来发个新版本,如果升级的话自然就可以升级到带 ai 功能的版本了。

这段时间,体重再也没下降,起起落~,只看到了起起,却没看到落落。至于中午吃饭,也是在是不知道吃什么。有时候中午干脆不吃,或者随便吃点什么。每到中午吃饭的时候也让人无比的犯愁,鉴于现在的状况,不知道是该控制饮食还是增加锻炼强度。

小腿酸胀的问题,前段去按摩了一次,但是效果感觉并不明显。不过按摩完小姐姐说自己现在穿的鞋子很舒服,说可以买一双。然后就提着让我拍照记录一下。

不过可能是拍照角度的问题吧,找同款的时候感觉总是有些区别,不过也无所谓啦。

临近中午,吃这么的问题就又出现了,上周百无聊赖跑万达转了一圈,汤姆家还有充值的余额,点了一份牛排。

这周前两天,连续点了两天外卖,都是沙拉。今天实在不想吃沙拉了,感觉再吃都变成吃草的驴了。

中午本来想叫着公司的一个姐妹一块去吃饭,结果她带饭了。自己继续往万达跑,在路上也想好了要吃什么。

中午人不算多,停车上黑咕隆咚的,再加上倒车影像有点暗,让自己有种做瞎子的感觉。

米线还没上来,服务员问能不能直接煮好了给送过来,就不在单独上了。这个倒是无所谓,也没那么讲究。

吃饭的时候,杜郎在抖音发了个消息,说一个特效可以试试。

原视频效果是酱紫的:

然后我生成的效果是酱紫的:

tm 这是幽会外星人?就尼玛离谱。

不得不说,这人工智能,真 tm 有创造力。这都能从天而降,看到那只脚的时候,小脑都萎缩了,就尼玛离谱。

不过这眼看到假期了,说实话也没什么心思工作,主要是也不大想做。现在对接的项目无法连接互联网每次更新代码都得 git 离线 am 代码,有时候还会出问题,就很烦。为了更新代码,得把 patch文件,从自己的 mac 复制到远程的内网的 windows,从 windows 在复制到内网的 linux。在服务器上 am 代码。

不过好在,过完今天下午就暂时解放啦,定了淮安的民宿,可以去吃淮扬菜啦。

不过这一个假期,希望体重不要上升啊。小红书上看有人说从青岛去淮安高速各种修路,于是决定早上两点爬起来出发,

虽然绕出去一百多公里,但是整体时间还是更划算的。

希望早上不要堵车哦~~

 

The post 今天中午吃啥子,罗非罗非罗非鱼 appeared first on obaby@mars.

  •  

一劳永逸?

✇obaby
作者obaby

忘了是周几了,在杜郎杜郎那里看到《Coze扣子空间邀请码》,取了一枚邀请码注册了一下。

刚拿到的时候试了一下,发现生成效率有点低。昨天又   尝试了一下,让 coze 根据上传的图片还原生成 ui。上传的图片:

输入的指令:

静态页面有了,那么是不是同样能够生成后端工程,然而让尝试生成后端项目的时候。表示无法生成项目,不过通过尝试创建目录和文件的方式是可以的。

不过后端代码的整体效果就比较一般了:

导入了序列化的类,但是却没有把数据序列化返回。

相对来说,前端的还原度和代码质量就要高很多,实际效果:

页面访问地址:https://space.coze.cn/web?uri=7497853897668313142%2F%E6%9B%B4%E6%96%B0%E5%90%8E%E7%9A%84uniapp%E8%BF%90%E5%8A%A8%E6%95%B0%E6%8D%AE%E9%A1%B5%E9%9D%A2-1d16562ed0.jsx

整体来说扣子空间前端代码能力还是不错的,包括图片识别,页面设计等等,体验还算不错。但是对于 django 框架,实际的体验就比较一般,当然也有可能是这个框架太小众了。

附几个邀请码:

小妖精 邀请你体验扣子空间,快来和 Agent 一起开始你的工作吧!
https://www.coze.cn/space-preview?invite_code=QL9FGUG1

https://www.coze.cn/space-preview?invite_code=8UQSUMXW

https://www.coze.cn/space-preview?invite_code=B13LD82P

https://www.coze.cn/space-preview?invite_code=1QP96OUJ

https://www.coze.cn/space-preview?invite_code=Y1V1I0YB

昨天收到联通发的消息,说要升级设备。

不出意外,今天早上家里服务都无法访问了,看下了报了个错误。这个错误,应该是 cdn 升级之后导致的,接口变了。

[E] 发生错误: 请求参数错误: username为必填字段
[T] 获取到 token: None
Traceback (most recent call last):
  File "/home/obaby/dnspod-ddns/ddns.py", line 346, in <module>
    loop.run_until_complete(main())
  File "/home/obaby/dnspod-ddns/ddns.py", line 235, in main
    update_ipv4_record(current_ip)
  File "/home/obaby/dnspod-ddns/ddns.py", line 109, in update_ipv4_record
    update_domain_source(domain_list_v2, new_ip_address)
  File "/home/obaby/dnspod-ddns/baby_dunyun.py", line 248, in update_domain_source
    site_list = get_site_list(token)
  File "/home/obaby/dnspod-ddns/baby_dunyun.py", line 54, in get_site_list
    'authorization': 'Bearer ' + token,
TypeError: can only concatenate str (not "NoneType") to str

对于这种错误,其实没什么好的预见手段,只能出了问题再去改。

上午把错误修复了,同事改了下更新 ipv6 地址的逻辑。

就在刚刚,收到华为应用商店的短信,提示 app 被下架了了。

果然跟自己预料的差不多,至于举报只是其中一部分,关键的一部分在于实名认证。但是这个实名认证真的是不怎么好办,不管哪种认证方式,都非自己所愿。就是个记录大姨妈的 app,干嘛非得实名认证,而至于 ai 生成的内容,实名认证了就不会生成敏感内容了吗?这个不应该是 ai 服务提供商来解决 ai 的问题吗?为什么要把问题转嫁给用户?

之前还说永不投降,其实啊,哪里有什么骨气?我不想收集那些用户敏感信息,对此也没兴趣。所以,对于实名认证这件事情我投降了。

至于一劳永逸?那就更不存在了,唯一的就是跟着不断的折腾,折腾,折腾。

天底下没什么免费的午餐,更没什么永葆青春的秘诀。

哪怕是再遥遥领先的高潮针,也解决不了时间的鞭挞。

补充邀请码:

小妖精 邀请你体验扣子空间,快来和 Agent 一起开始你的工作吧!
https://www.coze.cn/space-preview?invite_code=O0RDW20W

https://www.coze.cn/space-preview?invite_code=6WXD0SC0

https://www.coze.cn/space-preview?invite_code=WLCJH5LK

https://www.coze.cn/space-preview?invite_code=RESH9SKJ

https://www.coze.cn/space-preview?invite_code=DLKZFGTF

The post 一劳永逸? appeared first on obaby@mars.

  •  

拼多多?

✇obaby
作者obaby

每到各种节假日的时候,伴随而来的是各种调休。为了拼那几天假期而拼,看起来不少,但是实际上去了原来的周末,也没几天。拼多多?一点都 tm 不多。

一个好好的周末就剩了一天,而这一天基本也没什么自由时间。上午配宝子学钢琴,下午配宝子上网球,而至于中午,前几天看电视说青岛的鲜鲅鱼上市了。美食节目说什么一鱼四吃。这种鲅鱼就大了,得十几斤,问宝子要不要吃,表示只想吃鱼丸。既然剩下的三吃没了,自然也就不用买那么大的鲅鱼了。

上午下课,带宝子去超市。路上看到一辆车屁股上贴了好多小玩意儿,宝子说,想给他把苹果薅下来。当然,宝子肯定是不会这么干的。

不过这个加菲实在是太丑了啊,送给我我都不要。

鲅鱼 39 一斤,价格还算可以。转了一圈,有看到了最喜爱的大西瓜,不过价格嘛,稍微贵了点 6.99。本来想那个西瓜也就 50 块钱,称完发现 70。

做鱼丸的时候,宝子竟然想要自己动手。这倒是难得一见,平时宝子对做饭什么的都不感兴趣。把鱼肉做成鱼蓉,水开了,把宝子叫过来,教她怎么弄成小丸子。不得不说,虽然小了点,但是弄得还挺像那么回事的。

下午的网球课依然在小学的体育场,自己去周边的公园溜达。公园上面是高压线,一侧是青银高速。蒲公英的绽放的刚刚好,一朵朵圆圆的,毛茸茸的。如果有个人在身边真想让他吹的时候,猛的把花球塞他嘴里。

从另外一侧转回来的时候,问到一股熟悉的味道。不出意外的话,应该就是石楠了。

看似洁白无瑕的小花,散发出的味道真的当然一言难尽。其实更让人崩溃的是小区里面的那几颗板栗树,没当板栗树开花的时候,散发的味道比石楠花更接近精液的味道。每次从那几棵树边经过去开车的时候,都想绕道。

溜达完,回到停车的地方。发现里下课还有差不多半个小时。坐在车上百无聊赖刷短视频,这时候忽然脑子里蹦出来一个念头。趁着有时间不如把车上的 gps 拆掉吧。

这个 gps,不是平时导航用的 gps,而是贷款的时候安装的 gps。买第一辆车的时候,出了安装 gps 还办理了抵押,那个安装的 gps 给开放了权限,可以通过网页查询车辆位置,为了能实时查看车辆位置,自己还开发了个 app。

不过这个破玩意儿的确是耗电,车停在那里可能七八天不开就没电了。就来就把这个破玩意儿给拆掉了。

而第二辆车大白上的 gps,其实一直就没给开放权限。所以也无法查询定位信息,而之所以知道这个 gps 安装在那里是因为前年去威海玩的时候,在高速上忽然掉出来了。当时吓一跳,还想这怎么个情况怎么还掉装备了,找了个地方又给塞回去了。

之所以想拆掉,主要是这个东西出了耗电也实在是没啥别的用处。从方向盘下面扒拉了一下,找到了那个 gps,跟着是一根长长的线。拉出来发现一头接的保鲜盒的保险丝,另外两根线直接接的 obd的线。

最开始只是拆掉了盒子,后来一想那一大堆线留着也是隐患,直接打开保险丝盒子,把 obd 和后面的线一块给拆掉了。

这样就干净多啦。

不过不得不说,这个 gps 质量是真好啊,都八年了,拆下来,电池不鼓包。感觉一切运行还正常。

这一番折腾下来一天又没了。

而至于今天干嘛,那自然是上班了。还不是为了那个拼多多拼假期!不过,五一对象说定了个什么地方的酒店准备要去,而这个地方自己问了很多次了。到现在也没记住,希望到时候不要堵车。

The post 拼多多? appeared first on obaby@mars.

  •  

紫慕的明信片

✇obaby
作者obaby

前段时间,看到紫慕的百信计划紫慕。于是自己留了个地址,今天收到博友的明信片啦,开心。

对于寄明信片这件事情,多年以前度蜜月旅行,在很多地方给自己寄了明信片,然而,最终却一张都没收到。从此之后不管去哪里再也没寄过明信片。

这次收到紫慕的明信片还是很开心的,毕竟,貌似十几年没收到过明信片了。

The post 紫慕的明信片 appeared first on obaby@mars.

  •  

Inner Peace

✇obaby
作者obaby

该剪头发了,昨天中午约了 119group hair 。修剪完也不知道该吃啥,在万达溜达的时候看到了汤姆家的牛排西餐厅,想起来之前办的卡应该还有钱,去点了份牛排,最近减肥也的确在刻意的控制饮食。

不过饮料上来之后,用习惯喝了一口,直接吸不动了。

可能是看到我在给饮料拍照,不一会儿又给那里一根粗的吸管。咱就是说一开始不能给根粗的吗。

宝子上周三期中考试,昨天拿回去两张试卷,说要签字。语文 99,英语 100。数学的还没发,不过英语倒是不需要签字。而至于这个 99也不是什么大的错误,山羊招待小伙伴,做了很多好吃的,让写一句话给山羊,宝子写的简单了点,两分扣了一分。

等宝子做完作业,继续一起看《三叉戟 2》,原以为是 36 集,结果之后 34 集。豆瓣的评分,对于这一步刑侦剧来说稍微苛刻了一些,只有 7.7。我个人觉的 9 分还是值得。

早上起床,对象给宝子煮的馄饨,宝子洗刷完之后看着桌子上的馄饨来了一句:“怎么又是馄饨啊,昨晚就吃的馄饨”

好在还有几个面包,于是坐在那里刷手机等宝子吃完去送她。我约了今天早上七点半到八点的体检,眼看都七点半了,手里的那个面包还剩一半。而自己从七点二十就催促她快点。这慢悠悠的速度真是让人无语,不管你怎么着,都要急死了,她一副事不关己的样子。如果不是今天约了体检,真的跟她一起磨。

终于35 洗刷完,准备穿衣服,学校要求 50 前到校。今天开的车没登记,还得缴费出场。小区里各种逆行的,汇车就头大。终于四十出了停车场,怪上主路的时候,感觉车尾都甩出去了。路口又是两个连续 90 秒的红灯,也是在不想理她。终于送到学校,往医院赶,这一路,红的发紫。的确让人崩溃,更让人崩溃的是导航直接导到了高架桥底下,这里没停车长,继续转圈。

终于又转了一圈之后,终于找到一个停车场停车。不过好在今天人不是很多,这体检完,很多结果就能查到了。不出意外,去年有问题的今年依然有问题。

这还只是目前能看到的,看不到的东西自然更多了。

之前 萧俊介  说自己心态好,其实,也有崩的时候。

崩的时候,主要是因为宝子那个无所谓的态度,真的让人上火。哪怕自己要急死了,她都慢慢悠悠。这个心态的确是厉害。这时候,我总是能想到《功夫熊猫》里master oogwui 和 maste xifu 的 inner peace。

inner inner peace.

The post Inner Peace appeared first on obaby@mars.

  •  

我们的纪念日

✇obaby
作者obaby

周五晚上去海底捞的时候,在门口看到了个 hello kitty 的盲盒机。里面的 kitty 的造型真的挺别致的,特别符合自己的审美。于是跟宝子约定吃完饭出来抽盲盒。

39 一个的盲盒,看构造还是蛮复杂的。按照之前买的盲盒,这个东西价格可能得在 49 左右。先给宝子买了一个,自己也买了一个。回家组装完发现蛮有意思的,我的那个是一个背着汽笛喇叭的赛博朋克风格的 kitty。

宝子的那个是带着高科技眼镜的未来战士风格的 kitty,不过宝子说,可能是个潜水员。

而边上那个敖光,就更值得一说了。这个是在另外一家店买的哪吒盲盒,里面有个所谓的隐藏款就是他啦。周五的时候宝子说想再买一个,于是,不出意外的又抽到一个隐藏款的敖光。😂,忽然觉得抽到那么多隐藏款也不是什么特别让人开心的事情,不过宝子已经决定把这个敖光送人了。知我的蓝胖子后面的卡片,都是我买的,并且都是最低级的卡。这运气不得不说,区别有点大。当然宝子不可能就这几个,柜子里已经塞了很多了。

各种乱七八糟的小玩意儿。

周日早上打开电视,在播放青岛马拉松的直播赛事,对象提议去石老人的海边溜达一下。出门之后,打开导航发现目的地封路了。于是决定调转车头去高架桥底下的运动场打网球。这次原来的断头路,在公园入口直接停了一辆三轮车,把路堵死了,里面还有混凝土车在施工。球馆的场地依然没有划线,还有几个场地封闭施工,所以能选的地方并不多。

最终还是选了头上一个 pickle ball 的运动场地,在里面打了会儿,而至于这个吧 ball 到底是什么 ball,我也不甚了然。我跟宝子站了长的一半场地,另外一边还能容纳几个人,期间来个两个男士,看运动装备真的好专业,一个包里放了三种运动器械:网球拍,乒乓球拍还有高尔夫球杆。不得不说这运动技能应该是点了不少,不过在打球的时候明显不专业,应该也是业余爱好。

宝子不知道是累了还是怎么着的,到后来越打球越近。可能也是打累了吧,也近中午了,那就打道回府吧。

穿过曲折的小区,回到车上打开导航。虽然来过几次,但是每次不开导航还是不知道怎么走。就在到那个发卡弯的时候,对象忽然说道:“前面这条路是不是能直接到高架桥?我记得应该有条路能上去”

在得到这个提示之后,我毫不犹豫的开了过去,然而却发现越开越不对劲,虽然后面有辆车跟在后面,但是当路对面开过来一辆大货车的时候发现自己正在逆行,这应该是之前自己从学校出来的时候看到的那条匝道。

自己把车靠到路边,后面的黑色 suv 超了过去,对面的大货停了会儿,黑车逆行上了高架。此时自己的想法是直接倒出去,毕竟一个车道调头有些困难。踩着油门倒车速度是真快啊。怕后侧有人过来,自己还不断的看着倒车影像和后视镜。终于这几百里的路倒出来的时候还比较顺利,后方没有来车。

在路边挺好准备往发卡弯拐的时候,发现前面儿大货车距离有点近,于是想着再倒一点。然后就听到砰的一声,车屁股怼到了路边的车上。

“你倒的太 high 了”对象说。

“前面大车有点近,我想再倒点来着。”我说。不过侧面碰上去的感觉不是很严重,把车稍微往前挪一点,去找路边车上的联系方式。

围着车转了好几圈,只看到驾驶位前面放着一个二维码,扫了一下发现车牌也对应不上,二维码识别的电话还是个空号。这尼玛,心里一万个草泥马奔腾而过。那个破别克,真的也的确挺烂的,都是自己补漆,填补的前挡风玻璃还有一条巨大的裂纹,其实就算自己跑了应该也不会被发现。但是,这跑了总不是什么高招,更何况路上还有两个监控探头。跑了心里也难免不踏实,于是打 122 通报情况,同时告知保险公司自己出了事故的情况。

122 详细询问了下情况以及事故发生地址,过了没多久四方交警大队的交警打来电话了解情况,说帮忙联系车主。这时候保险公司也开始一个一个电话的了解情况,把报保险的手机给对象,让她跟保险公司沟通,自己继续和交警联系。

自己一边给事故现场拍照,一边等交警通知。不一会儿收到交警电话告知,车主在外地,联系上了车主的儿子,让下午四点去交警大队处理。既然有了明确答复那就不用再等了,直接回家吃饭,这一折腾又是半个多小时。自己在忙着拍照处理的时候,宝子叽叽喳喳来回跑,当然,她没经历过这种事情,反而觉得有点好玩。

从发卡弯拐过去没多久,又接到一个电话,是四儿子店的,说修车的话可以去他们那里,这的确是一条龙服务了,我还没决定休不休就先联系上了。

“还是得听导航的,他不让走那里肯定是有道理的”我说。

“路边也没指示标志神马的,谁知道那是单行啊”对象说。

回家,吃饭,下午继续刷自己的电视剧《三叉戟 2》。两点左右收到交警电话,说车主的儿子到了,现在在交警大队,你们看怎么协商处理下还是怎么着?

“私了吧”我答道,就那个破车,喷个漆也就两三百块钱,自己目前也没想给大白喷漆。

“好的,稍等,我让车主的儿子跟你聊”交警说道,正好趁这个功夫我打开了通话录音。

“你好,你看咱们是怎么弄啊?私了还是走保险”对方问。

“私了吧,你看是什么想法?”我问到。

“事故也不严重,我看了,喷个漆什么的也得三五百块钱”对方说。

“嗐,那个车也没那么新,喷不喷也没啥,两百块钱得了”

“现在人工多贵啊,这两百实在是弄不了啊。”对方继续说道。

“行吧,那看看三百?实在不行就走保险吧”

“行吧,那就三百,我把电话给交警”对方说完,一阵沉默之后,另外一个声音船里,“你们协商好了是吧?钱是怎么给?”

“我微信转给他?”我问。

“你现在有时间吗?来趟交警大队吧,我给录像也好记录下,留个凭证”交警问。

“可以,我现在过去得二十多分钟,你们得等下”我答复。

“没问题,你现在过来吧”

挂上电话,叫着对象一块去交警大队,宝子也想跟着去,毕竟没经历过有点好奇。让她去换衣服,结果找裤袜找了半天没找到,也就没再跟着。

剩下的就简单了,交警打开执法记录仪,我把 300 块钱给了车主的儿子,这件事情就算了了。剩下的就是买个补漆笔,简单的补下漆就完了。

“倒车的时候,是不是倒车雷达没响?那么一大片障碍物竟然没动静”我问。

“没响,这个之前我也遇到过,姥爷丧事,跟老妈回老家时候我也怼了,把车头给撞了。”对象说。

“咋撞的,我咋没发现?”

“上次回去,那个小胡同我不想进去,老妈非得让拐进去,结果就 duang 了。舅舅还说:”你那么近,雷达响你没听到啊”,其实雷达就没响我怀疑雷达坏了”

“这破玩意儿,我也怀疑是坏了,不过速度快了,那个雷达貌似不提示”我说道。

“高级车是不是能自动刹停?”对象问。

“啊,貌似不能吧,往前走可以,貌似倒车不行”我答道。毕竟有一次开粉皮去吃饭,倒车快了点,差点怼到宝子小姨的车上。而至于那些高科技的新能源自己就不知道了,毕竟也没体验过。

等红绿灯的时候,对象很神秘的拿过手来说,“你看,我忽然发现的”

我盯着看了半天,发现是个日历界面,上面也没什么标记。盯着那个当前日期看了半天才想起来:“我们去买个大金链子吧”我说,毕竟这结婚纪念日,或者说领证纪念日真的没什么存在感。差不多每年都会被忘记,也没什么存在感。

“不买,没啥意思”对象说道,“这纪念日也太没仪式感了”

“嗐,那里啊,毕竟咱们还听了个响不是?”我说。

晚上,一家人继续呆在一起刷电视剧。这几天宝子也跟着自己看警匪片,其实也没啥不好的。毕竟,比跟着她姥姥看那种家长里短的狗血电视剧,跟着姥爷看各种抗战片能好那么一丢丢。刷电视剧的时候,对象又把手机递给我说:“你看人家小陆还给老婆送了花,我都没想到,竟然是同一天的纪念日”

“这我也没想到啊,这还挺有仪式感的”我说,“要不咱们点饮料喝吧?”

“不减肥了?”对象问。

“嗐,周末沉了好几斤,不差这一点”,拿着手机翻了半天,发现美团的那个沪上阿姨,貌似不是真的沪上阿姨,叫什么沪上阿姨鲜果茶。打开瑞幸小程序,想买纳瑞冰,发现竟然没有。这也太离谱了,于是把手机放下说不买了。

“啊,你们不买了吧?我还想喝呢”宝子说。只好把手机递给宝子,让她自己去选自己要喝什么。

划拉了一会儿,看来是选好了,问:“用你的手机付款码?”

“你不考虑给我们也选一个吗?”我问。

“谁知道你们要喝啥,不是说不喝吗”宝子说完把手机递了回来。

划拉半天,总算是凑齐了三杯饮料。不过,送过啦之后发现貌似都不是很浓的样子,养乐多的比例明显要小很多。

“这饮料,越来越来越不行了,也不值二十块钱啊”对象说。

不过喝起来,味道也还行,就自己把饮料喝完了,他俩都喝了不到一半。这,看来的确是没那么好喝?至于纪念日,总得留下点东西,这砰的一声就挺好的,能记好几年。

等明年再到这时候,可能会比今年的印象深刻,就像多年前的愚人节,接到交警电话说自己车在路边挺着被撞了一样。原以为是玩笑,没想到是真的。

砰的一声,也算是给纪念日放了礼炮了。

The post 我们的纪念日 appeared first on obaby@mars.

  •  

投降?

✇obaby
作者obaby

上周的降温,最直接的结果就是自己被吹感冒了。天天流鼻涕🤧,昨晚睡觉的时候我跟对象说,我每天流鼻涕也能轻二两,助力了我的减肥事业。

对象回了我俩字:“恶心”。

最近这段时间,减肥似乎又停滞了。没什么下降的迹象,但是最近的运动量其实并没有下降。比较奇怪的是每天的生活路径跟之前是一样的,步数缺少了很多。做完结结束了 4200 个的跳绳之后,步数也只有 8000 多,的确让人有点匪夷所思。

可能是这十来天流鼻涕流的太多了,最开始的几天喝蒲地蓝口服液。喝了几天发现没什么好转,看了下说明书发现貌似并不治疗流鼻涕。

后面连天开始吃感康,然而,不知道是因为感冒问题,还是吃药导致的副作用,血氧饱和度在某一个时刻降到了 70。有时候竟然有些头晕,这尼玛就有些离谱了。吃了两天也果断放弃,后面几天就是硬扛着,每天用掉一包纸。好在这几天康复的差不多了,昨天上去想起来在按摩店办的会员卡,约了中午去做后背理疗。

中午回家吃点东西,也不知道该吃啥,早上还有剩下的一个玉米。抱着那个玉米啃了之后,直接开车去按摩,店门口虽然有位置,但是也不敢停。

停车位在马路牙子上,边上是水泥修的段斜坡。第一次来看上面一个车位上停了一辆奔驰 c200,想着它能上去,粉皮应该也可以。小心翼翼的往上尝试了一下,听到咔擦一声,就知道是蹭上去了。并不是真的我上我也行,转到路边,找个地方停下。这次路边竟然异常的火爆,没有任何的空闲位置,有转出去百十米才找到停车位。

按摩的时候,大姐说,这个后背可能会出痧没问题吧?

这个其实也在意料之内,出就出吧。其实每次按摩,最难熬的就是按摩师说自己有各种病。这次技师并不是前两次的那个,按的时候开始说自己的各种问题:脊柱有点侧弯,背部有的地方比较高,肝脏肾不好,有的地方有结节……

嗐,这些问题,也不是一天两天了。按到最后的时候,没那么用力了,就感觉很舒服,趴着差点睡着了。按的时候,技师还给后背拍了个照,按完给看了一下,其实也没什么好意外的,肯定是红彤彤的一片。

刚要走,收到一封邮件,华为开发者:

对于这种监管机制,真的挺头大的。主要是不知道该从哪里下手,也不知道该怎么下手。有时候,真的想,嗐,投降吧,何苦呢。

最近事情的确比较多,乱七八糟的搞得自己挺头大的。晚上回家,晚饭也不知道该吃啥,找了点零食对付了一下。

跳绳的时候,对象说;“明天老爸(Father in law)生日,还是继续海底捞。”

宝子说:“干嘛不早说啊,我可以提前画张贺卡啊。”

“现在也来的及,之前买了一些不是,直接写一张吧”我说。

现在聚餐,其实也没什么太多的好选的地方,一般就直接海底捞解决了。前段时间我在海底捞过生日的时候,遇到一个小朋友,特别喜欢唱生日歌,于是每桌唱歌的时候,他都要跟着一起唱,甚至还自带了道具。

有时候想,这种生活真的不错啊,可以做自己喜欢的事情。关键是父母能陪着一起玩,这才是生活该有的样子。

今天上午收到了 qq 邮箱的邮件,祝自己生日快乐。

哦吼,这都能凑到一天去,我的阳历,凑到了老爸的阴历上。这的确是巧合了,不然,可能自己都没注意今天是个什么日子。

白天的按摩可能的确管用吧,竟然一觉睡到了天亮,想再扛一会儿,也扛不住了。主要还是憋尿实在憋不住了,这件事情该投降就投降吧,硬抗也没什么好结果。

而至于其他的事情,投降?永不!

The post 投降? appeared first on obaby@mars.

  •  

南墙 WAF 系列(三)– API SDK

✇obaby
作者obaby

手里乱七八糟的域名实在是有点多,而这些域名,多数都解析到了同样的站点。或者说是回源是同样的站点,最开始其实并没有套 cdn,中间有段时间连这种 301 302 跳转都被打,就变得很尴尬。301 跳转都能让沙雕给打挂,也的确什么好办法了。

后来发现盾云的 scdn性价比的确不错,于是一股脑的全部套了 scdn,不过,有的也的确没什么流量,于是一些没那么重要的东西放哪里就有些浪费:

所以,思索再三,几天申请了一台 99 块钱的海外轻量,每个月 1t 的流量,感觉应该足够应付这些乱七八糟的服务了。当然,最主要的还是要套上南墙 waf,直接裸奔感觉也不大行。

当然,waf 后面的回源多数还是家里的服务器。这就需要有个工具来更新 waf 的回源地址,今天又完善了一下南墙的 api 接口,代码直接提交 github 的,有需要可以自取。

功能特性

 

  • 证书管理
    • 获取证书列表
    • 检查证书
    • 提交证书配置
    • 删除证书
  • 站点配置
    • 获取站点列表
    • 更新站点配置
  • 用户认证
    • 用户名密码登录
    • 支持双因素认证(OTP)

环境要求

 

  • Python 3.x
  • 必需的 Python 包:
    • requests
    • jwt
    • urllib3

安装说明

 

  1. 克隆仓库:
git clone https://github.com/obaby/baby-nanqiang-waf-api-toos.git 
cd baby-nanqiang-waf-api-toos
  1. 安装依赖包:
pip install -r requirements.txt

 

使用说明

基础用法

from baby_nanqiang_api_tools import NanQiangAPI

# 初始化 API 客户端
api = NanQiangAPI(base_url="https://lang.bi:443")

# 登录
result = api.login(username="your_username", password="your_password")
if result:
    print("登录成功")
    
    # 获取证书列表
    cert_list = api.get_cert_list()
    if cert_list:
        parsed_certs = api.parse_cert_list(cert_list)
        print("证书列表:", parsed_certs)
证书管理
# 从文件检查证书
cert_result = api.check_cert_from_files("cert.pem", "key.pem")
if cert_result:
    # 提交证书配置
    submit_result = api.submit_cert_config(cert_result)
    
# 删除证书
delete_result = api.delete_cert(cert_id=123)

站点配置

 

# 获取站点列表
site_list = api.get_site_list()
if site_list:
    parsed_sites = api.parse_site_list(site_list)
    print("站点列表:", parsed_sites)

# 更新站点配置
update_result = api.update_site_config(
    ip="192.168.1.1",
    port=443,
    site_id=123,
    uid=456,
    description="我的网站"
)

仓库地址:
https://github.com/obaby/baby-nanqiang-waf-api-tools

The post 南墙 WAF 系列(三)– API SDK appeared first on obaby@mars.

  •  

大风吹

✇obaby
作者obaby

周五宝子放学,老师在群里发布了大风预警的通知,让宝子们尽量不要外出。然而,周五晚上没有收到任何的班要停课的消息。十一点,夜已经静了,路上的车也变少了。今晚,破天换的没有渣土车的轰鸣声,让夜晚变得少有的宁静。

关灯之后,耳畔隐约传来了一点打在窗户上的声音。爬起来走到床边,窗户上已经密密麻麻的一层水珠,远处交通信号灯的红色灯光在经过水珠的折射之后,也变得氤氲起来。偶尔,有那么几个大雨点,打在金属的晾衣杆上发出清脆的金属撞击声。这或许才是夜本来的样子。

早上起床,与似乎没有停的样子,不知道是下了一夜,还是阵雨间歇。不过这早上的雨声,又变成了另外的样子,大风中裹挟着小冰雹,一片片的落下来,有的地方甚至出现了薄薄的一层冰雹粒。万幸的是,冰雹不算大,即使打在车上也会有什么损伤,如果真的有鸡蛋大小的话,那就该思考下怎么保护自己的车了。

天气愈发恶劣,然而,依然没有老师说要停课。看起来似乎只要天上不下刀子,这课就得去上。上午的钢琴课,宝子表现非常的差。清明假期,再加上最近两周确实没练琴,导致之前学的都忘了。一首谱子,最后能记住的只剩下了最后一段。要期中考试了,周末布置的作业也多了,下课回家,宝子想去小姨家。然而,这一堆作业都没做完,又怎么能让她去。

在一番“友好的”沟通之后,宝子气呼呼的躲进了自己的屋里,嘎巴一声把房门锁上了。我也懒得理她,就让她自己在里面呆着吧。不一会儿宝子的姥爷回来了,问为什么没看到孩子。“又绝交了,自己在屋里躲着呢”我答道。宝子姥爷小心翼翼的去门口敲门,好说歹说才把门给叫开。

一番折腾,时间已近中午。不想做饭也不知道该做什么,采用老办法外卖解决。下午的网球课依然没消息,不说停课,那就是要继续了。鉴于这种天气也着实不想让她去上课,果断给宝子请假了。请假之后下午的时间,似乎还该干点什么。搜了下附近的网球场,发现就在小区二期边上有一个带发球机的场地,价格也不算贵,优惠完 68 一小时。电话问了一下,下午场还能约上,定了下午三点的时间。

吃完饭,看到教练在群里发的消息,训练赚到了小学的室内场地。这课的确是非得上不行,不过既然请假了,也就没什么必要去了。饭后,宝子去写作业,自己去眯了一会儿。上午出去的时候似乎穿的太少了,回来之后就开始流鼻涕。这一风给吹感冒了,也是让人绝望。

下午迷迷糊糊的被叫起来,说快三点了,这才记起来还要继续去打球的。简单收拾下,开车过去倒是也快。三点准时到了球场。老板给调整好速度、正反手,然后就开始了。

一轮发球下来,自己看了下时间大约 8 分钟左右,不过发球机发出来的球速度比自己发的要快一些。之前跟宝子练球的时候,为了让球好接,球速并没有太快,现在发球机的速度的确会快一些。如果要将球打过网,需要发的力也更多一些。

两个人轮流接发球,还能稍微缓一缓,一轮球打下来。出的力气明显比对打要高很多,原本想着第二天继续去的。周日早上一睁眼发现已经八点多了,而宝子这个点了还没醒,又过了一个小时才醒。看来这一个小时的发球机训练强度的确是够的,原来自己就手腕有点疼,现在手腕感不到疼了,小手臂开始疼,大手臂也跟着疼,看来这才是全身运动。

自然,周日的打球训练也只得作罢。鼻涕流的越来越严重了,找了一盒蒲地蓝口服液喝了两瓶,的确难喝,相比三九那种甜甜的口感,蒲地蓝可以说是喝过的最难喝的药水了。

到了晚上依然没有好转的迹象,晚上睡觉前继续三九+蒲地蓝。可能还是以为三九的助眠作用吧,晚上睡得还是挺沉的,不过可能是晚上睡觉之前看了一道奥数的竞赛题讲解,做梦做了一晚的题。不过黑板上的题目,自己绞尽脑汁也没算出来,而老师还在不断的找人上黑板解题。

正在自己想怎么躲过被抽到的命运的时候,老师走到了身边,问:“通过这个题目,你还发现了什么?”

我看着自己的书上的答案,慢吞吞的答道:“发现了外生生物,还有一些不合常理的东西……”

“你看下这个图,”老师指着自己的笔记本说道:“这些东西发现了吗?”

我扭头看向他的笔记本,上面画了几个小人,只是,小人的形状都比较奇怪,一个像人民大学的 logo,另外一个,两个外星人把一个女人夹在中间,跟三明治一样,让我想到了自己很多年前玩的人体俄罗斯方块。

“3P?”我问。

“是的,外形繁殖就是用这种方式的……”老师解释到。

这是哪门子繁殖方式,不是,为什么要用这么诡异的姿势呢。正思考着,一阵阵尿意袭来,再也憋不住了,不然就该尿床了,赶紧爬起来上厕所。

回来之后总算是放松了不少,看来这四月的一阵风把自己给吹出毛病来了。脑子里装的乱七八糟的东西,都被这一阵风搅和到一起了。

用华佗的话说,叫做“风涎入脑”,唯有开颅以治之?

The post 大风吹 appeared first on obaby@mars.

  •  

白嫖百度网盘超级会员和7T空间

✇obaby
作者obaby

前几天看到文案姐姐发的这篇文章,尝试白嫖了一下百度网盘的vip。的确是可以白嫖成功的,不过白嫖的过程中需要准备一些材料,并不是直接就能白嫖到的。当然,多数能访问到我的博客的基本都具备这个条件,在申请的过程中需要提供自己的渠道信息。

渠道可以是:博客,微博,B站等等,所以在白嫖之前需要有相关的自媒体账号或者平台才能申请。

这件事情本质上是申请的推广联盟的账号,联盟提供svip和空间扩容。所以,继续之前,请先了解上面的信息哈,免得最后落差感太大。

这个东西本质上是推广来获取权益,也可以挂着看能不能获取到一些推广费用:

发放规则:
会员/空间权益根据推广渠道网盘账号官方自动发放会员权益以及扩容空间,(达到条件自动发放,不用找平台申请)
发放时间:权益目前是每天发1次,报备通过后在第二个工作日下午18:00点左右会发完
* SVIP权益
* 首次签约权益: 赠SVIP14天,拉新收益大于50元,可再赠送14天;
* 30天内转存收益达600元:赠SVIP季卡,每个账号一个季度限领一次;
* 30天内转存收益达1500元:赠SVIP年卡,每个账号一年限领一次;
* 30天内转存收益达3000元:赠40TB/季度,每个账号一季度限领一次;

* 空间权益:
* 7天内转存收益达6元,赠10TB/月,每个账号限1次/月
* 30天内转存收益达300元:赠20TB/月,每个账号限1次/月
* 30天内转存收益达1000元:赠20TB/季度,每个账号限1次/季度
* 首次累计拉新人数超10人,自动发放扩容20TB/月;
* 累计拉新人数超过100人,自动发放扩容20TB/季度;
* 累计拉新人数超过300人,自动发放扩容40TB/季度;
* 累计拉新人数超过1000人,自动发放扩容80TB/年。
具体拉新人数要求 会根据数据项目单价上下浮动。


会员权益发放调整通知:

自2024年12月5日起,签约报备成功的网盘账号 14天SVIP会员发放延迟T+1发放,遇周末节假日顺延。

 

最终白嫖效果:

vip有效期大约十几天,至于空间的有效期目前不清楚,我也不知道从哪里看网盘容量的有效期。

如果感兴趣可以开始下面的流程了,开通之后,用这十几天狂下资源还是可以的。

第一步:

登录百度网盘账号(【注意】一定要使用百度网盘app

第二步:

获取UK码

用微信或下载登录百度网盘app

点链接:https://snsyun.baidu.com/sl/j6fEY

进行渠道关系绑定(如果出现无法参与/绑定失败、则需换在重新注册一个网盘账号扫码绑定。)

第三步:

复制UK码+绑定成功截图,在任推邦【申请推广码】处填写报备信息,审核通过后即可推广。
温馨提示:BD联盟是平台机构名称,请复制UK填写报备UK号码,报备UK填错将影响您的结算

第四步:

点击(任推帮平台https://dt.bd.cn/)手机注册登录 

邀请码:4301575

第五步:

点击这个链接,申请推广百度网盘:https://dt.bd.cn/#/pages/index/components/detail?appId=649&invite_code=4301575

申请的时候,需要进行实名认证,添加渠道等一系列操作。

渠道可以写自己的博客,微博等信息,需要提供屏幕截图,管理后台截图等一系列信息。

第六步:

填写刚刚第三步获取的绑定码和截图!

点击申请就可以了。审核时间一般当天能过,而至于网盘容量和svip是第二天下午六点之前完成充值。

耐心等待吧,如果审核不通过新注册个百度网盘账号尝试。

 

The post 白嫖百度网盘超级会员和7T空间 appeared first on obaby@mars.

  •  

虎背熊腰

✇obaby
作者obaby

在清明假期之前,就约了这次的外景拍摄。过完年之后,气温忽高忽低跟过山车一样,厚衣服刚收起来,结果一阵寒潮袭来温度又到了 0 度。

一次次的波动之下,也不想再把厚衣服拿出来,就这几天倒是也好挨,就那么熬过去了。青岛四月的天气,至于暖和不暖和其实自己心里也没谱。不过好在,清明假期之后,天气真的开始暖和了,即使在外面穿短袖感觉也不会冷。

虽然,目前看起来之后不到二十度,中午出去即使光腿穿裙子也不会感觉到冷。当然,最主要的是现在穿裙子的太少,这时候穿裙子出去拍照也的确是显眼包无疑了。

 

早上换好衣服出门,为了让腿上的皮肤感觉更细一点,穿了条肉色的丝袜,结果开门拿东西的时候,让路边绿化带的树枝子直接给刮破了,出现了两个口子。好在现在也不是非得穿的那么整齐才行,有个破洞就有个吧,也没什么要紧的。出发之前,选了几双鞋子,每到换季的时候,总是要来回折腾,这次折腾出来的鞋子,其实就是觉得很好看,但是却不会穿出去的那种,在鞋柜里都放了很久很久,从来都没穿过,以后也不会穿出去,其实有时候感觉挺奇怪的,就是看别人的大长腿,穿什么鞋子都好看,还能让腿更长,但是自己却不行,主要也是没啥资本。

这次果断从鞋柜里拿出来,最后,扔到了回收柜里。毕竟,以后也不会穿他们。

虽然约的是十点,早上稍微一折腾也就到了这个点了。十点稍微多一点到工作室,化妆师小姐姐见面第一句话就是,“感觉你又瘦了哎”

听到这句话真的是让人开心,化妆的时候,摄影师也到了,也说感觉自己瘦了,小姐姐又补充道,感觉也更白了。

 

其实白倒是没怎么感觉,不过稍微能感觉到的还是应该瘦了那么一点点。等化好妆选好衣服,从工作室出门的时候已经十一点半了。至于去哪里拍其实也没啥太多的想法,主要是想拍点公路片的效果,刚开始提议去红岛的祥茂河公园,虽然不大,但是公园里面能把车开进去,另外一侧是河,也可以拍点河边的景色。

既然是公路片效果,摄影师建议不如直接去红岛的星河湾,一边是公路车也不多,另外近海边,有一点沙滩,正好可以拍几张海景。从工作室到海边的路异常的难走,可能这段路大车比较多,一路上坑坑洼洼,路中间的井盖子已经成了一个深井的样子,井盖子顶距离路面大约有十几公分,形成了一个个的炮弹坑,在这种路上开车的确有点不自信,一路蛇皮走位总算是通过了那几公里。

路边停了几辆车,有人躺在车上耍手机,有人坐在海边的凉亭下面聊天。

选的几套衣服,一条白色的裙子,一条黑色的短裙还有一条黑色抹胸长裙。白色的裙子比较宽松一些,两条黑色的裙子,穿起来稍微有点紧,如果自己最近没努力减肥的话应该也就穿不进去了。

拍照的时候,坐在凉亭里的大爷在交流。

“以前拍照的时候,还得用相机,对焦,快门什么的,现在好啊,直接手机就搞定了……”一个大爷说。

“是啊,你看现在多方便,手机,相机都行,这还有专门的摄影师……”另外一个大爷附和道。

拍照的时候,工作室主播小姐姐有些无聊,去扫码骑了一辆电动车来回跑,说一块五,找到了无穷的乐趣。

换好第三套衣服,去沙滩拍照。我穿着高跟鞋在沙滩上走,化妆师小姐姐喊,好厉害,高跟鞋在沙滩都走的这么稳。直播小姐姐也停好车跟了下来,蹲在地上玩沙子。

又过了几分钟,直播小姐姐从台阶上跑下来说,自己的电动车不见了。

想来是 有人看到电动车没锁,直接给骑走了。说能看到定位有好几公里,但是也找不到。这的确就比较麻烦了,到最后拍完走的时候也没见到电动车,只能花了 15 块钱,强制换车了。

看来,这一块五的快乐,最终快乐的价格也稍微贵了那么一点。

等这一切全部忙活了已经是一点半了。时间过得的确快,在台阶上拍照的时候,打扫卫生的阿姨就那么一直坐在那里看,似乎在想什么,就那么静静看着,直到在台阶上的照片全部拍完,她才转身离开,或许也在想自己年轻的时候吧,只是感觉自己现在也不年轻了。虽然不到 20 度的气温,中午的太阳异常的耀眼,一个多小时下来,身上已经微微出汗了。

回到车上,打开空调,感觉稍微舒服了一点。衣服也就不换了,但是为了开车,高跟鞋还是换掉了,毕竟,那个鞋子真的开不了车。

只是,这一天下来中午都没吃饭,晚上称体重的时候,昨天竟然比前天还沉了 500g,这就尼玛离谱。

减肥,毕竟是个持久战,还得继续。

The post 虎背熊腰 appeared first on obaby@mars.

  •  

Vidda(海信)电视安装第三方应用

✇obaby
作者obaby

每次电视第一次到手里,首要的事情就是安装 app。有两个是必须的:ds file 和 vlc 播放器。这两个的组合就解决了 nas 上影片播放的问题,至于为什么不用 nas 自带的影音系统,主要也是两个原因:1.需要安装解码器,然而每次安装都很麻烦,实际时候用效果也并不好;2.很多电影就看一次就行了,也不需要所谓的海报啊,简介之类的东西,看完就删掉了。

以前的时候,总感觉很多影片要留下来以后跟别人一起看。然而,现在更多的时候下载的电影自己看完了就删除了,甚至都没有保留的必要,甚至再也不会打开。

更离谱的事情在于,如果不删除这个影片,过一段时间之后再看到这不影片就会陷入怀疑,看过没有?看了一半之后才发现,哦,原来看过了。老了,这个记忆力真的不行了。现在每看完一部影片就会在豆瓣上标记一下,以后再看到的时候回去观影记录搜索一下,这样就不会再重新看了。

※观影记录/Movies※

以前的电视可以通过修改 apk 扩展名为 dat,让电视自带的资源管理器显示这个文件,打开方式选择安装器就行了,然而,现在却不行了。虽然 u 盘不可以了,但是 Hi 投屏还是可以传送文件的的。

打开电视的Hi 投屏:

切换到最后一项海信爱家 app,进入:

此时会出现个文件投屏,需要在点击一下,在这里是不能传送的。

进入接收界面就可以了,打开手机上的海信爱家 app:

点击文件,进入文件传输功能:

找到 apk 下面对应的安装包发送,在电视上就能接收了。接收完会自动进入安装界面。

安装完成就可以用啦。整体体验感觉还是可以的,并不会比用 u 盘差,说到 u 盘,忽然发现自己钥匙串上的 u 盘不知道什么时候丢了。

The post Vidda(海信)电视安装第三方应用 appeared first on obaby@mars.

  •  

虚无缥缈的 Hello Kitty

✇obaby
作者obaby

当时那个hello kitty离我近了 40公分,但是二分之一柱香之后,导航信号会彻底消失。 因为一边充电,一边导航,把 gps 芯片直接干歇菜了。

就在几天天,车机系统提示可以升级新版本,对于我这种喜欢跟着新版本跑的人,自然是毫不犹豫的就升级了。不得不说,这次升级体验的确提升了不少,最起码那个 pin 码总算有用了,不用每次都输入这个十几位的密码了。

其实,清明原本就没什么计划,至于去哪里没有提前规划。想要出去玩也似乎没什么方便去的地方,济南已经买不到高铁票了,至于开车去,其实是不大想去的,毕竟济南的交通状况,可能真的让人抓狂。规划了那么久,其实没问宝子的意思,当问她要不要回老家跟姐姐玩的时候,毫不犹豫的就答应了。

回家的时候顺便还得带着宝子的小姨,给送回县城。就这样,第一天的行程就算是计划好了。早上起床的时候也没有很晚,稍微收拾下就出发了。把宝子的安全座椅从大白搬到粉皮上,一切收拾好就出发了。开导航的时候发现仪表盘的导航面板现在可以显示导航地图了,这个升级的确不错啊。于是导航路线显示在仪表盘上,中控屏显示路线概览,真的不错唉。

然鹅,设置的时候🈶多开心,后面就会与多后悔。这一切看似非常人性化的体验升级,带来的是自己多跑了一百公里的代价。其实车子上了环湾过了跨海之后就开始提示信号弱,然后说什么开启北斗定位模式。这种情况之前也遇到过,想着应该没什么大问题,就继续前进了。

然而,在开了数十公里,经过了无数的拥堵路段之后,才感觉可能是跑错路了。

在跑完第一段 37km 之后,就已经完全没有 gps 信号了,完全是跟着路瞎跑。直到后来某个时间,宝子说,你看,那个是潍坊的出口。我心想,导航也没提示,应该不是走那个口吧,继续往前开,进入另外一段用户路段之后导航提示六公里右转,驶出当前高速。自己又开了三公里,然而导航地图没变的时候才发现可能真的是错过了无数个出口。

在看了下导航的额 hello kitty 已经变成了灰色,虚无缥缈的样子,这时候才发现问题严重了。把手机从无限充电的底座上拿下来,递给宝子的小姨,让她重新设置目的地。

“你手机怎么这么卡?”她说道,“点都点不动”

“不应该啊,之前不卡啊”我答道。

终于在他折腾了半天之后,算是重新上路了。原本是为了躲过 g20 的拥堵路段,结果绕着环湾跑了一圈,出门的时候两个小时,现在走了一个半小时了还是两个小时,直接麻了。

好在换了高速之后,路况好了不少,一路狂奔下来,终于在十二点之前到了县城。中午简单吃点东西,下午就回村里了。过节的时候,村里稍微热闹了一点,河里的水已经融化,有孩子在河边打水漂。

其实村里并没有太多的娱乐项目,回到村里。总是要带孩子去地里转悠转悠,挖野菜,或者纯粹就是溜达。野菜正好就可以带回家喂兔子,不过这次回家,并没有见到那个小傻狗。后来才知道是跑丢了,农村,丢狗变成了再正常不过的事情,一条狗,养着养着就不见了。

这三月的地里依然没有太多绿意,尽显黄土的本色,阡陌之上只剩下野草被焚烧过后留下的满目的黑灰。

在这灰烬之下,依然有野草顽强的冒出头,努力的生长。

“野火烧不尽,春风吹又生”

说是挖野菜,主要还是带孩子玩,现在自己很多野菜都不认识了。毕竟,似乎已经离开这块土地太久太久,不知道是野菜变了,还是自己变了。

看着地理的麦蒿,我拔了一些,做了一顶草帽,戴在头上,宝子说:“gang(三声)傻”(很傻)。我想套到他的头上,她死活不让,随后给了二姐。

荠菜早已过了最好的时间,已经开满了白花。目前最多的就是苦菜花,二姐说挖回去蘸酱吃。

“我才不吃来,冒苦”我说,其实我从来就不喜欢吃苦的东西,苦菜花,苦瓜,我都不喜欢。

短短个把小时,就挖满了满满的两个大号方便袋。这兔子未来几天的饭算是有了着落了。

晚上吃过晚饭,合计第二天去哪里,就在这时候看到对象发的消息说电视可能坏了。

这的确挺奇怪的,京东找了下订单,发过去问了余下,说可能是收到了伤害。

这,既然是外伤,并且已经过保,想免费修是不可能了。问了下换屏多少钱,得到的答复也真的不错。

以前六百块钱,这价格的确让人难绷,自己买的时候是 2600,这个 2000 是最后下架之前的价格。

既然如此就不如换新的了,二手东搜索了一下 vidda 的 65 寸现在 2200,算上国补,到手大约 1800。就这个价格,让自己怎么能有维修的心?既然如此就果断换新吧,旧电视走回收评估了一下,55 块钱。哈,我的破键盘还回收了 110 呢。

就在不就之后,找到了罪魁祸首,挂在右侧柜子上的一个铁架子,之前是要扔掉的,家里老人不舍得,就给挂到了柜子上面,也没有任何的固定,终于昨天掉下来了,就这样一下子砸没了两千块钱。

该直接扔掉的,大意了。

一番合计之后决定去诸城动物园,据说比潍坊的还要大一些。第二天一早简单吃点东西,八点就出发了。这动物园不得不说,开门是真早啊,七点半就开园了。九点半到了停车场之后比较近的位置已经停满了,只能继续往里走,好在里面树林里还是空着的,没有停车位,只能找两排树中间的位置停进去。

说是很大的动物园,其实人比动物多多了。入口是租电瓶车的地方,在入园之后电瓶车就开始堵车了。一路游览下来说不上有特色,就那些动物园常见的动物。只是最后一段让人颇为抑郁,周边没有任何动物,沿着河一直走,全程下来大约三公里没有任何风景,就这么溜达。九曲十八弯,不过边上的游乐设施不少,看到有人在玩滑道,宝子说自己也要去玩。

游乐场是单独收费的,并且我也是没有任何力气了。跟宝子商量,要不算了吧,回家之后我给搭一个,就酱紫把宝子给劝走了。

中午找地方吃饭,算是吃到了诸城烧肉,其实说就是烧猪肉头,不过这种做法,的确有不一样的感觉,吃起来味道还是蛮不错的。

也算是诸城特色美食了。

回家路上,顺路去五金店把绳子和滑轮买了。

买滑轮的时候老板问,你买多少吨的?

“多少吨?我就能承担一个孩子的重量就行了”我答道。

老板给找了个最小的承受 3 吨,35 块钱,剩下的就是买绳子,尼龙绳子七块钱一米,买了四十米。这样算是齐全了,剩下的就是在家里找地方把东西给搭建起来。一番忙活之后终于 ok 了。

一侧是门框,另外一侧是墙,承担二三十公斤的重量是完全没问题的。

就这样一个简单的滑道就算是完成了,最起码不用一直排队了。晚上吃过晚饭,宝子表示还想玩,于是打开外面所有的灯,看着她和她姐姐继续在那里玩。宝子可以自己骑上去,她姐姐去不行,需要自己在那里扶着。玩玩具还需要一个服务员,关键是服务员左手手腕还有点疼。

这一天下来,竟然走了两万多步,也第二次完成了 400% 的锻炼目标,不过多运动也好吧,毕竟这两天吃的也的确有点多。

第二天早上一早,宝子的奶奶就把自己搭的架子拆掉了,二姐说,也就是你弄的,要是别人把这个东西栓门框上,早就和你没完了。

剩下几个比较高的地方,自己也给拆掉了。也是昨天玩的太累了,早上快八点了,宝子还没起床,把他叫起来。吃点东西,再玩一会儿九点多就该回青岛了,毕竟还有些作业没做,周一的升旗仪式的主持词还得背一下。

这次,再也没敢把 hello kitty 放到中控屏上,因为这几天发现,自己的 iphone 13p,同时渲染两个屏幕,并且充电就会导致手机温度过高丢失 gps 信号。要么扒手机壳,不充电使用两个屏幕,要么直接使用中控单屏幕,即使带着手机壳充电问题也不大。

回程的路上,上午路况还算不错。

“那个 kitty 变成灰色的时候,就变成扁的了”宝子说道,“我给你看着中间的 kitty 如果她变成扁的了,我就告诉你”

“好的,不过这边路比较熟悉,变成扁的也不要紧,你可以不用一直盯着”我说道。

就在这时候,我看到左侧车道宝子小姨的车,“你小姨在左边,你给她打个电话呗”

宝子拨通之后提示正在通话,这时候车机电话响了,接起来就听到她小姨说:“看到你了,疯狂超车”

“我要赶紧去服务区上厕所,早上喝水喝多了”,简单说了几句挂掉电话也就到了服务区了。稍微休息一下,买个饮料继续出发。

到家的时候,电视早就送到了,对象说送电视的说架子太小,承担不住。其实我觉得是足够的,不过还是买了另外一个架子,安装需要用冲击钻,只能等海信的上门安装。终于晚上七点左右过来安装了。

其实新买的架子,质感反而不如之前的,只是感觉大了一些而已。由于现在壁挂电视的安装点都在上面,原来的支架高都不合适,只能重新打孔。一段折腾之后总算是撞上了。为此又付出 170 块钱的安装费。

到现在自己挣的 4000 块钱的外快算是彻底花没了,过完家里不能放现金,应该早就花掉的。换了电视之后,给老太太包了一千红包,再加上出去玩的费用,其实都不止 4000,不过最后老太太没要红包,说要不她给我点,这我自然是不能要的。老太太没收的红包,就打算给大白换轮胎了。上午问了下一条轮胎 440,这下四条轮胎下来,一分钱没剩,都没了。

这 4000 块钱外快,感觉啥也没干,就这么虚无缥缈的就没了。

The post 虚无缥缈的 Hello Kitty appeared first on obaby@mars.

  •  

南墙 WAF 系列(二)– 网站证书自动更新

✇obaby
作者obaby

相对管理后台的 ssl来说,其实网站的 ssl 证书才是正事,毕竟这个关系到网站的访问。按照官方的说法在开放 80 端口的情况下,南墙可以自动申请更新证书,不过后台没找到配置的地方,我的 v4 的 80 也是不通的,所以就需要自己去维护管理证书了。

然而,上午在问了管理之后,得到的答复是没有 api,可以自己抓包进行修改。

嗐,这么看来其实也没啥,最起码说明后台的 api 接口是可以直接拿来用的。即使是有 api 文档,也是得自己去看,去写,没有的话 curl 抓包一样能解决问题。按照之前的方法,只直接复制 curl 给 cursor 就可以了。

api 文件baby_nanqiang_api_tools.py内容:

#!/usr/bin/env python3
import requests
import json
import jwt
from datetime import datetime
import os
import urllib3

# 禁用 SSL 验证警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class NanQiangAPI:
    def __init__(self, base_url="https://lang.bi:443"):
        self.base_url = base_url
        self.session = requests.Session()
        self.session.verify = False  # 忽略SSL证书验证
        self.token = None
        self._setup_headers()

    def _setup_headers(self):
        """设置请求头"""
        self.headers = {
            'accept': 'application/json, text/plain, */*',
            'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
            'cache-control': 'no-cache',
            'content-type': 'application/json',
            'origin': self.base_url,
            'pragma': 'no-cache',
            'priority': 'u=1, i',
            'referer': f'{self.base_url}/',
            'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-origin',
            'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
        }

    def _update_headers_with_token(self):
        """更新请求头,添加token"""
        if self.token:
            self.headers['Authorization'] = self.token  # 直接使用token,不添加'Bearer '前缀

    def delete_cert(self, cert_id):
        """
        删除指定ID的证书
        :param cert_id: 证书ID
        :return: 删除结果
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        url = f"{self.base_url}/api/v1/certs/{cert_id}"

        try:
            response = self.session.delete(
                url,
                headers=self.headers
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"删除证书失败: {response_data['err']}")
                return None
            
            # 检查删除是否成功
            if response_data.get('result') == 'success' and response_data.get('RowsAffected') > 0:
                print(f"证书 {cert_id} 删除成功")
                return True
            else:
                print(f"证书 {cert_id} 删除失败: 未找到证书或删除操作未生效")
                return False
            
        except requests.exceptions.RequestException as e:
            print(f"删除证书请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def parse_cert_list(self, cert_list):
        """
        解析证书列表数据
        :param cert_list: 证书列表数据
        :return: 解析后的证书信息列表
        """
        if not cert_list:
            return None

        parsed_certs = []
        for cert in cert_list:
            try:
                # 解析SNI字段(JSON字符串)
                sni_list = json.loads(cert.get('sni', '[]'))
                
                parsed_cert = {
                    'id': cert.get('id'),
                    'sni': sni_list,
                    'expire_time': cert.get('expire_time'),
                    'update_time': cert.get('update_time')
                }
                parsed_certs.append(parsed_cert)
            except json.JSONDecodeError as e:
                print(f"解析SNI字段失败: {str(e)}")
                continue
            except Exception as e:
                print(f"解析证书数据失败: {str(e)}")
                continue

        return parsed_certs

    def get_cert_list(self):
        """
        获取证书列表
        :return: 证书列表
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        url = f"{self.base_url}/api/v1/certs/"

        try:
            response = self.session.get(
                url,
                headers=self.headers
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"获取证书列表失败: {response_data['err']}")
                return None
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"获取证书列表请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def login(self, username, password, otp=""):
        """
        登录接口
        :param username: 用户名
        :param password: 密码
        :param otp: 双因素认证码(可选)
        :return: 登录响应
        """
        url = f"{self.base_url}/api/v1/users/login"
        data = {
            "usr": username,
            "pwd": password,
            "otp": otp
        }

        try:
            response = self.session.post(
                url,
                headers=self.headers,
                json=data
            )
            
            # 获取响应数据
            response_data = response.json()
            
            # 检查是否有错误信息
            if 'err' in response_data:
                print(f"登录失败: {response_data['err']}")
                return None
            
            # 保存token
            if 'token' in response_data:
                self.token = response_data['token']
                self._update_headers_with_token()
                
                # # 解析token信息
                # try:
                #     # 使用 jwt.decode 替代 jwt.decode_complete
                #     token_data = jwt.decode(self.token, options={"verify_signature": False})
                #     exp_timestamp = token_data.get('exp')
                #     if exp_timestamp:
                #         exp_date = datetime.fromtimestamp(exp_timestamp)
                #         print(f"Token 有效期至: {exp_date}")
                # except Exception as e:
                #     print(f"无法解析token信息: {str(e)}")
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"登录请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def check_cert(self, cert_content, key_content, mode=0):
        """
        检查证书
        :param cert_content: 证书内容
        :param key_content: 私钥内容
        :param mode: 模式,默认为0
        :return: 检查结果
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        url = f"{self.base_url}/api/v1/certs/check"
        
        # 准备multipart/form-data数据
        files = {
            'mode': (None, str(mode)),
            'cert': (None, cert_content),
            'key': (None, key_content)
        }

        try:
            # 临时移除content-type,让requests自动设置
            headers = self.headers.copy()
            headers.pop('content-type', None)
            
            response = self.session.post(
                url,
                headers=headers,
                files=files
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"证书检查失败: {response_data['err']}")
                return None
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"证书检查请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def check_cert_from_files(self, cert_file_path, key_file_path, mode=0):
        """
        从文件检查证书
        :param cert_file_path: 证书文件路径
        :param key_file_path: 私钥文件路径
        :param mode: 模式,默认为0
        :return: 检查结果
        """
        try:
            with open(cert_file_path, 'r') as f:
                cert_content = f.read()
            with open(key_file_path, 'r') as f:
                key_content = f.read()
                
            return self.check_cert(cert_content, key_content, mode)
            
        except FileNotFoundError as e:
            print(f"文件不存在: {str(e)}")
            return None
        except Exception as e:
            print(f"读取文件失败: {str(e)}")
            return None

    def submit_cert_config(self, check_result):
        """
        提交证书配置
        :param check_result: 证书检查的结果数据
        :return: 提交结果
        """
        if not self.is_logged_in():
            print("请先登录")
            return None

        if not check_result:
            print("无效的证书检查结果")
            return None

        url = f"{self.base_url}/api/v1/certs/config"
        
        # 准备提交数据
        data = {
            "id": check_result.get("id", 0),
            "sni": check_result.get("sni", "[]"),
            "cert": check_result.get("cert", ""),
            "key": check_result.get("key", ""),
            "expire_time": check_result.get("expire_time", ""),
            "update_time": check_result.get("update_time", "")
        }

        try:
            response = self.session.post(
                url,
                headers=self.headers,
                json=data
            )
            
            response_data = response.json()
            
            if 'err' in response_data:
                print(f"证书配置提交失败: {response_data['err']}")
                return None
                
            return response_data
            
        except requests.exceptions.RequestException as e:
            print(f"证书配置提交请求失败: {str(e)}")
            return None
        except json.JSONDecodeError as e:
            print(f"解析响应数据失败: {str(e)}")
            return None

    def is_logged_in(self):
        """
        检查是否已登录
        :return: bool
        """
        return self.token is not None

def main():
    # 使用示例
    api = NanQiangAPI()
    
    # 登录信息
    username = "obaby"
    password = "obaby@mars"
    
    # 执行登录
    result = api.login(username, password)
    
    if result:
        print("登录成功:")
        print(json.dumps(result, indent=2, ensure_ascii=False))
        print(f"Token: {api.token}")
        
        # 获取证书列表
        cert_list = api.get_cert_list()
        if cert_list:
            # 解析证书列表
            parsed_certs = api.parse_cert_list(cert_list)
            if parsed_certs:
                print("解析后的证书列表:")
                print(json.dumps(parsed_certs, indent=2, ensure_ascii=False))
                
                # # 删除证书示例
                # cert_id = 4  # 要删除的证书ID
                # delete_result = api.delete_cert(cert_id)
                # if delete_result:
                #     print(f"证书 {cert_id} 删除成功")
                # else:
                #     print(f"证书 {cert_id} 删除失败")
        
        # 证书检查示例
        cert_file = "path/to/cert.pem"
        key_file = "path/to/key.pem"
        
        if os.path.exists(cert_file) and os.path.exists(key_file):
            return
            # 先检查证书
            cert_result = api.check_cert_from_files(cert_file, key_file)
            if cert_result:
                print("证书检查结果:")
                print(json.dumps(cert_result, indent=2, ensure_ascii=False))
                
                # 提交证书配置
                submit_result = api.submit_cert_config(cert_result)
                if submit_result:
                    print("证书配置提交成功:")
                    print(json.dumps(submit_result, indent=2, ensure_ascii=False))
                else:
                    print("证书配置提交失败")
    else:
        print("登录失败")

if __name__ == "__main__":
    main()

账号不要设置动态密码,如果设置了,那就创建一个新账号。

获取证书的脚本参考上一篇文章,对应的路径自己调整。更新证书的代码site_cert_auto_update_tool.py:

#!/usr/bin/env python3
import os
import subprocess
import hashlib
import json
from datetime import datetime
import logging
from baby_nanqiang_api_tools import NanQiangAPI

# Configuration
CERT_SOURCE_DIR = "/root/.acme.sh/h4ck.org.cn_ecc"
CERT_FILE = "fullchain.cer"
KEY_FILE = "h4ck.org.cn.key"
HASH_FILE = "web_cert_hash.json"
CERT_SCRIPT = "get_web_cert.sh"

def setup_logging():
    """设置日志"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler('web_cert_update.log'),
            logging.StreamHandler()
        ]
    )

def get_file_hash(file_path):
    """计算文件的SHA-256哈希值"""
    sha256_hash = hashlib.sha256()
    with open(file_path, "rb") as f:
        for byte_block in iter(lambda: f.read(4096), b""):
            sha256_hash.update(byte_block)
    return sha256_hash.hexdigest()

def save_cert_hash(cert_hash, key_hash):
    """保存证书和私钥的哈希值到JSON文件"""
    with open(HASH_FILE, 'w') as f:
        json.dump({
            'cert_hash': cert_hash,
            'key_hash': key_hash
        }, f)

def load_cert_hash():
    """从JSON文件加载证书和私钥的哈希值"""
    try:
        with open(HASH_FILE, 'r') as f:
            data = json.load(f)
            return data.get('cert_hash'), data.get('key_hash')
    except (FileNotFoundError, json.JSONDecodeError):
        return None, None

def run_get_cert_script(script_path=None):
    """
    执行获取证书的脚本
    :param script_path: 脚本路径,如果为None则使用默认的get_web_cert.sh
    :return: bool 是否执行成功
    """
    try:
        # 如果没有指定脚本路径,使用默认的get_web_cert.sh
        if script_path is None:
            script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CERT_SCRIPT)
        
        # 检查脚本是否存在
        if not os.path.exists(script_path):
            logging.error(f"错误: 脚本文件 {script_path} 不存在")
            return False
            
        # 检查脚本是否可执行
        if not os.access(script_path, os.X_OK):
            logging.error(f"错误: 脚本文件 {script_path} 没有执行权限")
            return False
            
        # 执行脚本
        result = subprocess.run(['sh', script_path], 
                              capture_output=True, 
                              text=True)
        
        # 检查执行结果
        if result.returncode == 0:
            logging.info("证书获取脚本执行成功")
            if result.stdout:
                logging.info("脚本输出:\n%s", result.stdout)
            return True
        else:
            logging.error(f"证书获取脚本执行异常,返回码: {result.returncode}")
            if result.stderr:
                logging.error("异常输出:\n%s", result.stderr)
            return True
            
    except Exception as e:
        logging.error(f"执行证书获取脚本时发生错误: {str(e)}")
        return False

def read_file_content(file_path):
    """读取文件内容"""
    try:
        with open(file_path, 'r') as f:
            return f.read()
    except Exception as e:
        logging.error(f"读取文件 {file_path} 失败: {str(e)}")
        return None

def is_cert_expired(expire_time_str):
    """
    检查证书是否过期或即将过期(7天内)
    :param expire_time_str: 过期时间字符串
    :return: bool 是否过期或即将过期
    """
    try:
        expire_time = datetime.strptime(expire_time_str, "%Y-%m-%d %H:%M:%S")
        now = datetime.now()
        days_until_expire = (expire_time - now).days
        return days_until_expire <= 7
    except Exception as e:
        logging.error(f"解析过期时间失败: {str(e)}")
        return False

def process_same_sni_certs(api, parsed_certs, current_sni, current_cert_id):
    """
    处理具有相同SNI的证书
    :param api: API实例
    :param parsed_certs: 解析后的证书列表
    :param current_sni: 当前证书的SNI
    :param current_cert_id: 当前证书的ID
    :return: None
    """
    # 筛选出相同SNI的证书
    same_sni_certs = [cert for cert in parsed_certs 
                     if cert['sni'] == current_sni and cert['id'] != current_cert_id]
    
    if not same_sni_certs:
        return
        
    # 按过期时间排序(从早到晚)
    same_sni_certs.sort(key=lambda x: datetime.strptime(x['expire_time'], "%Y-%m-%d %H:%M:%S"))
    
    # 检查是否有过期或即将过期的证书
    for cert in same_sni_certs:
        if is_cert_expired(cert['expire_time']):
            logging.info(f"删除过期证书 ID: {cert['id']}")
            if not api.delete_cert(cert['id']):
                logging.error(f"删除证书 {cert['id']} 失败")
    
    # 检查是否有过期时间相同的证书
    if len(same_sni_certs) > 1:
        # 获取第一个证书的过期时间作为基准
        base_expire_time = same_sni_certs[0]['expire_time']
        
        # 删除过期时间相同的证书(保留第一个)
        for cert in same_sni_certs[1:]:
            if cert['expire_time'] == base_expire_time:
                logging.info(f"删除重复过期时间的证书 ID: {cert['id']}")
                if not api.delete_cert(cert['id']):
                    logging.error(f"删除证书 {cert['id']} 失败")

def main():
    # 设置日志
    setup_logging()
    
    try:
        # 执行证书获取脚本
        if not run_get_cert_script():
            logging.error("获取证书失败,退出程序")
            return
            
        # 检查证书文件是否存在
        cert_path = os.path.join(CERT_SOURCE_DIR, CERT_FILE)
        key_path = os.path.join(CERT_SOURCE_DIR, KEY_FILE)
        
        if not (os.path.exists(cert_path) and os.path.exists(key_path)):
            logging.error("证书文件不存在,退出程序")
            return
            
        # 计算新文件的哈希值
        new_cert_hash = get_file_hash(cert_path)
        new_key_hash = get_file_hash(key_path)
        
        # 获取旧的哈希值
        old_cert_hash, old_key_hash = load_cert_hash()
        
        # 检查文件是否发生变化
        if new_cert_hash != old_cert_hash or new_key_hash != old_key_hash:
            logging.info("证书文件已发生变化,开始更新流程")
            
            # 读取证书和私钥内容
            cert_content = read_file_content(cert_path)
            key_content = read_file_content(key_path)
            
            if not cert_content or not key_content:
                logging.error("读取证书文件失败")
                return
                
            # 初始化API
            api = NanQiangAPI()
            
            # 登录
            if not api.login("obaby", "obaby@mars"):
                logging.error("登录失败")
                return
                
            # 检查证书
            check_result = api.check_cert(cert_content, key_content)
            if not check_result:
                logging.error("证书检查失败")
                return
                
            # 提交证书配置
            if not api.submit_cert_config(check_result):
                logging.error("提交证书配置失败")
                return
                
            # 获取证书列表
            cert_list = api.get_cert_list()
            if not cert_list:
                logging.error("获取证书列表失败")
                return
                
            # 解析证书列表
            parsed_certs = api.parse_cert_list(cert_list)
            if not parsed_certs:
                logging.error("解析证书列表失败")
                return
                
            # 获取当前证书的SNI
            current_sni = check_result.get('sni', '[]')
            try:
                current_sni = json.loads(current_sni)
            except json.JSONDecodeError:
                logging.error("解析当前证书SNI失败")
                return
                
            # 处理相同SNI的证书
            process_same_sni_certs(api, parsed_certs, current_sni, check_result.get('id'))
            
            # 保存新的哈希值
            save_cert_hash(new_cert_hash, new_key_hash)
            logging.info("证书更新完成")
        else:
            logging.info("证书文件未发生变化,无需更新")
            
    except Exception as e:
        logging.error(f"程序执行出错: {str(e)}", exc_info=True)

if __name__ == "__main__":
    main()

添加定时任务,每天,或者每几天:

0 2 * * * /usr/bin/python3 /home/soft/baby-nanqiang-cert-tools/site_cert_auto_update_tool.py >> /home/soft/baby-nanqiang-cert-tools/web_cert_manager.log 2>&1

最终效果:

The post 南墙 WAF 系列(二)– 网站证书自动更新 appeared first on obaby@mars.

  •  

南墙 WAF 系列(一)– 管理后台证书自动更新

✇obaby
作者obaby

管理后台这个东西,当然可以不在公网暴露,但是如果一旦在公网允许访问配置,此时就会出现一个很尴尬的问题,各种证书错误提示。

在上一篇文章提过,waf 是通过 docker 部署的。相对来说管理倒是也算方便,按照官方文档的说法,管理后台的证书在下面的位置:

南墙管理后台的配置位于/uuwaf/web/conf/config.json中,addr字段值即为ip地址和端口。替换SSL证书可以替换/uuwaf/web/conf/目录中的server.crt和server.key文件,之后执行systemctl restart uuwaf重启服务使配置生效。

那么要更新证书,只需要解决下面几个问题就行了,由于不想付费买证书,那么现在最好的思路就是直接通过 acme.sh 自动申请证书,让后写个小工具自动将相关的文件复制到指定的目录下,重启 docker 服务就可以了。

1.acme.sh 自动申请证书。

a.安装 acme.sh:

curl https://get.acme.sh | sh -s email=my@example.com

b.配置 dnspod的 api key 和 secret(使用子账号):

创建策略,输入以下内容保存:

{
 "statement": [
     {
         "action": [
             "dnspod:DescribeRecordFilterList",
             "dnspod:DescribeRecordList",
             "dnspod:CreateRecord",
             "dnspod:DeleteRecord"
         ],
         "effect": "allow",
         "resource": [
             "*"
         ]
     }
 ],
 "version": "2.0"
}

登录 腾讯云控制台,进入 访问管理 页面,单击左侧菜单栏的 用户列表,进入用户列表页面,并单击新建用户

创建 api 访问账号之后,写一个获取证书的脚本:

export Tencent_SecretId="key"
export Tencent_SecretKey="secret"
"/usr/local/acme.sh"/acme.sh --issue --dns dns_tencent -d lang.bi -d *.lang.bi

到这里第一步就完成了,不过需要注意的事有的扩展名不支持,例如 by,本来想用 oba.by 域名的,结果提示失败了:

sh cert_get.sh 
[Wed Apr  2 08:51:41 AM CST 2025] Using CA: https://acme.zerossl.com/v2/DV90
[Wed Apr  2 08:51:41 AM CST 2025] Account key creation OK.
[Wed Apr  2 08:51:41 AM CST 2025] No EAB credentials found for ZeroSSL, let's obtain them
[Wed Apr  2 08:51:43 AM CST 2025] Registering account: https://acme.zerossl.com/v2/DV90
[Wed Apr  2 08:52:14 AM CST 2025] Registered
[Wed Apr  2 08:52:14 AM CST 2025] ACCOUNT_THUMBPRINT='mri378DxKFRt5hzNd_P7HBLV1zo4c7n1g7HBVNAKG-s'
[Wed Apr  2 08:52:14 AM CST 2025] Creating domain key
[Wed Apr  2 08:52:14 AM CST 2025] The domain key is here: /root/.acme.sh/oba.by_ecc/oba.by.key
[Wed Apr  2 08:52:14 AM CST 2025] Multi domain='DNS:oba.by,DNS:*.oba.by'
[Wed Apr  2 08:52:16 AM CST 2025] Error creating new order. Le_OrderFinalize not found. {"type":"urn:ietf:params:acme:error:rejectedIdentifier","status":400,"detail":"DNS identifier is disallowed [oba.by]"}
[Wed Apr  2 08:52:16 AM CST 2025] Please add '--debug' or '--log' to see more information.
[Wed Apr  2 08:52:16 AM CST 2025] See: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh
h4ck# vim cert_get.sh
h4ck# sh cert_get.sh 
[Wed Apr  2 08:54:34 AM CST 2025] Using CA: https://acme.zerossl.com/v2/DV90
[Wed Apr  2 08:54:34 AM CST 2025] Multi domain='DNS:oba.by,DNS:www.oba.by,DNS:nas.oba.by'
[Wed Apr  2 08:55:23 AM CST 2025] Error creating new order. Le_OrderFinalize not found. {"type":"urn:ietf:params:acme:error:rejectedIdentifier","status":400,"detail":"DNS identifier is disallowed [oba.by]"}
[Wed Apr  2 08:55:23 AM CST 2025] Please add '--debug' or '--log' to see more information.
[Wed Apr  2 08:55:23 AM CST 2025] See: https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh

2.复制文件重启服务

至于第二步就更简单了,直接让 cursor 给写一个:

参考下面的内容给我编写一个 Python3 的文件,实现以下功能:首先需要调用 get_cert.sh 脚本,自动获取证书文件文件,如果获取成功,证书会保存在下面的路径:/root/.acme.sh/lang.bi_ecc/fullchain.cer 私钥会保存在下面的路径/root/.acme.sh/lang.bi_ecc/lang.bi.key;获取到这两个文件之后,需要根据判断证书文件是否变化(记录旧证书内容,用于判断文件变更),如果变化则需要更新对应 docker 下的证书和私钥文件;docker 对应container Id 为f7dd0b0a990b,对应的证书文件路径为/uuwaf/web/conf/目录中的server.crt和server.key文件,在替换文件之后,需要重启对应的 docker 容器。按照步骤实现上面的内容,并且完成代码编写

最终代码:

#!/usr/bin/env python3
import os
import subprocess
import hashlib
import json
from pathlib import Path

# Configuration
DOCKER_CONTAINER_ID = "f7dd0b0a990b"
CERT_SOURCE_DIR = "/root/.acme.sh/lang.bi_ecc"
CERT_DEST_DIR = "/uuwaf/web/conf"
CERT_FILE = "fullchain.cer"
KEY_FILE = "lang.bi.key"
DEST_CERT_FILE = "server.crt"
DEST_KEY_FILE = "server.key"
HASH_FILE = "cert_hash.json"
CERT_SCRIPT = "get_cert.sh"

def get_cert_hash(file_path):
    """Calculate SHA-256 hash of a file."""
    sha256_hash = hashlib.sha256()
    with open(file_path, "rb") as f:
        for byte_block in iter(lambda: f.read(4096), b""):
            sha256_hash.update(byte_block)
    return sha256_hash.hexdigest()

def save_cert_hash(cert_hash):
    """Save certificate hash to a JSON file."""
    with open(HASH_FILE, 'w') as f:
        json.dump({'cert_hash': cert_hash}, f)

def load_cert_hash():
    """Load certificate hash from JSON file."""
    try:
        with open(HASH_FILE, 'r') as f:
            data = json.load(f)
            return data.get('cert_hash')
    except (FileNotFoundError, json.JSONDecodeError):
        return None

def run_get_cert_script():
    """Run the get_cert.sh script."""
    script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CERT_SCRIPT)
    
    # Check if script exists
    if not os.path.exists(script_path):
        print(f"Error: {CERT_SCRIPT} not found in the current directory")
        print(f"Expected path: {script_path}")
        return False
    
    # Check if script is executable
    if not os.access(script_path, os.X_OK):
        print(f"Error: {CERT_SCRIPT} is not executable")
        print("Attempting to make it executable...")
        try:
            os.chmod(script_path, 0o755)
            print("Successfully made the script executable")
        except Exception as e:
            print(f"Failed to make script executable: {str(e)}")
            return False
    
    try:
        # Use absolute path to the script
        result = subprocess.run(['sh', script_path], capture_output=True, text=True)
        if result.returncode == 0:
            print("Certificate generation successful")
            return True
        else:
            print(f"Certificate generation ignored: {result.stderr}")
            return True
    except Exception as e:
        print(f"Error running {CERT_SCRIPT}: {str(e)}")
        return False

def copy_cert_files():
    """Copy certificate files to Docker container."""
    try:
        # Copy certificate
        subprocess.run([
            'docker', 'cp',
            f"{CERT_SOURCE_DIR}/{CERT_FILE}",
            f"{DOCKER_CONTAINER_ID}:{CERT_DEST_DIR}/{DEST_CERT_FILE}"
        ], check=True)
        
        # Copy private key
        subprocess.run([
            'docker', 'cp',
            f"{CERT_SOURCE_DIR}/{KEY_FILE}",
            f"{DOCKER_CONTAINER_ID}:{CERT_DEST_DIR}/{DEST_KEY_FILE}"
        ], check=True)
        
        print("Certificate files copied successfully")
        return True
    except subprocess.CalledProcessError as e:
        print(f"Error copying files: {str(e)}")
        return False

def restart_docker_container():
    """Restart the Docker container."""
    try:
        subprocess.run(['docker', 'restart', DOCKER_CONTAINER_ID], check=True)
        print("Docker container restarted successfully")
        return True
    except subprocess.CalledProcessError as e:
        print(f"Error restarting container: {str(e)}")
        return False

def main():
    # Step 1: Run get_cert.sh script
    if not run_get_cert_script():
        print("Failed to generate certificates")
        return

    # Step 2: Check if certificate files exist
    cert_path = os.path.join(CERT_SOURCE_DIR, CERT_FILE)
    key_path = os.path.join(CERT_SOURCE_DIR, KEY_FILE)
    
    if not (os.path.exists(cert_path) and os.path.exists(key_path)):
        print("Certificate files not found")
        return

    # Step 3: Calculate new certificate hash
    new_cert_hash = get_cert_hash(cert_path)
    old_cert_hash = load_cert_hash()

    # Step 4: Check if certificate has changed
    if new_cert_hash != old_cert_hash:
        print("Certificate has changed, updating...")
        
        # Copy new certificate files
        if copy_cert_files():
            # Restart Docker container
            if restart_docker_container():
                # Save new certificate hash
                save_cert_hash(new_cert_hash)
                print("Certificate update completed successfully")
            else:
                print("Failed to restart Docker container")
        else:
            print("Failed to copy certificate files")
    else:
        print("Certificate has not changed, no update needed")

if __name__ == "__main__":
    main()

除了运行sh 脚本有点问题,需要改一下,其他的基本都没啥问题。最终执行效果:

此时刷新页面,一切就都 ok 了:

The post 南墙 WAF 系列(一)– 管理后台证书自动更新 appeared first on obaby@mars.

  •  

逃之夭夭

✇obaby
作者obaby

就在上个周末,看李小白的话剧之前,在换票的地方领了张报名表。表示对于话剧有兴趣的可以报个名。

虽然不知道到底有没有用,但是想着,去看看也无所谓的态度,填写了报名表。

过了没几天,看到有人加微信,是青岛电视台的导演。简单的交流了一下,说邀请宝子去参加面试,最后时间定在了周日下午。

当然,还很贴心的给出了停车引导视频之类的。不过说实话,如果没有这个引导视频,真的找不到地方。曲里拐弯的,就给绕晕了。

然而,到了现场,发现小朋友还是挺多的,加起来有十来个。不过,按照之前发的通知来看,应该也快,毕竟就那么简单的几项。

加起来能有半个小时应该也就够了,然而,等一点半开场之后,现场导演简单介绍了一下情况,包括团队啊,演员啊,等等。最后说道考核,提到了初赛、复赛。一听瞬间头大了,今天竟然还有复赛,问题是本来我就是带宝子来打酱油的啊。这下算是上了贼船了,也不好意思直接说跑了。不知声溜了也不大合适。就这么熬着。

终于半个小时之后,进入了初赛环节。

十个小朋友依次自我介绍,表演才艺。宝子准备的是现代诗朗诵《找梦》,多数的小朋友都是朗诵,有两个唱歌的,还有一个跳《apt》的,说在幼儿园学的,不得不说,现在幼儿园也的确与时俱进了。

再往后是无实物临场发挥,一群小朋友去春游,表演路上遇到的各种情况,以及野餐。这一套下来,半个多小时又没了。就酱紫已经过了一个多小时,而现在我想的是赶快结束吧,我要跑了。

上去找地方去打网球,去了一个新的公园,结果没开放,宝子就嘟着嘴不开心的样子,现在我想的是尽快跑了,去打网球去。

结束后在外面等初试结果,又过了十分钟,出来公布结果,三个直接晋级的,剩下的进入加试环节。整体说起来就是全部都参与复试,复试的题目抽签决定,宝子抽了个《找作业本》,其他的题目还有《吃面条》,剩下的我忘了。

这个表演需要家长参与,于是跟宝子讨论了下具体的流程,整体结论就是一阵找:被窝里,柜子里,桌洞里,床底下等等,当然,最后在床底下找到了,扒拉出来就行。

不过鉴于是无实物表演,还有另外一个策略,我说 :“宝子,你就坐在那里,一边哼唧一边哭,说作业本没了,然后我去给你找”

反过来也不是不行,就是我坐在那里嚷嚷:“快找,快快快……马上要迟到了……”最后的结果就是打一仗。

在预演了好几个场景之后,第一场进去复试的还没结束,百无聊赖又想跑了。主要是呆着是真受罪啊,这酱油打的有点累。

在这件事情上,我成了孩子成长的绊脚石,应该鼓励孩子参与,而不是怂恿孩子快跑。

然而,想到现在周六已经没了,上午钢琴,下午网球,周日上午有可能带宝子继续练球。这周日再没了,那一周真的就没啥自己的时间了,所以也是真的想跑。

终于第一场复试的出来了,我看手里拿着一个类似书通知书的东西。看了下时间,半个小时又过去了,也是在是不想等了。站起来跟导演说了下自己还有事情,就带着宝子溜了。

到停车场找网球场,打电话问了几个,都是满场,最后还是回到了学校。

不过这边状态都不大行,自己腰疼,宝子也有点漫不经心。打了大约四十来分钟就停了,主要是天气也的确冷,风也大。就这样仓促的结束了第二场运动。

昨天上班的时候,看到班级群里老师发的照片,宝子参与校长杯的作文比赛,拿了个二等奖。

当时,看到照片的时候觉得真不错,聊胜于无嘛。

下班之后问宝子,你的奖状呢。对象说拿着拿着就拿没了。放学的时候东西的确有点多,书包、轮滑鞋、气球、奖状,嗯,最后就是奖状丢了。不得不说,这个丢的挺好的,啥都没见到。

这奖状也逃之夭夭了。

The post 逃之夭夭 appeared first on obaby@mars.

  •  

是 IPv6 吖 — V6 重生记

✇obaby
作者obaby

上周五的时候,在杜老师的聊天室聊天,hehe 提到一个关于家里 ipv6 地址申请的问题。这时候才想到自己家里的网络应该也是有 ipv6 的地址的。至于地址是不是公网的那就不知道了。

而至于想要弄这个东西,其实还有一个原因是 he.net 的 ipv6 徽章已经很久没更新了,还差最后一步 ipv6 only 的网络访问测试,而测试的域名就是 h4ck.org.cn。
IPv6 Certification Badge for obaby

为了通过这个测试,自然要折腾一下。通过之后,he说会免费邮寄一个 T 恤衫,尺码和地址都更新了。不过这跨国的快递,不知道能不能收到。

至于能不能收到,这就只能耐心等待啦。

远程登录路由器,直接访问 ip 地址,然后高级的一幕就出现了,竟然直接打开了路由器的登录页面:

那么也就是说联通在 v6 协议下竟然没有封禁 80 端口,这样的话我忽然就有了个大胆的想法。如果路由器将 v6 的映射打开,直接访问 v6 的地址,忽略证书错误。然后网站就顺利打开了:

既然如此,那么这一来也解决了自己的 cdn 流量超限的问题。

这个月流量超限之后,买了 100G 的扩展包,结果就用了四天就又没了。为了解决流量问题,文章中的视频,直接通过 url 转发了。而至于首页右下角的图片就直接去掉了。不知道是访问量还是神马问题,这些图片一天跑十几个 G 的流量。

然而,到现在就出现了另外几个问题,要想让网站直接在互联网上访问,没有任何的防护措施,的确感觉不怎么靠谱。

1.家里的 V6 地址也是动态的,需要能够动态更新 ipv6 的 AAAA 记录。

2.在家里的主机上安装 waf 系统,提供基础的防御功能。

3.其他的未知问题。

AAAA 记录

在测试的时候,添加 AAAA 记录,会因为存在 cname 记录而导致添加失败,AAAA 记录和 CNAME 记录有冲突,请先删除或暂停现有的 CNAME 记录后重试:

此时针对不同的线路分别添加解析就 ok 了:

那么,在这之后就来到了第二个问题,怎么获取本地的公网 ipv6地址。

最直接的想法是直接通过获取 ipv4 地址类似的写法,来获取 ipv6 的地址,让 cursor 给写了类似的代码:

def get_ipv6_by_httpbin():
    """通过 httpbin.org 获取 IPv6 地址"""
    url = 'https://api6.ipify.org?format=json'
    try:
        resp = request.urlopen(url=url, timeout=10).read()
        data = json.loads(resp.decode("utf-8"))
        if 'ip' in data:
            return data['ip']
        return None
    except Exception as e:
        logging.warning("get_ipv6_by_httpbin FAILED, error: %s", str(e))
        return None

def get_ipv6_by_icanhazip():
    """通过 icanhazip.com 获取 IPv6 地址"""
    url = 'https://icanhazip.com'
    try:
        resp = request.urlopen(url=url, timeout=10).read()
        ip = resp.decode("utf-8").strip()
        if regex_ipv6.match(ip):
            return ip
        return None
    except Exception as e:
        logging.warning("get_ipv6_by_icanhazip FAILED, error: %s", str(e))
        return None

def get_ipv6_by_ident_me():
    """通过 ident.me 获取 IPv6 地址"""
    url = 'https://v6.ident.me'
    try:
        resp = request.urlopen(url=url, timeout=10).read()
        ip = resp.decode("utf-8").strip()
        if regex_ipv6.match(ip):
            return ip
        return None
    except Exception as e:
        logging.warning("get_ipv6_by_ident_me FAILED, error: %s", str(e))
        return None

实际证明,代码写的不错,在自己的 mac 电脑上的确可以获取到 ipv6 的地址。

然而,在家里的服务器上却使用无法获取 ip 地址,所有 v6 协议的服务都是超时状态。搜索了一堆,问了一大圈的 ai,最终都没解决问题。

后来猜测是不是路由器的问题,于是重新登录路由器的 v6 配置页面,来回切换配置:

看网上有文章会所需要改为 slaac 模式,改过去之后无效,切换成原来的自动,继续沿用上面的配置。断线重连结果网络就好啦。注意,这两个 dns 是腾讯的 dns,不是联通默认的 dns。

然而,此时就出现了另外一个问题,直到这时候我才发现,获取到的这个地址是本地的 v6 地址,而不是路由器的 v6 地址,当然,更恐怖的是这个 v6 地址也是可以在互联网直接访问的。

那么怎么自动更新这个 dns 记录就成了问题,总不能自己去天天改啊。

问小杜无果之后,继续尝试通过路由或者 tracerout 的方式获取,最终都以失败告终。至此,简单的方法算是彻底没了招了,那么就剩下一条路了,之计通过路由器获取,然鹅,tplink 的企业路由器并没有开放相关的 api。只能自己去找接口。

结果在登录页面就被来了个下马威,获取到接口,让 cursor 写完代码之后登录不了。

看起来页面很简单不是,但是这个东西恶心的地方在于登录的密码是加密过得,直接使用明文密码是登录不了的。不过好在这个密码不是动态加密的,直接使用密码登录,截取登录的加密后密码进行登录就 ok 了。剩下的就是获取 ipv6 地址,更新 dnspod 的aaaa 记录:

tplink 相关代码:

import requests
import json
import urllib.parse

def login_tplink(ip, username, password):
    """
    Login to TP-Link router
    :param ip: Router IP address
    :param username: Login username
    :param password: Login password
    :return: Response from the router and stok if successful
    """
    url = f"http://{ip}/"
    
    headers = {
        'Accept': 'text/plain, */*; q=0.01',
        'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Content-Type': 'application/json; charset=UTF-8',
        'Origin': f'http://{ip}',
        'Pragma': 'no-cache',
        'Referer': f'http://{ip}/login.htm',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
        'X-Requested-With': 'XMLHttpRequest'
    }
    
    data = {
        "method": "do",
        "login": {
            "username": username,
            "password": password
        }
    }
    
    try:
        response = requests.post(
            url,
            headers=headers,
            json=data,
            verify=False
        )
        if response.status_code == 200:
            try:
                response_data = response.json()
                if 'stok' in response_data:
                    return response_data['stok']
            except json.JSONDecodeError:
                print("Failed to parse login response as JSON")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Login error occurred: {e}")
        return None

def get_network_info(ip, stok):
    """
    Get network information from TP-Link router
    :param ip: Router IP address
    :param stok: Session token from login
    :return: Network information response
    """
    url = f"http://{ip}/stok={stok}/ds"
    
    headers = {
        'Accept': 'text/plain, */*; q=0.01',
        'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Content-Type': 'application/json; charset=UTF-8',
        'Origin': f'http://{ip}',
        'Pragma': 'no-cache',
        'Referer': f'http://{ip}/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
        'X-Requested-With': 'XMLHttpRequest'
    }
    
    data = {
        "method": "get",
        "network": {
            "table": "if_info"
        }
    }
    
    try:
        response = requests.post(
            url,
            headers=headers,
            json=data,
            verify=False
        )
        return response
    except requests.exceptions.RequestException as e:
        print(f"Network info error occurred: {e}")
        return None

def get_wan1_pppoe_addresses(response_data):
    """
    Parse IPv4 and IPv6 addresses from network info response
    :param response_data: JSON response data
    :return: Dictionary containing IPv4 and IPv6 addresses
    """
    addresses = {
        'ipv4': None,
        'ipv6': None
    }
    
    try:
        if_info = response_data.get('network', {}).get('if_info', [])
        for interface in if_info:
            if 'wan1_pppoe' in interface:
                wan_data = interface['wan1_pppoe']
                if 'ipaddr' in wan_data:
                    addresses['ipv4'] = wan_data['ipaddr']
                if 'ip6addr' in wan_data:
                    addresses['ipv6'] = urllib.parse.unquote(wan_data['ip6addr'])
                break
    except Exception as e:
        print(f"Error parsing wan1_pppoe addresses: {e}")
    
    return addresses

def update_ipv6_nat_mapping(ip, stok, dest_ip):
    """
    Update IPv6 NAT mapping on TP-Link router
    :param ip: Router IP address
    :param stok: Session token from login
    :param dest_ip: Destination IPv6 address
    :return: Response from the router
    """
    url = f"http://{ip}/stok={stok}/ds"
    
    headers = {
        'Accept': 'text/plain, */*; q=0.01',
        'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Content-Type': 'application/json; charset=UTF-8',
        'Origin': f'http://{ip}',
        'Pragma': 'no-cache',
        'Referer': f'http://{ip}/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36',
        'X-Requested-With': 'XMLHttpRequest'
    }
    
    # URL encode the IPv6 address
    encoded_dest_ip = urllib.parse.quote(dest_ip)
    
    data = {
        "method": "set",
        "firewall": {
            "redirect_4313056213": {
                "name": "mac_server_v6",
                "ip_proto": "IPv6",
                "if": ["WAN"],
                "src_dport": "443",
                "dest_port": "443",
                "dest_ip": encoded_dest_ip,
                "proto": "ALL",
                "loopback_ipaddr": "",
                "enable": "on",
                "src_dport_start": "65536",
                "src_dport_end": "65536",
                "dest_port_start": "65536",
                "dest_port_end": "65536"
            }
        }
    }
    
    try:
        response = requests.post(
            url,
            headers=headers,
            json=data,
            verify=False
        )
        return response
    except requests.exceptions.RequestException as e:
        print(f"Error updating IPv6 NAT mapping: {e}")
        return None

if __name__ == "__main__":
    # Disable SSL warnings
    requests.packages.urllib3.disable_warnings()
    
    # Router credentials
    ip = '192.168.1.1'
    username = 'obaby'
    password = '123456***加密后密码'
    
    # First login to get stok
    stok = login_tplink(ip, username, password)
    
    if stok:
        print(f"Login successful! Got stok: {stok}")
        
        # Get network information using the stok
        network_response = get_network_info(ip, stok)
        
        if network_response:
            try:
                response_data = network_response.json()
                addresses = get_wan1_pppoe_addresses(response_data)
                
                print("\nWAN1 PPPoE Addresses:")
                if addresses['ipv4']:
                    print(f"IPv4: {addresses['ipv4']}")
                if addresses['ipv6']:
                    print(f"IPv6: {addresses['ipv6']}")
                    
                    # Update NAT mapping with the IPv6 address
                    nat_response = update_ipv6_nat_mapping(ip, stok, addresses['ipv6'])
                    if nat_response:
                        print(f"NAT mapping update response: {nat_response.text}")
                    else:
                        print("Failed to update NAT mapping")
            except json.JSONDecodeError:
                print("Failed to parse network response as JSON")
        else:
            print("Failed to get network information")
    else:
        print("Login failed!")

至此第一个问题解决了。

开始第二个小问题,更新 aaaa 记录,这个就比较简单了,直接让 curosr 写就行了:

def get_record_id(domain, sub_domain, record_type='A', record_line='默认'):
    """获取记录ID,支持A和AAAA记录,以及不同的记录线路"""
    url = 'https://dnsapi.cn/Record.List'
    params = parse.urlencode({
        'login_token': cfg['login_token'],
        'format': 'json',
        'domain': domain
    })
    req = request.Request(url=url, data=params.encode('utf-8'), method='POST', headers=header())
    try:
        resp = request.urlopen(req).read().decode()
    except (error.HTTPError, error.URLError, socket.timeout):
        return None
    records = json.loads(resp).get('records', {})
    for item in records:
        if (item.get('name') == sub_domain and 
            item.get('type') == record_type and 
            item.get('line') == record_line):
            return item.get('id')
    return None

def update_ipv6_record(current_ipv6):
    """更新IPv6记录,支持多个记录和不同的记录线路"""
    ipv6_count = int(cfg.get('ipv6_count', '1'))
    ipv6_pool = cfg.get('ipv6_pool', '').split(',')[:ipv6_count]
    cfg['current_ipv6'] = current_ipv6
    
    if current_ipv6 not in ipv6_pool:
        logging.info("new IPv6 found: %s", current_ipv6)
        ipv6_pool.insert(0, current_ipv6)
        cfg['ipv6_pool'] = ','.join([str(x) for x in ipv6_pool[:ipv6_count]])
        
        # 获取所有需要更新的AAAA记录配置
        aaaa_records = cfg.get('aaaa_records', '').split(',')
        for record in aaaa_records:
            if not record.strip():
                continue
            try:
                sub_domain, record_line = record.strip().split(':')
                if update_record('AAAA', current_ipv6, record_line, sub_domain):
                    logging.info(f"成功更新AAAA记录: {sub_domain}.{cfg['domain']} 线路: {record_line}")
                else:
                    logging.error(f"更新AAAA记录失败: {sub_domain}.{cfg['domain']} 线路: {record_line}")
            except ValueError:
                logging.error(f"无效的AAAA记录配置: {record}")
        save_config()
    else:
        logging.info('IPv6 地址无变化,跳过更新')

到这里网站就能正常访问了。

WAF:雷池&南墙

至于 waf 系统,其实自己之前也没怎么系统了解过,也是杜老师推荐了这两个。首先尝试的是雷池,也是杜老师最开始推荐的。

雷池:

个人版是免费的,相对来说配置也比较简单。

官网地址:https://waf-ce.chaitin.cn

自动安装一行命令即可:

bash -c "$(curl -fsSLk https://waf-ce.chaitin.cn/release/latest/manager.sh)"

安装为 docker 模式,相对来说侵入性比较小一些。并且不需要占用 80,443 端口,这一点其实相对比南墙安装配置要求要低一些。

安装之后就可以通过 9443 端口登录了。相关功能示例:

系统概览,不知道是不是因为是 v6 地址的原因,左侧地图都是空白的。

同样,这个地球上也是空白的,底部的功能都需要专业版才能查看

防护模块是全部可用的

加强防御需要专业版

通用配置模块也是 ok 的。

整体来说安装过程比较顺畅也没遇到什么问题,不过访问 ip 由于是通过路由转发进来的需要从 x-forward-for中取这个信息。

南墙

开源免费的 waf 系统

官网地址:https://waf.uusec.com/#/

在使用过程中遇到点问题,不过最后在他们的技术帮助下顺利解决了。在安装之后,首先遇到的问题就是获取的 ip 地址有问题,都是本地的地址。并且不管怎么选择地址,最后都是同一个 ip 地址。

使用测速工具测速之后,ip 地址还是一个,这肯定是有问题的。在群里问了一下,给了个指令修复这个问题:

firewall-cmd --permanent --zone=internal --change-interface=docker0
systemctl restart firewalld && systemctl daemon-reload && systemctl restart docker

不过这么执行之后可能会出现的问题就是所有的服务都访问不了了,需要在 public 区域重新开放相关服务:

sudo firewall-cmd --zone=public --permanent --add-port=10043/tcp
sudo firewall-cmd --zone=public --permanent --add-port=14443/tcp
sudo firewall-cmd --zone=public --permanent --add-port=880/tcp
sudo firewall-cmd --zone=public --permanent --add-port=3306/tcp
sudo firewall-cmd --zone=public --permanent --add-port=9443/tcp
sudo firewall-cmd --zone=public --permanent --add-port=8443/tcp

其他需要开放的服务和端口自行添加即可。

然而,这个命令并没有解决问题。包括卸载重装,其实重装这件事情对我来说有些麻烦,因为服务器的默认 80 和 443 都映射到公网了,如果直接改了也比较麻烦,只能去工控机上停掉 80 的监听,443 的修改端口重新添加映射,毕竟这台主机上相对服务少一些。

重新安装依然没解决问题,这时候提议安装主机版。

然而,更尴尬的事情粗线了,那就是主机版不支持 ubuntu,只能作罢继续使用 docker 版本。

并且安装主机版,需要提前备份数据库,安装脚本会重装 mysql。这一点一定要注意!

这时候管理员提议远程协助,于是将端口映射出去,提供账号密码,等管理员修复。

管理说可能是映射的问题,然而,雷池的没问题,那么说明一定是有解决办法的,管理提到 docker 的网络配置不一样的,于是提议修改网络配置。

最终,亲爱的管理员,成功的修复了问题:

这样这个问题算是解决了,整体而言,感觉雷池的在 v6 测速的时候更绿一些。

好啦,相对来说雷池基本所有的模块都是开放的,除了机器学习部分:

安全态势

系统信息

用户管理

日志

证书管理,这个证书管理直接上传即可,不需要去进行绑定。

cdn 加速,其实感觉更像缓存配置。

规则管理

网站管理,得添加多个。

整体来说体验还是不错的,然而,刚才去看了配置文件感觉还是 bridge 啊。奇怪了。

不过既然问题解决了,那也就不纠结了。

官方文档说明:

https://waf.uusec.com/#/guide/problems?id=%f0%9f%8d%8b-%e5%a6%82%e4%bd%95%e8%a7%a3%e5%86%b3%e5%8d%97%e5%a2%99docker%e7%89%88%e8%8e%b7%e5%8f%96%e7%9a%84%e5%ae%a2%e6%88%b7%e7%ab%afip%e4%b8%ba172%e7%9a%84%e9%97%ae%e9%a2%98%ef%bc%9f

 

🍋 解决部分南墙容器版获取的客户端ip为网桥ip的问题?

1.将Docker网桥加入到防火墙的internal区域,以便获取到真实的IP地址, 假设Docker网桥名称为docker0

firewall-cmd --permanent --zone=internal --change-interface=docker0
systemctl restart firewalld && systemctl daemon-reload && systemctl restart docker

2.如果方法1无效,可以修改docker-compose.yml文件,将uuwaf容器的网络设置为network_mode: host,同时修改数据库连接环境变量UUWAF_DB_DSN中的wafdb为127.0.0.1,并映射wafdb容器的3306端口,重启后生效。

 

其他问题

鉴于主机获取的 ipv6 地址能直接访问,其实我一度想直接把主机的地址更新到 dns aaaa 记录上,但是这么一搞,暴露主机的确不是我最终想要的。

于是想着映射本地的链路地址,然而,端口映射通过链路地址通过路由器的 v6 地址却打不开网站,但是这个链路地址在内网的主机上又能打开网站,于是只能放弃这个做法。获取 ipv6 地址的代码:

import re
import logging
import json
import subprocess
import socket
import os
from urllib import request, error, parse

# 匹配合法 IPv6 地址
regex_ipv6 = re.compile(
    r"(?:inet6\s+)?(fe80:[0-9a-fA-F:]+|"  # 特别处理链路本地地址格式
    + r"(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|"  # 标准格式
    + r"(?:[0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4}|"  # 压缩格式
    + r"(?:[0-9a-fA-F]{1,4}:){5}(?::[0-9a-fA-F]{1,4}){1,2}|"
    + r"(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,3}|"
    + r"(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,4}|"
    + r"(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,5}|"
    + r"(?:[0-9a-fA-F]{1,4}:){1}(?::[0-9a-fA-F]{1,4}){1,6}|"
    + r"(?::[0-9a-fA-F]{1,4}){1,7}|"
    + r"::"
    + r")")

# 特别匹配链路本地 IPv6 地址,确保能匹配到 fe80:: 开头的地址
regex_link_local_ipv6 = re.compile(r"inet6\s+(fe80:[0-9a-fA-F:]+)")

def get_ipv6():
    """获取公网 IPv6 地址,使用多个备选方法"""
    return (get_ipv6_by_ifconfig()  # 优先使用本地接口地址
        or get_ipv6_by_httpbin()
        or get_ipv6_by_icanhazip()
        or get_ipv6_by_ident_me()
        or get_ipv6_by_socket())

def get_ipv6_by_ifconfig():
    """通过 ifconfig 命令获取本地 IPv6 地址"""
    try:
        # Windows 系统使用 ipconfig
        if os.name == 'nt':
            cmd = ['ipconfig']
            output = subprocess.check_output(cmd, text=True)
        # Linux/Unix 系统使用 ifconfig
        else:
            cmd = ['ifconfig']
            output = subprocess.check_output(cmd, text=True)
            
        # 按行分割输出
        lines = output.split('\n')
        for line in lines:
            # 查找包含 inet6 的行
            if 'inet6' in line:
                # 使用正则表达式提取 IPv6 地址
                matches = regex_ipv6.findall(line)
                if matches:
                    ipv6 = matches[0]
                    # 排除本地回环地址
                    if not ipv6.startswith('::1'):
                        logging.info(f"Found IPv6 address: {ipv6}")
                        return ipv6
    except Exception as e:
        logging.warning("get_ipv6_by_ifconfig FAILED, error: %s", str(e))
    return None

def get_ipv6_by_socket():
    """通过 Python socket 库获取本地 IPv6 地址"""
    try:
        # 创建一个 IPv6 socket
        s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
        # 连接到一个外部地址(这里使用 Google 的 IPv6 DNS)
        s.connect(('2001:4860:4860::8888', 80))
        # 获取本地地址
        local_addr = s.getsockname()[0]
        s.close()
        return local_addr
    except Exception as e:
        logging.warning("get_ipv6_by_socket FAILED, error: %s", str(e))
        return None

def get_ipv6_by_httpbin():
    """通过 httpbin.org 获取 IPv6 地址"""
    url = 'https://api6.ipify.org?format=json'
    try:
        resp = request.urlopen(url=url, timeout=10).read()
        data = json.loads(resp.decode("utf-8"))
        if 'ip' in data:
            return data['ip']
        return None
    except Exception as e:
        logging.warning("get_ipv6_by_httpbin FAILED, error: %s", str(e))
        return None

def get_ipv6_by_icanhazip():
    """通过 icanhazip.com 获取 IPv6 地址"""
    url = 'https://icanhazip.com'
    try:
        resp = request.urlopen(url=url, timeout=10).read()
        ip = resp.decode("utf-8").strip()
        if regex_ipv6.match(ip):
            return ip
        return None
    except Exception as e:
        logging.warning("get_ipv6_by_icanhazip FAILED, error: %s", str(e))
        return None

def get_ipv6_by_ident_me():
    """通过 ident.me 获取 IPv6 地址"""
    url = 'https://v6.ident.me'
    try:
        resp = request.urlopen(url=url, timeout=10).read()
        ip = resp.decode("utf-8").strip()
        if regex_ipv6.match(ip):
            return ip
        return None
    except Exception as e:
        logging.warning("get_ipv6_by_ident_me FAILED, error: %s", str(e))
        return None

def get_link_local_ipv6():
    """专门获取链路本地 IPv6 地址"""
    try:
        # Windows 系统使用 ipconfig
        if os.name == 'nt':
            cmd = ['ipconfig']
            output = subprocess.check_output(cmd, text=True)
        # Linux/Unix 系统使用 ifconfig
        else:
            cmd = ['ifconfig']
            output = subprocess.check_output(cmd, text=True)
            
        # 按行分割输出
        lines = output.split('\n')
        for line in lines:
            # 查找包含 inet6 的行
            if 'inet6' in line:
                # 提取 IPv6 地址和 prefixlen
                if 'prefixlen 64' in line and 'scopeid 0x20<link>' in line:
                    # 调试输出
                    logging.debug(f"Processing line: {line}")
                    
                    # 使用特定正则表达式提取链路本地 IPv6 地址
                    matches = regex_link_local_ipv6.findall(line)
                    if matches:
                        ipv6 = matches[0]
                        logging.info(f"Found link-local IPv6 address with new regex: {ipv6}")
                        return ipv6
                    
                    # 如果特定正则表达式没有匹配到,尝试使用一般性正则表达式
                    matches = regex_ipv6.findall(line)
                    if matches:
                        ipv6 = matches[0]
                        logging.debug(f"Original regex matched: {ipv6}")
                        # 只返回链路本地地址
                        if ipv6.startswith('fe80'):
                            logging.info(f"Found link-local IPv6 address with original regex: {ipv6}")
                            return ipv6
                        elif 'fe80' in line:
                            # 如果行中包含fe80但匹配失败,记录错误
                            logging.warning(f"Regex failed to match fe80 in: {line}")
    except Exception as e:
        logging.warning("get_link_local_ipv6 FAILED, error: %s", str(e))
    return None

# 测试
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
    
    # 测试特定的 IPv6 地址匹配
    test_line = "inet6 fe80::e4d:e9ff:fec9:9de3  prefixlen 64  scopeid 0x20<link>"
    print("Testing regex with line:", test_line)
    
    # 测试链路本地特定正则
    matches = regex_link_local_ipv6.findall(test_line)
    if matches:
        print("Link-local regex matched:", matches[0])
    else:
        print("Link-local regex failed to match")
    
    # 测试一般 IPv6 正则
    matches = regex_ipv6.findall(test_line)
    if matches:
        print("General IPv6 regex matched:", matches[0])
    else:
        print("General IPv6 regex failed to match")
    
    print("\n--- Regular program output ---")
    print("Method 1 (httpbin):", get_ipv6_by_httpbin())
    print("Method 2 (icanhazip):", get_ipv6_by_icanhazip())
    print("Method 3 (ident.me):", get_ipv6_by_ident_me())
    print("Method 4 (ifconfig):", get_ipv6_by_ifconfig())
    print("Method 5 (socket):", get_ipv6_by_socket())
    print("Link-local IPv6:", get_link_local_ipv6())

获取到 v6 地址,就可以通过 tplink 的映射代码update_ipv6_nat_mapping进行地址映射了。

如果要用这个代码,需要根据自己的路由器配置获取映射的 id。

整体来说,速度还是可以的: 

 

The post 是 IPv6 吖 — V6 重生记 appeared first on obaby@mars.

  •