mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
Update On Fri Jan 2 19:40:16 CET 2026
This commit is contained in:
@@ -1230,3 +1230,4 @@ Update On Mon Dec 29 19:42:01 CET 2025
|
||||
Update On Tue Dec 30 19:43:26 CET 2025
|
||||
Update On Wed Dec 31 19:39:51 CET 2025
|
||||
Update On Thu Jan 1 19:42:21 CET 2026
|
||||
Update On Fri Jan 2 19:40:09 CET 2026
|
||||
|
||||
+60
-20
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,37 +110,75 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
|
||||
}
|
||||
|
||||
c = warpClientWithEdns0Subnet(c, s.Params)
|
||||
|
||||
if s.Params["disable-ipv4"] == "true" {
|
||||
c = warpClientWithDisableType(c, D.TypeA)
|
||||
}
|
||||
|
||||
if s.Params["disable-ipv6"] == "true" {
|
||||
c = warpClientWithDisableType(c, D.TypeAAAA)
|
||||
}
|
||||
c = warpClientWithDisableTypes(c, s.Params)
|
||||
|
||||
ret = append(ret, c)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type clientWithDisableType struct {
|
||||
type clientWithDisableTypes struct {
|
||||
dnsClient
|
||||
qType uint16
|
||||
disableTypes map[uint16]struct{}
|
||||
}
|
||||
|
||||
func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
if len(m.Question) > 0 {
|
||||
q := m.Question[0]
|
||||
if q.Qtype == c.qType {
|
||||
return handleMsgWithEmptyAnswer(m), nil
|
||||
func (c clientWithDisableTypes) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
// filter dns request
|
||||
if slices.ContainsFunc(m.Question, c.inQuestion) {
|
||||
// In fact, DNS requests are not allowed to contain multiple questions:
|
||||
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071
|
||||
// so, when we find a question containing the type, we can simply discard the entire dns request.
|
||||
return handleMsgWithEmptyAnswer(m), nil
|
||||
}
|
||||
|
||||
// do real exchange
|
||||
msg, err = c.dnsClient.ExchangeContext(ctx, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// filter dns response
|
||||
msg.Answer = slices.DeleteFunc(msg.Answer, c.inRR)
|
||||
msg.Ns = slices.DeleteFunc(msg.Ns, c.inRR)
|
||||
msg.Extra = slices.DeleteFunc(msg.Extra, c.inRR)
|
||||
return
|
||||
}
|
||||
|
||||
func (c clientWithDisableTypes) inQuestion(q D.Question) bool {
|
||||
_, ok := c.disableTypes[q.Qtype]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c clientWithDisableTypes) inRR(rr D.RR) bool {
|
||||
_, ok := c.disableTypes[rr.Header().Rrtype]
|
||||
return ok
|
||||
}
|
||||
|
||||
func warpClientWithDisableTypes(c dnsClient, params map[string]string) dnsClient {
|
||||
disableTypes := make(map[uint16]struct{})
|
||||
if params["disable-ipv4"] == "true" {
|
||||
disableTypes[D.TypeA] = struct{}{}
|
||||
}
|
||||
if params["disable-ipv6"] == "true" {
|
||||
disableTypes[D.TypeAAAA] = struct{}{}
|
||||
}
|
||||
for key, value := range params {
|
||||
const prefix = "disable-qtype-"
|
||||
if strings.HasPrefix(key, prefix) && value == "true" { // eg: disable-qtype-65=true
|
||||
qType, err := strconv.ParseUint(key[len(prefix):], 10, 16)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := D.TypeToRR[uint16(qType)]; !ok { // check valid RR_Header.Rrtype and Question.qtype
|
||||
continue
|
||||
}
|
||||
disableTypes[uint16(qType)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return c.dnsClient.ExchangeContext(ctx, m)
|
||||
}
|
||||
|
||||
func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient {
|
||||
return clientWithDisableType{c, qType}
|
||||
if len(disableTypes) > 0 {
|
||||
return clientWithDisableTypes{c, disableTypes}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type clientWithEdns0Subnet struct {
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ require (
|
||||
github.com/metacubex/http v0.1.0
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9
|
||||
github.com/metacubex/mlkem v0.1.0
|
||||
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72
|
||||
github.com/metacubex/randv2 v0.2.0
|
||||
github.com/metacubex/restls-client-go v0.1.7
|
||||
github.com/metacubex/sing v0.5.6
|
||||
|
||||
+2
-2
@@ -114,8 +114,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
||||
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
||||
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec h1:5ePGO2Xht06fpwjNIzfY5XS+82xwDHHx4xGbqgLbxjA=
|
||||
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 h1:kNlYHZ75itJwkerDiySpixX+dKsv/K0TYQsKvuxogNM=
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72/go.mod h1:N071X2oW2+kIhLlHW3mfcD2QP+zWu2bEs1EEAm66bvI=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
||||
|
||||
@@ -161,8 +161,8 @@ $(eval $(call SetupHostCommand,bzip2,Please install 'bzip2', \
|
||||
$(eval $(call SetupHostCommand,wget,Please install GNU 'wget', \
|
||||
wget --version | grep GNU))
|
||||
|
||||
$(eval $(call SetupHostCommand,install,Please install GNU 'install', \
|
||||
install --version | grep GNU, \
|
||||
$(eval $(call SetupHostCommand,install,Please install 'install', \
|
||||
install --version | grep 'GNU\|uutils', \
|
||||
ginstall --version | grep GNU))
|
||||
|
||||
$(eval $(call SetupHostCommand,perl,Please install Perl 5.x, \
|
||||
|
||||
@@ -115,8 +115,8 @@ type Session struct {
|
||||
ready chan struct{} // indicate the session is ready to use
|
||||
closeRequested atomic.Bool // the session is being closed or has been closed
|
||||
closedChan chan struct{} // indicate the session is closed
|
||||
readDeadline time.Time // read deadline
|
||||
writeDeadline time.Time // write deadline
|
||||
readDeadline atomic.Int64 // read deadline, in microseconds since Unix epoch
|
||||
writeDeadline atomic.Int64 // write deadline, in microseconds since Unix epoch
|
||||
inputHasErr atomic.Bool // input has error
|
||||
inputErr chan error // this channel is closed when input has error
|
||||
outputHasErr atomic.Bool // output has error
|
||||
@@ -176,8 +176,6 @@ func NewSession(id uint32, isClient bool, mtu int, users map[string]*appctlpb.Us
|
||||
users: users,
|
||||
ready: make(chan struct{}),
|
||||
closedChan: make(chan struct{}),
|
||||
readDeadline: time.Time{},
|
||||
writeDeadline: time.Time{},
|
||||
inputErr: make(chan error),
|
||||
outputErr: make(chan error),
|
||||
sendQueue: newSegmentTree(segmentTreeCapacity),
|
||||
@@ -210,7 +208,7 @@ func (s *Session) Read(b []byte) (n int, err error) {
|
||||
defer s.rLock.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.readDeadline = time.Time{}
|
||||
s.readDeadline.Store(0)
|
||||
}()
|
||||
if log.IsLevelEnabled(log.TraceLevel) {
|
||||
log.Tracef("%v trying to read %d bytes", s, len(b))
|
||||
@@ -235,8 +233,8 @@ func (s *Session) Read(b []byte) (n int, err error) {
|
||||
|
||||
// Stop reading when deadline is reached.
|
||||
var timeC <-chan time.Time
|
||||
if !s.readDeadline.IsZero() {
|
||||
timeC = time.After(time.Until(s.readDeadline))
|
||||
if readDeadline := s.readDeadline.Load(); readDeadline != 0 {
|
||||
timeC = time.After(time.Until(time.UnixMicro(readDeadline)))
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -304,7 +302,7 @@ func (s *Session) Write(b []byte) (n int, err error) {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
defer func() {
|
||||
s.writeDeadline = time.Time{}
|
||||
s.writeDeadline.Store(0)
|
||||
}()
|
||||
|
||||
// Before the first write, client needs to send open session request.
|
||||
@@ -385,20 +383,32 @@ func (s *Session) RemoteAddr() net.Addr {
|
||||
|
||||
// SetDeadline implements net.Conn.
|
||||
func (s *Session) SetDeadline(t time.Time) error {
|
||||
s.readDeadline = t
|
||||
s.writeDeadline = t
|
||||
micros := t.UnixMicro()
|
||||
if t.IsZero() {
|
||||
micros = 0
|
||||
}
|
||||
s.readDeadline.Store(micros)
|
||||
s.writeDeadline.Store(micros)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetReadDeadline implements net.Conn.
|
||||
func (s *Session) SetReadDeadline(t time.Time) error {
|
||||
s.readDeadline = t
|
||||
micros := t.UnixMicro()
|
||||
if t.IsZero() {
|
||||
micros = 0
|
||||
}
|
||||
s.readDeadline.Store(micros)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetWriteDeadline implements net.Conn.
|
||||
func (s *Session) SetWriteDeadline(t time.Time) error {
|
||||
s.writeDeadline = t
|
||||
micros := t.UnixMicro()
|
||||
if t.IsZero() {
|
||||
micros = 0
|
||||
}
|
||||
s.writeDeadline.Store(micros)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -500,8 +510,8 @@ func (s *Session) writeChunk(b []byte) (n int, err error) {
|
||||
|
||||
// Stop writing when deadline is reached.
|
||||
var timeC <-chan time.Time
|
||||
if !s.writeDeadline.IsZero() {
|
||||
timeC = time.After(time.Until(s.writeDeadline))
|
||||
if writeDeadline := s.writeDeadline.Load(); writeDeadline != 0 {
|
||||
timeC = time.After(time.Until(time.UnixMicro(writeDeadline)))
|
||||
}
|
||||
|
||||
seqBeforeWrite, _ := s.sendQueue.MinSeq()
|
||||
@@ -596,7 +606,7 @@ func (s *Session) writeChunk(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
if s.isClient {
|
||||
s.readDeadline = time.Now().Add(serverRespTimeout)
|
||||
s.readDeadline.Store(time.Now().Add(serverRespTimeout).UnixMicro())
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
+60
-20
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
|
||||
D "github.com/miekg/dns"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,37 +110,75 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient {
|
||||
}
|
||||
|
||||
c = warpClientWithEdns0Subnet(c, s.Params)
|
||||
|
||||
if s.Params["disable-ipv4"] == "true" {
|
||||
c = warpClientWithDisableType(c, D.TypeA)
|
||||
}
|
||||
|
||||
if s.Params["disable-ipv6"] == "true" {
|
||||
c = warpClientWithDisableType(c, D.TypeAAAA)
|
||||
}
|
||||
c = warpClientWithDisableTypes(c, s.Params)
|
||||
|
||||
ret = append(ret, c)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type clientWithDisableType struct {
|
||||
type clientWithDisableTypes struct {
|
||||
dnsClient
|
||||
qType uint16
|
||||
disableTypes map[uint16]struct{}
|
||||
}
|
||||
|
||||
func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
if len(m.Question) > 0 {
|
||||
q := m.Question[0]
|
||||
if q.Qtype == c.qType {
|
||||
return handleMsgWithEmptyAnswer(m), nil
|
||||
func (c clientWithDisableTypes) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
|
||||
// filter dns request
|
||||
if slices.ContainsFunc(m.Question, c.inQuestion) {
|
||||
// In fact, DNS requests are not allowed to contain multiple questions:
|
||||
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071
|
||||
// so, when we find a question containing the type, we can simply discard the entire dns request.
|
||||
return handleMsgWithEmptyAnswer(m), nil
|
||||
}
|
||||
|
||||
// do real exchange
|
||||
msg, err = c.dnsClient.ExchangeContext(ctx, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// filter dns response
|
||||
msg.Answer = slices.DeleteFunc(msg.Answer, c.inRR)
|
||||
msg.Ns = slices.DeleteFunc(msg.Ns, c.inRR)
|
||||
msg.Extra = slices.DeleteFunc(msg.Extra, c.inRR)
|
||||
return
|
||||
}
|
||||
|
||||
func (c clientWithDisableTypes) inQuestion(q D.Question) bool {
|
||||
_, ok := c.disableTypes[q.Qtype]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c clientWithDisableTypes) inRR(rr D.RR) bool {
|
||||
_, ok := c.disableTypes[rr.Header().Rrtype]
|
||||
return ok
|
||||
}
|
||||
|
||||
func warpClientWithDisableTypes(c dnsClient, params map[string]string) dnsClient {
|
||||
disableTypes := make(map[uint16]struct{})
|
||||
if params["disable-ipv4"] == "true" {
|
||||
disableTypes[D.TypeA] = struct{}{}
|
||||
}
|
||||
if params["disable-ipv6"] == "true" {
|
||||
disableTypes[D.TypeAAAA] = struct{}{}
|
||||
}
|
||||
for key, value := range params {
|
||||
const prefix = "disable-qtype-"
|
||||
if strings.HasPrefix(key, prefix) && value == "true" { // eg: disable-qtype-65=true
|
||||
qType, err := strconv.ParseUint(key[len(prefix):], 10, 16)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := D.TypeToRR[uint16(qType)]; !ok { // check valid RR_Header.Rrtype and Question.qtype
|
||||
continue
|
||||
}
|
||||
disableTypes[uint16(qType)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return c.dnsClient.ExchangeContext(ctx, m)
|
||||
}
|
||||
|
||||
func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient {
|
||||
return clientWithDisableType{c, qType}
|
||||
if len(disableTypes) > 0 {
|
||||
return clientWithDisableTypes{c, disableTypes}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type clientWithEdns0Subnet struct {
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ require (
|
||||
github.com/metacubex/http v0.1.0
|
||||
github.com/metacubex/kcp-go v0.0.0-20251111012849-7455698490e9
|
||||
github.com/metacubex/mlkem v0.1.0
|
||||
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72
|
||||
github.com/metacubex/randv2 v0.2.0
|
||||
github.com/metacubex/restls-client-go v0.1.7
|
||||
github.com/metacubex/sing v0.5.6
|
||||
|
||||
+2
-2
@@ -114,8 +114,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
||||
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
||||
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec h1:5ePGO2Xht06fpwjNIzfY5XS+82xwDHHx4xGbqgLbxjA=
|
||||
github.com/metacubex/quic-go v0.58.1-0.20251222092318-72a81ab195ec/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72 h1:kNlYHZ75itJwkerDiySpixX+dKsv/K0TYQsKvuxogNM=
|
||||
github.com/metacubex/quic-go v0.57.1-0.20251217071004-e89f497a2e72/go.mod h1:N071X2oW2+kIhLlHW3mfcD2QP+zWu2bEs1EEAm66bvI=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
||||
|
||||
+8
-2
@@ -118,9 +118,15 @@ Project **NodePass** is licensed under the [BSD 3-Clause License](LICENSE).
|
||||
|
||||
This project is provided "as is" without any warranties. Users assume all risks and must comply with local laws for legal use only. Developers are not liable for any direct, indirect, incidental, or consequential damages. Secondary development requires commitment to legal use and self-responsibility for legal compliance. Developers reserve the right to modify software features and this disclaimer at any time. Final interpretation rights belong to developers.
|
||||
|
||||
## 🔗 NFT Support
|
||||
## 🔗 Donation
|
||||
|
||||
Support **NodePass** in a unique way by checking out our NFT collection on [OpenSea](https://opensea.io/collection/nodepass).
|
||||
**Cryptocurrency:**
|
||||
|
||||
- EVM-compatible Address: `0x2ea4Ea9425BEe897ED74fC5512bd13ABC7100000`
|
||||
|
||||
**NFT collection:**
|
||||
|
||||
- Support **NodePass** in a unique way by checking out our NFT collection on [OpenSea](https://opensea.io/collection/nodepass).
|
||||
|
||||
## 🤝 Sponsors
|
||||
|
||||
|
||||
@@ -118,9 +118,15 @@ nodepass "master://:10101/api?log=debug&tls=1"
|
||||
|
||||
本项目以"现状"提供,开发者不提供任何明示或暗示的保证。用户使用风险自担,需遵守当地法律法规,仅限合法用途。开发者对任何直接、间接、偶然或后果性损害概不负责。进行二次开发须承诺合法使用并自负法律责任。开发者保留随时修改软件功能及本声明的权利。最终解释权归开发者所有。
|
||||
|
||||
## 🔗 NFT 支持
|
||||
## 🔗 捐赠
|
||||
|
||||
以独特方式支持 **NodePass**,查看我们在 [OpenSea](https://opensea.io/collection/nodepass) 上的 NFT 收藏。
|
||||
**加密货币:**
|
||||
|
||||
- EVM 兼容地址: `0x2ea4Ea9425BEe897ED74fC5512bd13ABC7100000`
|
||||
|
||||
**数字藏品:**
|
||||
|
||||
- 以独特方式支持 **NodePass**,查看我们在 [OpenSea](https://opensea.io/collection/nodepass) 上的 NFT 收藏。
|
||||
|
||||
## 🤝 赞助商
|
||||
|
||||
|
||||
@@ -73,8 +73,8 @@ The data flow mode is automatically determined based on tunnel address and targe
|
||||
```
|
||||
[Server] → [Generate Unique Connection ID] → [Signal Client via Unencrypted TCP Tunnel]
|
||||
```
|
||||
- For TCP: Generates a `//<connection_id>#1` signal
|
||||
- For UDP: Generates a `//<connection_id>#2` signal
|
||||
- For TCP: Generates a `{"action":"tcp","remote":"target_addr","id":"connection_id"}` signal
|
||||
- For UDP: Generates a `{"action":"udp","remote":"client_addr","id":"connection_id"}` signal
|
||||
|
||||
3. **Connection Preparation**:
|
||||
```
|
||||
@@ -95,7 +95,7 @@ The data flow mode is automatically determined based on tunnel address and targe
|
||||
```
|
||||
[Client] → [Read Signal from TCP Tunnel] → [Parse Connection ID]
|
||||
```
|
||||
- Client differentiates between TCP and UDP signals based on URL scheme
|
||||
- Client differentiates between signal types based on the action field
|
||||
|
||||
2. **Connection Establishment**:
|
||||
```
|
||||
@@ -163,27 +163,42 @@ The data flow mode is automatically determined based on tunnel address and targe
|
||||
|
||||
## Signal Communication Mechanism
|
||||
|
||||
NodePass uses a sophisticated URL-based signaling protocol through the TCP tunnel:
|
||||
NodePass uses a JSON-based signaling protocol through the TCP tunnel:
|
||||
|
||||
### Signal Types
|
||||
1. **Tunnel Signal**:
|
||||
- Format: `#<tls>`
|
||||
- Purpose: Informs the client about the tls code
|
||||
- Timing: Sent on tunnel handshake
|
||||
1. **Flush Signal**:
|
||||
- Format: `{"action":"flush"}`
|
||||
- Purpose: Flushes the connection pool and resets error count
|
||||
- Timing: Sent when connection pool health check fails
|
||||
|
||||
2. **TCP Launch Signal**:
|
||||
- Format: `//<connection_id>#1`
|
||||
2. **PING Signal**:
|
||||
- Format: `{"action":"ping"}`
|
||||
- Purpose: Checks client connection status and requests PONG response
|
||||
- Timing: Sent during periodic health checks
|
||||
|
||||
3. **PONG Signal**:
|
||||
- Format: `{"action":"pong"}`
|
||||
- Purpose: Responds to PING signal and reports system status
|
||||
- Timing: Sent when PING signal is received
|
||||
|
||||
4. **Verify Signal**:
|
||||
- Format: `{"action":"verify","id":"connection_id","fp":"tls_fingerprint"}`
|
||||
- Purpose: Verifies TLS certificate fingerprint
|
||||
- Timing: Sent after establishing TLS connection
|
||||
|
||||
5. **TCP Launch Signal**:
|
||||
- Format: `{"action":"tcp","remote":"remote_addr","id":"connection_id"}`
|
||||
- Purpose: Requests the client to establish a TCP connection for a specific ID
|
||||
- Timing: Sent when a new TCP connection to the target service is received
|
||||
|
||||
3. **UDP Launch Signal**:
|
||||
- Format: `//<connection_id>#2`
|
||||
6. **UDP Launch Signal**:
|
||||
- Format: `{"action":"udp","remote":"remote_addr","id":"connection_id"}`
|
||||
- Purpose: Requests the client to handle UDP traffic for a specific ID
|
||||
- Timing: Sent when UDP data is received on the target port
|
||||
|
||||
### Signal Flow
|
||||
1. **Signal Generation**:
|
||||
- Server creates URL-formatted signals for specific events
|
||||
- Server creates JSON-formatted signals for specific events
|
||||
- Signal is terminated with a newline character for proper parsing
|
||||
|
||||
2. **Signal Transmission**:
|
||||
@@ -192,7 +207,7 @@ NodePass uses a sophisticated URL-based signaling protocol through the TCP tunne
|
||||
|
||||
3. **Signal Reception**:
|
||||
- Client uses a buffered reader to read signals from the tunnel
|
||||
- Signals are trimmed and parsed into URL format
|
||||
- Signals are parsed into JSON format
|
||||
|
||||
4. **Signal Processing**:
|
||||
- Client places valid signals in a buffered channel (signalChan)
|
||||
@@ -200,8 +215,8 @@ NodePass uses a sophisticated URL-based signaling protocol through the TCP tunne
|
||||
- Semaphore pattern prevents signal overflow
|
||||
|
||||
5. **Signal Execution**:
|
||||
- Remote signals update the client's remote address configuration
|
||||
- Launch signals trigger the `clientOnce()` method to establish connections
|
||||
- Dispatches to appropriate handling logic based on the `action` field
|
||||
- Connection launch signals trigger respective methods to establish connections
|
||||
|
||||
### Signal Resilience
|
||||
- Buffered channel with configurable capacity prevents signal loss during high load
|
||||
|
||||
@@ -73,8 +73,8 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议
|
||||
```
|
||||
[服务端] → [生成唯一连接 ID] → [通过未加密的 TCP 隧道向客户端发送信号]
|
||||
```
|
||||
- 对于 TCP:生成 `//<connection_id>#1` 信号
|
||||
- 对于 UDP:生成 `//<connection_id>#2` 信号
|
||||
- 对于 TCP:生成 `{"action":"tcp","remote":"target_addr","id":"connection_id"}` 信号
|
||||
- 对于 UDP:生成 `{"action":"udp","remote":"client_addr","id":"connection_id"}` 信号
|
||||
|
||||
3. **连接准备**:
|
||||
```
|
||||
@@ -95,7 +95,7 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议
|
||||
```
|
||||
[客户端] → [从 TCP 隧道读取信号] → [解析连接 ID]
|
||||
```
|
||||
- 客户端根据 URL 方案区分 TCP 和 UDP 信号
|
||||
- 客户端根据 `action` 字段区分不同类型的信号
|
||||
|
||||
2. **连接建立**:
|
||||
```
|
||||
@@ -163,27 +163,42 @@ NodePass 通过其隧道架构建立双向数据流,支持 TCP 和 UDP 协议
|
||||
|
||||
## 信号通信机制
|
||||
|
||||
NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议:
|
||||
NodePass 通过 TCP 隧道使用基于 JSON 的信号协议:
|
||||
|
||||
### 信号类型
|
||||
1. **隧道信号**:
|
||||
- 格式:`#<tls>`
|
||||
- 目的:通知客户端 TLS 代号
|
||||
- 时机:在隧道握手时发送
|
||||
1. **刷新信号**:
|
||||
- 格式:`{"action":"flush"}`
|
||||
- 目的:刷新连接池并重置错误计数
|
||||
- 时机:当连接池健康检查失败时发送
|
||||
|
||||
2. **TCP 启动信号**:
|
||||
- 格式:`//<connection_id>#1`
|
||||
2. **PING 信号**:
|
||||
- 格式:`{"action":"ping"}`
|
||||
- 目的:检查客户端连接状态并请求 PONG 响应
|
||||
- 时机:定期健康检查时发送
|
||||
|
||||
3. **PONG 信号**:
|
||||
- 格式:`{"action":"pong"}`
|
||||
- 目的:响应 PING 信号并报告系统状态
|
||||
- 时机:收到 PING 信号时发送
|
||||
|
||||
4. **验证信号**:
|
||||
- 格式:`{"action":"verify","id":"connection_id","fp":"tls_fingerprint"}`
|
||||
- 目的:验证 TLS 证书指纹
|
||||
- 时机:建立 TLS 连接后发送
|
||||
|
||||
5. **TCP 启动信号**:
|
||||
- 格式:`{"action":"tcp","remote":"remote_addr","id":"connection_id"}`
|
||||
- 目的:请求客户端为特定 ID 建立 TCP 连接
|
||||
- 时机:当接收到目标服务的新 TCP 连接时发送
|
||||
|
||||
3. **UDP 启动信号**:
|
||||
- 格式:`//<connection_id>#2`
|
||||
6. **UDP 启动信号**:
|
||||
- 格式:`{"action":"udp","remote":"remote_addr","id":"connection_id"}`
|
||||
- 目的:请求客户端处理特定 ID 的 UDP 流量
|
||||
- 时机:当在目标端口接收到 UDP 数据时发送
|
||||
|
||||
### 信号流程
|
||||
1. **信号生成**:
|
||||
- 服务端为特定事件创建 URL 格式的信号
|
||||
- 服务端为特定事件创建 JSON 格式的信号
|
||||
- 信号以换行符终止,以便正确解析
|
||||
|
||||
2. **信号传输**:
|
||||
@@ -192,7 +207,7 @@ NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议:
|
||||
|
||||
3. **信号接收**:
|
||||
- 客户端使用缓冲读取器从隧道读取信号
|
||||
- 信号被修剪并解析为 URL 格式
|
||||
- 信号被解析为 JSON 格式
|
||||
|
||||
4. **信号处理**:
|
||||
- 客户端将有效信号放入缓冲通道 (signalChan)
|
||||
@@ -200,8 +215,8 @@ NodePass 通过 TCP 隧道使用复杂的基于 URL 的信号协议:
|
||||
- 信号量模式防止信号溢出
|
||||
|
||||
5. **信号执行**:
|
||||
- 远程信号更新客户端的远程地址配置
|
||||
- 启动信号触发 `clientOnce()` 方法建立连接
|
||||
- 根据 `action` 字段分派到相应的处理逻辑
|
||||
- 连接启动信号触发相应的处理方法建立连接
|
||||
|
||||
### 信号弹性
|
||||
- 具有可配置容量的缓冲通道防止在高负载下信号丢失
|
||||
|
||||
+2
-2
@@ -7,14 +7,14 @@ require (
|
||||
github.com/NodePassProject/conn v1.0.16
|
||||
github.com/NodePassProject/logs v1.0.3
|
||||
github.com/NodePassProject/nph2 v1.0.4
|
||||
github.com/NodePassProject/npws v1.0.5
|
||||
github.com/NodePassProject/npws v1.0.6
|
||||
github.com/NodePassProject/pool v1.0.50
|
||||
github.com/NodePassProject/quic v1.0.14
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/quic-go/quic-go v0.58.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
|
||||
+4
-6
@@ -6,8 +6,8 @@ github.com/NodePassProject/logs v1.0.3 h1:CDUZVQ477vmmFQHazrQCWM0gJPNINm0C2N3FzC
|
||||
github.com/NodePassProject/logs v1.0.3/go.mod h1:TwtPXOzLtb8iH+fdduQjEEywICXivsM39cy9AinMSks=
|
||||
github.com/NodePassProject/nph2 v1.0.4 h1:szvp7hlvMRt1g/g1hF++0DfgM++5yqWWAPlfMDh226c=
|
||||
github.com/NodePassProject/nph2 v1.0.4/go.mod h1:fzNbTk0zh+0gXer2aKpPXF69o4/BOd15Ys2gvicwWR0=
|
||||
github.com/NodePassProject/npws v1.0.5 h1:UpVm4UYytNQHq6Nkli4Qqq17wpuwvKqoCY571+vubBA=
|
||||
github.com/NodePassProject/npws v1.0.5/go.mod h1:cyVS2X3/8f07yOkVEl53D+ozHLljJvHQx/s6xoLl/s8=
|
||||
github.com/NodePassProject/npws v1.0.6 h1:LZ+ZqqZoZ7OnmdtEps/WHuq2S7Qda4nPtr+15tm0024=
|
||||
github.com/NodePassProject/npws v1.0.6/go.mod h1:cyVS2X3/8f07yOkVEl53D+ozHLljJvHQx/s6xoLl/s8=
|
||||
github.com/NodePassProject/pool v1.0.50 h1:Xmvb0hSHq24U06moHICr8CkqEHB4ZHBbY9Z+a7fSejQ=
|
||||
github.com/NodePassProject/pool v1.0.50/go.mod h1:joQFk1oocg56QpJ1QK/2g5Jv/AyqYUQgPXMG1gWe8iA=
|
||||
github.com/NodePassProject/quic v1.0.14 h1:zx5/cmQ8Tp1ntwCVAmtosODfBWTXTHHuprZdAspSjc8=
|
||||
@@ -18,8 +18,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
|
||||
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -32,7 +32,5 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -31,7 +31,8 @@ func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) {
|
||||
Common: Common{
|
||||
parsedURL: parsedURL,
|
||||
logger: logger,
|
||||
signalChan: make(chan string, semaphoreLimit),
|
||||
signalChan: make(chan Signal, semaphoreLimit),
|
||||
writeChan: make(chan []byte, semaphoreLimit),
|
||||
tcpBufferPool: &sync.Pool{
|
||||
New: func() any {
|
||||
buf := make([]byte, tcpDataBufSize)
|
||||
@@ -44,9 +45,6 @@ func NewClient(parsedURL *url.URL, logger *logs.Logger) (*Client, error) {
|
||||
return &buf
|
||||
},
|
||||
},
|
||||
flushURL: &url.URL{Scheme: "np", Fragment: "f"},
|
||||
pingURL: &url.URL{Scheme: "np", Fragment: "i"},
|
||||
pongURL: &url.URL{Scheme: "np", Fragment: "o"},
|
||||
},
|
||||
}
|
||||
if err := client.initConfig(); err != nil {
|
||||
@@ -284,6 +282,9 @@ func (c *Client) tunnelHandshake() error {
|
||||
c.maxPoolCapacity = config.Max
|
||||
c.tlsCode = config.TLS
|
||||
c.poolType = config.Type
|
||||
if c.tlsCode == "1" || c.tlsCode == "2" {
|
||||
c.verifyChan = make(chan struct{})
|
||||
}
|
||||
|
||||
c.logger.Info("Loading tunnel config: FLOW=%v|MAX=%v|TLS=%v|TYPE=%v",
|
||||
c.dataFlow, c.maxPoolCapacity, c.tlsCode, c.poolType)
|
||||
|
||||
+160
-133
@@ -10,6 +10,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
@@ -28,7 +29,6 @@ import (
|
||||
|
||||
// Common 包含所有模式共享的核心功能
|
||||
type Common struct {
|
||||
mu sync.Mutex // 互斥锁
|
||||
parsedURL *url.URL // 解析后的URL
|
||||
logger *logs.Logger // 日志记录器
|
||||
dnsCacheTTL time.Duration // DNS缓存TTL
|
||||
@@ -74,12 +74,11 @@ type Common struct {
|
||||
bufReader *bufio.Reader // 缓冲读取器
|
||||
tcpBufferPool *sync.Pool // TCP缓冲区池
|
||||
udpBufferPool *sync.Pool // UDP缓冲区池
|
||||
signalChan chan string // 信号通道
|
||||
signalChan chan Signal // 信号通道
|
||||
writeChan chan []byte // 写入通道
|
||||
verifyChan chan struct{} // 证书验证通道
|
||||
handshakeStart time.Time // 握手开始时间
|
||||
checkPoint time.Time // 检查点时间
|
||||
flushURL *url.URL // 重置信号
|
||||
pingURL *url.URL // PING信号
|
||||
pongURL *url.URL // PONG信号
|
||||
slotLimit int32 // 槽位限制
|
||||
tcpSlot int32 // TCP连接数
|
||||
udpSlot int32 // UDP连接数
|
||||
@@ -124,6 +123,14 @@ type TransportPool interface {
|
||||
ResetError()
|
||||
}
|
||||
|
||||
// Signal 操作信号结构体
|
||||
type Signal struct {
|
||||
ActionType string `json:"action"` // 操作类型
|
||||
RemoteAddr string `json:"remote,omitempty"` // 远程地址
|
||||
PoolConnID string `json:"id,omitempty"` // 池连接ID
|
||||
Fingerprint string `json:"fp,omitempty"` // TLS指纹
|
||||
}
|
||||
|
||||
// 配置变量,可通过环境变量调整
|
||||
var (
|
||||
semaphoreLimit = getEnvAsInt("NP_SEMAPHORE_LIMIT", 65536) // 信号量限制
|
||||
@@ -956,6 +963,8 @@ func (c *Common) stop() {
|
||||
|
||||
// 清空通道
|
||||
drain(c.signalChan)
|
||||
drain(c.writeChan)
|
||||
drain(c.verifyChan)
|
||||
|
||||
// 重置全局限速器
|
||||
if c.rateLimiter != nil {
|
||||
@@ -1006,6 +1015,24 @@ func (c *Common) setControlConn() error {
|
||||
c.controlConn = poolConn
|
||||
c.bufReader = bufio.NewReader(&conn.TimeoutReader{Conn: c.controlConn, Timeout: 3 * reportInterval})
|
||||
c.logger.Info("Marking tunnel handshake as complete in %vms", time.Since(c.handshakeStart).Milliseconds())
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case data := <-c.writeChan:
|
||||
_, err := c.controlConn.Write(data)
|
||||
if err != nil {
|
||||
c.logger.Error("startWriter: write failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if c.tlsCode == "1" || c.tlsCode == "2" {
|
||||
c.logger.Info("TLS certificate fingerprint verifying...")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1046,7 +1073,18 @@ func (c *Common) commonQueue() error {
|
||||
}
|
||||
continue
|
||||
}
|
||||
signal := string(signalData)
|
||||
|
||||
// 解析JSON信号
|
||||
var signal Signal
|
||||
if err := json.Unmarshal(signalData, &signal); err != nil {
|
||||
c.logger.Error("commonQueue: unmarshal signal failed: %v", err)
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return fmt.Errorf("commonQueue: context error: %w", c.ctx.Err())
|
||||
case <-time.After(contextCheckInterval):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 将信号发送到通道
|
||||
select {
|
||||
@@ -1080,20 +1118,12 @@ func (c *Common) healthCheck() error {
|
||||
}
|
||||
|
||||
for c.ctx.Err() == nil {
|
||||
// 尝试获取锁
|
||||
if !c.mu.TryLock() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 连接池健康度检查
|
||||
if c.tunnelPool.ErrorCount() > c.tunnelPool.Active()/2 {
|
||||
// 发送刷新信号到对端
|
||||
if c.ctx.Err() == nil && c.controlConn != nil {
|
||||
_, err := c.controlConn.Write(c.encode([]byte(c.flushURL.String())))
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("healthCheck: write flush signal failed: %w", err)
|
||||
}
|
||||
signalData, _ := json.Marshal(Signal{ActionType: "flush"})
|
||||
c.writeChan <- c.encode(signalData)
|
||||
}
|
||||
c.tunnelPool.Flush()
|
||||
c.tunnelPool.ResetError()
|
||||
@@ -1110,14 +1140,9 @@ func (c *Common) healthCheck() error {
|
||||
// 发送PING信号
|
||||
c.checkPoint = time.Now()
|
||||
if c.ctx.Err() == nil && c.controlConn != nil {
|
||||
_, err := c.controlConn.Write(c.encode([]byte(c.pingURL.String())))
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("healthCheck: write ping signal failed: %w", err)
|
||||
}
|
||||
signalData, _ := json.Marshal(Signal{ActionType: "ping"})
|
||||
c.writeChan <- c.encode(signalData)
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return fmt.Errorf("healthCheck: context error: %w", c.ctx.Err())
|
||||
@@ -1131,7 +1156,7 @@ func (c *Common) healthCheck() error {
|
||||
// incomingVerify 入口连接验证
|
||||
func (c *Common) incomingVerify() {
|
||||
for c.ctx.Err() == nil {
|
||||
if c.tunnelPool.Ready() {
|
||||
if c.tunnelPool.Ready() && c.tunnelPool.Active() > 0 {
|
||||
break
|
||||
}
|
||||
select {
|
||||
@@ -1141,39 +1166,41 @@ func (c *Common) incomingVerify() {
|
||||
}
|
||||
}
|
||||
|
||||
if c.tlsConfig == nil || len(c.tlsConfig.Certificates) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cert := c.tlsConfig.Certificates[0]
|
||||
if len(cert.Certificate) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 打印证书指纹
|
||||
c.logger.Info("TLS cert verified: %v", c.formatCertFingerprint(cert.Certificate[0]))
|
||||
|
||||
id, testConn, err := c.tunnelPool.IncomingGet(poolGetTimeout)
|
||||
if err != nil {
|
||||
c.logger.Error("incomingVerify: incomingGet failed: %v", err)
|
||||
c.cancel()
|
||||
return
|
||||
}
|
||||
defer testConn.Close()
|
||||
|
||||
// 构建并发送验证信号
|
||||
verifyURL := &url.URL{
|
||||
Scheme: "np",
|
||||
Host: c.controlConn.RemoteAddr().String(),
|
||||
Path: url.PathEscape(id),
|
||||
Fragment: "v", // TLS验证
|
||||
// 获取证书指纹
|
||||
var fingerprint string
|
||||
switch c.coreType {
|
||||
case "server":
|
||||
if c.tlsConfig != nil && len(c.tlsConfig.Certificates) > 0 {
|
||||
cert := c.tlsConfig.Certificates[0]
|
||||
if len(cert.Certificate) > 0 {
|
||||
fingerprint = c.formatCertFingerprint(cert.Certificate[0])
|
||||
}
|
||||
}
|
||||
case "client":
|
||||
if conn, ok := testConn.(interface{ ConnectionState() tls.ConnectionState }); ok {
|
||||
state := conn.ConnectionState()
|
||||
if len(state.PeerCertificates) > 0 {
|
||||
fingerprint = c.formatCertFingerprint(state.PeerCertificates[0].Raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建并发送验证信号
|
||||
if c.ctx.Err() == nil && c.controlConn != nil {
|
||||
c.mu.Lock()
|
||||
_, err = c.controlConn.Write(c.encode([]byte(verifyURL.String())))
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
signalData, _ := json.Marshal(Signal{
|
||||
ActionType: "verify",
|
||||
PoolConnID: id,
|
||||
Fingerprint: fingerprint,
|
||||
})
|
||||
c.writeChan <- c.encode(signalData)
|
||||
}
|
||||
|
||||
c.logger.Debug("TLS verify signal: cid %v -> %v", id, c.controlConn.RemoteAddr())
|
||||
@@ -1184,6 +1211,15 @@ func (c *Common) commonLoop() {
|
||||
for c.ctx.Err() == nil {
|
||||
// 等待连接池准备就绪
|
||||
if c.tunnelPool.Ready() {
|
||||
if c.verifyChan != nil {
|
||||
select {
|
||||
case <-c.verifyChan:
|
||||
// 证书验证完成
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.targetListener != nil || c.disableTCP != "1" {
|
||||
go c.commonTCPLoop()
|
||||
}
|
||||
@@ -1266,22 +1302,13 @@ func (c *Common) commonTCPLoop() {
|
||||
c.logger.Debug("Tunnel connection: %v <-> %v", remoteConn.LocalAddr(), remoteConn.RemoteAddr())
|
||||
|
||||
// 构建并发送启动信号
|
||||
launchURL := &url.URL{
|
||||
Scheme: "np",
|
||||
Host: targetConn.RemoteAddr().String(),
|
||||
Path: url.PathEscape(id),
|
||||
Fragment: "1", // TCP模式
|
||||
}
|
||||
|
||||
if c.ctx.Err() == nil && c.controlConn != nil {
|
||||
c.mu.Lock()
|
||||
_, err = c.controlConn.Write(c.encode([]byte(launchURL.String())))
|
||||
c.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
c.logger.Error("commonTCPLoop: write launch signal failed: %v", err)
|
||||
return
|
||||
}
|
||||
signalData, _ := json.Marshal(Signal{
|
||||
ActionType: "tcp",
|
||||
RemoteAddr: targetConn.RemoteAddr().String(),
|
||||
PoolConnID: id,
|
||||
})
|
||||
c.writeChan <- c.encode(signalData)
|
||||
}
|
||||
|
||||
c.logger.Debug("TCP launch signal: cid %v -> %v", id, c.controlConn.RemoteAddr())
|
||||
@@ -1397,21 +1424,13 @@ func (c *Common) commonUDPLoop() {
|
||||
}(remoteConn, clientAddr, sessionKey, id)
|
||||
|
||||
// 构建并发送启动信号
|
||||
launchURL := &url.URL{
|
||||
Scheme: "np",
|
||||
Host: clientAddr.String(),
|
||||
Path: url.PathEscape(id),
|
||||
Fragment: "2", // UDP模式
|
||||
}
|
||||
|
||||
if c.ctx.Err() == nil && c.controlConn != nil {
|
||||
c.mu.Lock()
|
||||
_, err = c.controlConn.Write(c.encode([]byte(launchURL.String())))
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
c.logger.Error("commonUDPLoop: write launch signal failed: %v", err)
|
||||
continue
|
||||
}
|
||||
signalData, _ := json.Marshal(Signal{
|
||||
ActionType: "udp",
|
||||
RemoteAddr: clientAddr.String(),
|
||||
PoolConnID: id,
|
||||
})
|
||||
c.writeChan <- c.encode(signalData)
|
||||
}
|
||||
|
||||
c.logger.Debug("UDP launch signal: cid %v -> %v", id, c.controlConn.RemoteAddr())
|
||||
@@ -1453,33 +1472,21 @@ func (c *Common) commonOnce() error {
|
||||
case <-c.ctx.Done():
|
||||
return fmt.Errorf("commonOnce: context error: %w", c.ctx.Err())
|
||||
case signal := <-c.signalChan:
|
||||
// 解析信号URL
|
||||
signalURL, err := url.Parse(signal)
|
||||
if err != nil {
|
||||
c.logger.Error("commonOnce: parse signal failed: %v", err)
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return fmt.Errorf("commonOnce: context error: %w", c.ctx.Err())
|
||||
case <-time.After(contextCheckInterval):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理信号
|
||||
switch signalURL.Fragment {
|
||||
case "v": // 验证
|
||||
switch signal.ActionType {
|
||||
case "verify":
|
||||
if c.tlsCode == "1" || c.tlsCode == "2" {
|
||||
go c.outgoingVerify(signalURL)
|
||||
go c.outgoingVerify(signal)
|
||||
}
|
||||
case "1": // TCP
|
||||
case "tcp":
|
||||
if c.disableTCP != "1" {
|
||||
go c.commonTCPOnce(signalURL)
|
||||
go c.commonTCPOnce(signal)
|
||||
}
|
||||
case "2": // UDP
|
||||
case "udp":
|
||||
if c.disableUDP != "1" {
|
||||
go c.commonUDPOnce(signalURL)
|
||||
go c.commonUDPOnce(signal)
|
||||
}
|
||||
case "f": // 连接池刷新
|
||||
case "flush":
|
||||
go func() {
|
||||
c.tunnelPool.Flush()
|
||||
c.tunnelPool.ResetError()
|
||||
@@ -1492,16 +1499,12 @@ func (c *Common) commonOnce() error {
|
||||
|
||||
c.logger.Debug("Tunnel pool flushed: %v active connections", c.tunnelPool.Active())
|
||||
}()
|
||||
case "i": // PING
|
||||
case "ping":
|
||||
if c.ctx.Err() == nil && c.controlConn != nil {
|
||||
c.mu.Lock()
|
||||
_, err := c.controlConn.Write(c.encode([]byte(c.pongURL.String())))
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("commonOnce: write pong signal failed: %w", err)
|
||||
}
|
||||
signalData, _ := json.Marshal(Signal{ActionType: "pong"})
|
||||
c.writeChan <- c.encode(signalData)
|
||||
}
|
||||
case "o": // PONG
|
||||
case "pong":
|
||||
// 发送检查点事件
|
||||
c.logger.Event("CHECK_POINT|MODE=%v|PING=%vms|POOL=%v|TCPS=%v|UDPS=%v|TCPRX=%v|TCPTX=%v|UDPRX=%v|UDPTX=%v",
|
||||
c.runMode, time.Since(c.checkPoint).Milliseconds(), c.tunnelPool.Active(),
|
||||
@@ -1518,7 +1521,7 @@ func (c *Common) commonOnce() error {
|
||||
}
|
||||
|
||||
// outgoingVerify 出口连接验证
|
||||
func (c *Common) outgoingVerify(signalURL *url.URL) {
|
||||
func (c *Common) outgoingVerify(signal Signal) {
|
||||
for c.ctx.Err() == nil {
|
||||
if c.tunnelPool.Ready() {
|
||||
break
|
||||
@@ -1530,24 +1533,44 @@ func (c *Common) outgoingVerify(signalURL *url.URL) {
|
||||
}
|
||||
}
|
||||
|
||||
id := strings.TrimPrefix(signalURL.Path, "/")
|
||||
if unescapedID, err := url.PathUnescape(id); err != nil {
|
||||
c.logger.Error("outgoingVerify: unescape id failed: %v", err)
|
||||
fingerPrint := signal.Fingerprint
|
||||
if fingerPrint == "" {
|
||||
c.logger.Error("outgoingVerify: no fingerprint in signal")
|
||||
c.cancel()
|
||||
return
|
||||
} else {
|
||||
id = unescapedID
|
||||
}
|
||||
|
||||
id := signal.PoolConnID
|
||||
c.logger.Debug("TLS verify signal: cid %v <- %v", id, c.controlConn.RemoteAddr())
|
||||
|
||||
testConn, err := c.tunnelPool.OutgoingGet(id, poolGetTimeout)
|
||||
if err != nil {
|
||||
c.logger.Error("outgoingVerify: request timeout: %v", err)
|
||||
c.tunnelPool.AddError()
|
||||
c.cancel()
|
||||
return
|
||||
}
|
||||
defer testConn.Close()
|
||||
|
||||
if testConn != nil {
|
||||
// 验证证书指纹
|
||||
var serverFingerprint, clientFingerprint string
|
||||
switch c.coreType {
|
||||
case "server":
|
||||
if c.tlsConfig == nil || len(c.tlsConfig.Certificates) == 0 {
|
||||
c.logger.Error("outgoingVerify: no local certificate")
|
||||
c.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
cert := c.tlsConfig.Certificates[0]
|
||||
if len(cert.Certificate) == 0 {
|
||||
c.logger.Error("outgoingVerify: empty local certificate")
|
||||
c.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
serverFingerprint = c.formatCertFingerprint(cert.Certificate[0])
|
||||
clientFingerprint = fingerPrint
|
||||
case "client":
|
||||
conn, ok := testConn.(interface{ ConnectionState() tls.ConnectionState })
|
||||
if !ok {
|
||||
return
|
||||
@@ -1556,23 +1579,32 @@ func (c *Common) outgoingVerify(signalURL *url.URL) {
|
||||
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
c.logger.Error("outgoingVerify: no peer certificates found")
|
||||
c.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
// 打印证书指纹
|
||||
c.logger.Info("TLS cert verified: %v", c.formatCertFingerprint(state.PeerCertificates[0].Raw))
|
||||
clientFingerprint = c.formatCertFingerprint(state.PeerCertificates[0].Raw)
|
||||
serverFingerprint = fingerPrint
|
||||
}
|
||||
|
||||
// 验证指纹匹配
|
||||
if serverFingerprint != clientFingerprint {
|
||||
c.logger.Error("outgoingVerify: certificate fingerprint mismatch: server: %v - client: %v", serverFingerprint, clientFingerprint)
|
||||
c.cancel()
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Info("TLS certificate fingerprint verified: %v", fingerPrint)
|
||||
|
||||
// 通知验证完成
|
||||
if c.verifyChan != nil {
|
||||
c.verifyChan <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// commonTCPOnce 共用处理单个TCP请求
|
||||
func (c *Common) commonTCPOnce(signalURL *url.URL) {
|
||||
id := strings.TrimPrefix(signalURL.Path, "/")
|
||||
if unescapedID, err := url.PathUnescape(id); err != nil {
|
||||
c.logger.Error("commonTCPOnce: unescape id failed: %v", err)
|
||||
return
|
||||
} else {
|
||||
id = unescapedID
|
||||
}
|
||||
func (c *Common) commonTCPOnce(signal Signal) {
|
||||
id := signal.PoolConnID
|
||||
c.logger.Debug("TCP launch signal: cid %v <- %v", id, c.controlConn.RemoteAddr())
|
||||
|
||||
// 从连接池获取连接
|
||||
@@ -1620,7 +1652,7 @@ func (c *Common) commonTCPOnce(signalURL *url.URL) {
|
||||
c.logger.Debug("Target connection: %v <-> %v", targetConn.LocalAddr(), targetConn.RemoteAddr())
|
||||
|
||||
// 发送PROXY v1
|
||||
if err := c.sendProxyV1Header(signalURL.Host, targetConn); err != nil {
|
||||
if err := c.sendProxyV1Header(signal.RemoteAddr, targetConn); err != nil {
|
||||
c.logger.Error("commonTCPOnce: sendProxyV1Header failed: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -1638,14 +1670,9 @@ func (c *Common) commonTCPOnce(signalURL *url.URL) {
|
||||
}
|
||||
|
||||
// commonUDPOnce 共用处理单个UDP请求
|
||||
func (c *Common) commonUDPOnce(signalURL *url.URL) {
|
||||
id := strings.TrimPrefix(signalURL.Path, "/")
|
||||
if unescapedID, err := url.PathUnescape(id); err != nil {
|
||||
c.logger.Error("commonUDPOnce: unescape id failed: %v", err)
|
||||
return
|
||||
} else {
|
||||
id = unescapedID
|
||||
}
|
||||
// commonUDPOnce 共用处理单个UDP请求
|
||||
func (c *Common) commonUDPOnce(signal Signal) {
|
||||
id := signal.PoolConnID
|
||||
c.logger.Debug("UDP launch signal: cid %v <- %v", id, c.controlConn.RemoteAddr())
|
||||
|
||||
// 获取池连接
|
||||
@@ -1668,7 +1695,7 @@ func (c *Common) commonUDPOnce(signalURL *url.URL) {
|
||||
}()
|
||||
|
||||
var targetConn net.Conn
|
||||
sessionKey := signalURL.Host
|
||||
sessionKey := signal.RemoteAddr
|
||||
isNewSession := false
|
||||
|
||||
// 获取或创建目标UDP会话
|
||||
|
||||
@@ -35,7 +35,8 @@ func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger
|
||||
tlsCode: tlsCode,
|
||||
tlsConfig: tlsConfig,
|
||||
logger: logger,
|
||||
signalChan: make(chan string, semaphoreLimit),
|
||||
signalChan: make(chan Signal, semaphoreLimit),
|
||||
writeChan: make(chan []byte, semaphoreLimit),
|
||||
tcpBufferPool: &sync.Pool{
|
||||
New: func() any {
|
||||
buf := make([]byte, tcpDataBufSize)
|
||||
@@ -48,9 +49,6 @@ func NewServer(parsedURL *url.URL, tlsCode string, tlsConfig *tls.Config, logger
|
||||
return &buf
|
||||
},
|
||||
},
|
||||
flushURL: &url.URL{Scheme: "np", Fragment: "f"},
|
||||
pingURL: &url.URL{Scheme: "np", Fragment: "i"},
|
||||
pongURL: &url.URL{Scheme: "np", Fragment: "o"},
|
||||
},
|
||||
}
|
||||
if err := server.initConfig(); err != nil {
|
||||
@@ -215,6 +213,10 @@ func (s *Server) initTunnelPool() error {
|
||||
|
||||
// tunnelHandshake 与客户端进行HTTP握手
|
||||
func (s *Server) tunnelHandshake() error {
|
||||
if s.tlsCode == "1" || s.tlsCode == "2" {
|
||||
s.verifyChan = make(chan struct{})
|
||||
}
|
||||
|
||||
var clientIP string
|
||||
done := make(chan struct{})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=lucky
|
||||
PKG_VERSION:=2.20.2
|
||||
PKG_VERSION:=2.24.0
|
||||
PKG_RELEASE:=1
|
||||
PKGARCH:=all
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
||||
t.Parallel()
|
||||
const testSNI = "test.example.com"
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.13.0-alpha.36
|
||||
|
||||
* Downgrade quic-go to v0.57.1
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.0-alpha.35
|
||||
|
||||
* Add pre-match support for `auto_redirect` **1**
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
"method": "2022-blake3-aes-128-gcm",
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||
"managed": false,
|
||||
"multiplex": {}
|
||||
}
|
||||
```
|
||||
@@ -86,6 +87,10 @@ Both if empty.
|
||||
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
||||
| other methods | any string |
|
||||
|
||||
#### managed
|
||||
|
||||
Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user.
|
||||
|
||||
#### multiplex
|
||||
|
||||
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
"method": "2022-blake3-aes-128-gcm",
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||
"managed": false,
|
||||
"multiplex": {}
|
||||
}
|
||||
```
|
||||
@@ -86,6 +87,10 @@
|
||||
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
||||
| other methods | 任意字符串 |
|
||||
|
||||
#### managed
|
||||
|
||||
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ require (
|
||||
github.com/sagernet/quic-go v0.58.0-sing-box-mod.1
|
||||
github.com/sagernet/sing v0.8.0-beta.8
|
||||
github.com/sagernet/sing-mux v0.3.3
|
||||
github.com/sagernet/sing-quic v0.6.0-beta.7
|
||||
github.com/sagernet/sing-quic v0.6.0-beta.8
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
|
||||
+2
-2
@@ -210,8 +210,8 @@ github.com/sagernet/sing v0.8.0-beta.8 h1:hUo0wZ2HGTieV1flEIai96HFhF34mMHVnduRqJ
|
||||
github.com/sagernet/sing v0.8.0-beta.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
|
||||
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
|
||||
github.com/sagernet/sing-quic v0.6.0-beta.7 h1:Sh6KltQ6nB69S9ZdDKs5oARqkyY99gOaHe1JPxNV7ag=
|
||||
github.com/sagernet/sing-quic v0.6.0-beta.7/go.mod h1:0NodMFjlAvfLp87Enpx46fQPrGRvmbUsmy5hRLzomtM=
|
||||
github.com/sagernet/sing-quic v0.6.0-beta.8 h1:Y0P8WTqWpfg80rLFsDfF22QumM+HEAjRQ2o+8Dv+vDs=
|
||||
github.com/sagernet/sing-quic v0.6.0-beta.8/go.mod h1:Y3YVjPutLHLQvYjGtUFH+w8YmM4MTd19NDzxLZGNGIs=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||
|
||||
+22
-18
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
)
|
||||
|
||||
@@ -60,37 +61,40 @@ func checkOptions(options *Options) error {
|
||||
|
||||
func checkInbounds(inbounds []Inbound) error {
|
||||
seen := make(map[string]bool)
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Tag == "" {
|
||||
continue
|
||||
for i, inbound := range inbounds {
|
||||
tag := inbound.Tag
|
||||
if tag == "" {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
if seen[inbound.Tag] {
|
||||
return E.New("duplicate inbound tag: ", inbound.Tag)
|
||||
if seen[tag] {
|
||||
return E.New("duplicate inbound tag: ", tag)
|
||||
}
|
||||
seen[inbound.Tag] = true
|
||||
seen[tag] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
||||
seen := make(map[string]bool)
|
||||
for _, outbound := range outbounds {
|
||||
if outbound.Tag == "" {
|
||||
continue
|
||||
for i, outbound := range outbounds {
|
||||
tag := outbound.Tag
|
||||
if tag == "" {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
if seen[outbound.Tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
|
||||
if seen[tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", tag)
|
||||
}
|
||||
seen[outbound.Tag] = true
|
||||
seen[tag] = true
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Tag == "" {
|
||||
continue
|
||||
for i, endpoint := range endpoints {
|
||||
tag := endpoint.Tag
|
||||
if tag == "" {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
if seen[endpoint.Tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
|
||||
if seen[tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", tag)
|
||||
}
|
||||
seen[endpoint.Tag] = true
|
||||
seen[tag] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,13 +21,13 @@ define Download/geoip
|
||||
HASH:=6878dbacfb1fcb1ee022f63ed6934bcefc95a3c4ba10c88f1131fb88dbf7c337
|
||||
endef
|
||||
|
||||
GEOSITE_VER:=20260101111132
|
||||
GEOSITE_VER:=20260102111739
|
||||
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
|
||||
define Download/geosite
|
||||
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
|
||||
URL_FILE:=dlc.dat
|
||||
FILE:=$(GEOSITE_FILE)
|
||||
HASH:=f8ba540481e5aab5b5f18224331b5981296def00cdb61481aed85c9fca078a04
|
||||
HASH:=a535bde603510e96f2623ba081865a63ea2aae6d23c14c3a4430805bcd300f4f
|
||||
endef
|
||||
|
||||
GEOSITE_IRAN_VER:=202512290048
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ require (
|
||||
github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-playground/validator/v10 v10.28.0
|
||||
github.com/go-playground/validator/v10 v10.30.1
|
||||
github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/golang/protobuf v1.5.4
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/sys v0.39.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1
|
||||
@@ -60,7 +60,7 @@ require (
|
||||
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
@@ -91,6 +91,6 @@ require (
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
nhooyr.io/websocket v1.8.6 // indirect
|
||||
)
|
||||
|
||||
+8
-8
@@ -107,8 +107,8 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
@@ -140,8 +140,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
@@ -807,8 +807,8 @@ google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvx
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
@@ -823,8 +823,8 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
@@ -77,6 +77,9 @@ func (h *httpTripperClient) RoundTrip(ctx context.Context, req request.Request,
|
||||
return resp, err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
newError("non-200 response: ", httpResp.Status).AtInfo().WriteToLog()
|
||||
}
|
||||
if streamingWriter == nil {
|
||||
result, err := io.ReadAll(httpResp.Body)
|
||||
if err != nil {
|
||||
|
||||
+5
-1
@@ -90,7 +90,11 @@ func (c *Client) Dial(ctx context.Context) (net.Conn, error) {
|
||||
return nil, newError("failed to dial to destination").Base(err).AtError()
|
||||
}
|
||||
if c.config.SecurityConfig != nil {
|
||||
securityEngine, err := common.CreateObject(c.ctx, c.config.SecurityConfig)
|
||||
securityConfigSetting, err := serial.GetInstanceOf(c.config.SecurityConfig)
|
||||
if err != nil {
|
||||
return nil, newError("unable to get security config instance").Base(err)
|
||||
}
|
||||
securityEngine, err := common.CreateObject(c.ctx, securityConfigSetting)
|
||||
if err != nil {
|
||||
return nil, newError("unable to create security engine from security settings").Base(err)
|
||||
}
|
||||
|
||||
+5
-1
@@ -99,7 +99,11 @@ func (c *ServerInverseRole) Dial(ctx context.Context) (net.Conn, error) {
|
||||
return nil, newError("failed to dial to destination").Base(err).AtError()
|
||||
}
|
||||
if c.config.SecurityConfig != nil {
|
||||
securityEngine, err := common.CreateObject(ctx, c.config.SecurityConfig)
|
||||
securityConfigSetting, err := serial.GetInstanceOf(c.config.SecurityConfig)
|
||||
if err != nil {
|
||||
return nil, newError("unable to get security config instance").Base(err)
|
||||
}
|
||||
securityEngine, err := common.CreateObject(c.ctx, securityConfigSetting)
|
||||
if err != nil {
|
||||
return nil, newError("unable to create security engine from security settings").Base(err)
|
||||
}
|
||||
|
||||
+34
-1
@@ -2,6 +2,7 @@ name: Release (nightly)
|
||||
on:
|
||||
schedule:
|
||||
- cron: '23 23 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -19,6 +20,22 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Retrieve HEAD commit hash
|
||||
id: head
|
||||
shell: bash
|
||||
run: echo "head=$(git rev-parse HEAD)" | tee -a "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache nightly commit hash
|
||||
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
env:
|
||||
SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
|
||||
with:
|
||||
path: .nightly_commit_hash
|
||||
key: release-nightly-${{ steps.head.outputs.head }}
|
||||
restore-keys: |
|
||||
release-nightly-
|
||||
|
||||
- name: Check for new commits
|
||||
id: check_for_new_commits
|
||||
shell: bash
|
||||
@@ -35,7 +52,22 @@ jobs:
|
||||
".github/workflows/release.yml"
|
||||
".github/workflows/release-nightly.yml"
|
||||
)
|
||||
echo "commit=$(git log --format=%H -1 --since="24 hours ago" -- "${relevant_files[@]}")" | tee "$GITHUB_OUTPUT"
|
||||
if [[ -f .nightly_commit_hash ]]; then
|
||||
limit_args=(
|
||||
"$(cat .nightly_commit_hash)..HEAD"
|
||||
)
|
||||
else
|
||||
limit_args=(
|
||||
--since="24 hours ago"
|
||||
)
|
||||
fi
|
||||
echo "commit=$(git log --format=%H -1 "${limit_args[@]}" -- "${relevant_files[@]}")" | tee -a "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Record new nightly commit hash
|
||||
env:
|
||||
HEAD: ${{ steps.head.outputs.head }}
|
||||
shell: bash
|
||||
run: echo "${HEAD}" | tee .nightly_commit_hash
|
||||
|
||||
release:
|
||||
name: Publish Github release
|
||||
@@ -66,6 +98,7 @@ jobs:
|
||||
with:
|
||||
path: dist
|
||||
name: build-pypi
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
|
||||
Vendored
+2
-2
@@ -215,7 +215,7 @@ jobs:
|
||||
python -m build --no-isolation .
|
||||
|
||||
- name: Upload artifacts
|
||||
if: github.event_name != 'workflow_dispatch'
|
||||
if: github.event.workflow != '.github/workflows/release.yml' # Reusable workflow_call
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: build-pypi
|
||||
@@ -224,7 +224,7 @@ jobs:
|
||||
compression-level: 0
|
||||
|
||||
- name: Publish to PyPI
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
if: github.event.workflow == '.github/workflows/release.yml' # Direct workflow_dispatch
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
verbose: true
|
||||
|
||||
@@ -2007,6 +2007,11 @@ from .taptap import (
|
||||
TapTapMomentIE,
|
||||
TapTapPostIntlIE,
|
||||
)
|
||||
from .tarangplus import (
|
||||
TarangPlusEpisodesIE,
|
||||
TarangPlusPlaylistIE,
|
||||
TarangPlusVideoIE,
|
||||
)
|
||||
from .tass import TassIE
|
||||
from .tbs import TBSIE
|
||||
from .tbsjp import (
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
import base64
|
||||
import binascii
|
||||
import functools
|
||||
import re
|
||||
|
||||
from .common import InfoExtractor
|
||||
from ..dependencies import Cryptodome
|
||||
from ..utils import (
|
||||
ExtractorError,
|
||||
OnDemandPagedList,
|
||||
clean_html,
|
||||
extract_attributes,
|
||||
urljoin,
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
find_element,
|
||||
find_elements,
|
||||
require,
|
||||
traverse_obj,
|
||||
)
|
||||
|
||||
|
||||
class TarangPlusBaseIE(InfoExtractor):
|
||||
_BASE_URL = 'https://tarangplus.in'
|
||||
|
||||
|
||||
class TarangPlusVideoIE(TarangPlusBaseIE):
|
||||
IE_NAME = 'tarangplus:video'
|
||||
_VALID_URL = r'https?://(?:www\.)?tarangplus\.in/(?:movies|[^#?/]+/[^#?/]+)/(?!episodes)(?P<id>[^#?/]+)'
|
||||
_TESTS = [{
|
||||
'url': 'https://tarangplus.in/tarangaplus-originals/khitpit/khitpit-ep-10',
|
||||
'md5': '78ce056cee755687b8a48199909ecf53',
|
||||
'info_dict': {
|
||||
'id': '67b8206719521d054c0059b7',
|
||||
'display_id': 'khitpit-ep-10',
|
||||
'ext': 'mp4',
|
||||
'title': 'Khitpit Ep-10',
|
||||
'description': 'md5:a45b805cb628e15c853d78b0406eab48',
|
||||
'thumbnail': r're:https?://.*\.jpg',
|
||||
'duration': 756.0,
|
||||
'timestamp': 1740355200,
|
||||
'upload_date': '20250224',
|
||||
'media_type': 'episode',
|
||||
'categories': ['Originals'],
|
||||
},
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/tarang-serials/bada-bohu/bada-bohu-ep-233',
|
||||
'md5': 'b4f9beb15172559bb362203b4f48382e',
|
||||
'info_dict': {
|
||||
'id': '680b9d6c19521d054c007782',
|
||||
'display_id': 'bada-bohu-ep-233',
|
||||
'ext': 'mp4',
|
||||
'title': 'Bada Bohu | Ep -233',
|
||||
'description': 'md5:e6b8e7edc9e60b92c1b390f8789ecd69',
|
||||
'thumbnail': r're:https?://.*\.jpg',
|
||||
'duration': 1392.0,
|
||||
'timestamp': 1745539200,
|
||||
'upload_date': '20250425',
|
||||
'media_type': 'episode',
|
||||
'categories': ['Prime'],
|
||||
},
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/short/ai-maa/ai-maa',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/shows/tarang-cine-utsav-2024/tarang-cine-utsav-2024-seg-1',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/music-videos/chori-chori-bohu-chori-songs/nijara-laguchu-dhire-dhire',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/kids-shows/chhota-jaga/chhota-jaga-ep-33-jamidar-ra-khajana-adaya',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/movies/swayambara',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def decrypt(self, data, key):
|
||||
if not Cryptodome.AES:
|
||||
raise ExtractorError('pycryptodomex not found. Please install', expected=True)
|
||||
iv = binascii.unhexlify('00000000000000000000000000000000')
|
||||
cipher = Cryptodome.AES.new(base64.b64decode(key), Cryptodome.AES.MODE_CBC, iv)
|
||||
return cipher.decrypt(base64.b64decode(data)).decode('utf-8')
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
hidden_inputs_data = self._hidden_inputs(webpage)
|
||||
json_ld_data = self._search_json_ld(webpage, display_id)
|
||||
json_ld_data.pop('url', None)
|
||||
|
||||
iframe_url = traverse_obj(webpage, (
|
||||
{find_element(tag='iframe', attr='src', value=r'.+[?&]contenturl=.+', html=True, regex=True)},
|
||||
{extract_attributes}, 'src', {require('iframe URL')}))
|
||||
# Can't use parse_qs here since it would decode the encrypted base64 `+` chars to spaces
|
||||
content = self._search_regex(r'[?&]contenturl=(.+)', iframe_url, 'content')
|
||||
encrypted_data, _, attrs = content.partition('|')
|
||||
metadata = {
|
||||
m.group('k'): m.group('v')
|
||||
for m in re.finditer(r'(?:^|\|)(?P<k>[a-z_]+)=(?P<v>(?:(?!\|[a-z_]+=).)+)', attrs)
|
||||
}
|
||||
m3u8_url = self.decrypt(encrypted_data, metadata['key'])
|
||||
|
||||
return {
|
||||
'id': display_id, # Fallback
|
||||
'display_id': display_id,
|
||||
**json_ld_data,
|
||||
**traverse_obj(metadata, {
|
||||
'id': ('content_id', {str}),
|
||||
'title': ('title', {str}),
|
||||
'thumbnail': ('image', {str}),
|
||||
}),
|
||||
**traverse_obj(hidden_inputs_data, {
|
||||
'id': ('content_id', {str}),
|
||||
'media_type': ('theme_type', {str}),
|
||||
'categories': ('genre', {str}, filter, all, filter),
|
||||
}),
|
||||
'formats': self._extract_m3u8_formats(m3u8_url, display_id),
|
||||
}
|
||||
|
||||
|
||||
class TarangPlusEpisodesIE(TarangPlusBaseIE):
|
||||
IE_NAME = 'tarangplus:episodes'
|
||||
_VALID_URL = r'https?://(?:www\.)?tarangplus\.in/(?P<type>[^#?/]+)/(?P<id>[^#?/]+)/episodes/?(?:$|[?#])'
|
||||
_TESTS = [{
|
||||
'url': 'https://tarangplus.in/tarangaplus-originals/balijatra/episodes',
|
||||
'info_dict': {
|
||||
'id': 'balijatra',
|
||||
'title': 'Balijatra',
|
||||
},
|
||||
'playlist_mincount': 7,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/tarang-serials/bada-bohu/episodes',
|
||||
'info_dict': {
|
||||
'id': 'bada-bohu',
|
||||
'title': 'Bada Bohu',
|
||||
},
|
||||
'playlist_mincount': 236,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/shows/dr-nonsense/episodes',
|
||||
'info_dict': {
|
||||
'id': 'dr-nonsense',
|
||||
'title': 'Dr. Nonsense',
|
||||
},
|
||||
'playlist_mincount': 15,
|
||||
}]
|
||||
_PAGE_SIZE = 20
|
||||
|
||||
def _entries(self, playlist_url, playlist_id, page):
|
||||
data = self._download_json(
|
||||
playlist_url, playlist_id, f'Downloading playlist JSON page {page + 1}',
|
||||
query={'page_no': page})
|
||||
for item in traverse_obj(data, ('items', ..., {str})):
|
||||
yield self.url_result(
|
||||
urljoin(self._BASE_URL, item.split('$')[3]), TarangPlusVideoIE)
|
||||
|
||||
def _real_extract(self, url):
|
||||
url_type, display_id = self._match_valid_url(url).group('type', 'id')
|
||||
series_url = f'{self._BASE_URL}/{url_type}/{display_id}'
|
||||
webpage = self._download_webpage(series_url, display_id)
|
||||
|
||||
entries = OnDemandPagedList(
|
||||
functools.partial(self._entries, f'{series_url}/episodes', display_id),
|
||||
self._PAGE_SIZE)
|
||||
return self.playlist_result(
|
||||
entries, display_id, self._hidden_inputs(webpage).get('title'))
|
||||
|
||||
|
||||
class TarangPlusPlaylistIE(TarangPlusBaseIE):
|
||||
IE_NAME = 'tarangplus:playlist'
|
||||
_VALID_URL = r'https?://(?:www\.)?tarangplus\.in/(?P<id>[^#?/]+)/all/?(?:$|[?#])'
|
||||
_TESTS = [{
|
||||
'url': 'https://tarangplus.in/chhota-jaga/all',
|
||||
'info_dict': {
|
||||
'id': 'chhota-jaga',
|
||||
'title': 'Chhota Jaga',
|
||||
},
|
||||
'playlist_mincount': 33,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/kids-yali-show/all',
|
||||
'info_dict': {
|
||||
'id': 'kids-yali-show',
|
||||
'title': 'Yali',
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/trailer/all',
|
||||
'info_dict': {
|
||||
'id': 'trailer',
|
||||
'title': 'Trailer',
|
||||
},
|
||||
'playlist_mincount': 57,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/latest-songs/all',
|
||||
'info_dict': {
|
||||
'id': 'latest-songs',
|
||||
'title': 'Latest Songs',
|
||||
},
|
||||
'playlist_mincount': 46,
|
||||
}, {
|
||||
'url': 'https://tarangplus.in/premium-serials-episodes/all',
|
||||
'info_dict': {
|
||||
'id': 'premium-serials-episodes',
|
||||
'title': 'Primetime Latest Episodes',
|
||||
},
|
||||
'playlist_mincount': 100,
|
||||
}]
|
||||
|
||||
def _entries(self, webpage):
|
||||
for url_path in traverse_obj(webpage, (
|
||||
{find_elements(cls='item')}, ...,
|
||||
{find_elements(tag='a', attr='href', value='/.+', html=True, regex=True)},
|
||||
..., {extract_attributes}, 'href',
|
||||
)):
|
||||
yield self.url_result(urljoin(self._BASE_URL, url_path), TarangPlusVideoIE)
|
||||
|
||||
def _real_extract(self, url):
|
||||
display_id = self._match_id(url)
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
|
||||
return self.playlist_result(
|
||||
self._entries(webpage), display_id,
|
||||
traverse_obj(webpage, ({find_element(id='al_title')}, {clean_html})))
|
||||
Reference in New Issue
Block a user