分类 编程/笔记 下的文章

前言

没有前言。

生成Payload

我们可以直接使用golang-jwt中的RegisteredClaims结构:

jwt.RegisteredClaims{
    // 发行人主题
    Issuer:    "",
    // 被签发者主题
    Subject:   "",
    // 用户标识符
    Audience:  nil,
    // 过期时间
    ExpiresAt: nil,
    // 不早于
    NotBefore: nil,
    // 签发时间
    IssuedAt:  nil,
    // JWT的唯一标识符
    ID:        "",
}

此部分参见:https://datatracker.ietf.org/doc/html/rfc7519#section-4.1

或者我们可以自定义一个结构体,只需要引用RegisteredClaims就行:

// 嵌套结构
type CustomInfo struct {
    Name string `json:"name"`
}

// 自定义 payload
type CustomTokenBody struct {
    *jwt.RegisteredClaims
    User CustomInfo `json:"user"`
    Role string
}

生成JWT

选择签署算法:

// HMAC_HS256_HS384_HS512
// RSA_RS256_RS384_RS512
// ECDSA_ES256_ES384_ES512
// EdDSA_Ed25519
signingMethodECSDA := jwt.GetSigningMethod("ES256")

新建JWT并签署Payload

token := jwt.New(signingMethodECSDA)

// payload
token.Claims = &CustomTokenBody{
    &jwt.RegisteredClaims{
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 10)),
    },
    CustomInfo{
        Name: "yeziruo",
    },
    "yeziruo",
}
// 使用私钥签署
// key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
signedString, err := token.SignedString(key)
if err != nil {
    panic(err)
}
fmt.println(signedString)

最后生成:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjYxNTk2NjMsInVzZXIiOnsibmFtZSI6InllemlydW8ifSwiUm9sZSI6InllemlydW8ifQ.mCatXHq0ooh49Eusr2aWLJKwwy6K_zogDBVCTc3eBzY_ygePVnFL78QpzkGeuCCyhtjdobuKGx0VxBs6PlYrQA
//payload:{"exp":1666159663,"user":{"name":"yeziruo"},"Role":"yeziruo"}

解析JWT

共有三种解析方法:

// 解析并验证
jwt.Parse
// 仅解析
jwt.ParseUnverified
// 解析并验证
jwt.ParseWithClaims

ParseParseWithClaims有一定区别:

// 解析JWT
parse, err := jwt.Parse(signedString, func(token *jwt.Token) (interface{}, error) {
    // 如果你有多个私钥用于签名,那么可以在此通过 kid 来选择相应的密钥
    // 这里获取的 token.Claims 是一个 map
    // 返回公钥
    return key.Public(), nil
})

parse, err = jwt.ParseWithClaims(signedString, &CustomTokenBody{}, func(token *jwt.Token) (interface{}, error) {
    // 这里获取的 token.Claims 会被填充到指定的结构体
    return key.Public(), nil
})

// err 可以返回验证失败原因
// fmt.println(err)
fmt.Println(parse.Valid)

将其作为Gin中间件

func JWTAuth(priKey *ecdsa.PrivateKey) gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        parse, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
            return priKey.Public(), nil
        })
        if err != nil || !parse.Valid {
            msg := "token verification failed"
            if err != nil {
                msg = err.Error()
            }
            c.JSON(http.StatusUnauthorized, gin.H{
                "code":    http.StatusUnauthorized,
                "message": msg,
            })
            c.Abort()
        } else {
            c.Set("jwt", parse)
            c.Next()
        }
    }
}

2219143001.png

参见

https://datatracker.ietf.org/doc/html/rfc7519
https://pkg.go.dev/github.com/golang-jwt/jwt

前言

本文为以下两篇文章的续文:

使用OpenSSL搭建简单一级CA

使用OpenSSL搭建简单多级CA与CA管理

本文有一些概念涉及前文,故建议阅读后再实践本文章。

生成与解析csr

生成密钥

# "crypto/rand","crypto/rsa"
priKey, err := rsa.GenerateKey(rand.Reader, 2048)
// error_check
pubKey := priKey.Public()

保存密钥

密钥通常使用PEM格式保存,以开头,以结尾,中间是使用Base64编码的内容,为方便下文的操作,封装成一个工具函数:

# "encoding/pem"
func PEMEncoding(key []byte, blockType int) []byte {
    keyType := "PUBLIC KEY"
    if blockType == 1 {
        keyType = "RSA PRIVATE KEY"
    } else if blockType == 2 {
        keyType = "CERTIFICATE REQUEST"
    } else if blockType == 3 {
        keyType = "CERTIFICATE"
    } else if blockType == 4 {
        keyType = "X509 CRL"
    }
    block := &pem.Block{
        Type:  keyType,
        Bytes: key,
    }
    return pem.EncodeToMemory(block)
}

写入文件:

# "crypto/x509"
// 使用 PKCS1 编码密钥
priKeySource := x509.MarshalPKCS1PrivateKey(priKey)
pubKeySource, err := x509.MarshalPKIXPublicKey(pubKey)
// error_check
pubKeySource := x509.MarshalPKCS1PrivateKey(priKey)
_ = os.WriteFile("ca_pri.key", PEMEncoding(priKeySource, 1), 0755)
_ = os.WriteFile("ca_pub.key", PEMEncoding(pubKeySource, 0), 0755)

生成请求

生成证书请求需要使用x509.CertificateRequest结构体:

# "crypto/x509"
x509.CertificateRequest{
    // 经过 DER 编码的ASN.1信息(包含CSR、签名算法和签名)
    // 读取时自动填充
    Raw:                      nil,
    // 经过 DER 编码的证书请求信息
    RawTBSCertificateRequest: nil,
    // 经过 DER 编码的公钥信息
    RawSubjectPublicKeyInfo:  nil,
    // 经过 DER 编码的主题信息
    RawSubject:               nil,
    // CSR版本
    Version:                  0,
    // 签名
    Signature:                nil,
    // 签名算法
    SignatureAlgorithm:       0,
    // 公钥算法
    PublicKeyAlgorithm:       0,
    // 公钥
    PublicKey:                nil,
    // 主题信息
    Subject:                  pkix.Name{},
    // 已弃用,可被解析的 CSR 信息
    Attributes:               nil,
    // CSR 的扩展信息
    Extensions:               nil,
    // CSR 的原始扩展信息,当这项不为空时将覆盖其他指定的扩展
    // 解析时不会填充
    ExtraExtensions:          nil,
    // 主题备用名称
    DNSNames:                 nil,
    EmailAddresses:           nil,
    IPAddresses:              nil,
    URIs:                     nil,
}

生成请求:

# "crypto/x509","crypto/rand"
caRequest := x509.CertificateRequest{
    Subject: pkix.Name{
        Country:            []string{"CN"},
        Organization:       []string{"blog.yeziruo.com"},
        CommonName:         "Yeziruo Root CA",
    },
}
// 随机数流,CertificateRequest,私钥
csr, err := x509.CreateCertificateRequest(rand.Reader, &caRequest, priKey)
// error_check
_ = os.WriteFile("ca_req.csr", PemEncoding(csr, 2), 0755)

解析请求

使用x509.ParseCertificateRequest函数来解析CSR文件,返回CertificateRequest对象:

# "crypto/x509"
caRequest, err = x509.ParseCertificateRequest(csr)
// error_check

签发根证书

证书的签发需要使用x509.Certificate结构体,但该结构体参数过多,故只解释本文所使用的:

# "crypto/x509"
x509.Certificate{
    // 颁发者主题
    Issuer:                      pkix.Name{},
    // 被颁发者主题
    Subject:                     pkix.Name{},
    // 不早于
    NotBefore:                   time.Time{},
    // 不晚于
    NotAfter:                    time.Time{},
    // 基础密钥用法
    KeyUsage:                    0,
    // 扩展密钥用法
    ExtKeyUsage:                 nil,
    // 基本约束
    BasicConstraintsValid:       false,
    // 是证书颁发机构
    IsCA:                        false,
    // 证书最大路径,该级 CA 能签发子 CA 的证书链路径深度
    MaxPathLen:                  0,
    // 不允许该 CA 签发子 CA
    MaxPathLenZero:              false,
    // OCSP 响应服务器
    OCSPServer:                  nil,
    // 备用 DNS 名称
    ExcludedDNSDomains:          nil,
    // CRL 分发点
    CRLDistributionPoints:       nil,
}

序列号生成:

# rand2 "math/rand"
// sn := big.NewInt(rand2.Int63())
sn := big.NewInt(rand2.Uint64())
// 序列号不能为默认,也不能为有序序号,必须是一个很大的随机数
// ”序列号熵“问题

生成根证书:

# "crypto/x509"
ca := x509.Certificate{
    // 从 CSR 中复制主题信息
    Subject: caRequest.Subject,
    // 序列号
    SerialNumber:          sn,
    NotBefore:             time.Now(),
    NotAfter:              time.Now().AddDate(5, 0, 0),
    BasicConstraintsValid: true,
    IsCA:                  true,
    MaxPathLenZero:        true,
    ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageCertSign,
}

// 随机数流,被颁发者,颁发者,被颁发者公钥,颁发者私钥
// 可以通过这个函数简单的新建子 CA
crt, err := x509.CreateCertificate(rand.Reader, &ca, &ca, pubKey, priKey)
// error_check
_ = os.WriteFile("ca.crt", PEMEncoding(crt, 3), 0755)

23231501.png

签发最终证书

同上文所述:

# "crypto/x509","crypto/rand","crypto/rsa",rand2 "math/rand"
cliPriKey, err := rsa.GenerateKey(rand.Reader, 2048)
// error_check
_ = os.WriteFile("cli_pri.key", PEMEncoding(x509.MarshalPKCS1PrivateKey(cliPriKey), 1), 0755)

sn = big.NewInt(rand2.Int63())
cli := x509.Certificate{
    Subject:               pkix.Name{CommonName: "local1.yeziruo.com"},
    DNSNames:              []string{"local1.yeziruo.com", "local2.yeziruo.com"},
    SerialNumber:          sn,
    NotBefore:             time.Now(),
    NotAfter:              time.Now().AddDate(1, 0, 0),
    BasicConstraintsValid: true,
    IsCA:                  false,
    ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    KeyUsage:              x509.KeyUsageDigitalSignature,
    CRLDistributionPoints: []string{"http://192.168.122.250/ca.crl"},
    OCSPServer:            []string{"http://192.168.122.250/"},
}
cliCrt, err := x509.CreateCertificate(rand.Reader, &cli, &ca, cliPriKey.Public(), priKey)
// error_check
_ = os.WriteFile("cli.crt", PEMEncoding(cliCrt, 3), 0755)

23232401.png

生成CRL列表

// 重新读取证书,让其自动填充一些信息
cca, err := x509.ParseCertificate(crt)
// error_check
revokedObj := pkix.RevokedCertificate{
    // 被吊销证书序列号
    SerialNumber: cli.SerialNumber,
    // 吊销时间
    RevocationTime: time.Now(),
}

revokedList := x509.RevocationList{
    // 签发者主题信息
    Issuer: ca.Subject,
    // 被吊销的证书列表
    RevokedCertificates: []pkix.RevokedCertificate{revokedObj},
    // 一个自增的 CRL 序列号
    Number: big.NewInt(1),
    // 生成时间
    ThisUpdate: time.Time{},
    // 下一次更新时间
    NextUpdate: time.Time{},
}
list, err := x509.CreateRevocationList(rand.Reader, &revokedList, cca, priKey)
// error_check
_ = os.WriteFile("ca.crl", PEMEncoding(list, 4), 0755)

23235201.png

搭建OCSP响应服务器

OCSP协议使用HTTP承载,同时解析请求需要用到扩展加密库:

package main

import (
    "bytes"
    "crypto/rsa"
    "crypto/x509"
    "encoding/pem"
    "fmt"
    "golang.org/x/crypto/ocsp"
    "net/http"
    "os"
    "time"
)

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("http://localhost:8081/")
    err := http.ListenAndServe("localhost:8081", nil)
    if err != nil {
        panic(err)
    }
}

func loadPriKey(fn string) (*rsa.PrivateKey, error) {
    key, err := os.ReadFile(fn)
    if err != nil {
        return nil, err
    }
    block, _ := pem.Decode(key)
    priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        return nil, err
    }
    return priKey, nil
}

func loadCertificate(fn string) (*x509.Certificate, error) {
    key, err := os.ReadFile(fn)
    if err != nil {
        return nil, err
    }
    block, _ := pem.Decode(key)
    crt, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        return nil, err
    }
    return crt, nil
}

// no nonce ext support
// https://github.com/grimm-co/GOCSP-responder
func handler(w http.ResponseWriter, r *http.Request) {
    ca, err := loadCertificate("ca.crt")
    cli, err := loadCertificate("cli.crt")
    priKry, err := loadPriKey("ca_pri.key")
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    if r.Header.Get("Content-Type") != "application/ocsp-request" && r.Method != "POST" {
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    blob := new(bytes.Buffer)
    _, err = blob.ReadFrom(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

    //request, err := ocsp.ParseRequest(blob.Bytes())
    //if err != nil {
    //    return
    //}
    status := ocsp.Response{
        Status:           ocsp.Revoked,
        SerialNumber:     cli.SerialNumber,
        ProducedAt:       cli.NotBefore,
        ThisUpdate:       time.Now(),
        NextUpdate:       time.Now().AddDate(0, 0, 15),
        RevokedAt:        time.Now().AddDate(0, 0, -1),
        RevocationReason: 0,
        // Certificate:      ca,
    }

    response, err := ocsp.CreateResponse(ca, cli, status, priKry)
    if err != nil {
        return
    }
    w.Write(response)
}

23230001.png

参考

https://pkg.go.dev/crypto/x509

https://pkg.go.dev/golang.org/x/crypto/ocsp

https://github.com/grimm-co/GOCSP-responder

前言

本文主要介绍Golang Gin框架中全局处理错误的方法,水一下文章。

使用嵌套

就是在路由函数外面套层壳,侵入性大,不推荐这种处理方式。

type handle func(c *gin.Context) error

func errorHandle(f handle) func(c *gin.Context) {
    return func(c *gin.Context) {
        err := f(c)
        if err != nil {
            c.JSON(http.StatusInternalServerError, err.Error())
        }
    }
}

使用:

// 套在原路由函数,有多少个套多少次
router.GET("/b", errorHandle(func(c *gin.Context) error {
    return fmt.Errorf("1145141919810")
}))

使用panicrecover

Gin带有一个gin.Recovery中间件,用于捕获panic,说明我们可以通过中间件的方式捕获错误,而且panicrecover是原生的,非常方便,但只能捕获同一协程下的错误。

自定义错误接口:

package custom

var ErrorMessage map[int]string

type RequestError struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Stack   string `json:"stack,omitempty"`
}

func (err *RequestError) Error() string {
    return err.Message
}

func NewRequestError(code int, err ...error) *RequestError {
    e := new(RequestError)
    e.Code = code
    if v, ok := ErrorMessage[code]; ok == true {
        e.Message = v
    } else {
        e.Message = "server error"
    }
    // runtime.Stack()
    if len(err) >= 1 {
        for i := range err {
            e.Stack += err[i].Error()
            e.Stack += "\n"
        }
    }
    return e
}

func init() {
    ErrorMessage = make(map[int]string)
    // 以此类推定义错误信息
    ErrorMessage[401] = "Unauthorized"
}

自定义中间件:

package custom

import (
    "github.com/gin-gonic/gin"
    "net/http"
)

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                // 类型断言,因为 panic 是个 any ,什么都可以丢
                if v, ok := err.(*RequestError); ok == true {
                    c.JSON(v.Code, v)
                } else {
                    c.String(http.StatusInternalServerError, "Internal Server Error")
                }
                c.Abort()
            }
        }()
        // 传递下一棒
        c.Next()
    }
}

使用:

router.Use(custom.ErrorHandler())

router.GET("/a", func(c *gin.Context) {
    // 抛个错误
    panic(custom.NewRequestError(401))
    c.String(http.StatusOK, "hello")
})

15154424.png

前言

本文不是Lego使用教程。

由于SSL证书一年也就申请给几次,所以一般我都是手动去配置申请,上次介绍了Certbot提供acme模块用法,本文将介绍lego这个ACME客户端所提供的的库的用法。基本上重新造了个轮子,没有去用内部具体的接口。

实现

package main

import (
    "crypto"
    "crypto/x509"
    "encoding/pem"
    "github.com/go-acme/lego/v4/certcrypto"
    "github.com/go-acme/lego/v4/certificate"
    "github.com/go-acme/lego/v4/challenge/dns01"
    "github.com/go-acme/lego/v4/lego"
    "github.com/go-acme/lego/v4/log"
    "github.com/go-acme/lego/v4/registration"
    "io/ioutil"
    "time"
)

// 参见: https://pkg.go.dev/github.com/go-acme/lego/[email protected]/registration#User
type AcmeUser struct {
    Email        string
    Registration *registration.Resource
    key          crypto.PrivateKey
}

func (u *AcmeUser) GetEmail() string {
    return u.Email
}

func (u *AcmeUser) GetRegistration() *registration.Resource {
    return u.Registration
}
func (u *AcmeUser) GetPrivateKey() crypto.PrivateKey {
    return u.key
}

// 实现 Provider 接口
// 参见: https://pkg.go.dev/github.com/go-acme/lego/v4/[email protected]#Provider
// lego本身提供一些DNS服务商的接口,但我一般都是手动申请
type plainDnsProvider struct{}

func (p *plainDnsProvider) Present(domain, token, keyAuth string) error {
    log.Infof("domain: _acme-challenge.%s token: %s", domain, token)
    return nil
}

func (p *plainDnsProvider) CleanUp(domain, token, keyAuth string) error {
    return nil
}

func main() {
    // 本地ACME测试用
    // httpClient := &http.Client{
    //     Transport: &http.Transport{
    //         TLSClientConfig: &tls.Config{InsecureSkipVerify: true
    //     },
    // }}

    key, err := ioutil.ReadFile("account_private.key")
    if err != nil {
        panic(err)
    }
    block, _ := pem.Decode(key)
    priKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
    if err != nil {
        panic(err)
    }

    // 加载账户信息
    user := AcmeUser{
        Email: "[email protected]",
        key:   priKey,
    }
    config := lego.NewConfig(&user)
    // Pebble 默认地址
    // config.CADirURL = "https://127.0.0.1:14000/dir"
    // config.CADirURL = "https://api.buypass.com/acme/directory"
    config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory"
    config.UserAgent = "acm_go/0.0.1"
    // 秘钥类型
    config.Certificate.KeyType = certcrypto.RSA2048
    // 本地测试用
    // config.HTTPClient = httpClient

    // 新建客户端
    client, err := lego.NewClient(config)
    if err != nil {
        panic(err)
    }

    // 账户注册
    reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
    // 使用秘钥查找账户
    // reg, err := client.Registration.ResolveAccountByKey()
    if err != nil {
        panic(err)
    }
    user.Registration = reg

    // 生成证书请求
    request := certificate.ObtainRequest{
        Domains: []string{"yajyusenpai.coat.co.jp"},
        // 证书链
        Bundle: true,
    }

    dnsProvider := plainDnsProvider{}
    // dns.NewDNSChallengeProviderByName(provider_name)
    // dnspodConfig := dnspod.Config{config...}
    // dnspodProvider := dnspod.NewDNSProviderConfig(&dnspodConfig)

    // 以DNS方式验证
    err = client.Challenge.SetDNS01Provider(&dnsProvider, dns01.AddDNSTimeout(6*time.Minute))
    if err != nil {
        panic(err)
    }

    // 自动轮询
    certificates, err := client.Certificate.Obtain(request)
    if err != nil {
        panic(err)
    }

    // 导出
    _ = ioutil.WriteFile("certificate.crt", certificates.Certificate, 0755)
    _ = ioutil.WriteFile("certificate_private.key", certificates.PrivateKey, 0755)
}

220707063204.jpg

前言

Golang中没有原生的队列实现,故实现了一个简单的队列,了解队列的工作方式。

互斥锁

互斥锁是并发控制常用的一种锁,保证了同一时刻同一个资源对象只有一个线程或协程访问,避免出现对变量进行拷贝再赋值导致的问题,而在队列中,使用互斥锁能够保证同一时刻只有入队或出队操作,避免出现混乱,简单的用法如下:

package main

import (
    "fmt"
    "sync"
)

func main() {
    // 互斥锁
    var locker sync.Mutex
    // 一个共享变量
    num := 0

    for i := 0; i < 114514; i++ {
        go func() {
            locker.Lock()
            num++
            locker.Unlock()
            fmt.Println(num)
        }()
    }
    for {
        if num >= 114514 {
            break
        }
    }
    fmt.Println("---------")
    fmt.Println(num)
}

可以尝试去除这个互斥锁运行,但你会发现最后num并没有达到114514,所以程序不会自动结束运行。

切片实现

队列的实现方式有很多种,使用链表性能更优,适合先进先出,但我这里使用了切片来实现,实现了入队出队操作,对内置切片进行维护,包括前移和增容操作。

package main

import (
    "fmt"
    "sync"
)

type Queue struct {
    // 内部维护的切片
    queueSlice []interface{}
    // 容量
    cap uint
    // 起始队列位置
    begin uint
    // 队列尾部位置
    end uint
    // 默认队列大小
    defaultLength uint
    // 并发互斥锁
    locker sync.Mutex
}

func QueueInit(defaultLength uint) *Queue {
    return &Queue{
        queueSlice:    make([]interface{}, defaultLength, defaultLength),
        cap:           defaultLength,
        begin:         0,
        end:           0,
        defaultLength: defaultLength,
        locker:        sync.Mutex{},
    }
}

func (q *Queue) Length() uint {
    // 通过队尾队列开始位置计算长度
    q.locker.Lock()
    length := q.end - q.begin
    q.locker.Unlock()
    return length
}

func (q *Queue) Push(value interface{}) {
    q.locker.Lock()
    // 未超出容量
    if q.end < q.cap {
        q.queueSlice[q.end] = value
        q.end++
    } else {
        // 当长度小于容量进行前移
        if q.end-q.begin < q.cap {
                    // 用for移动会更好
                    // copy,append 也是有开销的
            temp := q.queueSlice[q.begin:q.end]
            q.queueSlice = make([]interface{}, q.cap, q.cap)
            copy(q.queueSlice, temp)
            q.begin = 0
            q.end = uint(len(temp))
            q.queueSlice[q.end] = value
            q.end++
        }
        // 长度达到容量对切片进行扩容
        if q.end >= q.cap {
            temp := make([]interface{}, q.end-q.begin)
            copy(temp, q.queueSlice[q.begin:q.end])
                    // 扩增一倍容量
            q.queueSlice = make([]interface{}, q.cap+q.defaultLength, q.cap+q.defaultLength*2)
            q.cap = q.cap + q.defaultLength
            copy(q.queueSlice, temp)
            q.queueSlice[q.end] = value
            q.end++
        }
    }
    // fmt.Println("|-value", value, "cap", q.cap, "begin", q.begin, "end", q.end)
    q.locker.Unlock()
}

func (q *Queue) Get() interface{} {
    q.locker.Lock()
    if q.begin < q.end {
        value := q.queueSlice[q.begin]
        q.begin++
        q.locker.Unlock()
        return value
    }
    q.locker.Unlock()
    return nil
}

func main() {
    q := QueueInit(6)
    go func(queue *Queue) {
        for {
            value := queue.Get()
            if value == nil {
                continue
            }
            fmt.Println("out", value)
        }
    }(q)
    for i := 0; i <= 27; i++ {
        q.Push(i)
        fmt.Println("in", i)
    }
    for {
        if q.Length() == 0 {
            break
        }
    }
    fmt.Println("len", q.Length())
}

链表实现

package main

import (
    // container/list
    "fmt"
)

type Node struct {
    prev  *Node
    next  *Node
    value interface{}
}

type Queue struct {
    length uint
    // 始节点
    begin  *Node
    // 尾节点
    end    *Node
}

func (q *Queue) Push(value interface{}) {
    node := &Node{prev: nil, next: nil, value: value}
    if q.length == 0 {
        q.begin = node
        q.end = node
        q.length++
        return
    }
    q.end.next = node
    node.prev = q.end
    q.end = node
    q.length++
}

func (q *Queue) Get() interface{} {
    if q.length == 0 {
        return nil
    }
    value := q.begin.value
    q.begin = q.begin.next
    q.length--
    return value
}

func NewQueue() *Queue {
    return &Queue{
        length: 0,
        begin:  nil,
        end:    nil,
    }
}

func main() {
    q := NewQueue()
    go func(q *Queue) {
        for {
            value := q.Get()
            if value == nil {
                break
            }
            fmt.Println("out", value)
        }
    }(q)
    for i := 0; i < 100; i++ {
        q.Push(i)
        fmt.Println("in", i)
    }
    for {
        if q.length == 0 {
            break
        }
    }
}

前言

我常用的网站要手机验证了。

代码

这里使用buypass的证书,最多五个子域名,这个脚本使用DNS验证。

import json
import datetime

from acme import messages
from acme import challenges
from acme import client
from acme import messages

import josepy as jose

from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def new_csr(domain):
    key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
    )
    with open(domain[0] + ".key", "wb") as f:
        f.write(key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption(),
        ))
    csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
        x509.NameAttribute(NameOID.COMMON_NAME, domain[0]),
    ])).add_extension(
        x509.SubjectAlternativeName([x509.DNSName(i) for i in domain]),
        critical=False,
    ).sign(key, hashes.SHA256())
    return csr.public_bytes(serialization.Encoding.PEM)

def main():
    domain = ["your_domain_list"]
    # 生成csr
    csr = new_csr(domain)
    # 加载用户秘钥
    # RSA 2048bits-4096bits
    with open("./private.key", "r") as f:
        key = load_pem_private_key(f.read().encode("utf-8"), password=None)
    # 获取jwk
    acc_key = jose.JWKRSA(key=key)
    net = client.ClientNetwork(acc_key, user_agent="acm/0.0.1")
    # 本地测试用
    # net.verify_ssl = False
    # 取得acme目录
    directory = messages.Directory.from_json(net.get("https://api.buypass.com/acme/directory").json())
    # directory = messages.Directory.from_json(net.get("https://acme-v02.api.letsencrypt.org/directory").json())

    client_acme = client.ClientV2(directory, net=net)

    # 加载账户信息
    # with open("./account.json", "r") as f:
    #     user = messages.RegistrationResource.json_loads(f.read())
    # client_acme.query_registration(user)

    # 首次注册
    regr = client_acme.new_account(
        messages.NewRegistration.from_data(
            email=("[email protected]"),
            terms_of_service_agreed=True,
        )
    )

    # 保存账户信息
    with open("./account.json", "w") as f:
        f.write(json.dumps(regr.to_json()))

    # 新建订单
    order = client_acme.new_order(csr)
    challenges_list = []
    for authz in order.authorizations:
        for i in authz.body.challenges:
            # 选取DNS验证
            if isinstance(i.chall, challenges.DNS01):
                response, validation = i.response_and_validation(client_acme.net.key)
                challenges_list.append([i, response])
                print("[DNS]", "_acme-challenge." + authz.body.identifier.value)
                print("[DNS]", validation)

    while input("ok? (y) ") != "y":
        pass

    # 通知准备验证
    for i in challenges_list:
        client_acme.answer_challenge(i[0], i[1])
    print("[INFO]", "wait...")

    # 自动轮询2分钟并取回证书
    finalized_orderr = client_acme.poll_and_finalize(order, deadline=datetime.datetime.now() + datetime.timedelta(minutes=2))
    print("[OUT]", finalized_orderr.fullchain_pem)
    with open(domain[0] + ".crt", "w") as f:
        f.write(finalized_orderr.fullchain_pem)   
    pass

if __name__ == "__main__":
    main()

JSON Web Key

使用JSON的结构来承载公钥。(RFC7517)

import json
import base64
import binascii
from cryptography.hazmat.primitives.serialization import load_pem_private_key

def b64(text):
    return base64.urlsafe_b64encode(text).decode('utf8').replace("=", "")

def get_jwk(path):
    with open(path, "r") as f:
        pri = load_pem_private_key(f.read().encode("utf-8"), password=None)
    pub = pri.public_key().public_numbers()
    e = "{:x}".format(pub.e)
    e = "0{0}".format(e) if int(e) % 2 else e
    e = b64(binascii.unhexlify(e))
    n = b64(binascii.unhexlify(hex(pub.n)[2:]))
    return {"n": n, "e": e, "kty": "RSA"}

if __name__ == "__main__":
    print(get_jwk("./private.key"))

使用了RSA的模数与指数来代表一份公钥,如果是椭圆则是基点坐标。

N久以前

N久前遇到的问题,当时简单的解决了一下,现在在写一个StorageBucket,需要有一个Demo,所以又回过头来研究。
来看看之前是这么解决的:

let slice_size = 1024 * 1024; // 1MB 分块大小
let totalSliceNum = 0; //总分块数量
let now = 0; //当前分块
let file = document.getElementById("file").files[0];
totalSliceNum = Math.ceil(file.size / slice_size);

print("size|" + file.size);
print("name|" + file.name);
print("type|" + file.type);
print("slice|" + totalSliceNum);

while(now < totalSliceNum) {
   let s = now * slice_size;
   let e = (now + 1) * slice_size;
   if(e > file.size) {
       e = file.size;
   }
   let chunk = file.slice(s,e); //切取一块
   //jquery.ajax 发送文件块数据
   now += 1;
}

看起来没错,网上也大部分都是这么写的,按顺序一块一块上传,但实际上使用你会发现,浏览器卡爆了,页面完全不渲染,幸好是给同学用的,让他F12在控制台看进度。
我们浏览器中的js是单线程的,我们分片时会卡死在file.slice这里,因为它要从文件中读出一块,它卡死在这占据了整个线程,导致整个页面无法渲染。不过我们可以用FileReader来异步读取,而不是等他读取,卡到无法渲染,所以有了下面的版本:

let slice_size = 1024 * 1024; // 1MB 分块大小
let totalSliceNum = 0; //总分块数量
let now = 0; //当前分块
let file = document.getElementById("file").files[0];
totalSliceNum = Math.ceil(file.size / slice_size);

print("size|" + file.size);
print("name|" + file.name);
print("type|" + file.type);
print("slice|" + totalSliceNum);

while(now < totalSliceNum) {
   let s = now * slice_size;
   let e = (now + 1) * slice_size;
   if(e > file.size) {
       e = file.size;
   }
   let chunk = file.slice(s,e); //切取一块
   let reader = new FileReader();
   reader.readAsArrayBuffer(chunk);
   reader.onload = function() {
       //当块加载好了,会执行这个函数
       //console.log(new Blob([reader.result]));
       //jquery.ajax 发送文件块数据
       //这里要传递当前分块与总块数量,方便后端合成,因为块不再是顺序上传
   }
   now += 1;
}

效果

21072601.png

最后

博客很少更了,我现在不像以前一样那么有动力,不过...

???

帮别人提出的解决方案(吹B过头),临时突击翻文档学移动开发写的,虽然没有多少行。

得到歌曲名与作者

这里用到了监听器,得到通知栏的网易云的播放控件,读取歌曲名与作者。

来点权限:

<service android:name=".NotificationListener"
        android:label="DemoApp"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
        <intent-filter>
            <action android:name="android.service.notification.NotificationListenerService" />
        </intent-filter>
        <meta-data
            android:name="android.service.notification.default_filter_types"
            android:value="1,2">
        </meta-data>
        <meta-data
            android:name="android.service.notification.disabled_filter_types"
            android:value="2">
        </meta-data>
</service>

监听器类:

package com.test.demoapplication;

import android.app.Notification;
import android.content.Intent;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.TextView;

public class NotificationListener extends NotificationListenerService {
    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        //网易云
        if(!sbn.getPackageName().equals("com.netease.cloudmusic")) return;

        //去除一般性通知
        if(!sbn.getNotification().extras.getString(Notification.EXTRA_TEXT,"").equals("")) return;

        //取得通知栏组件
        //另: 网易云支持安卓的MediaBroswerService,这个以后再说
        ViewGroup view = (ViewGroup) sbn.getNotification().bigContentView.apply(this, null);

        String author = (String) ((TextView) (((ViewGroup) view.getChildAt(2))).getChildAt(1)).getText();
        String songName = (String) ((TextView) (((ViewGroup) view.getChildAt(2))).getChildAt(0)).getText();

        Log.i("Debug",songName + "|" + author);

        super.onNotificationPosted(sbn);
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        if(!sbn.getPackageName().equals("com.netease.cloudmusic")) return;
        super.onNotificationRemoved(sbn);
    }
}

控制音乐播放

这个用模拟按键来实现,是通用的,不过要注意开启线控。

//模拟按键要在线程中执行,否则会卡死
class UPressKey extends Thread {
    private int keyCode;
    public UPressKey(int keyCode) {
        this.keyCode = keyCode;
    }

    @Override
    public void run() {
        Instrumentation mInst = new Instrumentation();
        mInst.sendKeyDownUpSync(this.keyCode);
    }
}


//以下为控制按键
new UPressKey(KeyEvent.KEYCODE_MEDIA_PREVIOUS).start();
new UPressKey(KeyEvent.KEYCODE_VOLUME_UP).start();
new UPressKey(KeyEvent.KEYCODE_MEDIA_PLAY).start();
new UPressKey(KeyEvent.KEYCODE_VOLUME_DOWN).start();
new UPressKey(KeyEvent.KEYCODE_MEDIA_NEXT).start();
new UPressKey(KeyEvent.KEYCODE_MEDIA_PAUSE).start();

实现

24 185911.jpg