1.4.5.2
This commit is contained in:
5
EdgeNode/internal/uam/compile-proto.sh
Normal file
5
EdgeNode/internal/uam/compile-proto.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
protoc --go_out=. --proto_path=. ./key.proto
|
||||
|
||||
echo "DONE"
|
||||
12
EdgeNode/internal/uam/i18n_en.go
Normal file
12
EdgeNode/internal/uam/i18n_en.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package uam
|
||||
|
||||
func I18NForEN() map[string]string {
|
||||
return map[string]string{
|
||||
"checking": "Checking your browser before accessing %s",
|
||||
"waiting": "Please allow up to <span class=\"ui-counter\">5</span> seconds ...",
|
||||
"by": "DDoS protection by %s",
|
||||
}
|
||||
}
|
||||
20
EdgeNode/internal/uam/i18n_zh.go
Normal file
20
EdgeNode/internal/uam/i18n_zh.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package uam
|
||||
|
||||
func I18NForZH_CN() map[string]string {
|
||||
return map[string]string{
|
||||
"checking": "在访问 %s 之前,系统需要检查你的浏览器",
|
||||
"waiting": "请稍等<span class=\"ui-counter\">5</span>秒钟左右 ...",
|
||||
"by": "由 %s 提供DDoS防护",
|
||||
}
|
||||
}
|
||||
|
||||
func I18NForZH_TW() map[string]string {
|
||||
return map[string]string{
|
||||
"checking": "在訪問 %s 之前,系統需要檢查你的瀏覽器",
|
||||
"waiting": "請稍等<span class=\"ui-counter\">5</span>秒鐘左右 ...",
|
||||
"by": "由 %s 提供DDoS防護",
|
||||
}
|
||||
}
|
||||
72
EdgeNode/internal/uam/js.go
Normal file
72
EdgeNode/internal/uam/js.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package uam
|
||||
|
||||
// JSCode source code
|
||||
const JSCode = `function loadFunc() {
|
||||
var cookie = document.cookie
|
||||
var count = 5
|
||||
if (cookie == null) {
|
||||
window.setTimeout(function () {
|
||||
window.location.reload()
|
||||
}, sleepMs);
|
||||
return;
|
||||
}
|
||||
var cookieString = cookie.toString()
|
||||
var cookies = cookieString.split(";")
|
||||
var key = ""
|
||||
for (var i = 0; i < cookies.length; i ++) {
|
||||
var cookie = cookies[i].trim();
|
||||
var pieces = cookie.split("=");
|
||||
if (pieces.length == 2 && pieces[0] == cpk) {
|
||||
key = pieces[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (key.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var sum = 0;
|
||||
for (var i = 0; i < key.length; i ++) {
|
||||
var c = key[i];
|
||||
if (/^[a-zA-Z0-9]$/.test(c)) {
|
||||
sum += key.charCodeAt(i) * (nonce+i);
|
||||
}
|
||||
}
|
||||
|
||||
var u = window.location.toString();
|
||||
var req = new XMLHttpRequest();
|
||||
req.open("POST", u, true);
|
||||
req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
req.setRequestHeader("X-GE-UA-Step", step);
|
||||
req.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
window.setTimeout(function () {
|
||||
window.location.reload()
|
||||
}, count * 1000);
|
||||
window.setInterval(function () {
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
var counterElements = document.getElementsByClassName("ui-counter");
|
||||
if (counterElements.length > 0) {
|
||||
for (var i = 0; i < counterElements.length; i ++) {
|
||||
counterElements[i].innerHTML = count.toString();
|
||||
count --;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
req.send("sum=" + sum + "&nonce=" + nonce);
|
||||
}
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener("load", loadFunc);
|
||||
} else {
|
||||
window.onload = loadFunc;
|
||||
}`
|
||||
|
||||
// JSMinifyCode generated on site: https://jscompress.com/
|
||||
const JSMinifyCode = `function loadFunc(){var e=document.cookie,t=5;if(null!=e){for(var n=e.toString().split(";"),o="",i=0;i<n.length;i++){var a=(e=n[i].trim()).split("=");if(2==a.length&&a[0]==cpk){o=a[1];break}}if(0!=o.length){for(var d=0,i=0;i<o.length;i++){var r=o[i];/^[a-zA-Z0-9]$/.test(r)&&(d+=o.charCodeAt(i)*(nonce+i))}var l=window.location.toString(),s=new XMLHttpRequest;s.open("POST",l,!0),s.setRequestHeader("Content-type","application/x-www-form-urlencoded"),s.setRequestHeader("X-GE-UA-Step",step),s.onreadystatechange=function(){4==this.readyState&&200==this.status&&(window.setTimeout(function(){window.location.reload()},1e3*t),window.setInterval(function(){if(!(t<=0)){var e=document.getElementsByClassName("ui-counter");if(0<e.length)for(var n=0;n<e.length;n++)e[n].innerHTML=t.toString(),t--}},1e3))},s.send("sum="+d+"&nonce="+nonce)}}else window.setTimeout(function(){window.location.reload()},sleepMs)}window.addEventListener?window.addEventListener("load",loadFunc):window.onload=loadFunc;`
|
||||
23
EdgeNode/internal/uam/key.go
Normal file
23
EdgeNode/internal/uam/key.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package uam
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fnv"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const Version1 = 1
|
||||
|
||||
func (this *Key) Put(remoteAddr string, userAgent string) {
|
||||
this.Hash = fnv.HashString(remoteAddr + "@" + userAgent)
|
||||
}
|
||||
|
||||
func (this *Key) IsSame(remoteAddr string, userAgent string) bool {
|
||||
return this.Hash == fnv.HashString(remoteAddr+"@"+userAgent)
|
||||
}
|
||||
|
||||
func (this *Key) AsPB() ([]byte, error) {
|
||||
return proto.Marshal(this)
|
||||
}
|
||||
168
EdgeNode/internal/uam/key.pb.go
Normal file
168
EdgeNode/internal/uam/key.pb.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.31.0
|
||||
// protoc v3.19.4
|
||||
// source: key.proto
|
||||
|
||||
package uam
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,json=1,proto3" json:"timestamp,omitempty"` // 生成的时间戳
|
||||
Hash uint64 `protobuf:"varint,2,opt,name=hash,json=2,proto3" json:"hash,omitempty"` // hash(remoteAddr@ua)
|
||||
Host string `protobuf:"bytes,4,opt,name=host,json=4,proto3" json:"host,omitempty"` // 访问域名
|
||||
Version int32 `protobuf:"varint,5,opt,name=version,json=5,proto3" json:"version,omitempty"` // 版本号
|
||||
}
|
||||
|
||||
func (x *Key) Reset() {
|
||||
*x = Key{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_key_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Key) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Key) ProtoMessage() {}
|
||||
|
||||
func (x *Key) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_key_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Key.ProtoReflect.Descriptor instead.
|
||||
func (*Key) Descriptor() ([]byte, []int) {
|
||||
return file_key_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Key) GetTimestamp() int64 {
|
||||
if x != nil {
|
||||
return x.Timestamp
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Key) GetHash() uint64 {
|
||||
if x != nil {
|
||||
return x.Hash
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Key) GetHost() string {
|
||||
if x != nil {
|
||||
return x.Host
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Key) GetVersion() int32 {
|
||||
if x != nil {
|
||||
return x.Version
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_key_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_key_proto_rawDesc = []byte{
|
||||
0x0a, 0x09, 0x6b, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x75, 0x61, 0x6d,
|
||||
0x22, 0x51, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
|
||||
0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x01, 0x31, 0x12, 0x0f, 0x0a,
|
||||
0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x01, 0x32, 0x12, 0x0f,
|
||||
0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x34, 0x12,
|
||||
0x12, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05,
|
||||
0x52, 0x01, 0x35, 0x42, 0x08, 0x5a, 0x06, 0x2e, 0x2e, 0x2f, 0x75, 0x61, 0x6d, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_key_proto_rawDescOnce sync.Once
|
||||
file_key_proto_rawDescData = file_key_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_key_proto_rawDescGZIP() []byte {
|
||||
file_key_proto_rawDescOnce.Do(func() {
|
||||
file_key_proto_rawDescData = protoimpl.X.CompressGZIP(file_key_proto_rawDescData)
|
||||
})
|
||||
return file_key_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_key_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_key_proto_goTypes = []interface{}{
|
||||
(*Key)(nil), // 0: uam.Key
|
||||
}
|
||||
var file_key_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_key_proto_init() }
|
||||
func file_key_proto_init() {
|
||||
if File_key_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_key_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Key); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_key_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_key_proto_goTypes,
|
||||
DependencyIndexes: file_key_proto_depIdxs,
|
||||
MessageInfos: file_key_proto_msgTypes,
|
||||
}.Build()
|
||||
File_key_proto = out.File
|
||||
file_key_proto_rawDesc = nil
|
||||
file_key_proto_goTypes = nil
|
||||
file_key_proto_depIdxs = nil
|
||||
}
|
||||
11
EdgeNode/internal/uam/key.proto
Normal file
11
EdgeNode/internal/uam/key.proto
Normal file
@@ -0,0 +1,11 @@
|
||||
syntax = "proto3";
|
||||
option go_package = "../uam";
|
||||
|
||||
package uam;
|
||||
|
||||
message Key {
|
||||
int64 timestamp = 1 [json_name = "1"]; // 生成的时间戳
|
||||
uint64 hash = 2 [json_name = "2"]; // hash(remoteAddr@ua)
|
||||
string host = 4 [json_name = "4"]; // 访问域名
|
||||
int32 version = 5 [json_name = "5"]; // 版本号
|
||||
}
|
||||
33
EdgeNode/internal/uam/key_test.go
Normal file
33
EdgeNode/internal/uam/key_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2023 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||
//go:build plus
|
||||
|
||||
package uam_test
|
||||
|
||||
import (
|
||||
"github.com/TeaOSLab/EdgeNode/internal/uam"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKey_AsPB(t *testing.T) {
|
||||
var key = &uam.Key{
|
||||
Timestamp: fasttime.Now().Unix(),
|
||||
Hash: 123456,
|
||||
Host: "example.com",
|
||||
}
|
||||
pbData, err := key.AsPB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("["+types.String(len(pbData))+"]", pbData)
|
||||
|
||||
var key2 = &uam.Key{}
|
||||
err = proto.Unmarshal(pbData, key2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(key2)
|
||||
}
|
||||
519
EdgeNode/internal/uam/manager.go
Normal file
519
EdgeNode/internal/uam/manager.go
Normal file
@@ -0,0 +1,519 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package uam
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/nodeconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/firewallconfigs"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/compressions"
|
||||
teaconst "github.com/TeaOSLab/EdgeNode/internal/const"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/counters"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/encrypt"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/fasttime"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/utils/ttlcache"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/waf"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CookiePrevKey = "ge_ua_p"
|
||||
CookieKey = "ge_ua_key"
|
||||
PrevKeyLife int = 5
|
||||
DefaultKeyLife int = 3600
|
||||
|
||||
StepPrev = "prev"
|
||||
|
||||
DefaultMaxFails = 30 // 单IP攻击最大次数
|
||||
DefaultBlockSeconds = 1800 // 攻击封锁时间
|
||||
)
|
||||
|
||||
// Manager UAM管理器
|
||||
type Manager struct {
|
||||
encryptMethod encrypt.MethodInterface
|
||||
}
|
||||
|
||||
// NewManager 获取新的UAM管理器
|
||||
func NewManager(key string, secret string) (*Manager, error) {
|
||||
method, err := encrypt.NewMethodInstance("aes-256-cfb", key, secret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init encrypt method failed: %w", err)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
encryptMethod: method,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckKey 检查是否已经通过验证
|
||||
func (this *Manager) CheckKey(policy *nodeconfigs.UAMPolicy, req *http.Request, writer http.ResponseWriter, remoteAddr string, serverId int64, keyLife int) (isOk bool, isAttack bool, err error) {
|
||||
// 对攻击行为进行惩罚
|
||||
defer func() {
|
||||
if !isOk {
|
||||
this.IncreaseFails(policy, remoteAddr, serverId)
|
||||
}
|
||||
}()
|
||||
|
||||
// 读取Cookie
|
||||
keyCookie, err := req.Cookie(CookieKey)
|
||||
if err != nil {
|
||||
return false, false, fmt.Errorf("read cookie failed: %w", err)
|
||||
}
|
||||
|
||||
if keyCookie == nil {
|
||||
return false, false, errors.New("cookie not found")
|
||||
}
|
||||
|
||||
var cookieValue, _ = url.QueryUnescape(keyCookie.Value)
|
||||
if len(cookieValue) == 0 {
|
||||
return false, false, errors.New("unable to read cookie value")
|
||||
}
|
||||
|
||||
keyData, err := base64.StdEncoding.DecodeString(cookieValue)
|
||||
if err != nil {
|
||||
return false, true, fmt.Errorf("decode key failed: %w", err)
|
||||
}
|
||||
keyJSON, err := this.encryptMethod.Decrypt(keyData)
|
||||
if err != nil {
|
||||
return false, true, fmt.Errorf("decrypt key failed: %w", err)
|
||||
}
|
||||
|
||||
var key = &Key{}
|
||||
err = proto.Unmarshal(keyJSON, key)
|
||||
if err != nil {
|
||||
return false, true, fmt.Errorf("unmarshal key failed: %w", err)
|
||||
}
|
||||
|
||||
if keyLife <= 0 {
|
||||
keyLife = DefaultKeyLife
|
||||
|
||||
if policy != nil && policy.KeyLife > 0 {
|
||||
keyLife = policy.KeyLife
|
||||
}
|
||||
}
|
||||
|
||||
var unixTime = time.Now().Unix()
|
||||
if key.Timestamp >= unixTime-int64(PrevKeyLife)+1 /** 离生成时间过近 **/ || key.Timestamp < unixTime-int64(keyLife) /** 过了有效期 **/ {
|
||||
return false, true, errors.New("verify key failed")
|
||||
}
|
||||
|
||||
if key.Version >= Version1 {
|
||||
if !key.IsSame(remoteAddr, req.UserAgent()) {
|
||||
return false, true, errors.New("verify key hash failed")
|
||||
}
|
||||
|
||||
if policy.IncludeSubdomains {
|
||||
if this.ParseTopDomain(key.Host) != this.ParseTopDomain(req.Host) {
|
||||
return false, true, errors.New("verify key domain failed")
|
||||
}
|
||||
} else {
|
||||
if key.Host != req.Host {
|
||||
return false, true, errors.New("verify key domain failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
// LoadPage 显示加载页面
|
||||
func (this *Manager) LoadPage(policy *nodeconfigs.UAMPolicy, req *http.Request, formatter func(s string) string, remoteAddr string, writer http.ResponseWriter) error {
|
||||
var prevKey = this.ComposeKey(req, remoteAddr)
|
||||
prevKeyString, err := this.EncodeKey(prevKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
http.SetCookie(writer, &http.Cookie{
|
||||
Name: CookiePrevKey,
|
||||
Value: url.QueryEscape(prevKeyString),
|
||||
Expires: time.Now().Add(time.Duration(PrevKeyLife) * time.Second),
|
||||
MaxAge: PrevKeyLife,
|
||||
Path: "/",
|
||||
})
|
||||
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.Header().Set("Cache-Control", "no-cache")
|
||||
|
||||
var jsCode = JSMinifyCode
|
||||
if Tea.IsTesting() {
|
||||
jsCode = JSCode
|
||||
}
|
||||
|
||||
// 压缩
|
||||
var ioWriter io.Writer = writer
|
||||
|
||||
var compressionWriter = this.prepareCompression(req, writer)
|
||||
if compressionWriter != nil {
|
||||
ioWriter = compressionWriter
|
||||
defer func() {
|
||||
_ = compressionWriter.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
|
||||
var productName = teaconst.GlobalProductName
|
||||
if len(productName) == 0 {
|
||||
productName = teaconst.ProductName
|
||||
}
|
||||
|
||||
var messages = map[string]string{}
|
||||
switch this.lang(req) {
|
||||
case "zh-CN":
|
||||
messages = I18NForZH_CN()
|
||||
case "zh-TW":
|
||||
messages = I18NForZH_TW()
|
||||
default:
|
||||
messages = I18NForEN()
|
||||
}
|
||||
|
||||
var title = policy.UITitle
|
||||
if len(title) > 0 {
|
||||
title = formatter(title)
|
||||
}
|
||||
|
||||
var body = policy.UIBody
|
||||
if len(body) > 0 {
|
||||
body = formatter(body)
|
||||
} else {
|
||||
body = `<div class="ui-uam-box">
|
||||
<h1>` + fmt.Sprintf(messages["checking"], req.Host) + `</h1>
|
||||
<p>` + messages["waiting"] + `</p>
|
||||
<p> </p>
|
||||
<p>` + fmt.Sprintf(messages["by"], productName) + `</p>
|
||||
</div>`
|
||||
}
|
||||
|
||||
_, _ = ioWriter.Write([]byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>` + title + `</title>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||
<style type="text/css">
|
||||
.ui-uam-box {
|
||||
text-align: center;
|
||||
font-family: font-family: Roboto,"Helvetica Neue Light","Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ui-uam-box .ui-counter {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
var cpk = "` + CookiePrevKey + `"
|
||||
var step = "` + StepPrev + `";
|
||||
var nonce = ` + types.String(rands.Int(1000, 9999)) + `;
|
||||
` + jsCode + `
|
||||
</script>
|
||||
</head>
|
||||
<body>` + body +
|
||||
`</body>
|
||||
</html>`))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPrevKey 检查第一个Key
|
||||
func (this *Manager) CheckPrevKey(policy *nodeconfigs.UAMPolicy, config *serverconfigs.UAMConfig, req *http.Request, remoteAddr string, writer http.ResponseWriter) bool {
|
||||
// 检查Cookie
|
||||
cookie, err := req.Cookie(CookiePrevKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var escapedValue = cookie.Value
|
||||
value, _ := url.QueryUnescape(escapedValue)
|
||||
valueData, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
valueJSON, err := this.encryptMethod.Decrypt(valueData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var key = &Key{}
|
||||
err = proto.Unmarshal(valueJSON, key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if key.Version >= Version1 {
|
||||
if !key.IsSame(remoteAddr, req.UserAgent()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查域名
|
||||
if policy.IncludeSubdomains {
|
||||
if this.ParseTopDomain(key.Host) != this.ParseTopDomain(req.Host) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if key.Host != req.Host {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(req.Referer(), req.URL.String()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if key.Timestamp < time.Now().Unix()-int64(PrevKeyLife) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查连接端口
|
||||
// 因为每个浏览器实现机制不同,可能会导致ajax新开端口,所以这里暂时不判断
|
||||
/**if key.Port > 0 {
|
||||
var portIndex = strings.LastIndex(req.RemoteAddr, ":")
|
||||
if portIndex > 0 && key.Port != types.Int32(req.RemoteAddr[portIndex+1:]) {
|
||||
return false
|
||||
}
|
||||
}**/
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(req.Body, 64))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return false
|
||||
}
|
||||
var bodyString = string(body)
|
||||
var sum int64 = 0
|
||||
var nonce int64 = 0
|
||||
for _, param := range strings.Split(bodyString, "&") {
|
||||
var eqIndex = strings.Index(param, "=")
|
||||
if eqIndex > 0 {
|
||||
if param[:eqIndex] == "sum" {
|
||||
sum = types.Int64(param[eqIndex+1:])
|
||||
} else if param[:eqIndex] == "nonce" {
|
||||
nonce = types.Int64(param[eqIndex+1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sum != this.sumKey(escapedValue, nonce) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 设置新的Cookie
|
||||
newKey, err := this.EncodeKey(this.ComposeKey(req, remoteAddr))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var keyLife = config.KeyLife
|
||||
if keyLife <= 0 {
|
||||
keyLife = DefaultKeyLife
|
||||
|
||||
if policy != nil && policy.KeyLife > 0 {
|
||||
keyLife = policy.KeyLife
|
||||
}
|
||||
}
|
||||
|
||||
var keyCookie = &http.Cookie{
|
||||
Name: CookieKey,
|
||||
Value: url.QueryEscape(newKey),
|
||||
Expires: time.Now().Add(time.Duration(keyLife) * time.Second),
|
||||
MaxAge: keyLife,
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
if policy != nil && policy.IncludeSubdomains {
|
||||
keyCookie.Domain = this.ParseTopDomain(req.Host)
|
||||
}
|
||||
|
||||
http.SetCookie(writer, keyCookie)
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
|
||||
// 记录到IP白名单
|
||||
if config.AddToWhiteList {
|
||||
ttlcache.SharedInt64Cache.Write("UAM:WHITE:"+remoteAddr, 1, fasttime.Now().Unix()+int64(keyLife))
|
||||
}
|
||||
|
||||
// 清理
|
||||
this.resetFails(remoteAddr)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ComposeKey 组合Key
|
||||
func (this *Manager) ComposeKey(req *http.Request, remoteAddr string) *Key {
|
||||
var key = &Key{
|
||||
Version: Version1,
|
||||
Timestamp: fasttime.Now().Unix(),
|
||||
Host: req.Host,
|
||||
}
|
||||
key.Put(remoteAddr, req.UserAgent())
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// EncodeKey 对Key进行编码
|
||||
func (this *Manager) EncodeKey(key *Key) (string, error) {
|
||||
keyPB, err := key.AsPB()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dst, err := this.encryptMethod.Encrypt(keyPB)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(dst), nil
|
||||
}
|
||||
|
||||
// ExistsActivePreKey 检查是否已经生成PrevKey
|
||||
func (this *Manager) ExistsActivePreKey(req *http.Request) bool {
|
||||
// 检查Cookie
|
||||
cookie, err := req.Cookie(CookiePrevKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var escapedValue = cookie.Value
|
||||
value, _ := url.QueryUnescape(escapedValue)
|
||||
valueData, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
valueJSON, err := this.encryptMethod.Decrypt(valueData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var key = &Key{}
|
||||
err = proto.Unmarshal(valueJSON, key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().Unix()-key.Timestamp <= 1
|
||||
}
|
||||
|
||||
// IncreaseFails 增加失败次数
|
||||
// 以便于对客户端进行处罚
|
||||
func (this *Manager) IncreaseFails(policy *nodeconfigs.UAMPolicy, remoteAddr string, serverId int64) (isAttack bool) {
|
||||
const statSeconds = 60
|
||||
|
||||
var maxFails = DefaultMaxFails
|
||||
if policy != nil && policy.MaxFails > 0 {
|
||||
maxFails = policy.MaxFails
|
||||
}
|
||||
|
||||
var blockSeconds = DefaultBlockSeconds
|
||||
if policy != nil && policy.BlockSeconds > 0 {
|
||||
blockSeconds = policy.BlockSeconds
|
||||
}
|
||||
|
||||
var count = counters.SharedCounter.IncreaseKey("UAM:CheckKey:"+remoteAddr, statSeconds)
|
||||
if count >= types.Uint32(maxFails) {
|
||||
waf.SharedIPBlackList.RecordIP(waf.IPTypeAll, firewallconfigs.FirewallScopeGlobal, serverId, remoteAddr, fasttime.Now().Unix()+int64(blockSeconds), 0, policy != nil && policy.Firewall.Scope == firewallconfigs.FirewallScopeGlobal, 0, 0, "5秒盾认证不通过("+types.String(statSeconds)+"秒内尝试"+types.String(maxFails)+"次)")
|
||||
return true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 清除失败次数
|
||||
func (this *Manager) resetFails(remoteAddr string) {
|
||||
counters.SharedCounter.ResetKey("UAM:CheckKey:" + remoteAddr)
|
||||
}
|
||||
|
||||
// 准备压缩
|
||||
func (this *Manager) prepareCompression(req *http.Request, writer http.ResponseWriter) compressions.Writer {
|
||||
var acceptEncodings = req.Header.Get("Accept-Encoding")
|
||||
var encodings = strings.Split(acceptEncodings, ",")
|
||||
var compressionWriter compressions.Writer
|
||||
for _, piece := range encodings {
|
||||
var qualityIndex = strings.Index(piece, ";")
|
||||
if qualityIndex >= 0 {
|
||||
piece = piece[:qualityIndex]
|
||||
}
|
||||
|
||||
if piece == "br" {
|
||||
compressionWriter, _ = compressions.NewBrotliWriter(writer, 6)
|
||||
if compressionWriter != nil {
|
||||
writer.Header().Set("Content-Encoding", "br")
|
||||
writer.Header().Del("Content-Length")
|
||||
}
|
||||
break
|
||||
} else if piece == "gzip" {
|
||||
compressionWriter, _ = compressions.NewGzipWriter(writer, 6)
|
||||
if compressionWriter != nil {
|
||||
writer.Header().Set("Content-Encoding", "gzip")
|
||||
writer.Header().Del("Content-Length")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return compressionWriter
|
||||
}
|
||||
|
||||
func (this *Manager) sumKey(key string, nonce int64) int64 {
|
||||
var result int64 = 0
|
||||
for i, r := range key {
|
||||
if (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
|
||||
result += int64(r) * (nonce + int64(i))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (this *Manager) lang(req *http.Request) (lang string) {
|
||||
var acceptLanguage = req.Header.Get("Accept-Language")
|
||||
if len(acceptLanguage) > 0 {
|
||||
langIndex := strings.Index(acceptLanguage, ",")
|
||||
if langIndex > 0 {
|
||||
lang = acceptLanguage[:langIndex]
|
||||
}
|
||||
}
|
||||
if len(lang) == 0 {
|
||||
lang = "en-US"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (this *Manager) ParseTopDomain(domain string) string {
|
||||
if strings.Contains(domain, ":") {
|
||||
newHost, _, splitErr := net.SplitHostPort(domain)
|
||||
if splitErr == nil && len(newHost) > 0 {
|
||||
domain = newHost
|
||||
}
|
||||
}
|
||||
|
||||
if len(net.ParseIP(domain)) > 0 {
|
||||
return domain
|
||||
}
|
||||
|
||||
var pieces = strings.Split(domain, ".")
|
||||
var l = len(pieces)
|
||||
if l <= 2 {
|
||||
return domain
|
||||
}
|
||||
|
||||
var topDomain string
|
||||
if pieces[l-2] == "net" || pieces[l-2] == "com" || pieces[l-2] == "org" {
|
||||
if l == 3 { // *.[net|com|org].abc
|
||||
return domain
|
||||
}
|
||||
|
||||
// a.b.c.abc.[net|com|org].abc
|
||||
topDomain = strings.Join(pieces[len(pieces)-3:], ".")
|
||||
} else { // a.b.c.abc.[com|...]
|
||||
topDomain = strings.Join(pieces[len(pieces)-2:], ".")
|
||||
}
|
||||
return topDomain
|
||||
}
|
||||
105
EdgeNode/internal/uam/manager_test.go
Normal file
105
EdgeNode/internal/uam/manager_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved.
|
||||
//go:build plus
|
||||
|
||||
package uam_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/TeaOSLab/EdgeNode/internal/uam"
|
||||
"github.com/iwind/TeaGo/assert"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManager_ComposeKey(t *testing.T) {
|
||||
manager, err := uam.NewManager("abc", "123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1/hello", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36")
|
||||
t.Logf("%#v", manager.ComposeKey(req, "127.0.0.1"))
|
||||
}
|
||||
|
||||
func TestManager_GeneratePrevKey(t *testing.T) {
|
||||
manager, err := uam.NewManager("abc", "123")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1/hello", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36")
|
||||
key, err := manager.EncodeKey(manager.ComposeKey(req, "127.0.0.1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("[" + types.String(len(key)) + "]" + key)
|
||||
}
|
||||
|
||||
func TestParseTopDomain(t *testing.T) {
|
||||
var a = assert.NewAssertion(t)
|
||||
|
||||
manager, err := uam.NewManager("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.IsTrue(manager.ParseTopDomain("") == "")
|
||||
a.IsTrue(manager.ParseTopDomain("192.168.1.1") == "192.168.1.1")
|
||||
a.IsTrue(manager.ParseTopDomain("192.168.1.1:1234") == "192.168.1.1")
|
||||
a.IsTrue(manager.ParseTopDomain("example.com") == "example.com")
|
||||
a.IsTrue(manager.ParseTopDomain("sub.example.com") == "example.com")
|
||||
a.IsTrue(manager.ParseTopDomain("google") == "google")
|
||||
a.IsTrue(manager.ParseTopDomain("sub1.sub2.sub3.sub.example.com") == "example.com")
|
||||
a.IsTrue(manager.ParseTopDomain("example.com.cn") == "example.com.cn")
|
||||
a.IsTrue(manager.ParseTopDomain("example.net.cn") == "example.net.cn")
|
||||
a.IsTrue(manager.ParseTopDomain("example.org.cn") == "example.org.cn")
|
||||
a.IsTrue(manager.ParseTopDomain("sub.example.com.cn") == "example.com.cn")
|
||||
a.IsTrue(manager.ParseTopDomain("a.中国") == "a.中国")
|
||||
a.IsTrue(manager.ParseTopDomain("b.a.中国") == "a.中国")
|
||||
}
|
||||
|
||||
func BenchmarkManager_MarshalKey_PB(b *testing.B) {
|
||||
manager, err := uam.NewManager("abc", "123")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1/hello", nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = manager.ComposeKey(req, "127.0.0.1").AsPB()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkManager_MarshalKey_JSON(b *testing.B) {
|
||||
manager, err := uam.NewManager("abc", "123")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://127.0.0.1/hello", nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.83 Safari/537.36")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = json.Marshal(manager.ComposeKey(req, "127.0.0.1"))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user