服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
478 lines
17 KiB
Go
478 lines
17 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// lidTestSigner stands in for the launcher: it holds the private half and signs
|
|
// the canonical statement the same way .NET's ECDsa.SignData does.
|
|
type lidTestSigner struct {
|
|
key *ecdsa.PrivateKey
|
|
}
|
|
|
|
func newLIDTestSigner(t *testing.T) *lidTestSigner {
|
|
t.Helper()
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("generate key: %v", err)
|
|
}
|
|
return &lidTestSigner{key: key}
|
|
}
|
|
|
|
func (s *lidTestSigner) encodedPublicKey(t *testing.T) string {
|
|
t.Helper()
|
|
der, err := x509.MarshalPKIXPublicKey(&s.key.PublicKey)
|
|
if err != nil {
|
|
t.Fatalf("marshal public key: %v", err)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(der)
|
|
}
|
|
|
|
func (s *lidTestSigner) sign(t *testing.T, statement string) string {
|
|
t.Helper()
|
|
digest := sha256.Sum256([]byte(statement))
|
|
signature, err := ecdsa.SignASN1(rand.Reader, s.key, digest[:])
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
// Base64 with both alphabets is accepted on the way back in; StdEncoding is
|
|
// what .NET Convert.ToBase64String produces.
|
|
return statement + "|" + base64.StdEncoding.EncodeToString(signature)
|
|
}
|
|
|
|
func newLIDTestVerifier(t *testing.T, mode string, signer *lidTestSigner) *launcherIdentityVerifier {
|
|
t.Helper()
|
|
verifier, err := newLauncherIdentityVerifier(mode, []string{signer.encodedPublicKey(t)})
|
|
if err != nil {
|
|
t.Fatalf("new verifier: %v", err)
|
|
}
|
|
return verifier
|
|
}
|
|
|
|
func TestLauncherIdentityDisabledModeAcceptsNoProof(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier, err := newLauncherIdentityVerifier("off", []string{signer.encodedPublicKey(t)})
|
|
if err != nil {
|
|
t.Fatalf("new verifier: %v", err)
|
|
}
|
|
if verifier.enabled() || verifier.enforcing() {
|
|
t.Fatal("off mode must be neither enabled nor enforcing")
|
|
}
|
|
if err := verifier.verifyLocalLease(1, "dnf:1", launcherIdentityLease{}, time.Now()); err == nil {
|
|
t.Fatal("off mode must reject everything rather than silently accept")
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityMissingKeyIsAConfigurationError(t *testing.T) {
|
|
for _, mode := range []string{LauncherIdentityModeAudit, LauncherIdentityModeEnforce} {
|
|
if _, err := newLauncherIdentityVerifier(mode, nil); err == nil {
|
|
t.Fatalf("mode %q with no key must fail loudly", mode)
|
|
}
|
|
if _, err := newLauncherIdentityVerifier(mode, []string{"not-a-key"}); err == nil {
|
|
t.Fatalf("mode %q with a broken key must fail loudly", mode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityLocalLeaseRoundTrip(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier := newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
|
|
lease := signer.sign(t, LauncherIdentityLocalStatement(verifier.serverID, 4242, "dnf:abc", expiresAt))
|
|
parsed, err := parseLauncherIdentityLease(lease)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if parsed.scope != launcherIdentityScopeLocal || parsed.pid != 4242 || parsed.accountID != "dnf:abc" {
|
|
t.Fatalf("parsed lease fields = %+v", parsed)
|
|
}
|
|
if err := verifier.verifyLocalLease(4242, "dnf:abc", parsed, time.Now()); err != nil {
|
|
t.Fatalf("verify local lease: %v", err)
|
|
}
|
|
}
|
|
|
|
// The binding must cover the process and the account, not just "some launcher
|
|
// was here": a proof minted for one PID or one account must not seat another.
|
|
func TestLauncherIdentityLocalLeaseIsBoundToPIDAccountAndServer(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier := newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
|
|
parse := func(t *testing.T, statement string) launcherIdentityLease {
|
|
t.Helper()
|
|
parsed, err := parseLauncherIdentityLease(signer.sign(t, statement))
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
lease := parse(t, LauncherIdentityLocalStatement(verifier.serverID, 100, "dnf:a", expiresAt))
|
|
if err := verifier.verifyLocalLease(101, "dnf:a", lease, time.Now()); err == nil {
|
|
t.Fatal("a lease for PID 100 must not authorize PID 101")
|
|
}
|
|
if err := verifier.verifyLocalLease(100, "dnf:b", lease, time.Now()); err == nil {
|
|
t.Fatal("a lease for dnf:a must not authorize dnf:b")
|
|
}
|
|
|
|
otherServer := parse(t, LauncherIdentityLocalStatement("00112233445566778899aabbccddeeff", 100, "dnf:a", expiresAt))
|
|
if err := verifier.verifyLocalLease(100, "dnf:a", otherServer, time.Now()); err == nil {
|
|
t.Fatal("a lease from another server instance must not be replayed")
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityLeaseLifetimeIsEnforced(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier := newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)
|
|
now := time.Now()
|
|
|
|
expired := signer.sign(t, LauncherIdentityLocalStatement(verifier.serverID, 7, "dnf:a", now.Add(-time.Minute).Unix()))
|
|
parsed, err := parseLauncherIdentityLease(expired)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(7, "dnf:a", parsed, now); err == nil {
|
|
t.Fatal("an expired lease must be rejected")
|
|
}
|
|
|
|
// A long-lived lease would turn a one-off leak into a permanent backdoor.
|
|
forever := signer.sign(t, LauncherIdentityLocalStatement(
|
|
verifier.serverID, 7, "dnf:a", now.Add(7*24*time.Hour).Unix()))
|
|
parsed, err = parseLauncherIdentityLease(forever)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(7, "dnf:a", parsed, now); err == nil {
|
|
t.Fatal("a lease beyond the maximum lifetime must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityRejectsTamperedStatement(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier := newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
|
|
lease := signer.sign(t, LauncherIdentityLocalStatement(verifier.serverID, 5, "dnf:a", expiresAt))
|
|
// Re-encode a different account under the same signature.
|
|
tampered := strings.Replace(lease, encodeLauncherIdentityField("dnf:a"), encodeLauncherIdentityField("dnf:b"), 1)
|
|
if tampered == lease {
|
|
t.Fatal("test setup failed to tamper with the lease")
|
|
}
|
|
parsed, err := parseLauncherIdentityLease(tampered)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(5, "dnf:b", parsed, time.Now()); err == nil {
|
|
t.Fatal("a re-encoded account must invalidate the signature")
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityAcceptsEveryKeyInTheWhitelist(t *testing.T) {
|
|
first := newLIDTestSigner(t)
|
|
second := newLIDTestSigner(t)
|
|
verifier, err := newLauncherIdentityVerifier(LauncherIdentityModeEnforce, []string{
|
|
first.encodedPublicKey(t),
|
|
second.encodedPublicKey(t),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("new verifier: %v", err)
|
|
}
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
lease := second.sign(t, LauncherIdentityLocalStatement(verifier.serverID, 9, "dnf:a", expiresAt))
|
|
parsed, err := parseLauncherIdentityLease(lease)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(9, "dnf:a", parsed, time.Now()); err != nil {
|
|
t.Fatalf("a whitelisted key must verify: %v", err)
|
|
}
|
|
|
|
outsider := newLIDTestSigner(t)
|
|
rejected := outsider.sign(t, LauncherIdentityLocalStatement(verifier.serverID, 9, "dnf:a", expiresAt))
|
|
parsed, err = parseLauncherIdentityLease(rejected)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(9, "dnf:a", parsed, time.Now()); err == nil {
|
|
t.Fatal("a key that is not pinned must not verify")
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityRemoteLeaseBindsTheTicket(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier := newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
ticket := strings.Repeat("ab", 32)
|
|
|
|
lease := signer.sign(t, LauncherIdentityRemoteStatement(ticket, expiresAt))
|
|
parsed, err := parseLauncherIdentityLease(lease)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyRemoteLease(ticket, parsed, time.Now()); err != nil {
|
|
t.Fatalf("verify remote lease: %v", err)
|
|
}
|
|
// The lease is signed for one ticket; presenting another must fail.
|
|
if err := verifier.verifyRemoteLease(strings.Repeat("cd", 32), parsed, time.Now()); err == nil {
|
|
t.Fatal("a lease bound to ticket ab.. must not authorize ticket cd..")
|
|
}
|
|
if accountID, ok := parsed.leaseAccountID(); ok || accountID != "" {
|
|
t.Fatalf("a ticket lease must not report an account, got %q", accountID)
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityRemoteLeaseCanCarryAnAccount(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
verifier := newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)
|
|
expiresAt := time.Now().Add(time.Hour).Unix()
|
|
|
|
lease := signer.sign(t, LauncherIdentityRemoteStatement(
|
|
launcherIdentityAccountSubjectPrefix+"dnf:z", expiresAt))
|
|
parsed, err := parseLauncherIdentityLease(lease)
|
|
if err != nil {
|
|
t.Fatalf("parse lease: %v", err)
|
|
}
|
|
if err := verifier.verifyRemoteLease("", parsed, time.Now()); err != nil {
|
|
t.Fatalf("a ticketless lease must verify without a frame ticket: %v", err)
|
|
}
|
|
accountID, ok := parsed.leaseAccountID()
|
|
if !ok || accountID != "dnf:z" {
|
|
t.Fatalf("lease account = %q ok=%t", accountID, ok)
|
|
}
|
|
if ticket, ok := parsed.leaseTicket(); ok || ticket != "" {
|
|
t.Fatalf("a ticketless lease must not report a ticket, got %q", ticket)
|
|
}
|
|
}
|
|
|
|
func TestLauncherIdentityParsesRejectsMalformedLeases(t *testing.T) {
|
|
cases := []string{
|
|
"",
|
|
"|",
|
|
"no-separator",
|
|
"|only-signature",
|
|
"DNF90-LID-1|local|a|1|b64|999|notbase64!!",
|
|
"DNF90-LID-1|bogus|a|1|b64|999|" + base64.StdEncoding.EncodeToString([]byte("x")),
|
|
"DNF90-LID-1|local|a|0|b64|999|" + base64.StdEncoding.EncodeToString([]byte("x")),
|
|
"DNF90-LID-1|local|a|notanumber|b64|999|" + base64.StdEncoding.EncodeToString([]byte("x")),
|
|
"DNF90-LID-1|local|a|1||999|" + base64.StdEncoding.EncodeToString([]byte("x")),
|
|
}
|
|
for _, value := range cases {
|
|
if _, err := parseLauncherIdentityLease(value); err == nil {
|
|
t.Fatalf("lease %q must not parse", value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The LANA version 2 frame must round-trip, and a version 1 frame must keep
|
|
// meaning exactly what it meant before the lease existed.
|
|
func TestLauncherAuthFrameRoundTrip(t *testing.T) {
|
|
ticket := strings.Repeat("1f", 32)
|
|
machine := "90CN"
|
|
|
|
writeV1 := func() []byte {
|
|
body := append([]byte(ticket), machine...)
|
|
frame := make([]byte, launcherAuthHeaderLen+len(body))
|
|
copy(frame, launcherAuthMagicBytes)
|
|
frame[4] = launcherAuthFrameVersion
|
|
binary.BigEndian.PutUint16(frame[5:7], uint16(len(body)))
|
|
copy(frame[launcherAuthHeaderLen:], body)
|
|
return frame
|
|
}
|
|
decoded, err := readLauncherAuthFrame(&lidScriptedConn{read: writeV1()})
|
|
if err != nil {
|
|
t.Fatalf("read v1 frame: %v", err)
|
|
}
|
|
if decoded.Version != launcherAuthFrameVersion || decoded.Ticket != ticket || decoded.MachineID != machine {
|
|
t.Fatalf("v1 frame = %+v", decoded)
|
|
}
|
|
if decoded.Lease != "" {
|
|
t.Fatal("a v1 frame must not carry a lease")
|
|
}
|
|
|
|
lease := "DNF90-LID-1|remote|" + encodeLauncherIdentityField(ticket) + "|999|" + base64.StdEncoding.EncodeToString([]byte("sig"))
|
|
writeV2 := func(leaseText string) []byte {
|
|
body := append([]byte(ticket), machine...)
|
|
body = append(body, leaseText...)
|
|
// The lease length is the last field in the body, mirroring the server
|
|
// reader: machine_id then lease then its length.
|
|
body = binary.BigEndian.AppendUint16(body, uint16(len(leaseText)))
|
|
frame := make([]byte, launcherAuthHeaderLen+len(body))
|
|
copy(frame, launcherAuthMagicBytes)
|
|
frame[4] = launcherAuthFrameVersionLease
|
|
binary.BigEndian.PutUint16(frame[5:7], uint16(len(body)))
|
|
copy(frame[launcherAuthHeaderLen:], body)
|
|
return frame
|
|
}
|
|
decoded, err = readLauncherAuthFrame(&lidScriptedConn{read: writeV2(lease)})
|
|
if err != nil {
|
|
t.Fatalf("read v2 frame: %v", err)
|
|
}
|
|
if decoded.Version != launcherAuthFrameVersionLease || decoded.MachineID != machine {
|
|
t.Fatalf("v2 frame = %+v", decoded)
|
|
}
|
|
if decoded.Lease != lease {
|
|
t.Fatalf("v2 lease = %q want %q", decoded.Lease, lease)
|
|
}
|
|
if decoded.LeaseParsed.scope != launcherIdentityScopeRemote {
|
|
t.Fatalf("v2 lease scope = %q", decoded.LeaseParsed.scope)
|
|
}
|
|
|
|
// A version 1 capable client challenged by a version 2 server reports an
|
|
// empty lease rather than a frame error, so enforce mode can log a precise
|
|
// reason instead of a parse failure.
|
|
decoded, err = readLauncherAuthFrame(&lidScriptedConn{read: writeV2("")})
|
|
if err != nil {
|
|
t.Fatalf("read v2 frame without a lease: %v", err)
|
|
}
|
|
if decoded.Lease != "" || decoded.MachineID != machine {
|
|
t.Fatalf("v2 frame without lease = %+v", decoded)
|
|
}
|
|
|
|
// An unknown version must be refused, not read as its predecessor.
|
|
bad := writeV1()
|
|
bad[4] = 3
|
|
if _, err := readLauncherAuthFrame(&lidScriptedConn{read: bad}); err == nil {
|
|
t.Fatal("an unknown frame version must be rejected")
|
|
}
|
|
}
|
|
|
|
// TestLauncherIdentityMatchesLauncherSignature is the cross-language guard: the
|
|
// launcher signs with .NET's ECDsa and this server verifies with
|
|
// ecdsa.VerifyASN1, so the statement layout, the base64url field encoding and
|
|
// the DER signature encoding all have to agree exactly. A drift in any of them
|
|
// would only show up as "every player is refused" at runtime, which is why the
|
|
// check is written down instead of trusted.
|
|
//
|
|
// It is skipped unless DNF_LID_CROSSCHECK points at the JSON produced by the
|
|
// harness in 登录器's cross-check build, so an ordinary `go test` never depends
|
|
// on a .NET toolchain.
|
|
func TestLauncherIdentityMatchesLauncherSignature(t *testing.T) {
|
|
path := strings.TrimSpace(os.Getenv("DNF_LID_CROSSCHECK"))
|
|
if path == "" {
|
|
t.Skip("set DNF_LID_CROSSCHECK to a launcher-produced lease JSON to run this check")
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read cross-check payload: %v", err)
|
|
}
|
|
var payload struct {
|
|
PublicKey string `json:"public_key"`
|
|
ServerID string `json:"server_id"`
|
|
PID uint32 `json:"pid"`
|
|
AccountID string `json:"account_id"`
|
|
Ticket string `json:"ticket"`
|
|
LocalLease string `json:"local_lease"`
|
|
RemoteLease string `json:"remote_lease"`
|
|
}
|
|
if err := json.Unmarshal(raw, &payload); err != nil {
|
|
t.Fatalf("decode cross-check payload: %v", err)
|
|
}
|
|
|
|
verifier, err := newLauncherIdentityVerifier(
|
|
LauncherIdentityModeEnforce, []string{payload.PublicKey})
|
|
if err != nil {
|
|
t.Fatalf("new verifier: %v", err)
|
|
}
|
|
// The launcher signed against the fixed ID the harness uses, so pin it here
|
|
// rather than the random per-process one.
|
|
verifier.serverID = payload.ServerID
|
|
|
|
local, err := parseLauncherIdentityLease(payload.LocalLease)
|
|
if err != nil {
|
|
t.Fatalf("parse launcher local lease: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(payload.PID, payload.AccountID, local, time.Now()); err != nil {
|
|
t.Fatalf("launcher-signed local lease did not verify: %v", err)
|
|
}
|
|
if err := verifier.verifyLocalLease(payload.PID+1, payload.AccountID, local, time.Now()); err == nil {
|
|
t.Fatal("the cross-check lease must still be bound to its PID")
|
|
}
|
|
|
|
remote, err := parseLauncherIdentityLease(payload.RemoteLease)
|
|
if err != nil {
|
|
t.Fatalf("parse launcher remote lease: %v", err)
|
|
}
|
|
if err := verifier.verifyRemoteLease(payload.Ticket, remote, time.Now()); err != nil {
|
|
t.Fatalf("launcher-signed remote lease did not verify: %v", err)
|
|
}
|
|
}
|
|
|
|
// Audit mode must not inject a LANA challenge into the operator's own loopback
|
|
// tooling: that traffic is already covered by the out-of-band signed PID bind,
|
|
// and a stray challenge frame would only add a timeout and seven unexpected
|
|
// bytes. Enforce mode has no such exemption.
|
|
func TestLauncherIdentityInBandChallengeScope(t *testing.T) {
|
|
signer := newLIDTestSigner(t)
|
|
loopback := &lidScriptedConn{remote: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 50000}}
|
|
remote := &lidScriptedConn{remote: &net.TCPAddr{IP: net.IPv4(203, 0, 113, 9), Port: 50000}}
|
|
|
|
off := &Service{launcherIdentity: &launcherIdentityVerifier{mode: LauncherIdentityModeOff}}
|
|
if off.launcherIdentityWantsInBandChallenge(loopback) {
|
|
t.Fatal("an unconfigured binding must not challenge anything")
|
|
}
|
|
|
|
audit := &Service{launcherIdentity: newLIDTestVerifier(t, LauncherIdentityModeAudit, signer)}
|
|
if audit.launcherIdentityWantsInBandChallenge(loopback) {
|
|
t.Fatal("audit mode must leave loopback peers to the out-of-band carrier")
|
|
}
|
|
if !audit.launcherIdentityWantsInBandChallenge(remote) {
|
|
t.Fatal("audit mode must still observe remote peers")
|
|
}
|
|
|
|
enforce := &Service{launcherIdentity: newLIDTestVerifier(t, LauncherIdentityModeEnforce, signer)}
|
|
if !enforce.launcherIdentityWantsInBandChallenge(loopback) {
|
|
t.Fatal("enforce mode must challenge loopback peers too")
|
|
}
|
|
if !enforce.launcherIdentityWantsInBandChallenge(remote) {
|
|
t.Fatal("enforce mode must challenge remote peers")
|
|
}
|
|
}
|
|
|
|
// lidScriptedConn is the smallest net.Conn that can serve a canned byte slice.
|
|
type lidScriptedConn struct {
|
|
read []byte
|
|
pos int
|
|
remote net.Addr
|
|
}
|
|
|
|
func (c *lidScriptedConn) Read(p []byte) (int, error) {
|
|
if c.pos >= len(c.read) {
|
|
return 0, io.EOF
|
|
}
|
|
n := copy(p, c.read[c.pos:])
|
|
c.pos += n
|
|
return n, nil
|
|
}
|
|
|
|
func (c *lidScriptedConn) Write(p []byte) (int, error) { return len(p), nil }
|
|
func (c *lidScriptedConn) Close() error { return nil }
|
|
func (c *lidScriptedConn) LocalAddr() net.Addr { return lidAddr{} }
|
|
func (c *lidScriptedConn) RemoteAddr() net.Addr {
|
|
if c.remote != nil {
|
|
return c.remote
|
|
}
|
|
return lidAddr{}
|
|
}
|
|
func (c *lidScriptedConn) SetDeadline(time.Time) error { return nil }
|
|
func (c *lidScriptedConn) SetReadDeadline(time.Time) error { return nil }
|
|
func (c *lidScriptedConn) SetWriteDeadline(time.Time) error { return nil }
|
|
|
|
type lidAddr struct{}
|
|
|
|
func (lidAddr) Network() string { return "tcp" }
|
|
func (lidAddr) String() string { return "127.0.0.1:0" }
|