昨天发现Debian服务器上的Aria2居然不能下载https的链接,才发现编译安装时,忘了设置开启SSL的参数。还是记录一下,以免后面又犯错了。

关于编译安装的教程,可以直接查看官方说明:
https://github.com/aria2/aria2/blob/master/README.rst
https://aria2.github.io/manual/en/html/README.html#how-to-build

1. 安装相关依赖

详见官方文档。注意的是,Linux上,开启SSL,要安装openssllibssl-dev

2. 编译

官方文档已经很详细了,总结脚本如下:

$ git clone https://github.com/aria2/aria2.git
$ cd aria2
$ ./configure --without-gnutls --with-openssl
$ make
$ sudo cp ./src/aria2c /usr/local/bin/

3. 部署服务

关于Aria2配置文件的说明,参考官方文档:
https://aria2.github.io/manual/en/html/aria2c.html#aria2-conf
示例配置文件如下(参考路径:/etc/aria2/aria2.conf):

#OPTIONS
#下载路径
dir=/opt/aria2_download
#log路径
log=/var/log/aria2/aria2.log
#log-level: debug, info, notice, warn or error
log-level=warn
console-log-level=warn
#session
input-file=/var/cache/aria2/aria2.session
#最大下载数,默认5
max-concurrent-downloads=5
#校验完整性,只在bt下有效果,默认false
check-integrity=true
#断点续传
continue=true
 
#HTTP/FTP/SFTP Options
#同时连接的服务器数量,默认1
max-connection-per-server=5
#最大尝试次数,默认5
max-tries=20
#最小文件分割大小,默认20M
#min-split-size=20M
#单个文件最大线程,默认5
#split=5
#超时时间,默认60
#timeout=60
 
#BitTorrent Specific Options
#启用本地发现
bt-enable-lpd=true
#hash校验种子,默认true
bt-hash-check-seed=true
#最大打开文件数量,默认100
bt-max-open-files=200
#单个种子最大连接数
bt-max-peers=100
#在磁力下载中,保留torrent文件
bt-save-metadata=true
#监听端口,默认6881-6999
#listen-port=6881-6999
#最大上传限制,0是无限制
max-overall-upload-limit=100K
#下载完成后做种的设置
seed-ratio=1.0
seed-time=120
#bt-tracker=需要相关的服务地址
 
#RPC Options
#启用rpc
enable-rpc=true
#允许所有访问
rpc-allow-origin-all=true
#监听所有网络
rpc-listen-all=true
#监听端口
rpc-listen-port=6800
#rcp保存上传的元数据,默认false
rpc-save-upload-metadata=true
 
#Advanced Options
#下载时覆盖已经存在的文件,默认false
allow-overwrite=false
#此选项为true可能会导致下载进度丢失,默认false
allow-piece-length-change=true
#总是尝试恢复下载,默认true
always-resume=true
#指定dns服务器
#async-dns=true
#async-dns-server=8.8.4.4,208.67.222.222
#如果文件存在,自动重命名,仅适用于http,ftp
auto-file-renaming=true
#自动保存间隔,控制文件保存在.aria2中
auto-save-interval=60
#作为守护进程启用
daemon=true
#禁用ipv6
disable-ipv6=true
#磁盘缓存,默认16M
disk-cache=16M
#文件是否启用预先分配,默认prealloc
file-allocation=falloc
#最大下载结果在内存中保留数量,默认1000
max-download-result=500
#最大失败重试次数,默认0
max-resume-failure-tries=0
#下载完成时候执行的脚本
#on-bt-download-complete=/etc/aria2/on-bt-download-complete
#on-download-complete=/etc/aria2/on-download-complete
#on-download-error=/etc/aria2/on-download-error
#总体下载速度限制
max-overall-download-limit=1024K
#单个下载最大速度限制
max-download-limit=1024K
#保存下载进度,很有用的配置
save-session=/var/cache/aria2/aria2.session
#保存间隔,默认0
save-session-interval=60

# token验证
rpc-secret=123456

Systemd的服务配置文件(参考路径:/etc/systemd/system/aria2.service):

[Unit]
Description=Aria2 Service
After=network.target

[Service]
Type=forking
User=www-data
Group=www-data
WorkingDirectory=/var/cache/aria2
ExecStart=/usr/local/bin/aria2c --conf-path=/etc/aria2/aria2.conf -D
ExecReload=/usr/bin/kill -HUP $MAINPID
RestartSec=1min
Restart=on-failure

[Install]
WantedBy=multi-user.target

4. 客户端

我用yaaw,纯静态页面,服务器上部署个Nginx即可:
https://github.com/binux/yaaw

前几天打开Twitter,发现Chrome(版本80)的地址栏右边出现了十字图标,并提示“Install”。点击安装后,桌面和Chrome Apps都新增了Twitter图标。再双击该图标,就会以窗口形式打开Twitter。窗口像本地应用,但细看,只是没有了地址栏的Chrome窗口。好奇之下,发现这是PWA的最新形态!

Progressive Web Apps,简PWA,就是把网页应用化,或者是利用网页技术开发的应用。这家伙的好处是,对于绝大部分的网站来说,加几个文件就在原来网页的基础上,完成了客户端的开发。第一次看到这个技术提案时,非常激动,简直就是我们网站的救星,不用考虑如何开发iOS和Android的应用了。但其困难的地方是,统一标准和普及的问题。虽然得到越来越多浏览器的支持,但是Safari不太积极。

关于PWA相关的知识和教程,都可以在这里找到:
Progressive Web Apps
https://web.dev/progressive-web-apps/

比较齐全的PWA资源:
awesome-pwa
https://github.com/hemanth/awesome-pwa

收集了一堆PWA的网站:
https://pwa.rocks/

值得推荐的是,对PWA技术很积极的Twitter:
https://twitter.com/

注意的是,要使PWA被浏览器识别为可安装,需要一个正方形的图标。这个在做入门实例时折腾了很久才发现。

后面还想试试结合WASM,看看能否做出更好玩的东西~

由于新冠肺炎疫情严重,国内推行在线教育,CFO下令入手个平板。找来找去,最后选择了小米平板1代。

本来个人偏好Nexus 9,8.9英寸2K屏,NVidia Tegra K1平台,64位CPU……重点是系统升级不是问题。但是发现屏幕普遍出现气泡的通病,比较难找到完美屏。有个谈到350RMB包邮了,因为迟了一点付款而错失了。无奈之际又看了一直心仪已久的联想Yoga平板,可惜还是贵。最后遇到这个小米平板1代,200RMB包邮,成色还不错。

说说小米平板的优点吧。大小适中,8英寸2K屏,NVidia Tegra K1平台,32位CPU,可插TF卡,有非官方的LineageOS 16(Android 9)可刷等等。

缺点嘛,只能买二手。电池肯定不耐用。32位CPU,虽然使用上没什么问题,但这个CPU(NVidia的GPU拖着CPU跑)还是慢。需要相机可用的话,目前只能刷LineageOS 14(Android 7.1.2),再上去就不能用相机了。自带外方(音箱)是垃圾。

总体来说,这个价格以及成色,加上续航能有4小时左右,已很超值了。就算作为儿童画板也不错。

记录一下刷机过程吧。

1)刷Recovery
TWRP for Xiaomi Mi Pad
https://twrp.me/xiaomi/xiaomimipad.html

刷机方法就不详细说了,一般adb或者fastboot命令都可以。但是发现原来TWRP的Recovery支持在刷img文件,包括Recovery。就是说,如果机器已经刷了TWRP,就可以进入Recovery刷最新的TWRP了。

2)刷ROM,LineageOS 14.1
由于LineageOS 15和16都不能使用相机,所以只能刷14了。详细介绍、教程,以及下载地址,见下文:
[UNOFFICIAL][14.1][7.1.2][2017-09-11] LineageOS 14.1 for Xiaomi MiPad (mocha)
https://forum.xda-developers.com/mi-pad/development/unofficial-lineageos-14-1-xiami-mipad-t3557616

进入Recovery里刷就可以了。该ROM的作者提到,小米平板原来是有两个640MB的系统分区,需要合并分区后再刷。可能我买的已经合并了,所以不能执行这个(二手的好处)。作者提供的链接已不能访问,找到一个翻译的:
【翻译】MiPad Mocha 合并分区教程
https://www.xiaomi.cn/post/4968452

3)刷Root包
刷入LineageOS 14的官方root包即可:
https://download.lineageos.org/extras

4)刷GApps,可选
由于是给小孩用的,没必要刷这个了。跳过。

5)装“冰箱”
由于机器只有2GB内存,所以有必要装个“冰箱”去冻结那些常驻后台的应用。由于安装后需要删除所有用户,再用adb设置“冰箱”的管理员权限,所以需要刷后系统后优先安装。免费版可以冻结10个应用,基本够用吧。下载地址:
冰箱 IceBox:自动冻结・省电神器
https://coolapk.com/apk/com.catchingnow.icebox

如果“冰箱”不能满足需求,可以考虑使用Shelter,免费无限制。但其实现方式是把应用安装到“工作资料”区域,会造成文件、数据互通不方便。由于是小孩使用的,就没搞那么复杂了。下载地址:
Shelter - Isolate your Big Brother Apps / Multiple Accounts
https://f-droid.org/en/packages/net.typeblog.shelter/

6)消除WiFi图标的叉
国情问题,按教程去做就可以了。参考这个:
(类) 原生 Android 网络去叉/叹号 Android 5.0-9.0
https://ericclose.github.io/Captive-Portal-Android.html

小高姐的视频是蒜香面包,但是我用了包汤圆留下的红豆沙做馅,所以标题取名“花朵面包”。这个面包的特点是颜值非常高,不需要揉面也很松软。

【小高姐】蒜香浓郁 造型优美 不用揉出手膜的面包
https://www.bilibili.com/video/av50801965

总结如下:
1)材料:
酵母 2克
温水 200克(我改为160克)
高筋面粉 200克
杜兰面粉 100克(用来增加香味,可以用麦香面粉或者直接用高筋面粉代替)
糖 12克
盐 2克
油 24克
蛋黄液 适量(扫面包表面)
红豆沙 一包(购买现成的)

2)做法:
a)室温水放入酵母,混合成酵母水。面粉中加入糖、盐、油,混合,再倒入酵母水。揉面到没有干面粉,饧面20分钟。再揉面1分钟,揉到光面。表面加油,饧面(发酵)1.5到2小时。排气,分成4等份。拿一份面团擀平,成圆形,大概手掌大小,放到烤盘纸上,上面铺一层红豆沙。依次再铺擀平的面团、红豆沙,直到最后一层面团。
b)中间按个圆圈(用碗底、杯底之类),沿着圆圈切8等份。取一份再切一刀,长一点,插入圆圈范围。两个手拿起刚切开的两小份,分别往外卷两圈,把后面捏起来再收回去,再轻轻把层次打散。剩下7个一样的做法。包上保鲜膜,发酵45分钟到1小时。这里文字描述不太清晰,直接看视频吧。
c)最后表面刷蛋黄液。烤箱预热,205摄氏度烤25到28分钟。

我这次的做法是材料份量减半,烤箱预热,190摄氏度烤15分钟。外形漂亮而且好吃,发到朋友圈获得不少赞。馅料除了蒜和红豆沙,好像还可以用其它,例如花生酱。

元宵节将近,利用空余时间做汤圆。以前做出来的面团总是很粘手,还是参考了小高姐的材料比例,才做出不粘手的面团。不粘手,才容易包。

【小高姐】酒酿芝麻汤圆 简单手法包汤圆
https://www.bilibili.com/video/av43721292

总结如下:
1)材料:
糯米粉 250克
热开水 140克
室温水 70克
馅料 购买现成的红豆沙

2)做法:
糯米中导入热开水,一边倒一边搅拌。再慢慢加入室温水,调整其用量。揉合成有光面的团,盖好放置半小时。然后搓成长条,平均切成3、4段,放到有盖容器以免风干。拿一段出来搓成直径与汤圆一致的长条,再切成小粒来包馅。按小高姐的做法,用拇指在小面团中间压下去,弄成小碗状,放入馅料,再包上即可。

3)煮:
个人比较喜欢用清水煮汤圆,更能突出馅料的甜味。水烧开后放入汤圆,等漂起来后就可以出锅开吃了。

一次包的太多,可以放到冰箱冷冻,想吃就可以直接拿来煮。

最近迷上小高姐的视频,容易上手且很实在的料理教程。昨天做了土豆面包,比较成功,于是今天就做了汉堡。说是汉堡,其实更像是三明治(Sandwich),用调过味的肥牛代替汉堡扒,加上生菜,也挺好吃。其实肉汁足够,就会有不错的口感了。

视频教程如下:
1)【小高姐】土豆面包 做汉堡的面包 免揉 一样柔软拉丝
https://www.bilibili.com/video/av55784486
2)【小高姐】牛肉汉堡包 汉堡肉饼多汁的秘密
https://www.bilibili.com/video/av56260910

土豆面包8个,总结如下:
1)材料:
黄皮土豆 150克(去皮后的重量)
牛奶 220克
酵母 2克
鸡蛋 1个
糖 25克
油 25克(个人喜欢20克)
高筋面粉 320克
有盐黄油 小许(刷面包表面)
注:懒得买牛奶,冲了杯奶粉代替

2)做法:
a)土豆削皮、切小块,微波炉热4~5分钟,弄成土豆泥。没微波炉可以蒸熟。
b)牛奶在微波炉热30秒,加一点到土豆泥,搅拌成糊,再倒入剩下的混合。混合物温度不高于体温,依次加入酵母、鸡蛋、糖、油,搅拌混合。最后加入高筋面粉,弄成面粉团。
c)饧面(发酵)2小时后,撒点干面粉,排气。取出切成8等份,各自进一步排气后再揉成球,放到烤盘的纸上。表面扫第一层黄油,盖上保鲜膜发酵45到60分钟,再扫第二层黄油。
d)烤箱预热,190摄氏度烤17到20分钟。

汉堡的做法:
做好的土豆面包放凉,中间切开,里面涂上黄油。再把涂有黄油的一面放到锅里煎一下,煎香即可。至于馅料,可以参考小高姐的视频,做个汉堡排加相关配料。我们直接用煎香的肥牛、自家种的生菜、煎蛋等,夹到面包里即可。

后记:本文涉及的Pizza是硬底,不推荐。但可以看看文中小高姐的软底做法。

昨天找出冰箱里的一堆材料,于是计划今天做Pizza。以前做过很多次,一直以Pizzahut的为目标,也没有实现满意的饼底。看过小高姐视频,才知道面团不是最重要,烘烤方法才是重点。虽然视频中的烘焙石板实力劝退,但是小高姐的视频很有启发性:
Youtube版:小高姐的 Magic Ingredients - 披萨做法 Pizza Margherita, Cheese Pizza and Steak Pizza
https://www.youtube.com/watch?v=ATEnM1YPQQE
Bilibili版:小高姐的魔法调料 - 详细的解说,带你做出真正经典的意大利披萨
https://www.bilibili.com/video/av35740109

说回正题,由于囤了高筋面粉,而且没有烘焙石板(Pizza石头),参考了这个12吋Pizza的面团做法:
奶牛小厨发消息 - 1分钟学会必胜客披萨,完美拉丝,芝士控千万别错过啊!
https://www.bilibili.com/video/av21021967

这个12吋Pizza总结如下:
1)材料:
高筋面粉 165克
温水 95克
酵母 2克
糖 一小勺
盐 2克

2)做法:
a)酵母加入温水、糖(糖是用于激活酵母)、盐搅拌至融化。再倒入面粉中,继续加温水搅拌成絮状,揉成团至表面光滑。盖上保鲜膜,醒发一个半小时左右,直到面团成两倍大。发酵过程注意室温不能太低,否则不能发大。然后拉伸到盘子大小再放到盘子上,放上其它材料。尽量拉薄,更容易烤熟。
b)预热到200摄氏度度的烤箱(上下烤),烤20分钟左右。注意不能烤太久,否则会变太硬,甚至烤焦。

做了个烤盘大小的Pizza,效果还不错。还放了Pizza香料,增加风味。

某天接触到某个用Golang实现的程序,不仅体积小,还支持几乎所有种类的CPU,于是下决心学习一下Go这门语言。虽然刚出来的时候就想学,但那时据说有很多坑(比如语法可能会变),就放弃了。现在连被成为垃圾的包管理也有了升级,觉得是时候去学。恰好要把一个二级域名绑定IP的小服务迁移到VPS上,于是干脆用Go重新实现(原来是用Python3)这个服务。

首先要阅读相关教程。初学者教程当然是官方入门教程:
https://tour.go-zh.org/

官方入门教程太简单(毕竟Go本身语法就是简单),还需要阅读其它相关知识:
1)Go搭建一个Web服务器
https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/03.2.md
2)golang读取json配置文件
[https://blog.csdn.net/benben_2015/article/details/79134734]
3)文件读写
https://wiki.jikexueyuan.com/project/the-way-to-go/12.2.html
4)go-extend,获取请求的IP的代码
https://github.com/thinkeridea/go-extend/blob/master/exnet/ip.go
5)golang 发送GET和POST示例
https://segmentfault.com/a/1190000013262746
6)GoDNS中dnspod客户端的代码
https://github.com/TimothyYe/godns/blob/master/handler/dnspod/dnspod_handler.go
7)Go语言(golang)的错误(error)处理的推荐方案
https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html

这个小服务,就是个web服务。客户端发起带有key和token的请求,此服务会验证有效的授权,然后把对应的二级域名与客户端IP绑定。配置信息以json格式保存在文本文件。客户端IP会记录在对应log文件,以方便每次比较客户端IP是否变化了。每次更新二级域名与IP的绑定,则会记录log。相关文件及代码如下:

配置文件,config.json:

{
"ServIpPort": ":12345"
,"DnsKey": "dnspod的key"
,"DnsToken": "dnspod的token"
,"DomainId": "dnspod的域名id"
,"SubDomainId": {"abc":"dnspod的二级域名id", "efg":"dnspod的二级域名id"}
,"Users": [
        {
                "Key":"client1"
                ,"Token":"aaa123456"
                ,"SubDomains": ["abc"]
        }
        ,{
                "Key":"client2"
                ,"Token":"xxx789012"
                ,"SubDomains": ["efg"]
        }
]
}

小服务的代码,ddnsServ.go:

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "net/url"
    "strings"
    "os"
    "path/filepath"
    "io/ioutil"
    "time"
    "encoding/json"
)

type User struct {
    Key string
    Token string
    SubDomains []string
}

type Config struct {
    ServIpPort string
    DnsKey string
    DnsToken string
    DomainId string
    SubDomainId map[string]string /*key:sub domain name, value:sub domain id*/
    Users []User
}

var (
    curPath string
    ipLogPath string
    historyPath string
    config Config
)

func init() {
    curPath, _ = filepath.Abs(filepath.Dir(os.Args[0])) 
    ipLogPath = curPath + "/ip"
    historyPath = curPath + "/log"
    for _, path := range []string{ipLogPath, historyPath} {
        if err := initPath(path); err != nil {
            fmt.Printf("Init failed. Error info:%s\n", err)
            os.Exit(-1)
            return
        }
    }

    if err := initConfig(curPath + "/config.json"); err != nil {
        fmt.Printf("Init failed. Error info:%s\n", err)
        os.Exit(-1)
        return
    }
}

func initPath(path string) error {
    s, err := os.Stat(path)
    if err == nil && !s.IsDir() {
        return fmt.Errorf("The path is existed, but it is not a directory! Path is:%s", path)
    }
    if err != nil && os.IsNotExist(err) {
        e := os.Mkdir(path, os.ModePerm)
        return e 
    }
    return nil
}

func initConfig(configPath string) error {
    configData, err := ioutil.ReadFile(configPath)
    if err != nil {
        return fmt.Errorf("Failed to read config file: %s! Error info: \n%s", configPath, err)
    }

    err = json.Unmarshal(configData, &config)
    if err != nil {
        return fmt.Errorf("Failed to load config data! Error info: \n%s", err)
    }
    return nil
}

// get client IP address 
func GetClientIP(r *http.Request) string {
    xForwardedFor := r.Header.Get("X-Forwarded-For")
    ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
    if ip != "" {
        return ip
    }

    ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
    if ip != "" {
        return ip
    }

    if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
        return ip
    }

    return ""
}

func GetLogFilePath(logPath string, subDomain string) string {
    return fmt.Sprintf("%s/%s.log", logPath, subDomain)
}

// get the file path which saved ip address of subDomain
func GetIpLog(subDomain string) string {
    path := GetLogFilePath(ipLogPath, subDomain)
    buf, err := ioutil.ReadFile(path)
    if err != nil {
        return ""
    }
    return string(buf)
}

func SaveIpLog(subDomain string, ip string) {
    path := GetLogFilePath(ipLogPath, subDomain)
    ioutil.WriteFile(path, []byte(ip), 0644)
    /*
    err := ioutil.WriteFile(path, []byte(ip), 0644)
    if err != nil {
        panic(err.Error())
    }
    */
}

func SaveHistoryLog(subDomain string, ip string) error {
    path := GetLogFilePath(historyPath, subDomain)
    logFile, err := os.OpenFile(path, os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0644)
    if err != nil {
        return fmt.Errorf("Failed to open history log file: %s! Error info: \n%s", path, err)
    }
    defer logFile.Close()

    nowStr := time.Now().Format("2006-01-02 15:04:05")
    log := fmt.Sprintf("%s, ip:%s\n", nowStr, ip)
    logByte := []byte(log)
    n, err := logFile.Write(logByte)
    if err == nil && n < len(logByte) {
        return fmt.Errorf("Failed to save history log file: %s! Error info: \nwrite file failed", path)
    }
    return nil
}

func UpdateDns(subDomain string, ip string) error {
    values := url.Values{}
    values.Add("login_token", config.DnsKey + "," + config.DnsToken)
    values.Add("format", "json")
    values.Add("lang", "en")
    values.Add("error_on_empty", "no")

    values.Add("domain_id", config.DomainId)
    values.Add("record_id", config.SubDomainId[subDomain])
    values.Add("sub_domain", subDomain)
    values.Add("record_type", "A")
    values.Add("record_line", "默认")
    values.Add("value", ip)

    client := &http.Client{}
    req, err := http.NewRequest("POST", "https://dnsapi.cn/Record.Modify", strings.NewReader(values.Encode()))
    if err != nil {
        // handle error
    }

    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    req.Header.Set("Accept", "text/json")
    resp, err := client.Do(req)

    defer resp.Body.Close()

    _, err2 := ioutil.ReadAll(resp.Body)
//    fmt.Println(string(s))
    if err2 != nil {
        return err2
    }
    return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Verify authorization
    var subDomains []string
    key := r.PostFormValue("key") // get key of POST
    token := r.PostFormValue("token") // get token of POST
    for _, user := range config.Users {
        if user.Key == key && user.Token == token {
            subDomains = user.SubDomains
            break
        }
    }
    if subDomains == nil || len(subDomains) <= 0 {
        w.WriteHeader(http.StatusNotFound)
        return
    }

    // get IP
    ip := GetClientIP(r)
    fmt.Fprintf(w, "%s", ip)

    for _, subDomain := range subDomains {
        // get last IP of subDomain
        ipLog := GetIpLog(subDomain) 
        if ip == ipLog {
            // IP is not changed
            continue
        }
    
        // update DNS, bind IP to subDomain
        err := UpdateDns(subDomain, ip)
        if err == nil {
            // update success, save new IP
            SaveIpLog(subDomain, ip)
            SaveHistoryLog(subDomain, ip)
        }
    }
}

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(config.ServIpPort, nil))
}

关于部署,就用nginx弄个反向代理,指向这个小服务的端口。由于VPS上装了Debian9,可以配置systemd来设置系统服务。

客户端只需发起post请求,把key和token发过来就可以了。以下用curl实现:

#!/bin/sh

#curl命令的参数解析:
# -X 请求的方法。这里用了POST,在HTTPS传输中,数据被加密
# --connect-timeout 连接超时时间,单位:秒
# -m/--max-time 数据传输的最大允许时间,单位:秒
# https://rpi.f...... 请求的URL
# -H/--header 请求头。要设置多个请求头,则设置多个-H参数
# -d/--data 请求数据。
curl -k -X POST --connect-timeout 5 -m 10 https://youdomain.xxx:12345/api/update_dns -H 'cache-control: no-cache' -H 'content-type: application/x-www-form-urlencoded' -d 'key=client1&token=aaa123456'

目前这个小服务工作良好。除了体积有点大(约7MB),其它都挺满意的。

斐讯真是个神奇的公司,凭着其高质量的产品,本来可以好好做个科技公司。其崩盘后,这些优秀的产品给我们带来了一波又一波的惊喜。在某网站的推广下,加上大学同学推坑,终于入手了N1。刷上被清理过的Android系统后,非常适合作为智能机顶盒!主要还是价格不算贵。整理一下相关的资料:

一、相关介绍
N1相关的一切,这个文章都说得很清楚了,我也是跟着上面步骤的来折腾。只是相关的资源都不能很好地下载。
我购买了斐讯N1做电视盒子,发现它远比想象的...
https://pockies.github.io/2019/03/07/phicomm-n1/

这个值得看看是由于N1的功耗很低,就算作为服务器不关机也不用担心电费的问题。
[N1盒子] 闲着无聊测试了下N1的功耗,曲线解决开关机问题
https://www.right.com.cn/forum/thread-322865-1-1.html

二、刷机经历
所需硬件:
1)双公头的USB 2.0线,用来连电脑刷系统。找两根USB线,把两个公头按同颜色的线连接即可。
2)网线,连接路由,用于远程adb操作N1。
3)USB鼠标或键盘,接到N1操作。
4)一台x86CPU的电脑,并且装了Windows的电脑,用来运行刷机工具。

所需软件:
1)webpad的rom,里面的“工具”包含了adb、降级工具、线刷工具等。喜欢这个rom的话,可以刷上,不用下载其它了。
[2018-9-22]斐讯N1 官改v2.2线刷包,精简版,扩展功能可选
https://www.right.com.cn/forum/thread-338759-1-1.html
工具说明:
a)“android-adb-fastboot_1.0.39.7z”,adb和fastboot工具
b)“斐讯T1、N1官方系统降级工具.zip”,官方ROM降级工具
c)“使N1进入线刷模式.zip”,降级后进入线刷模式,其实就是进入bootloader
d)“USB_Burning_Tool_v2.1.6.zip”,Amlogic的线刷工具,只支持windows

2)RUSH固件
[T1] 极限精简斐讯T1/N1 极客开发者强迫症福音6.23/6.24
https://www.right.com.cn/forum/thread-315889-1-1.html
我跟着前面的刷机教程,选择了这个固件。但由于是精简的,几乎应用什么都没有,后面需要折腾。

3)救砖
由于刷ROM中途以为失败了而强制结束,导致刷成砖。最后找到网上的教程,算是比较简单的解决方法,如下:
[N1盒子] 小白折腾了几天刷N1,稍总结下N1刷机和N1救砖,大神略过
https://www.right.com.cn/forum/thread-480477-1-1.html
简单来说是,拆机,短接两个触点,刷入T1固件(RUSH的rom包内有T1 ROM和救砖教程),再刷入所需的ROM

三、使用经验
1)遥控
本来可以买个蓝牙遥控,或者2.4无线遥控。但是本着省钱的原则,接了个有线鼠标上去,应付特殊情况。另外配对了一个蓝牙手柄,基本可以胜任遥控的工作。家里还有个Rapoo 1800 2.4G无线鼠键,可以考虑用上。

2)相关应用

  • 媒体中心:VLC,开源播放器,可以播放各种格式的视频,支持DLNA,支持多语音频等。个人感觉比Kodi好。
  • 横屏工具:还没找到好用无需破解的。由于很多apk都是手机端,默认不支持横屏,需要转个强制横屏工具。
  • 浏览器:无脑推荐Chrome,没考虑操作是否舒适之类,反正能在线追番就可以了。
  • 电视直播:这是个灰色地带。详细关注微信公众号”KUMI分享“,会有不定期推介。
  • 文件管理:ES Explorer,RUSH的ROM自带。支持FTP服务端和客户端,方便局域网内互传文件。
  • 桌面:直接用RUSH自带的那个,很简单,也有一点不方便,没去找其它的,凑合用吧。

3)游戏
N1自带空间不大,不适合玩大型游戏,而且大部分Android游戏都不支持手柄。
a)Minecraft破解版。家里部署了bedrock服务,手机装了正版Minecraft,但是N1没刷GAPPS,所以只能找可以登录的破解版了。跟孩子一起玩,大屏幕,还不错,但是画面略有卡顿。
b)赛车游戏。只是装了个SuperTuxKart来试试,效果还行,手柄还是不如手机的体感操控。
c)游戏机模拟器及游戏。能支持手柄,占用空间小,资源丰富,就只有模拟器了。曾经很期待地装上Dolphin模拟器,以为能玩Wii游戏,但是跑不起来。看来只能考虑旧主机的模拟器。

宽带的80、443端口不能使用了,更新免费的SSL证书(Let’s Encrypt的免费证书)就成问题了。后来找到相关的文章,说是可以通过DNS验证并更新,指向以下官方网址:
User Guide -> Getting certificates (and choosing plugins) -> dns-plugins
https://certbot.eff.org/docs/using.html#dns-plugins

找DNSPod的插件时,发现github上居然有不同的版本(名称却是一样的),因此走了弯路(浪费了一个下午)。最后按照这个的说明,成功更新了证书。
DNSPOD DNS Authenticator plugin for Certbot
https://github.com/SkyLothar/certbot-dns-dnspod/blob/master/README.rst

简单来说,就是
1)去DNSPod.cn申请api授权
2)安装插件

pip install certbot-dns-dnspod

3)生成插件配置文件,例如保存到文件/etc/cetbot-dns-dnspod-credentials.ini。重点是双引号不可缺,token的格式是id和token以逗号分隔

certbot_dns_dnspod:dns_dnspod_email = "DNSPod账户的Email"
certbot_dns_dnspod:dns_dnspod_api_token = "api_id,api_token"

4)配置文件设置权限(只是为了安全,此步可不做)

sudo chmod 600 /etc/cetbot-dns-dnspod-credentials.ini

5)更新证书。xxx.com需要替换为相关域名。

certbot certonly -a certbot-dns-dnspod:dns-dnspod \
  --certbot-dns-dnspod:dns-dnspod-credentials /etc/cetbot-dns-dnspod-credentials.ini \
  -d xxx.com

证书更新成功后,会发现certbot的配置文件(/etc/letsencrypt/renewal/xxx.com.conf)也更新了。