Compare commits
22 Commits
Author | SHA1 | Date |
---|---|---|
兔子 | 44678fa0ff | 1 week ago |
兔子 | 9b123d8bb9 | 3 weeks ago |
兔子 | f855d23e8f | 3 weeks ago |
兔子 | 54864722f2 | 3 weeks ago |
兔子 | 572a6059bb | 1 month ago |
兔子 | 2f40158945 | 1 month ago |
兔子 | 9ab117f51e | 1 month ago |
兔子 | e80e5c5679 | 1 month ago |
兔子 | 02f79f2eb0 | 1 month ago |
兔子 | 68f1eef7a6 | 2 months ago |
兔子 | 5ece8b96bb | 2 months ago |
兔子 | 033d327f4f | 2 months ago |
兔子 | 231cad54d0 | 2 months ago |
兔子 | 35cf23e5ad | 2 months ago |
兔子 | 88a3fdf2bf | 2 months ago |
兔子 | e5db22aa52 | 2 months ago |
兔子 | 4074adfcd9 | 2 months ago |
兔子 | 1276d3b6dd | 2 months ago |
兔子 | d2feccf3b3 | 3 months ago |
兔子 | c485d070a3 | 1 year ago |
兔子 | 7f73bb3d4d | 1 year ago |
兔子 | 54c881b3be | 1 year ago |
@ -0,0 +1,52 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"b612.me/starcrypto"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
func MakeCert(caKey any, caCrt *x509.Certificate, csr *x509.Certificate, pub any) ([]byte, error) {
|
||||
der, err := x509.CreateCertificate(rand.Reader, csr, caCrt, pub, caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemData := pem.EncodeToMemory(certBlock)
|
||||
return pemData, nil
|
||||
}
|
||||
|
||||
func LoadCA(caKeyPath, caCertPath, KeyPwd string) (crypto.PrivateKey, *x509.Certificate, error) {
|
||||
caKeyBytes, err := os.ReadFile(caKeyPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
caCertBytes, err := os.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
caKey, err := starcrypto.DecodePrivateKey(caKeyBytes, KeyPwd)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
block, _ := pem.Decode(caCertBytes)
|
||||
if block == nil || (block.Type != "CERTIFICATE" && block.Type != "CERTIFICATE REQUEST") {
|
||||
return nil, nil, errors.New("Failed to decode PEM block containing the certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return caKey, cert, nil
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GenerateCsr(country, province, city, org, orgUnit, name string, dnsName []string, start, end time.Time, isCa bool, maxPathLenZero bool, maxPathLen int) *x509.Certificate {
|
||||
var trueDNS []string
|
||||
var trueIp []net.IP
|
||||
for _, v := range dnsName {
|
||||
ip := net.ParseIP(v)
|
||||
if ip == nil {
|
||||
trueDNS = append(trueDNS, v)
|
||||
continue
|
||||
}
|
||||
trueIp = append(trueIp, ip)
|
||||
}
|
||||
ku := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
|
||||
eku := x509.ExtKeyUsageServerAuth
|
||||
if isCa {
|
||||
ku = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature
|
||||
eku = x509.ExtKeyUsageAny
|
||||
}
|
||||
return &x509.Certificate{
|
||||
Version: 3,
|
||||
SerialNumber: big.NewInt(time.Now().Unix()),
|
||||
Subject: pkix.Name{
|
||||
Country: s2s(country),
|
||||
Province: s2s(province),
|
||||
Locality: s2s(city),
|
||||
Organization: s2s((org)),
|
||||
OrganizationalUnit: s2s(orgUnit),
|
||||
CommonName: name,
|
||||
},
|
||||
DNSNames: trueDNS,
|
||||
IPAddresses: trueIp,
|
||||
NotBefore: start,
|
||||
NotAfter: end,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: isCa,
|
||||
MaxPathLen: maxPathLen,
|
||||
MaxPathLenZero: maxPathLenZero,
|
||||
KeyUsage: ku,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{eku},
|
||||
}
|
||||
}
|
||||
|
||||
func outputCsr(csr *x509.Certificate) []byte {
|
||||
return pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: csr.Raw,
|
||||
})
|
||||
}
|
||||
|
||||
func s2s(str string) []string {
|
||||
if len(str) == 0 {
|
||||
return nil
|
||||
}
|
||||
return []string{str}
|
||||
}
|
||||
|
||||
func LoadCsr(csrPath string) (*x509.Certificate, error) {
|
||||
csrBytes, err := os.ReadFile(csrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, _ := pem.Decode(csrBytes)
|
||||
if block == nil || block.Type != "CERTIFICATE REQUEST" {
|
||||
return nil, errors.New("Failed to decode PEM block containing the certificate")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cert, nil
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Res *dns.Msg
|
||||
Str string
|
||||
}
|
||||
|
||||
type DnsClient interface {
|
||||
Exchange(req *dns.Msg, address string) (r *dns.Msg, rtt time.Duration, err error)
|
||||
}
|
||||
|
||||
func QueryDns(domain string, queryType string, serverType int, dnsServer string) (Result, error) {
|
||||
var c DnsClient
|
||||
c = new(dns.Client)
|
||||
m := new(dns.Msg)
|
||||
if dnsServer == "" {
|
||||
dnsServer = "223.5.5.5:53"
|
||||
}
|
||||
switch serverType {
|
||||
case 1:
|
||||
c.(*dns.Client).Net = "tcp"
|
||||
case 2:
|
||||
c = &dns.Client{
|
||||
Net: "tcp-tls",
|
||||
Dialer: &net.Dialer{
|
||||
Resolver: net.DefaultResolver,
|
||||
},
|
||||
}
|
||||
case 3:
|
||||
c = NewDoHClient(WithTimeout(10 * time.Second))
|
||||
}
|
||||
switch queryType {
|
||||
case "A":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeA)
|
||||
case "CNAME":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME)
|
||||
case "MX":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeMX)
|
||||
case "NS":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeNS)
|
||||
case "TXT":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
|
||||
case "SOA":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeSOA)
|
||||
case "SRV":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeSRV)
|
||||
case "AAAA":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeAAAA)
|
||||
case "PTR":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypePTR)
|
||||
case "ANY":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeANY)
|
||||
case "CAA":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeCAA)
|
||||
case "TLSA":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeTLSA)
|
||||
case "DS":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeDS)
|
||||
case "DNSKEY":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeDNSKEY)
|
||||
case "NSEC":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeNSEC)
|
||||
case "NSEC3":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeNSEC3)
|
||||
case "NSEC3PARAM":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeNSEC3PARAM)
|
||||
case "RRSIG":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeRRSIG)
|
||||
case "SPF":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeSPF)
|
||||
case "SSHFP":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeSSHFP)
|
||||
case "TKEY":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeTKEY)
|
||||
case "TSIG":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeTSIG)
|
||||
case "URI":
|
||||
m.SetQuestion(dns.Fqdn(domain), dns.TypeURI)
|
||||
default:
|
||||
return Result{}, errors.New("not support query type,only support A,CNAME,MX,NS,SOA,SRV,AAAA,PTR,ANY,CAA,TLSA,DS,DNSKEY,NSEC,NSEC3,NSEC3PARAM,RRSIG,SPF,SSHFP,TKEY,TSIG,URI")
|
||||
}
|
||||
r, rtt, err := c.Exchange(m, dnsServer)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
return Result{
|
||||
Res: r,
|
||||
Str: r.String() + "\n" + ";; RTT:\n" + fmt.Sprintf("%v milliseconds", rtt.Milliseconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const DoHMediaType = "application/dns-message"
|
||||
|
||||
type clientOptions struct {
|
||||
Timeout time.Duration // Timeout for one DNS query
|
||||
}
|
||||
|
||||
type ClientOption func(*clientOptions) error
|
||||
|
||||
func WithTimeout(t time.Duration) ClientOption {
|
||||
return func(o *clientOptions) error {
|
||||
o.Timeout = t
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type DoHClient struct {
|
||||
opt *clientOptions
|
||||
cli *http.Client
|
||||
}
|
||||
|
||||
func NewDoHClient(opts ...ClientOption) *DoHClient {
|
||||
o := new(clientOptions)
|
||||
for _, f := range opts {
|
||||
f(o)
|
||||
}
|
||||
return &DoHClient{
|
||||
opt: o,
|
||||
cli: &http.Client{
|
||||
Timeout: o.Timeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *DoHClient) Exchange(req *dns.Msg, address string) (r *dns.Msg, rtt time.Duration, err error) {
|
||||
var (
|
||||
buf, b64 []byte
|
||||
begin = time.Now()
|
||||
origID = req.Id
|
||||
)
|
||||
|
||||
// Set DNS ID as zero accoreding to RFC8484 (cache friendly)
|
||||
req.Id = 0
|
||||
buf, err = req.Pack()
|
||||
b64 = make([]byte, base64.RawURLEncoding.EncodedLen(len(buf)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
base64.RawURLEncoding.Encode(b64, buf)
|
||||
|
||||
// No need to use hreq.URL.Query()
|
||||
hreq, _ := http.NewRequest("GET", address+"?dns="+string(b64), nil)
|
||||
hreq.Header.Add("Accept", DoHMediaType)
|
||||
resp, err := c.cli.Do(hreq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
content, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = errors.New("DoH query failed: " + string(content))
|
||||
return
|
||||
}
|
||||
|
||||
r = new(dns.Msg)
|
||||
err = r.Unpack(content)
|
||||
r.Id = origID
|
||||
rtt = time.Since(begin)
|
||||
return
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultDns(t *testing.T) {
|
||||
p, e := QueryDns("google.com", "A", 3, "https://dns.b612.me/dns-query")
|
||||
if e != nil {
|
||||
t.Error(e)
|
||||
}
|
||||
fmt.Println(p)
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
//go:build !js && !windows
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetDNSServers() (nameservers []netip.AddrPort) {
|
||||
const filename = "/etc/resolv.conf"
|
||||
return getLocalNameservers(filename)
|
||||
}
|
||||
|
||||
func getLocalNameservers(filename string) (nameservers []netip.AddrPort) {
|
||||
const defaultNameserverPort = 53
|
||||
defaultLocalNameservers := []netip.AddrPort{
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), defaultNameserverPort),
|
||||
netip.AddrPortFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 1}), defaultNameserverPort),
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return defaultLocalNameservers
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 || fields[0] != "nameserver" {
|
||||
continue
|
||||
}
|
||||
for _, field := range fields[1:] {
|
||||
ip, err := netip.ParseAddr(field)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
nameservers = append(nameservers,
|
||||
netip.AddrPortFrom(ip, defaultNameserverPort))
|
||||
}
|
||||
}
|
||||
|
||||
if len(nameservers) == 0 {
|
||||
return defaultLocalNameservers
|
||||
}
|
||||
return nameservers
|
||||
}
|
@ -0,0 +1,275 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func GetDNSServers() (nameservers []netip.AddrPort) {
|
||||
const defaultDNSPort = 53
|
||||
defaultLocalNameservers := []netip.AddrPort{
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{127, 0, 0, 1}), defaultDNSPort),
|
||||
netip.AddrPortFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 1}), defaultDNSPort),
|
||||
}
|
||||
|
||||
adapterAddresses, err := getAdapterAddresses()
|
||||
if err != nil {
|
||||
return defaultLocalNameservers
|
||||
}
|
||||
|
||||
for _, adapterAddress := range adapterAddresses {
|
||||
const statusUp = 0x01
|
||||
if adapterAddress.operStatus != statusUp {
|
||||
continue
|
||||
}
|
||||
|
||||
if adapterAddress.firstGatewayAddress == nil {
|
||||
// Only search DNS servers for adapters having a gateway
|
||||
continue
|
||||
}
|
||||
|
||||
dnsServerAddress := adapterAddress.firstDnsServerAddress
|
||||
for dnsServerAddress != nil {
|
||||
ip, ok := sockAddressToIP(dnsServerAddress.address.rawSockAddrAny)
|
||||
if !ok || ipIsSiteLocalAnycast(ip) {
|
||||
// fec0/10 IPv6 addresses are site local anycast DNS
|
||||
// addresses Microsoft sets by default if no other
|
||||
// IPv6 DNS address is set. Site local anycast is
|
||||
// deprecated since 2004, see
|
||||
// https://datatracker.ietf.org/doc/html/rfc3879
|
||||
dnsServerAddress = dnsServerAddress.next
|
||||
continue
|
||||
}
|
||||
|
||||
nameserver := netip.AddrPortFrom(ip, defaultDNSPort)
|
||||
nameservers = append(nameservers, nameserver)
|
||||
dnsServerAddress = dnsServerAddress.next
|
||||
}
|
||||
}
|
||||
|
||||
if len(nameservers) == 0 {
|
||||
return defaultLocalNameservers
|
||||
}
|
||||
return nameservers
|
||||
}
|
||||
|
||||
var (
|
||||
errBufferOverflowUnexpected = errors.New("unexpected buffer overflowed because buffer was large enough")
|
||||
)
|
||||
|
||||
func getAdapterAddresses() (
|
||||
adapterAddresses []*ipAdapterAddresses, err error) {
|
||||
var buffer []byte
|
||||
const initialBufferLength uint32 = 15000
|
||||
sizeVar := initialBufferLength
|
||||
|
||||
for {
|
||||
buffer = make([]byte, sizeVar)
|
||||
err := runProcGetAdaptersAddresses(
|
||||
(*ipAdapterAddresses)(unsafe.Pointer(&buffer[0])),
|
||||
&sizeVar)
|
||||
if err != nil {
|
||||
if err.(syscall.Errno) == syscall.ERROR_BUFFER_OVERFLOW {
|
||||
if sizeVar <= uint32(len(buffer)) {
|
||||
return nil, fmt.Errorf("%w: buffer size variable %d is "+
|
||||
"equal or lower to the buffer current length %d",
|
||||
errBufferOverflowUnexpected, sizeVar, len(buffer))
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("getting adapters addresses: %w", err)
|
||||
}
|
||||
|
||||
noDataFound := sizeVar == 0
|
||||
if noDataFound {
|
||||
return nil, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
adapterAddress := (*ipAdapterAddresses)(unsafe.Pointer(&buffer[0]))
|
||||
for adapterAddress != nil {
|
||||
adapterAddresses = append(adapterAddresses, adapterAddress)
|
||||
adapterAddress = adapterAddress.next
|
||||
}
|
||||
|
||||
return adapterAddresses, nil
|
||||
}
|
||||
|
||||
var (
|
||||
procGetAdaptersAddresses = syscall.NewLazyDLL("iphlpapi.dll").
|
||||
NewProc("GetAdaptersAddresses")
|
||||
)
|
||||
|
||||
func runProcGetAdaptersAddresses(adapterAddresses *ipAdapterAddresses,
|
||||
sizePointer *uint32) (errcode error) {
|
||||
const family = syscall.AF_UNSPEC
|
||||
const GAA_FLAG_SKIP_UNICAST = 0x0001
|
||||
const GAA_FLAG_SKIP_ANYCAST = 0x0002
|
||||
const GAA_FLAG_SKIP_MULTICAST = 0x0004
|
||||
const GAA_FLAG_SKIP_FRIENDLY_NAME = 0x0020
|
||||
const GAA_FLAG_INCLUDE_GATEWAYS = 0x0080
|
||||
const flags = GAA_FLAG_SKIP_UNICAST | GAA_FLAG_SKIP_ANYCAST |
|
||||
GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_FRIENDLY_NAME |
|
||||
GAA_FLAG_INCLUDE_GATEWAYS
|
||||
const reserved = 0
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses
|
||||
r1, _, err := syscall.SyscallN(procGetAdaptersAddresses.Addr(),
|
||||
uintptr(family), uintptr(flags), uintptr(reserved),
|
||||
uintptr(unsafe.Pointer(adapterAddresses)),
|
||||
uintptr(unsafe.Pointer(sizePointer)))
|
||||
switch {
|
||||
case err != 0:
|
||||
return err
|
||||
case r1 != 0:
|
||||
return syscall.Errno(r1)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func sockAddressToIP(rawSockAddress *syscall.RawSockaddrAny) (ip netip.Addr, ok bool) {
|
||||
if rawSockAddress == nil {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
sockAddress, err := rawSockAddress.Sockaddr()
|
||||
if err != nil {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
switch sockAddress := sockAddress.(type) {
|
||||
case *syscall.SockaddrInet4:
|
||||
return netip.AddrFrom4([4]byte{
|
||||
sockAddress.Addr[0], sockAddress.Addr[1], sockAddress.Addr[2], sockAddress.Addr[3]}),
|
||||
true
|
||||
case *syscall.SockaddrInet6:
|
||||
return netip.AddrFrom16([16]byte{
|
||||
sockAddress.Addr[0], sockAddress.Addr[1], sockAddress.Addr[2], sockAddress.Addr[3],
|
||||
sockAddress.Addr[4], sockAddress.Addr[5], sockAddress.Addr[6], sockAddress.Addr[7],
|
||||
sockAddress.Addr[8], sockAddress.Addr[9], sockAddress.Addr[10], sockAddress.Addr[11],
|
||||
sockAddress.Addr[12], sockAddress.Addr[13], sockAddress.Addr[14], sockAddress.Addr[15]}),
|
||||
true
|
||||
default:
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func ipIsSiteLocalAnycast(ip netip.Addr) bool {
|
||||
if !ip.Is6() {
|
||||
return false
|
||||
}
|
||||
|
||||
array := ip.As16()
|
||||
return array[0] == 0xfe && array[1] == 0xc0
|
||||
}
|
||||
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/api/iptypes/ns-iptypes-ip_adapter_addresses_lh
|
||||
type ipAdapterAddresses struct {
|
||||
// The order of fields DOES matter since they are read
|
||||
// raw from a bytes buffer. However, we are only interested
|
||||
// in a few select fields, so unneeded fields are either
|
||||
// named as "_" or removed if they are after the fields
|
||||
// we are interested in.
|
||||
_ uint32
|
||||
_ uint32
|
||||
next *ipAdapterAddresses
|
||||
_ *byte
|
||||
_ *ipAdapterUnicastAddress
|
||||
_ *ipAdapterAnycastAddress
|
||||
_ *ipAdapterMulticastAddress
|
||||
firstDnsServerAddress *ipAdapterDnsServerAdapter
|
||||
_ *uint16
|
||||
_ *uint16
|
||||
_ *uint16
|
||||
_ [syscall.MAX_ADAPTER_ADDRESS_LENGTH]byte
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ uint32
|
||||
operStatus uint32
|
||||
_ uint32
|
||||
_ [16]uint32
|
||||
_ *ipAdapterPrefix
|
||||
_ uint64
|
||||
_ uint64
|
||||
_ *ipAdapterWinsServerAddress
|
||||
firstGatewayAddress *ipAdapterGatewayAddress
|
||||
// Additional fields not needed here
|
||||
}
|
||||
|
||||
type ipAdapterUnicastAddress struct {
|
||||
// The order of fields DOES matter since they are read raw
|
||||
// from a bytes buffer. However, we are not interested in
|
||||
// the value of any field, so they are all named as "_".
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ *ipAdapterUnicastAddress
|
||||
_ ipAdapterSocketAddress
|
||||
_ int32
|
||||
_ int32
|
||||
_ int32
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ uint8
|
||||
}
|
||||
|
||||
type ipAdapterAnycastAddress struct {
|
||||
// The order of fields DOES matter since they are read raw
|
||||
// from a bytes buffer. However, we are not interested in
|
||||
// the value of any field, so they are all named as "_".
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ *ipAdapterAnycastAddress
|
||||
_ ipAdapterSocketAddress
|
||||
}
|
||||
|
||||
type ipAdapterMulticastAddress struct {
|
||||
// The order of fields DOES matter since they are read raw
|
||||
// from a bytes buffer. However, we are only interested in
|
||||
// a few select fields, so unneeded fields are named as "_".
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ *ipAdapterMulticastAddress
|
||||
_ ipAdapterSocketAddress
|
||||
}
|
||||
|
||||
type ipAdapterDnsServerAdapter struct {
|
||||
// The order of fields DOES matter since they are read raw
|
||||
// from a bytes buffer. However, we are only interested in
|
||||
// a few select fields, so unneeded fields are named as "_".
|
||||
_ uint32
|
||||
_ uint32
|
||||
next *ipAdapterDnsServerAdapter
|
||||
address ipAdapterSocketAddress
|
||||
}
|
||||
|
||||
type ipAdapterPrefix struct {
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ *ipAdapterPrefix
|
||||
_ ipAdapterSocketAddress
|
||||
_ uint32
|
||||
}
|
||||
|
||||
type ipAdapterWinsServerAddress struct {
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ *ipAdapterWinsServerAddress
|
||||
_ ipAdapterSocketAddress
|
||||
}
|
||||
|
||||
type ipAdapterGatewayAddress struct {
|
||||
_ uint32
|
||||
_ uint32
|
||||
_ *ipAdapterGatewayAddress
|
||||
_ ipAdapterSocketAddress
|
||||
}
|
||||
|
||||
type ipAdapterSocketAddress struct {
|
||||
rawSockAddrAny *syscall.RawSockaddrAny
|
||||
}
|
@ -0,0 +1 @@
|
||||
##
|
@ -0,0 +1,43 @@
|
||||
package httproxy
|
||||
|
||||
import (
|
||||
"b612.me/starlog"
|
||||
"github.com/elazarl/goproxy"
|
||||
"github.com/elazarl/goproxy/ext/auth"
|
||||
"github.com/spf13/cobra"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var username, password string
|
||||
var listen string
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().StringVarP(&username, "username", "u", "", "用户名")
|
||||
Cmd.Flags().StringVarP(&password, "password", "p", "", "密码")
|
||||
Cmd.Flags().StringVarP(&listen, "listen", "l", ":8000", "监听地址")
|
||||
}
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "httproxy",
|
||||
Short: "http代理",
|
||||
Long: "http代理",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
run()
|
||||
},
|
||||
}
|
||||
|
||||
func run() {
|
||||
// Create a http server
|
||||
p := goproxy.NewProxyHttpServer()
|
||||
p.Verbose = true
|
||||
starlog.Infof("start http proxy server on %s username %s password %s \n", listen, username, password)
|
||||
if username != "" && password != "" {
|
||||
auth.ProxyBasic(p, "B612 Http Proxy Need Password", func(user, pwd string) bool {
|
||||
return user == username && pwd == password
|
||||
})
|
||||
}
|
||||
err := http.ListenAndServe(listen, p)
|
||||
if err != nil {
|
||||
starlog.Errorln("http proxy server error:", err)
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,185 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>B612 File Upload Page</title>
|
||||
<link rel="stylesheet" href="/css?bootstrap=true">
|
||||
<script src="/js?jquery=true"></script>
|
||||
<style>
|
||||
@media (max-width: 600px) {
|
||||
.progress-bar, p {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.file-upload {
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="text-center">B612 File Upload Page</h1>
|
||||
<form id="uploadForm" action="/recv?upload=true" method="post" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="victorique">上传文件:</label>
|
||||
<input type="file" class="form-control-file" id="victorique" name="victorique" multiple>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">上传</button>
|
||||
<button type="button" id="clearButton" class="btn btn-secondary">清除</button>
|
||||
<button type="button" id="cancelAllButton" class="btn btn-danger">取消所有上传</button>
|
||||
</form>
|
||||
<div id="progressContainer"></div>
|
||||
</div>
|
||||
<script>
|
||||
var completedUploads = 0;
|
||||
var anyUploadFailed = false;
|
||||
var xhrs = []; // Array to hold all the XHR objects
|
||||
|
||||
$("#victorique").on("change", function(e){
|
||||
document.getElementById('progressContainer').innerHTML = '';
|
||||
var files = document.getElementById('victorique').files;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
createProgressBar(files[i]);
|
||||
}
|
||||
});
|
||||
|
||||
$("#clearButton").on("click", function(e){
|
||||
document.getElementById('victorique').value = '';
|
||||
document.getElementById('progressContainer').innerHTML = '';
|
||||
});
|
||||
|
||||
$("#cancelAllButton").on("click", function(e){
|
||||
for (var i = 0; i < xhrs.length; i++) {
|
||||
xhrs[i].abort();
|
||||
}
|
||||
});
|
||||
|
||||
$("#uploadForm").on("submit", function(e){
|
||||
e.preventDefault();
|
||||
completedUploads = 0; // Reset the counter
|
||||
anyUploadFailed = false; // Reset the flag
|
||||
var files = document.getElementById('victorique').files;
|
||||
var fileUploads = document.querySelectorAll('.file-upload');
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
uploadFile(files[i], fileUploads[i]);
|
||||
}
|
||||
});
|
||||
|
||||
function createProgressBar(file) {
|
||||
var progressContainer = document.getElementById('progressContainer');
|
||||
var fileUpload = document.createElement('div');
|
||||
fileUpload.className = 'file-upload';
|
||||
progressContainer.appendChild(fileUpload);
|
||||
var fileNameLabel = document.createElement('p');
|
||||
fileNameLabel.innerHTML = file.name;
|
||||
fileUpload.appendChild(fileNameLabel);
|
||||
var progressBar = document.createElement('div');
|
||||
progressBar.className = 'progress-bar';
|
||||
progressBar.setAttribute('role', 'progressbar');
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.setAttribute('aria-valuenow', '0');
|
||||
progressBar.setAttribute('aria-valuemin', '0');
|
||||
progressBar.setAttribute('aria-valuemax', '100');
|
||||
fileUpload.appendChild(progressBar);
|
||||
var speedLabel = document.createElement('p');
|
||||
var sizeLabel = document.createElement('p');
|
||||
var cancelButton = document.createElement('button');
|
||||
cancelButton.innerHTML = '取消上传';
|
||||
cancelButton.className = 'btn btn-danger';
|
||||
fileUpload.appendChild(speedLabel);
|
||||
fileUpload.appendChild(sizeLabel);
|
||||
fileUpload.appendChild(cancelButton);
|
||||
|
||||
// Save these elements as custom properties
|
||||
fileUpload.progressBar = progressBar;
|
||||
fileUpload.speedLabel = speedLabel;
|
||||
fileUpload.sizeLabel = sizeLabel;
|
||||
fileUpload.cancelButton = cancelButton;
|
||||
return fileUpload;
|
||||
}
|
||||
|
||||
function uploadFile(file, fileUpload) {
|
||||
var formData = new FormData();
|
||||
formData.append('victorique', file);
|
||||
var start = Date.now();
|
||||
var lastLoaded = 0;
|
||||
var progressBar = fileUpload.progressBar;
|
||||
var speedLabel = fileUpload.speedLabel;
|
||||
var sizeLabel = fileUpload.sizeLabel;
|
||||
var cancelButton = fileUpload.cancelButton;
|
||||
|
||||
var xhr = $.ajax({
|
||||
xhr: function(){
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
xhr.upload.addEventListener("progress", function(evt){
|
||||
if(evt.lengthComputable){
|
||||
var percentComplete = ((evt.loaded / evt.total) * 100).toFixed(2);
|
||||
var timeElapsed = (Date.now() - start) / 1000; // Time elapsed in seconds
|
||||
var bytesLoaded = evt.loaded - lastLoaded;
|
||||
lastLoaded = evt.loaded;
|
||||
var speed = bytesLoaded / timeElapsed; // Speed in bytes/second
|
||||
var formattedSpeed = formatSize(speed);
|
||||
progressBar.style.width = percentComplete + '%';
|
||||
progressBar.innerHTML = percentComplete + '%';
|
||||
speedLabel.innerHTML = "上传速度: " + formattedSpeed + "/秒";
|
||||
sizeLabel.innerHTML = "已上传: " + formatSize(evt.loaded) + " / " + formatSize(evt.total);
|
||||
start = Date.now();
|
||||
}
|
||||
}, false);
|
||||
return xhr;
|
||||
},
|
||||
url: '/recv?upload=true',
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function(response){
|
||||
completedUploads++;
|
||||
if (completedUploads === document.getElementById('victorique').files.length) {
|
||||
if (anyUploadFailed) {
|
||||
alert('一些文件上传失败!');
|
||||
} else {
|
||||
alert('所有文件上传成功!');
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function(response){
|
||||
anyUploadFailed = true;
|
||||
completedUploads++;
|
||||
if (completedUploads === document.getElementById('victorique').files.length) {
|
||||
alert('一些文件上传失败!');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
xhrs.push(xhr); // Add the XHR object to the array
|
||||
|
||||
// Add click event to the cancel button
|
||||
cancelButton.onclick = function() {
|
||||
xhr.abort();
|
||||
};
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
var i = 0;
|
||||
var units = ['字节', 'KB', 'MB', 'GB'];
|
||||
while (size >= 1024) {
|
||||
size /= 1024;
|
||||
i++;
|
||||
}
|
||||
return size.toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
</script>
|
||||
<footer class="footer mt-auto py-3">
|
||||
<div class="container text-center">
|
||||
<span class="text-muted">Copyright@b612.me</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,15 @@
|
||||
package image
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCompressPhoto(t *testing.T) {
|
||||
p, err := OpenImage("../bin/original.jpg")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
p = CompressPhoto(p)
|
||||
err = SavePhoto("../bin/compressed.jpg", p)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
package keygen
|
||||
|
||||
import (
|
||||
"b612.me/starcrypto"
|
||||
"b612.me/staros"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type KeyGen struct {
|
||||
Type string
|
||||
Encrypt string
|
||||
Bits int
|
||||
Outfolder string
|
||||
Prefix string
|
||||
Force bool
|
||||
//
|
||||
Country string
|
||||
Locality string
|
||||
Organization string
|
||||
OrganizationalUnit string
|
||||
CommonName string
|
||||
StartDate time.Time
|
||||
EndDate time.Time
|
||||
}
|
||||
|
||||
func (k *KeyGen) Gen() error {
|
||||
if !k.Force && staros.Exists(filepath.Join(k.Outfolder, k.Prefix+".crt")) {
|
||||
return errors.New("crt file exists")
|
||||
}
|
||||
if !k.Force && staros.Exists(filepath.Join(k.Outfolder, k.Prefix)) {
|
||||
return errors.New("key file exists")
|
||||
}
|
||||
if !k.Force && staros.Exists(filepath.Join(k.Outfolder, k.Prefix+".pub")) {
|
||||
return errors.New("ssh pub file exists")
|
||||
}
|
||||
if !k.Force && staros.Exists(filepath.Join(k.Outfolder, k.Prefix+".key.pub")) {
|
||||
return errors.New("pub file exists")
|
||||
}
|
||||
var sshPubByte, keyPubByte, keyPrivByte, Crt []byte
|
||||
|
||||
var priv, pub any
|
||||
var err error
|
||||
switch strings.ToLower(k.Type) {
|
||||
case "rsa":
|
||||
priv, pub, err = starcrypto.GenerateRsaKey(k.Bits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "ecdsa", "ecdh":
|
||||
var cr elliptic.Curve
|
||||
switch k.Bits {
|
||||
case 224:
|
||||
cr = elliptic.P224()
|
||||
case 256:
|
||||
cr = elliptic.P256()
|
||||
case 384:
|
||||
cr = elliptic.P384()
|
||||
case 521:
|
||||
cr = elliptic.P521()
|
||||
default:
|
||||
return errors.New("invalid bits,should be 224,256,384,521")
|
||||
}
|
||||
priv, pub, err = starcrypto.GenerateEcdsaKey(cr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "ed25519":
|
||||
pub, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("invalid key type,only support rsa,ecdsa")
|
||||
}
|
||||
sshPubByte, err = starcrypto.EncodeSSHPublicKey(pub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyPubByte, err = starcrypto.EncodePublicKey(pub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keyPrivByte, err = starcrypto.EncodePrivateKey(priv, k.Encrypt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, Crt, err = k.GenerateCert(priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(k.Outfolder, k.Prefix+".crt"), Crt, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(k.Outfolder, k.Prefix), keyPrivByte, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(k.Outfolder, k.Prefix+".pub"), sshPubByte, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(k.Outfolder, k.Prefix+".key.pub"), keyPubByte, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *KeyGen) GenerateCert(priv crypto.PrivateKey) ([]byte, []byte, error) {
|
||||
//csr,pub
|
||||
tmpByte := make([]byte, 64)
|
||||
io.ReadFull(rand.Reader, tmpByte)
|
||||
hexStr := starcrypto.String(tmpByte)
|
||||
data, _ := hex.DecodeString(hexStr)
|
||||
num := new(big.Int).SetBytes(data)
|
||||
var country, locality, organization, organizationalUnit []string
|
||||
if k.Country != "" {
|
||||
country = []string{k.Country}
|
||||
}
|
||||
if k.Locality != "" {
|
||||
locality = []string{k.Locality}
|
||||
}
|
||||
if k.Organization != "" {
|
||||
organization = []string{k.Organization}
|
||||
}
|
||||
if k.OrganizationalUnit != "" {
|
||||
organizationalUnit = []string{k.OrganizationalUnit}
|
||||
|
||||
}
|
||||
var rootCsr = &x509.Certificate{
|
||||
Version: 3,
|
||||
SerialNumber: num,
|
||||
Subject: pkix.Name{
|
||||
Country: country,
|
||||
Locality: locality,
|
||||
Organization: organization,
|
||||
OrganizationalUnit: organizationalUnit,
|
||||
CommonName: k.CommonName,
|
||||
},
|
||||
NotBefore: k.StartDate,
|
||||
NotAfter: k.EndDate,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
MaxPathLenZero: false,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageDigitalSignature,
|
||||
}
|
||||
var cert []byte
|
||||
var err error
|
||||
switch priv.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
cert, err = MakeCert(priv.(*rsa.PrivateKey), rootCsr, rootCsr, priv.(*rsa.PrivateKey).Public())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
case *ecdsa.PrivateKey:
|
||||
cert, err = MakeCert(priv.(*ecdsa.PrivateKey), rootCsr, rootCsr, priv.(*ecdsa.PrivateKey).Public())
|
||||
case ed25519.PrivateKey:
|
||||
cert, err = MakeCert(priv.(ed25519.PrivateKey), rootCsr, rootCsr, priv.(ed25519.PrivateKey).Public())
|
||||
default:
|
||||
return nil, nil, errors.New("invalid private key type:" + fmt.Sprintf("%T", priv))
|
||||
}
|
||||
|
||||
csrPem := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE REQUEST",
|
||||
Bytes: rootCsr.Raw,
|
||||
})
|
||||
return csrPem, cert, nil
|
||||
}
|
||||
|
||||
func MakeCert(caKey any, caCrt *x509.Certificate, csr *x509.Certificate, pub any) ([]byte, error) {
|
||||
der, err := x509.CreateCertificate(rand.Reader, csr, caCrt, pub, caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemData := pem.EncodeToMemory(certBlock)
|
||||
return pemData, nil
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package keygen
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeyGen_Gen(t *testing.T) {
|
||||
var k = KeyGen{
|
||||
Type: "rsa",
|
||||
Encrypt: "",
|
||||
Bits: 2048,
|
||||
Outfolder: ".",
|
||||
Prefix: "mykey",
|
||||
Force: true,
|
||||
Country: "CN",
|
||||
Locality: "Beijing",
|
||||
Organization: "B612",
|
||||
OrganizationalUnit: "B612",
|
||||
CommonName: "Little Prince",
|
||||
}
|
||||
err := k.Gen()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
@ -1,39 +1,62 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"b612.me/apps/b612/aes"
|
||||
"b612.me/apps/b612/attach"
|
||||
"b612.me/apps/b612/base64"
|
||||
"b612.me/apps/b612/base85"
|
||||
"b612.me/apps/b612/base91"
|
||||
"b612.me/apps/b612/calc"
|
||||
"b612.me/apps/b612/cert"
|
||||
"b612.me/apps/b612/detach"
|
||||
"b612.me/apps/b612/df"
|
||||
"b612.me/apps/b612/dfinder"
|
||||
"b612.me/apps/b612/dns"
|
||||
"b612.me/apps/b612/ftp"
|
||||
"b612.me/apps/b612/generate"
|
||||
"b612.me/apps/b612/hash"
|
||||
"b612.me/apps/b612/httpreverse"
|
||||
"b612.me/apps/b612/httproxy"
|
||||
"b612.me/apps/b612/httpserver"
|
||||
"b612.me/apps/b612/image"
|
||||
"b612.me/apps/b612/keygen"
|
||||
"b612.me/apps/b612/merge"
|
||||
"b612.me/apps/b612/net"
|
||||
"b612.me/apps/b612/rmt"
|
||||
"b612.me/apps/b612/search"
|
||||
"b612.me/apps/b612/smtpclient"
|
||||
"b612.me/apps/b612/smtpserver"
|
||||
"b612.me/apps/b612/socks5"
|
||||
"b612.me/apps/b612/split"
|
||||
"b612.me/apps/b612/tcping"
|
||||
"b612.me/apps/b612/tls"
|
||||
"b612.me/apps/b612/uac"
|
||||
"b612.me/apps/b612/vic"
|
||||
"b612.me/apps/b612/whois"
|
||||
"b612.me/stario"
|
||||
"b612.me/starlog"
|
||||
"github.com/inconshreveable/mousetrap"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cmdRoot = &cobra.Command{
|
||||
Use: "b612",
|
||||
Version: "2.0.1",
|
||||
Version: "2.1.0.beta.6",
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.MousetrapHelpText = ""
|
||||
cmdRoot.AddCommand(tcping.Cmd, uac.Cmd, httpserver.Cmd, httpreverse.Cmd,
|
||||
base64.Cmd, base85.Cmd, base91.Cmd, attach.Cmd, detach.Cmd, df.Cmd, dfinder.Cmd,
|
||||
ftp.Cmd, generate.Cmd, hash.Cmd, image.Cmd, merge.Cmd, search.Cmd, split.Cmd, vic.Cmd)
|
||||
ftp.Cmd, generate.Cmd, hash.Cmd, image.Cmd, merge.Cmd, search.Cmd, split.Cmd, vic.Cmd,
|
||||
calc.Cmd, net.Cmd, rmt.Cmds, rmt.Cmdc, keygen.Cmd, dns.Cmd, whois.Cmd, socks5.Cmd, httproxy.Cmd, smtpserver.Cmd, smtpclient.Cmd,
|
||||
cert.Cmd, aes.Cmd, tls.Cmd)
|
||||
}
|
||||
|
||||
func main() {
|
||||
starlog.SetLevelColor(starlog.LvError, []starlog.Attr{starlog.FgHiMagenta})
|
||||
cmdRoot.Execute()
|
||||
if mousetrap.StartedByExplorer() {
|
||||
stario.StopUntil("Press Any Key to Continue...", "", true)
|
||||
}
|
||||
}
|
||||
|
@ -1,213 +0,0 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"b612.me/starlog"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NetForward struct {
|
||||
LocalAddr string
|
||||
LocalPort int
|
||||
RemoteURI string
|
||||
EnableTCP bool
|
||||
EnableUDP bool
|
||||
DialTimeout time.Duration
|
||||
UDPTimeout time.Duration
|
||||
stopCtx context.Context
|
||||
stopFn context.CancelFunc
|
||||
running int32
|
||||
}
|
||||
|
||||
func (n *NetForward) Close() {
|
||||
n.stopFn()
|
||||
}
|
||||
func (n *NetForward) Run() error {
|
||||
if !atomic.CompareAndSwapInt32(&n.running, 0, 1) {
|
||||
return errors.New("already running")
|
||||
}
|
||||
n.stopCtx, n.stopFn = context.WithCancel(context.Background())
|
||||
if n.DialTimeout == 0 {
|
||||
n.DialTimeout = time.Second * 10
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
if n.EnableTCP {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
n.runTCP()
|
||||
}()
|
||||
}
|
||||
|
||||
if n.EnableUDP {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
n.runUDP()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NetForward) runTCP() error {
|
||||
listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", n.LocalAddr, n.LocalPort))
|
||||
if err != nil {
|
||||
starlog.Errorln("Listening On Tcp Failed:", err)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
<-n.stopCtx.Done()
|
||||
listen.Close()
|
||||
}()
|
||||
starlog.Infof("Listening TCP on %v\n", fmt.Sprintf("%s:%d", n.LocalAddr, n.LocalPort))
|
||||
for {
|
||||
conn, err := listen.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log := starlog.Std.NewFlag()
|
||||
log.Infof("Accept New TCP Conn from %v\n", conn.RemoteAddr().String())
|
||||
go func(conn net.Conn) {
|
||||
rmt, err := net.DialTimeout("tcp", n.RemoteURI, n.DialTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("Dial Remote %s Failed:%v\n", n.RemoteURI, err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
log.Infof("Connect %s <==> %s\n", conn.RemoteAddr().String(), n.RemoteURI)
|
||||
Copy(rmt, conn)
|
||||
log.Noticef("Connection Closed %s <==> %s", conn.RemoteAddr().String(), n.RemoteURI)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
type UDPConn struct {
|
||||
net.Conn
|
||||
listen *net.UDPConn
|
||||
remoteAddr *net.UDPAddr
|
||||
lastbeat int64
|
||||
}
|
||||
|
||||
func (u UDPConn) Write(p []byte) (n int, err error) {
|
||||
u.lastbeat = time.Now().Unix()
|
||||
return u.Conn.Write(p)
|
||||
}
|
||||
|
||||
func (u UDPConn) Read(p []byte) (n int, err error) {
|
||||
u.lastbeat = time.Now().Unix()
|
||||
return u.Conn.Read(p)
|
||||
}
|
||||
|
||||
func (u UDPConn) Work() {
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
count, err := u.Read(buf)
|
||||
if err != nil {
|
||||
u.Close()
|
||||
u.lastbeat = 0
|
||||
return
|
||||
}
|
||||
_, err = u.listen.Write(buf[0:count])
|
||||
if err != nil {
|
||||
u.lastbeat = 0
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetForward) runUDP() error {
|
||||
var mu sync.RWMutex
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%v", n.LocalAddr, n.LocalPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
starlog.Infof("Listening UDP on %v\n", fmt.Sprintf("%s:%d", n.LocalAddr, n.LocalPort))
|
||||
go func() {
|
||||
<-n.stopCtx.Done()
|
||||
listen.Close()
|
||||
}()
|
||||
udpMap := make(map[string]UDPConn)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-n.stopCtx.Done():
|
||||
return
|
||||
case <-time.After(time.Second * 60):
|
||||
mu.Lock()
|
||||
for k, v := range udpMap {
|
||||
if time.Now().Unix() > int64(n.UDPTimeout.Seconds())+v.lastbeat {
|
||||
delete(udpMap, k)
|
||||
starlog.Noticef("Connection Closed %s <==> %s", v.remoteAddr.String(), n.RemoteURI)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
count, rmt, err := listen.ReadFromUDP(buf)
|
||||
if err != nil || rmt.String() == n.RemoteURI {
|
||||
continue
|
||||
}
|
||||
go func(data []byte, rmt *net.UDPAddr) {
|
||||
log := starlog.Std.NewFlag()
|
||||
mu.Lock()
|
||||
addr, ok := udpMap[rmt.String()]
|
||||
if !ok {
|
||||
log.Infof("Accept New UDP Conn from %v\n", rmt.String())
|
||||
conn, err := net.Dial("udp", n.RemoteURI)
|
||||
if err != nil {
|
||||
log.Errorf("Dial Remote %s Failed:%v\n", n.RemoteURI, err)
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
addr = UDPConn{
|
||||
Conn: conn,
|
||||
remoteAddr: rmt,
|
||||
listen: listen,
|
||||
lastbeat: time.Now().Unix(),
|
||||
}
|
||||
udpMap[rmt.String()] = addr
|
||||
go addr.Work()
|
||||
log.Infof("Connect %s <==> %s\n", rmt.String(), n.RemoteURI)
|
||||
}
|
||||
mu.Unlock()
|
||||
_, err := addr.Write(data)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
addr.Close()
|
||||
delete(udpMap, addr.remoteAddr.String())
|
||||
mu.Unlock()
|
||||
log.Noticef("Connection Closed %s <==> %s", rmt.String(), n.RemoteURI)
|
||||
}
|
||||
}(buf[0:count], rmt)
|
||||
}
|
||||
}
|
||||
|
||||
func Copy(dst, src net.Conn) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(dst, src)
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
io.Copy(src, dst)
|
||||
}()
|
||||
wg.Wait()
|
||||
dst.Close()
|
||||
src.Close()
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package net
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestForward(t *testing.T) {
|
||||
var f = NetForward{
|
||||
LocalAddr: "127.0.0.1",
|
||||
LocalPort: 22232,
|
||||
RemoteURI: "127.0.0.1:1127",
|
||||
EnableTCP: true,
|
||||
EnableUDP: true,
|
||||
DialTimeout: 0,
|
||||
UDPTimeout: 0,
|
||||
}
|
||||
f.Run()
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNat(t *testing.T) {
|
||||
var s = NatServer{
|
||||
ListenAddr: "0.0.0.0:10020",
|
||||
enableTCP: true,
|
||||
enableUDP: true,
|
||||
}
|
||||
var c = NatClient{
|
||||
ServiceTarget: "dns.b612.me:521",
|
||||
CmdTarget: "127.0.0.1:10020",
|
||||
enableTCP: true,
|
||||
enableUDP: true,
|
||||
}
|
||||
go s.Run()
|
||||
go c.Run()
|
||||
for {
|
||||
time.Sleep(time.Second * 20)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrace(t *testing.T) {
|
||||
//Traceroute("b612.me", "", 32, time.Millisecond*800, "https://ip.b612.me/{ip}/detail")
|
||||
}
|
@ -1,27 +1,343 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"b612.me/starlog"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SimpleNatClient struct {
|
||||
type NatClient struct {
|
||||
mu sync.RWMutex
|
||||
cmdTCPConn net.Conn
|
||||
cmdUDPConn *net.UDPAddr
|
||||
cmdUDPConn *net.UDPConn
|
||||
ServiceTarget string
|
||||
CmdTarget string
|
||||
tcpAlived bool
|
||||
DialTimeout int
|
||||
UdpTimeout int
|
||||
enableTCP bool
|
||||
enableUDP bool
|
||||
Passwd string
|
||||
udpAlived bool
|
||||
stopCtx context.Context
|
||||
stopFn context.CancelFunc
|
||||
}
|
||||
|
||||
func (s *SimpleNatClient) tcpCmdConn() net.Conn {
|
||||
func (s *NatClient) tcpCmdConn() net.Conn {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cmdTCPConn
|
||||
}
|
||||
|
||||
func (s *SimpleNatClient) tcpCmdConnAlived() bool {
|
||||
func (s *NatClient) udpCmdConn() *net.UDPConn {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.cmdUDPConn
|
||||
}
|
||||
|
||||
func (s *NatClient) tcpCmdConnAlived() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.tcpAlived
|
||||
}
|
||||
|
||||
func (s *NatClient) setTcpCmdConnAlived(v bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.tcpAlived = v
|
||||
}
|
||||
|
||||
func (s *NatClient) udpCmdConnAlived() bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.udpAlived
|
||||
}
|
||||
|
||||
func (s *NatClient) setUdpCmdConnAlived(v bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.udpAlived = v
|
||||
}
|
||||
|
||||
func (s *NatClient) Run() error {
|
||||
s.stopCtx, s.stopFn = context.WithCancel(context.Background())
|
||||
if s.DialTimeout == 0 {
|
||||
s.DialTimeout = 10000
|
||||
}
|
||||
if s.Passwd != "" {
|
||||
MSG_CMD_HELLO = sha256.New().Sum(append(MSG_CMD_HELLO, []byte(s.Passwd)...))[:16]
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
if s.enableUDP {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.runUdp()
|
||||
}()
|
||||
}
|
||||
if s.enableTCP {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.runTcp()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *NatClient) runTcp() error {
|
||||
var err error
|
||||
starlog.Noticeln("nat client tcp module start run")
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
if s.cmdTCPConn != nil {
|
||||
s.setTcpCmdConnAlived(false)
|
||||
s.cmdTCPConn.Close()
|
||||
return nil
|
||||
}
|
||||
case <-time.After(time.Millisecond * 1500):
|
||||
}
|
||||
if s.cmdTCPConn != nil && s.tcpCmdConnAlived() {
|
||||
continue
|
||||
}
|
||||
s.cmdTCPConn, err = net.DialTimeout("tcp", s.CmdTarget, time.Millisecond*time.Duration(s.DialTimeout))
|
||||
if err != nil {
|
||||
starlog.Errorf("dail remote tcp cmd server %v fail:%v;will retry\n", s.CmdTarget, err)
|
||||
time.Sleep(time.Second * 2)
|
||||
s.cmdTCPConn = nil
|
||||
continue
|
||||
}
|
||||
starlog.Infoln("dail remote tcp cmd server ok,remote:", s.CmdTarget)
|
||||
s.tcpCmdConn().Write(MSG_CMD_HELLO)
|
||||
s.setTcpCmdConnAlived(true)
|
||||
go s.handleTcpCmdConn(s.tcpCmdConn())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NatClient) runUdp() error {
|
||||
starlog.Noticeln("nat client udp module start run")
|
||||
if s.UdpTimeout == 0 {
|
||||
s.UdpTimeout = 600000
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
if s.cmdTCPConn != nil {
|
||||
s.setUdpCmdConnAlived(false)
|
||||
s.cmdUDPConn.Close()
|
||||
return nil
|
||||
}
|
||||
case <-time.After(time.Millisecond * 3000):
|
||||
}
|
||||
if s.cmdUDPConn != nil && s.udpCmdConnAlived() {
|
||||
continue
|
||||
}
|
||||
rmt, err := net.ResolveUDPAddr("udp", s.CmdTarget)
|
||||
if err != nil {
|
||||
starlog.Errorf("dail remote udp cmd server %v fail:%v;will retry\n", s.CmdTarget, err)
|
||||
time.Sleep(time.Second * 2)
|
||||
continue
|
||||
}
|
||||
s.cmdUDPConn, err = net.DialUDP("udp", nil, rmt)
|
||||
if err != nil {
|
||||
starlog.Errorf("dail remote udp cmd server %v fail:%v;will retry\n", s.CmdTarget, err)
|
||||
time.Sleep(time.Second * 2)
|
||||
s.cmdTCPConn = nil
|
||||
continue
|
||||
}
|
||||
starlog.Infoln("dail remote udp cmd server ok,remote:", s.CmdTarget)
|
||||
s.udpCmdConn().Write(MSG_CMD_HELLO)
|
||||
s.setUdpCmdConnAlived(true)
|
||||
go s.handleUdpCmdConn(s.udpCmdConn())
|
||||
}
|
||||
}
|
||||
func (s *NatClient) handleUdpCmdConn(conn *net.UDPConn) {
|
||||
for {
|
||||
header := make([]byte, 16)
|
||||
_, err := io.ReadFull(conn, header)
|
||||
if err != nil {
|
||||
starlog.Infoln("udp cmd server read fail:", err)
|
||||
conn.Close()
|
||||
s.setUdpCmdConnAlived(false)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(header, MSG_CMD_HELLO_REPLY) {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(header, MSG_NEW_CONN_HELLO) {
|
||||
go s.newRemoteUdpConn()
|
||||
}
|
||||
if bytes.Equal(header, MSG_HEARTBEAT) {
|
||||
_, err = conn.Write(MSG_HEARTBEAT)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
s.setUdpCmdConnAlived(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NatClient) handleTcpCmdConn(conn net.Conn) {
|
||||
for {
|
||||
header := make([]byte, 16)
|
||||
_, err := io.ReadFull(conn, header)
|
||||
if err != nil {
|
||||
starlog.Infoln("tcp cmd server read fail:", err)
|
||||
conn.Close()
|
||||
s.setTcpCmdConnAlived(false)
|
||||
return
|
||||
}
|
||||
if bytes.Equal(header, MSG_CMD_HELLO_REPLY) {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(header, MSG_NEW_CONN_HELLO) {
|
||||
go s.newRemoteTcpConn()
|
||||
}
|
||||
if bytes.Equal(header, MSG_HEARTBEAT) {
|
||||
_, err = conn.Write(MSG_HEARTBEAT)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
s.setTcpCmdConnAlived(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *NatClient) newRemoteTcpConn() {
|
||||
log := starlog.Std.NewFlag()
|
||||
starlog.Infoln("recv request,create new tcp conn")
|
||||
nconn, err := net.DialTimeout("tcp", s.CmdTarget, time.Millisecond*time.Duration(s.DialTimeout))
|
||||
if err != nil {
|
||||
log.Errorf("dail server tcp conn %v fail:%v\n", s.CmdTarget, err)
|
||||
return
|
||||
}
|
||||
_, err = nconn.Write(MSG_NEW_CONN_HELLO)
|
||||
if err != nil {
|
||||
nconn.Close()
|
||||
log.Errorf("write new tcp client hello to server %v fail:%v\n", s.CmdTarget, err)
|
||||
return
|
||||
}
|
||||
cconn, err := net.DialTimeout("tcp", s.ServiceTarget, time.Millisecond*time.Duration(s.DialTimeout))
|
||||
if err != nil {
|
||||
log.Errorf("dail remote tcp conn %v fail:%v\n", s.CmdTarget, err)
|
||||
nconn.Close()
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
data := make([]byte, 8192)
|
||||
nconn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(s.UdpTimeout)))
|
||||
n, err := nconn.Read(data)
|
||||
if err != nil {
|
||||
starlog.Infoln("read from tcp server fail:", nconn.RemoteAddr(), err)
|
||||
nconn.Close()
|
||||
cconn.Close()
|
||||
return
|
||||
}
|
||||
_, err = cconn.Write(data[:n])
|
||||
//starlog.Debugln("write to udp client:", p, err, cconn.LocalAddr(), cconn.RemoteAddr())
|
||||
if err != nil {
|
||||
starlog.Infoln("write to tcp client fail:", cconn.RemoteAddr(), err)
|
||||
nconn.Close()
|
||||
cconn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
data := make([]byte, 8192)
|
||||
cconn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(s.UdpTimeout)))
|
||||
n, err := cconn.Read(data)
|
||||
if err != nil {
|
||||
starlog.Infoln("read from tcp server fail:", cconn.RemoteAddr(), err)
|
||||
nconn.Close()
|
||||
cconn.Close()
|
||||
return
|
||||
}
|
||||
_, err = nconn.Write(data[:n])
|
||||
if err != nil {
|
||||
starlog.Infoln("write to tcp client fail:", nconn.RemoteAddr(), err)
|
||||
nconn.Close()
|
||||
cconn.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *NatClient) newRemoteUdpConn() {
|
||||
log := starlog.Std.NewFlag()
|
||||
starlog.Infoln("recv request,create new udp conn")
|
||||
rmt, err := net.ResolveUDPAddr("udp", s.CmdTarget)
|
||||
if err != nil {
|
||||
log.Errorf("dail server udp conn %v fail:%v\n", s.CmdTarget, err)
|
||||
return
|
||||
}
|
||||
nconn, err := net.DialUDP("udp", nil, rmt)
|
||||
if err != nil {
|
||||
log.Errorf("dail server udp conn %v fail:%v\n", s.CmdTarget, err)
|
||||
return
|
||||
}
|
||||
log.Infof("dail server udp conn %v ok\n", s.CmdTarget)
|
||||
_, err = nconn.Write(MSG_NEW_CONN_HELLO)
|
||||
if err != nil {
|
||||
nconn.Close()
|
||||
log.Errorf("write new udp client hello to server %v fail:%v\n", s.CmdTarget, err)
|
||||
return
|
||||
}
|
||||
|
||||
rmt, err = net.ResolveUDPAddr("udp", s.ServiceTarget)
|
||||
if err != nil {
|
||||
log.Errorf("dail server udp conn %v fail:%v\n", s.ServiceTarget, err)
|
||||
return
|
||||
}
|
||||
cconn, err := net.DialUDP("udp", nil, rmt)
|
||||
if err != nil {
|
||||
log.Errorf("dail remote udp conn %v fail:%v\n", s.ServiceTarget, err)
|
||||
return
|
||||
}
|
||||
log.Infof("dail remote udp conn %v ok\n", s.ServiceTarget)
|
||||
go func() {
|
||||
for {
|
||||
data := make([]byte, 8192)
|
||||
nconn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(s.UdpTimeout)))
|
||||
n, err := nconn.Read(data)
|
||||
if err != nil {
|
||||
starlog.Infoln("read from udp server fail:", err)
|
||||
return
|
||||
}
|
||||
_, err = cconn.Write(data[:n])
|
||||
//starlog.Debugln("write to udp client:", p, err, cconn.LocalAddr(), cconn.RemoteAddr())
|
||||
if err != nil {
|
||||
starlog.Infoln("write to udp client fail:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
data := make([]byte, 8192)
|
||||
cconn.SetReadDeadline(time.Now().Add(time.Millisecond * time.Duration(s.UdpTimeout)))
|
||||
n, err := cconn.Read(data)
|
||||
if err != nil {
|
||||
starlog.Infoln("read from udp server fail:", err)
|
||||
return
|
||||
}
|
||||
_, err = nconn.Write(data[:n])
|
||||
if err != nil {
|
||||
starlog.Infoln("write to udp client fail:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
//go:build darwin
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetTcpInfo(conn *net.TCPConn, usingKeepAlive bool, keepAliveIdel, keepAlivePeriod, keepAliveCount, userTimeout int) error {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usingKeepAlive {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPALIVE, keepAliveIdel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 0x101, keepAlivePeriod)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
} else {
|
||||
err = conn.SetKeepAlive(false)
|
||||
}
|
||||
if userTimeout > 0 {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 0x12, userTimeout)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
//go:build !(windows && darwin)
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetTcpInfo(conn *net.TCPConn, usingKeepAlive bool, keepAliveIdel, keepAlivePeriod, keepAliveCount, userTimeout int) error {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usingKeepAlive {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, keepAliveIdel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, keepAlivePeriod)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, keepAliveCount)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
} else {
|
||||
err = conn.SetKeepAlive(false)
|
||||
}
|
||||
if userTimeout > 0 {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 0x12, userTimeout)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
//go:build windows
|
||||
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func SetTcpInfo(conn *net.TCPConn, usingKeepAlive bool, keepAliveIdel, keepAlivePeriod, keepAliveCount, userTimeout int) error {
|
||||
if usingKeepAlive {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
ka := syscall.TCPKeepalive{
|
||||
OnOff: 1,
|
||||
Time: uint32(keepAliveIdel),
|
||||
Interval: uint32(keepAlivePeriod),
|
||||
}
|
||||
ret := uint32(0)
|
||||
size := uint32(unsafe.Sizeof(ka))
|
||||
err = syscall.WSAIoctl(syscall.Handle(fd), syscall.SIO_KEEPALIVE_VALS, (*byte)(unsafe.Pointer(&ka)), size, nil, 0, &ret, nil, 0)
|
||||
runtime.KeepAlive(fd)
|
||||
})
|
||||
return os.NewSyscallError("wsaioctl", err)
|
||||
}
|
||||
return conn.SetKeepAlive(false)
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"b612.me/starcrypto"
|
||||
"b612.me/starlog"
|
||||
"b612.me/starnet"
|
||||
"crypto/elliptic"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
listenAddr string
|
||||
keyFile string
|
||||
KeyPasswd string
|
||||
outpath string
|
||||
curlUrl string
|
||||
curlArg []string
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmdSSHJar.Flags().StringVarP(&listenAddr, "listen", "l", "0.0.0.0:22", "监听地址")
|
||||
cmdSSHJar.Flags().StringVarP(&keyFile, "key", "k", "", "私钥文件")
|
||||
cmdSSHJar.Flags().StringVarP(&KeyPasswd, "passwd", "p", "", "私钥密码")
|
||||
cmdSSHJar.Flags().StringVarP(&outpath, "output", "o", "", "输出文件")
|
||||
}
|
||||
|
||||
var cmdSSHJar = &cobra.Command{
|
||||
Use: "sshjar",
|
||||
Short: "SSH蜜罐",
|
||||
Long: "SSH蜜罐",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
runSSHHoneyJar(listenAddr, keyFile, KeyPasswd, outpath)
|
||||
},
|
||||
}
|
||||
|
||||
func runSSHHoneyJar(listenAddr, keyFile, KeyPasswd, outpath string) {
|
||||
var f *os.File
|
||||
var err error
|
||||
if outpath != "" {
|
||||
f, err = os.OpenFile(outpath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
starlog.Errorf("Failed to open file %s (%s)", outpath, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
defer f.Close()
|
||||
config := &ssh.ServerConfig{
|
||||
// 密码验证回调函数
|
||||
PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||
starlog.Infof("Login attempt from %s with %s %s\n", c.RemoteAddr(), c.User(), string(pass))
|
||||
data := []string{c.RemoteAddr().String(), c.User(), string(pass)}
|
||||
bts, _ := json.Marshal(data)
|
||||
if f != nil {
|
||||
f.Write(bts)
|
||||
f.Write([]byte("\n"))
|
||||
}
|
||||
if curlUrl != "" {
|
||||
go func() {
|
||||
data := map[string]string{
|
||||
"ip": c.RemoteAddr().String(),
|
||||
"user": c.User(),
|
||||
"passwd": string(pass),
|
||||
}
|
||||
if curlArg != nil && len(curlArg) > 0 {
|
||||
for _, v := range curlArg {
|
||||
args := strings.SplitN(v, ":", 2)
|
||||
if len(args) == 2 {
|
||||
data[args[0]] = args[1]
|
||||
}
|
||||
}
|
||||
starnet.Curl(starnet.NewRequests(curlUrl, []byte(starnet.BuildQuery(data)), "POST"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil, fmt.Errorf("password rejected for %q", c.User())
|
||||
},
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
return nil, fmt.Errorf("public key rejected for %q", conn.User())
|
||||
},
|
||||
}
|
||||
if keyFile == "" {
|
||||
secKey, _, err := starcrypto.GenerateEcdsaKey(elliptic.P256())
|
||||
if err != nil {
|
||||
starlog.Errorf("Failed to generate ECDSA key (%s)", err)
|
||||
return
|
||||
}
|
||||
key, err := ssh.NewSignerFromKey(secKey)
|
||||
if err != nil {
|
||||
starlog.Errorf("Failed to generate signer from key (%s)", err)
|
||||
return
|
||||
}
|
||||
config.AddHostKey(key)
|
||||
} else {
|
||||
keyByte, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
starlog.Errorf("Failed to read private key from %s (%s)", keyFile, err)
|
||||
return
|
||||
}
|
||||
var key ssh.Signer
|
||||
if KeyPasswd != "" {
|
||||
key, err = ssh.ParsePrivateKeyWithPassphrase(keyByte, []byte(KeyPasswd))
|
||||
} else {
|
||||
key, err = ssh.ParsePrivateKey(keyByte)
|
||||
}
|
||||
if err != nil {
|
||||
starlog.Errorf("Failed to load private key from %s (%s)", keyFile, err)
|
||||
return
|
||||
}
|
||||
config.AddHostKey(key)
|
||||
}
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
starlog.Errorf("Failed to listen on %s (%s)", listenAddr, err)
|
||||
return
|
||||
}
|
||||
starlog.Noticeln("SSH HoneyJar is listening on", listenAddr)
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, os.Kill)
|
||||
for {
|
||||
select {
|
||||
case <-sig:
|
||||
starlog.Noticef("SSH HoneyJar is shutting down")
|
||||
listener.Close()
|
||||
return
|
||||
default:
|
||||
}
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
starlog.Infof("New connection from %s\n", conn.RemoteAddr())
|
||||
go func(conn net.Conn) {
|
||||
ssh.NewServerConn(conn, config)
|
||||
conn.Close()
|
||||
}(conn)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package net
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSSHJar(t *testing.T) {
|
||||
//runSSHHoneyJar("0.0.0.0:22")
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"b612.me/stario"
|
||||
"b612.me/starlog"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TcpClient struct {
|
||||
LocalAddr string
|
||||
RemoteAddr string
|
||||
UsingKeepAlive bool
|
||||
KeepAlivePeriod int
|
||||
KeepAliveIdel int
|
||||
KeepAliveCount int
|
||||
Interactive bool
|
||||
UserTimeout int
|
||||
ShowRecv bool
|
||||
ShowAsHex bool
|
||||
SaveToFolder string
|
||||
Rmt *TcpConn
|
||||
LogPath string
|
||||
stopCtx context.Context
|
||||
stopFn context.CancelFunc
|
||||
}
|
||||
|
||||
func (s *TcpClient) Close() error {
|
||||
return s.Rmt.Close()
|
||||
}
|
||||
|
||||
func (s *TcpClient) handleInteractive() {
|
||||
var currentCmd string
|
||||
notifyMap := make(map[string]chan struct{})
|
||||
if !s.Interactive {
|
||||
return
|
||||
}
|
||||
starlog.Infoln("Interactive mode enabled")
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
starlog.Infoln("Interactive mode stopped due to context done")
|
||||
return
|
||||
default:
|
||||
}
|
||||
cmd := stario.MessageBox("", "").MustString()
|
||||
if cmd == "" {
|
||||
continue
|
||||
}
|
||||
cmdf := strings.Fields(cmd)
|
||||
switch cmdf[0] {
|
||||
case "hex":
|
||||
currentCmd = "hex"
|
||||
starlog.Infoln("Switch to hex mode,send hex to remote client")
|
||||
case "text":
|
||||
currentCmd = "text"
|
||||
starlog.Infoln("Switch to text mode,send text to remote client")
|
||||
case "close":
|
||||
if s.Rmt.TCPConn == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
s.Rmt.TCPConn.Close()
|
||||
starlog.Infof("Client %s closed\n", s.Rmt.RemoteAddr().String())
|
||||
s.Rmt = nil
|
||||
currentCmd = ""
|
||||
case "startauto":
|
||||
if s.Rmt == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
notifyMap[s.Rmt.RemoteAddr().String()] = make(chan struct{})
|
||||
go func(conn *TcpConn) {
|
||||
for {
|
||||
select {
|
||||
case <-notifyMap[conn.RemoteAddr().String()]:
|
||||
starlog.Infoln("Auto send stopped")
|
||||
return
|
||||
default:
|
||||
}
|
||||
_, err := conn.Write([]byte(strings.Repeat("B612", 256)))
|
||||
if err != nil {
|
||||
starlog.Errorln("Write error:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(s.Rmt)
|
||||
starlog.Infoln("Auto send started")
|
||||
case "closeauto":
|
||||
if s.Rmt == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
close(notifyMap[s.Rmt.RemoteAddr().String()])
|
||||
case "send":
|
||||
if s.Rmt == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
if currentCmd == "hex" {
|
||||
data, err := hex.DecodeString(strings.TrimSpace(strings.TrimPrefix(cmd, "send")))
|
||||
if err != nil {
|
||||
starlog.Errorln("Hex decode error:", err)
|
||||
continue
|
||||
}
|
||||
_, err = s.Rmt.Write(data)
|
||||
if err != nil {
|
||||
starlog.Errorln("Write error:", err)
|
||||
} else {
|
||||
if s.Rmt.f != nil {
|
||||
s.Rmt.f.Write([]byte(time.Now().String() + " send\n"))
|
||||
s.Rmt.f.Write(data)
|
||||
s.Rmt.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err := s.Rmt.Write([]byte(strings.TrimSpace(strings.TrimPrefix(cmd, "send"))))
|
||||
if err != nil {
|
||||
starlog.Errorln("Write error:", err)
|
||||
} else {
|
||||
if s.Rmt.f != nil {
|
||||
s.Rmt.f.Write([]byte(time.Now().String() + " send\n"))
|
||||
s.Rmt.f.Write([]byte(cmdf[1]))
|
||||
s.Rmt.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
starlog.Infof("Send to %s success\n", s.Rmt.RemoteAddr().String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpClient) Run() error {
|
||||
var err error
|
||||
s.stopCtx, s.stopFn = context.WithCancel(context.Background())
|
||||
if s.LogPath != "" {
|
||||
err := starlog.SetLogFile(s.LogPath, starlog.Std, true)
|
||||
if err != nil {
|
||||
starlog.Errorln("SetLogFile error:", err)
|
||||
return fmt.Errorf("SetLogFile error: %w", err)
|
||||
}
|
||||
}
|
||||
var localAddr *net.TCPAddr
|
||||
if s.LocalAddr != "" {
|
||||
localAddr, err = net.ResolveTCPAddr("tcp", s.LocalAddr)
|
||||
if err != nil {
|
||||
starlog.Errorln("ResolveTCPAddr error:", err)
|
||||
return fmt.Errorf("ResolveTCPAddr error: %w", err)
|
||||
}
|
||||
}
|
||||
remoteAddr, err := net.ResolveTCPAddr("tcp", s.RemoteAddr)
|
||||
if err != nil {
|
||||
starlog.Errorln("ResolveTCPAddr error:", err)
|
||||
return fmt.Errorf("ResolveTCPAddr error: %w", err)
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", localAddr, remoteAddr)
|
||||
if err != nil {
|
||||
starlog.Errorln("Dial TCP error:", err)
|
||||
return fmt.Errorf("Dial TCP error: %w", err)
|
||||
}
|
||||
starlog.Infof("Connected to %s LocalAddr: %s\n", conn.RemoteAddr().String(), conn.LocalAddr().String())
|
||||
if s.Interactive {
|
||||
go s.handleInteractive()
|
||||
}
|
||||
s.Rmt = s.getTcpConn(conn)
|
||||
|
||||
s.handleConn(s.Rmt)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TcpClient) getTcpConn(conn *net.TCPConn) *TcpConn {
|
||||
var err error
|
||||
var f *os.File
|
||||
if s.SaveToFolder != "" {
|
||||
f, err = os.Create(filepath.Join(s.SaveToFolder, strings.ReplaceAll(conn.RemoteAddr().String(), ":", "_")))
|
||||
if err != nil {
|
||||
starlog.Errorf("Create file error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
}
|
||||
}
|
||||
return &TcpConn{
|
||||
TCPConn: conn,
|
||||
f: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpClient) handleConn(conn *TcpConn) {
|
||||
var err error
|
||||
log := starlog.Std.NewFlag()
|
||||
err = SetTcpInfo(conn.TCPConn, s.UsingKeepAlive, s.KeepAliveIdel, s.KeepAlivePeriod, s.KeepAliveCount, s.UserTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("SetTcpInfo error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
log.Infof("SetKeepAlive success for %s\n", conn.RemoteAddr().String())
|
||||
log.Infof("KeepAlivePeriod: %d, KeepAliveIdel: %d, KeepAliveCount: %d, UserTimeout: %d\n", s.KeepAlivePeriod, s.KeepAliveIdel, s.KeepAliveCount, s.UserTimeout)
|
||||
if runtime.GOOS != "linux" {
|
||||
log.Warningln("keepAliveCount and userTimeout only work on linux")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
log.Infof("Connection from %s closed due to context done\n", conn.RemoteAddr().String())
|
||||
conn.Close()
|
||||
return
|
||||
default:
|
||||
}
|
||||
buf := make([]byte, 8192)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
log.Errorf("Read error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
if s.ShowRecv {
|
||||
if s.ShowAsHex {
|
||||
log.Printf("Recv from %s: %x\n", conn.RemoteAddr().String(), buf[:n])
|
||||
} else {
|
||||
log.Printf("Recv from %s: %s\n", conn.RemoteAddr().String(), string(buf[:n]))
|
||||
}
|
||||
}
|
||||
if conn.f != nil {
|
||||
conn.f.Write([]byte(time.Now().String() + " recv\n"))
|
||||
conn.f.Write(buf[:n])
|
||||
conn.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpClient) Stop() {
|
||||
s.stopFn()
|
||||
if s.Rmt != nil {
|
||||
s.Rmt.Close()
|
||||
}
|
||||
}
|
@ -0,0 +1,285 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"b612.me/stario"
|
||||
"b612.me/starlog"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TcpConn struct {
|
||||
*net.TCPConn
|
||||
f *os.File
|
||||
}
|
||||
|
||||
type TcpServer struct {
|
||||
LocalAddr string
|
||||
UsingKeepAlive bool
|
||||
KeepAlivePeriod int
|
||||
KeepAliveIdel int
|
||||
KeepAliveCount int
|
||||
sync.Mutex
|
||||
Clients map[string]*TcpConn
|
||||
Interactive bool
|
||||
UserTimeout int
|
||||
ShowRecv bool
|
||||
ShowAsHex bool
|
||||
SaveToFolder string
|
||||
Listen *net.TCPListener
|
||||
LogPath string
|
||||
stopCtx context.Context
|
||||
stopFn context.CancelFunc
|
||||
}
|
||||
|
||||
func (s *TcpServer) Close() error {
|
||||
return s.Listen.Close()
|
||||
}
|
||||
|
||||
func (s *TcpServer) handleInteractive() {
|
||||
var conn *TcpConn
|
||||
var currentCmd string
|
||||
notifyMap := make(map[string]chan struct{})
|
||||
if !s.Interactive {
|
||||
return
|
||||
}
|
||||
starlog.Infoln("Interactive mode enabled")
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
starlog.Infoln("Interactive mode stopped due to context done")
|
||||
return
|
||||
default:
|
||||
}
|
||||
cmd := stario.MessageBox("", "").MustString()
|
||||
if cmd == "" {
|
||||
continue
|
||||
}
|
||||
cmdf := strings.Fields(cmd)
|
||||
switch cmdf[0] {
|
||||
case "list":
|
||||
s.Lock()
|
||||
for k, v := range s.Clients {
|
||||
starlog.Green("Client %s: %s\n", k, v.RemoteAddr().String())
|
||||
}
|
||||
s.Unlock()
|
||||
case "use":
|
||||
if len(cmdf) < 2 {
|
||||
starlog.Errorln("use command need a client address")
|
||||
continue
|
||||
}
|
||||
conn = s.Clients[cmdf[1]]
|
||||
if conn == nil {
|
||||
starlog.Errorln("Client not found")
|
||||
continue
|
||||
}
|
||||
starlog.Infof("Using client %s\n", conn.RemoteAddr().String())
|
||||
case "hex":
|
||||
currentCmd = "hex"
|
||||
starlog.Infoln("Switch to hex mode,send hex to remote client")
|
||||
case "text":
|
||||
currentCmd = "text"
|
||||
starlog.Infoln("Switch to text mode,send text to remote client")
|
||||
case "close":
|
||||
if conn.TCPConn == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
conn.TCPConn.Close()
|
||||
starlog.Infof("Client %s closed\n", conn.RemoteAddr().String())
|
||||
conn = nil
|
||||
currentCmd = ""
|
||||
case "startauto":
|
||||
if conn == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
notifyMap[conn.RemoteAddr().String()] = make(chan struct{})
|
||||
go func(conn *TcpConn) {
|
||||
for {
|
||||
select {
|
||||
case <-notifyMap[conn.RemoteAddr().String()]:
|
||||
starlog.Infoln("Auto send stopped")
|
||||
return
|
||||
default:
|
||||
}
|
||||
_, err := conn.Write([]byte(strings.Repeat("B612", 256)))
|
||||
if err != nil {
|
||||
starlog.Errorln("Write error:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(conn)
|
||||
starlog.Infoln("Auto send started")
|
||||
case "closeauto":
|
||||
if conn == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
close(notifyMap[conn.RemoteAddr().String()])
|
||||
case "send":
|
||||
if conn == nil {
|
||||
starlog.Errorln("No client selected")
|
||||
continue
|
||||
}
|
||||
if currentCmd == "hex" {
|
||||
data, err := hex.DecodeString(strings.TrimSpace(strings.TrimPrefix(cmd, "send")))
|
||||
if err != nil {
|
||||
starlog.Errorln("Hex decode error:", err)
|
||||
continue
|
||||
}
|
||||
_, err = conn.Write(data)
|
||||
if err != nil {
|
||||
starlog.Errorln("Write error:", err)
|
||||
} else {
|
||||
if conn.f != nil {
|
||||
conn.f.Write([]byte(time.Now().String() + " send\n"))
|
||||
conn.f.Write(data)
|
||||
conn.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_, err := conn.Write([]byte(strings.TrimSpace(strings.TrimPrefix(cmd, "send"))))
|
||||
if err != nil {
|
||||
starlog.Errorln("Write error:", err)
|
||||
} else {
|
||||
if conn.f != nil {
|
||||
conn.f.Write([]byte(time.Now().String() + " send\n"))
|
||||
conn.f.Write([]byte(cmdf[1]))
|
||||
conn.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
starlog.Infof("Send to %s success\n", conn.RemoteAddr().String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpServer) Run() error {
|
||||
s.stopCtx, s.stopFn = context.WithCancel(context.Background())
|
||||
if s.LogPath != "" {
|
||||
err := starlog.SetLogFile(s.LogPath, starlog.Std, true)
|
||||
if err != nil {
|
||||
starlog.Errorln("SetLogFile error:", err)
|
||||
return fmt.Errorf("SetLogFile error: %w", err)
|
||||
}
|
||||
}
|
||||
s.Clients = make(map[string]*TcpConn)
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", s.LocalAddr)
|
||||
if err != nil {
|
||||
starlog.Errorln("ResolveTCPAddr error:", err)
|
||||
return fmt.Errorf("ResolveTCPAddr error: %w", err)
|
||||
}
|
||||
s.Listen, err = net.ListenTCP("tcp", tcpAddr)
|
||||
if err != nil {
|
||||
starlog.Errorln("ListenTCP error:", err)
|
||||
return fmt.Errorf("ListenTCP error: %w", err)
|
||||
}
|
||||
starlog.Infof("TcpServer listen on %s\n", s.LocalAddr)
|
||||
if s.Interactive {
|
||||
go s.handleInteractive()
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
starlog.Infoln("TcpServer stopped due to context done")
|
||||
return s.Listen.Close()
|
||||
default:
|
||||
}
|
||||
conn, err := s.Listen.AcceptTCP()
|
||||
if err != nil {
|
||||
starlog.Errorln("AcceptTCP error:", err)
|
||||
continue
|
||||
}
|
||||
starlog.Infof("Accept new connection from %s", conn.RemoteAddr().String())
|
||||
s.Lock()
|
||||
s.Clients[conn.RemoteAddr().String()] = s.getTcpConn(conn)
|
||||
s.Unlock()
|
||||
go s.handleConn(s.Clients[conn.RemoteAddr().String()])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpServer) getTcpConn(conn *net.TCPConn) *TcpConn {
|
||||
var err error
|
||||
var f *os.File
|
||||
if s.SaveToFolder != "" {
|
||||
f, err = os.Create(filepath.Join(s.SaveToFolder, strings.ReplaceAll(conn.RemoteAddr().String(), ":", "_")))
|
||||
if err != nil {
|
||||
starlog.Errorf("Create file error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
}
|
||||
}
|
||||
return &TcpConn{
|
||||
TCPConn: conn,
|
||||
f: f,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpServer) handleConn(conn *TcpConn) {
|
||||
var err error
|
||||
log := starlog.Std.NewFlag()
|
||||
err = SetTcpInfo(conn.TCPConn, s.UsingKeepAlive, s.KeepAliveIdel, s.KeepAlivePeriod, s.KeepAliveCount, s.UserTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("SetTcpInfo error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
s.Lock()
|
||||
delete(s.Clients, conn.RemoteAddr().String())
|
||||
s.Unlock()
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
log.Infof("SetKeepAlive success for %s\n", conn.RemoteAddr().String())
|
||||
log.Infof("KeepAlivePeriod: %d, KeepAliveIdel: %d, KeepAliveCount: %d, UserTimeout: %d\n", s.KeepAlivePeriod, s.KeepAliveIdel, s.KeepAliveCount, s.UserTimeout)
|
||||
if runtime.GOOS != "linux" {
|
||||
log.Warningln("keepAliveCount and userTimeout only work on linux")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stopCtx.Done():
|
||||
log.Infof("Connection from %s closed due to context done\n", conn.RemoteAddr().String())
|
||||
s.Lock()
|
||||
delete(s.Clients, conn.RemoteAddr().String())
|
||||
s.Unlock()
|
||||
conn.Close()
|
||||
return
|
||||
default:
|
||||
}
|
||||
buf := make([]byte, 8192)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
log.Errorf("Read error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
s.Lock()
|
||||
delete(s.Clients, conn.RemoteAddr().String())
|
||||
s.Unlock()
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
if s.ShowRecv {
|
||||
if s.ShowAsHex {
|
||||
log.Printf("Recv from %s: %x\n", conn.RemoteAddr().String(), buf[:n])
|
||||
} else {
|
||||
log.Printf("Recv from %s: %s\n", conn.RemoteAddr().String(), string(buf[:n]))
|
||||
}
|
||||
}
|
||||
if conn.f != nil {
|
||||
conn.f.Write([]byte(time.Now().String() + " recv\n"))
|
||||
conn.f.Write(buf[:n])
|
||||
conn.f.Write([]byte("\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TcpServer) Stop() {
|
||||
s.stopFn()
|
||||
if s.Listen != nil {
|
||||
s.Close()
|
||||
}
|
||||
}
|
@ -0,0 +1,357 @@
|
||||
package netforward
|
||||
|
||||
import (
|
||||
"b612.me/stario"
|
||||
"b612.me/starlog"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NetForward struct {
|
||||
LocalAddr string
|
||||
LocalPort int
|
||||
RemoteURI string
|
||||
EnableTCP bool
|
||||
EnableUDP bool
|
||||
DelayMilSec int
|
||||
DelayToward int
|
||||
StdinMode bool
|
||||
IgnoreEof bool
|
||||
DialTimeout time.Duration
|
||||
UDPTimeout time.Duration
|
||||
stopCtx context.Context
|
||||
stopFn context.CancelFunc
|
||||
running int32
|
||||
|
||||
KeepAlivePeriod int
|
||||
KeepAliveIdel int
|
||||
KeepAliveCount int
|
||||
UserTimeout int
|
||||
UsingKeepAlive bool
|
||||
}
|
||||
|
||||
func (n *NetForward) Close() {
|
||||
n.stopFn()
|
||||
}
|
||||
|
||||
func (n *NetForward) Status() int32 {
|
||||
return atomic.LoadInt32(&n.running)
|
||||
}
|
||||
|
||||
func (n *NetForward) Run() error {
|
||||
if n.running > 0 {
|
||||
starlog.Errorln("already running")
|
||||
return errors.New("already running")
|
||||
}
|
||||
n.stopCtx, n.stopFn = context.WithCancel(context.Background())
|
||||
if n.DialTimeout == 0 {
|
||||
n.DialTimeout = time.Second * 5
|
||||
}
|
||||
if n.StdinMode {
|
||||
go func() {
|
||||
for {
|
||||
cmd := strings.TrimSpace(stario.MessageBox("", "").MustString())
|
||||
for strings.Contains(cmd, " ") {
|
||||
cmd = strings.Replace(cmd, " ", " ", -1)
|
||||
}
|
||||
starlog.Debugf("Recv Command %s\n", cmd)
|
||||
cmds := strings.Split(cmd, " ")
|
||||
if len(cmds) < 3 {
|
||||
starlog.Errorln("Invalid Command", cmd)
|
||||
continue
|
||||
}
|
||||
switch cmds[0] + cmds[1] {
|
||||
case "setremote":
|
||||
n.RemoteURI = cmds[2]
|
||||
starlog.Noticef("Remote URI Set to %s\n", n.RemoteURI)
|
||||
case "setdelaytoward":
|
||||
tmp, err := strconv.Atoi(cmds[2])
|
||||
if err != nil {
|
||||
starlog.Errorln("Invalid Delay Toward Value", cmds[2])
|
||||
continue
|
||||
}
|
||||
n.DelayToward = tmp
|
||||
starlog.Noticef("Delay Toward Set to %d\n", n.DelayToward)
|
||||
case "setdelay":
|
||||
tmp, err := strconv.Atoi(cmds[2])
|
||||
if err != nil {
|
||||
starlog.Errorln("Invalid Delay Value", cmds[2])
|
||||
continue
|
||||
}
|
||||
n.DelayMilSec = tmp
|
||||
starlog.Noticef("Delay Set to %d\n", n.DelayMilSec)
|
||||
case "setdialtimeout":
|
||||
tmp, err := strconv.Atoi(cmds[2])
|
||||
if err != nil {
|
||||
starlog.Errorln("Invalid Dial Timeout Value", cmds[2])
|
||||
continue
|
||||
}
|
||||
n.DialTimeout = time.Millisecond * time.Duration(tmp)
|
||||
starlog.Noticef("Dial Timeout Set to %d\n", n.DialTimeout)
|
||||
case "setudptimeout":
|
||||
tmp, err := strconv.Atoi(cmds[2])
|
||||
if err != nil {
|
||||
starlog.Errorln("Invalid UDP Timeout Value", cmds[2])
|
||||
continue
|
||||
}
|
||||
n.UDPTimeout = time.Millisecond * time.Duration(tmp)
|
||||
starlog.Noticef("UDP Timeout Set to %d\n", n.UDPTimeout)
|
||||
case "setstdin":
|
||||
if cmds[2] == "off" {
|
||||
n.StdinMode = false
|
||||
starlog.Noticef("Stdin Mode Off\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
if n.EnableTCP {
|
||||
go n.runTCP()
|
||||
}
|
||||
|
||||
if n.EnableUDP {
|
||||
go n.runUDP()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NetForward) runTCP() error {
|
||||
atomic.AddInt32(&n.running, 1)
|
||||
defer atomic.AddInt32(&n.running, -1)
|
||||
listen, err := net.Listen("tcp", fmt.Sprintf("%s:%d", n.LocalAddr, n.LocalPort))
|
||||
if err != nil {
|
||||
starlog.Errorln("Listening On Tcp Failed:", err)
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
<-n.stopCtx.Done()
|
||||
listen.Close()
|
||||
}()
|
||||
starlog.Infof("Listening TCP on %v\n", fmt.Sprintf("%s:%d", n.LocalAddr, n.LocalPort))
|
||||
for {
|
||||
select {
|
||||
case <-n.stopCtx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
conn, err := listen.Accept()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
log := starlog.Std.NewFlag()
|
||||
log.Infof("Accept New TCP Conn from %v\n", conn.RemoteAddr().String())
|
||||
if n.DelayMilSec > 0 && (n.DelayToward == 0 || n.DelayToward == 1) {
|
||||
log.Infof("Delay %d ms\n", n.DelayMilSec)
|
||||
time.Sleep(time.Millisecond * time.Duration(n.DelayMilSec))
|
||||
}
|
||||
err = SetTcpInfo(conn.(*net.TCPConn), n.UsingKeepAlive, n.KeepAliveIdel, n.KeepAlivePeriod, n.KeepAliveCount, n.UserTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("SetTcpInfo error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
go func(conn net.Conn) {
|
||||
rmt, err := net.DialTimeout("tcp", n.RemoteURI, n.DialTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("TCP:Dial Remote %s Failed:%v\n", n.RemoteURI, err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
err = SetTcpInfo(rmt.(*net.TCPConn), n.UsingKeepAlive, n.KeepAliveIdel, n.KeepAlivePeriod, n.KeepAliveCount, n.UserTimeout)
|
||||
if err != nil {
|
||||
log.Errorf("SetTcpInfo error for %s: %v\n", conn.RemoteAddr().String(), err)
|
||||
rmt.Close()
|
||||
return
|
||||
}
|
||||
log.Infof("TCP Connect %s <==> %s\n", conn.RemoteAddr().String(), rmt.RemoteAddr().String())
|
||||
n.copy(rmt, conn)
|
||||
log.Noticef("TCP Connection Closed %s <==> %s\n", conn.RemoteAddr().String(), n.RemoteURI)
|
||||
conn.Close()
|
||||
rmt.Close()
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
|
||||
type UDPConn struct {
|
||||
net.Conn
|
||||
listen *net.UDPConn
|
||||
remoteAddr *net.UDPAddr
|
||||
lastbeat int64
|
||||
}
|
||||
|
||||
func (u UDPConn) Write(p []byte) (n int, err error) {
|
||||
u.lastbeat = time.Now().Unix()
|
||||
return u.Conn.Write(p)
|
||||
}
|
||||
|
||||
func (u UDPConn) Read(p []byte) (n int, err error) {
|
||||
u.lastbeat = time.Now().Unix()
|
||||
return u.Conn.Read(p)
|
||||
}
|
||||
|
||||
func (u UDPConn) Work(delay int) {
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
if delay > 0 {
|
||||
time.Sleep(time.Millisecond * time.Duration(delay))
|
||||
}
|
||||
count, err := u.Read(buf)
|
||||
if err != nil {
|
||||
u.Close()
|
||||
u.lastbeat = 0
|
||||
return
|
||||
}
|
||||
_, err = u.listen.Write(buf[0:count])
|
||||
if err != nil {
|
||||
u.lastbeat = 0
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetForward) runUDP() error {
|
||||
var mu sync.RWMutex
|
||||
atomic.AddInt32(&n.running, 1)
|
||||
defer atomic.AddInt32(&n.running, -1)
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%v", n.LocalAddr, n.LocalPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listen, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
starlog.Infof("Listening UDP on %v\n", fmt.Sprintf("%s:%d", n.LocalAddr, n.LocalPort))
|
||||
go func() {
|
||||
<-n.stopCtx.Done()
|
||||
listen.Close()
|
||||
}()
|
||||
udpMap := make(map[string]UDPConn)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-n.stopCtx.Done():
|
||||
return
|
||||
case <-time.After(time.Second * 60):
|
||||
mu.Lock()
|
||||
for k, v := range udpMap {
|
||||
if time.Now().Unix() > int64(n.UDPTimeout.Seconds())+v.lastbeat {
|
||||
delete(udpMap, k)
|
||||
starlog.Noticef("UDP Connection Closed %s <==> %s\n", v.remoteAddr.String(), n.RemoteURI)
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
select {
|
||||
case <-n.stopCtx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
count, rmt, err := listen.ReadFromUDP(buf)
|
||||
if err != nil || rmt.String() == n.RemoteURI {
|
||||
continue
|
||||
}
|
||||
go func(data []byte, rmt *net.UDPAddr) {
|
||||
log := starlog.Std.NewFlag()
|
||||
mu.Lock()
|
||||
addr, ok := udpMap[rmt.String()]
|
||||
if !ok {
|
||||
log.Infof("Accept New UDP Conn from %v\n", rmt.String())
|
||||
conn, err := net.Dial("udp", n.RemoteURI)
|
||||
if err != nil {
|
||||
log.Errorf("UDP:Dial Remote %s Failed:%v\n", n.RemoteURI, err)
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
addr = UDPConn{
|
||||
Conn: conn,
|
||||
remoteAddr: rmt,
|
||||
listen: listen,
|
||||
lastbeat: time.Now().Unix(),
|
||||
}
|
||||
udpMap[rmt.String()] = addr
|
||||
go addr.Work(n.DelayMilSec)
|
||||
log.Infof("UDP Connect %s <==> %s\n", rmt.String(), n.RemoteURI)
|
||||
}
|
||||
mu.Unlock()
|
||||
if n.DelayMilSec > 0 || (n.DelayToward == 0 || n.DelayToward == 1) {
|
||||
time.Sleep(time.Millisecond * time.Duration(n.DelayMilSec))
|
||||
}
|
||||
_, err := addr.Write(data)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
addr.Close()
|
||||
delete(udpMap, addr.remoteAddr.String())
|
||||
mu.Unlock()
|
||||
log.Noticef("UDP Connection Closed %s <==> %s\n", rmt.String(), n.RemoteURI)
|
||||
}
|
||||
}(buf[0:count], rmt)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *NetForward) copy(dst, src net.Conn) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bufsize := make([]byte, 32*1024)
|
||||
for {
|
||||
count, err := src.Read(bufsize)
|
||||
if err != nil {
|
||||
if n.IgnoreEof && err == io.EOF {
|
||||
continue
|
||||
}
|
||||
dst.Close()
|
||||
src.Close()
|
||||
return
|
||||
}
|
||||
_, err = dst.Write(bufsize[:count])
|
||||
if err != nil {
|
||||
src.Close()
|
||||
dst.Close()
|
||||
return
|
||||
}
|
||||
if n.DelayMilSec > 0 && (n.DelayToward == 0 || n.DelayToward == 1) {
|
||||
time.Sleep(time.Millisecond * time.Duration(n.DelayMilSec))
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bufsize := make([]byte, 32*1024)
|
||||
for {
|
||||
count, err := dst.Read(bufsize)
|
||||
if err != nil {
|
||||
if n.IgnoreEof && err == io.EOF {
|
||||
continue
|
||||
}
|
||||
src.Close()
|
||||
dst.Close()
|
||||
return
|
||||
}
|
||||
_, err = src.Write(bufsize[:count])
|
||||
if err != nil {
|
||||
src.Close()
|
||||
dst.Close()
|
||||
return
|
||||
}
|
||||
if n.DelayMilSec > 0 && (n.DelayToward == 0 || n.DelayToward == 2) {
|
||||
time.Sleep(time.Millisecond * time.Duration(n.DelayMilSec))
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package netforward
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestForward(t *testing.T) {
|
||||
var f = NetForward{
|
||||
LocalAddr: "127.0.0.1",
|
||||
LocalPort: 22232,
|
||||
RemoteURI: "192.168.2.1:80",
|
||||
EnableTCP: true,
|
||||
EnableUDP: true,
|
||||
DialTimeout: 6 * time.Second,
|
||||
UDPTimeout: 7 * time.Second,
|
||||
}
|
||||
f.Run()
|
||||
go func() {
|
||||
time.Sleep(time.Second * 10)
|
||||
fmt.Println("closing")
|
||||
f.Close()
|
||||
}()
|
||||
for {
|
||||
time.Sleep(time.Second * 2)
|
||||
if f.Status() > 0 {
|
||||
fmt.Println(f.Status())
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
//go:build darwin
|
||||
|
||||
package netforward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetTcpInfo(conn *net.TCPConn, usingKeepAlive bool, keepAliveIdel, keepAlivePeriod, keepAliveCount, userTimeout int) error {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usingKeepAlive {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPALIVE, keepAliveIdel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 0x101, keepAlivePeriod)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
} else {
|
||||
err = conn.SetKeepAlive(false)
|
||||
}
|
||||
if userTimeout > 0 {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 0x12, userTimeout)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
//go:build !(windows && darwin)
|
||||
|
||||
package netforward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SetTcpInfo(conn *net.TCPConn, usingKeepAlive bool, keepAliveIdel, keepAlivePeriod, keepAliveCount, userTimeout int) error {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if usingKeepAlive {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, keepAliveIdel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, keepAlivePeriod)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, keepAliveCount)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
})
|
||||
} else {
|
||||
err = conn.SetKeepAlive(false)
|
||||
}
|
||||
if userTimeout > 0 {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_TCP, 0x12, userTimeout)
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
//go:build windows
|
||||
|
||||
package netforward
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func SetTcpInfo(conn *net.TCPConn, usingKeepAlive bool, keepAliveIdel, keepAlivePeriod, keepAliveCount, userTimeout int) error {
|
||||
if usingKeepAlive {
|
||||
rawConn, err := conn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
ka := syscall.TCPKeepalive{
|
||||
OnOff: 1,
|
||||
Time: uint32(keepAliveIdel),
|
||||
Interval: uint32(keepAlivePeriod),
|
||||
}
|
||||
ret := uint32(0)
|
||||
size := uint32(unsafe.Sizeof(ka))
|
||||
err = syscall.WSAIoctl(syscall.Handle(fd), syscall.SIO_KEEPALIVE_VALS, (*byte)(unsafe.Pointer(&ka)), size, nil, 0, &ret, nil, 0)
|
||||
runtime.KeepAlive(fd)
|
||||
})
|
||||
return os.NewSyscallError("wsaioctl", err)
|
||||
}
|
||||
return conn.SetKeepAlive(false)
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmoji(t *testing.T) {
|
||||
fmt.Println(replaceUnicodeEmoji("\\U1F441\\UFE0F"))
|
||||
fmt.Println("👁️")
|
||||
p, _ := replaceUnicodeEmoji("\\U1F441\\UFE0F")
|
||||
fmt.Println("👁️" == p)
|
||||
for _, v := range p {
|
||||
fmt.Printf("%U", v)
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Jordan Wright
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -0,0 +1,809 @@
|
||||
// Package email is designed to provide an "email interface for humans."
|
||||
// Designed to be robust and flexible, the email package aims to make sending email easy without getting in the way.
|
||||
package email
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxLineLength = 76 // MaxLineLength is the maximum line length per RFC 2045
|
||||
defaultContentType = "text/plain; charset=us-ascii" // defaultContentType is the default Content-Type according to RFC 2045, section 5.2
|
||||
)
|
||||
|
||||
// ErrMissingBoundary is returned when there is no boundary given for a multipart entity
|
||||
var ErrMissingBoundary = errors.New("No boundary found for multipart entity")
|
||||
|
||||
// ErrMissingContentType is returned when there is no "Content-Type" header for a MIME entity
|
||||
var ErrMissingContentType = errors.New("No Content-Type found for MIME entity")
|
||||
|
||||
// Email is the type used for email messages
|
||||
type Email struct {
|
||||
ReplyTo []string
|
||||
From string
|
||||
To []string
|
||||
Bcc []string
|
||||
Cc []string
|
||||
Subject string
|
||||
Text []byte // Plaintext message (optional)
|
||||
HTML []byte // Html message (optional)
|
||||
Sender string // override From as SMTP envelope sender (optional)
|
||||
Headers textproto.MIMEHeader
|
||||
Attachments []*Attachment
|
||||
ReadReceipt []string
|
||||
}
|
||||
|
||||
// part is a copyable representation of a multipart.Part
|
||||
type part struct {
|
||||
header textproto.MIMEHeader
|
||||
body []byte
|
||||
}
|
||||
|
||||
// NewEmail creates an Email, and returns the pointer to it.
|
||||
func NewEmail() *Email {
|
||||
return &Email{Headers: textproto.MIMEHeader{}}
|
||||
}
|
||||
|
||||
// trimReader is a custom io.Reader that will trim any leading
|
||||
// whitespace, as this can cause email imports to fail.
|
||||
type trimReader struct {
|
||||
rd io.Reader
|
||||
trimmed bool
|
||||
}
|
||||
|
||||
// Read trims off any unicode whitespace from the originating reader
|
||||
func (tr *trimReader) Read(buf []byte) (int, error) {
|
||||
n, err := tr.rd.Read(buf)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
if !tr.trimmed {
|
||||
t := bytes.TrimLeftFunc(buf[:n], unicode.IsSpace)
|
||||
tr.trimmed = true
|
||||
n = copy(buf, t)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func handleAddressList(v []string) []string {
|
||||
res := []string{}
|
||||
for _, a := range v {
|
||||
w := strings.Split(a, ",")
|
||||
for _, addr := range w {
|
||||
decodedAddr, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(addr))
|
||||
if err == nil {
|
||||
res = append(res, decodedAddr)
|
||||
} else {
|
||||
res = append(res, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// NewEmailFromReader reads a stream of bytes from an io.Reader, r,
|
||||
// and returns an email struct containing the parsed data.
|
||||
// This function expects the data in RFC 5322 format.
|
||||
func NewEmailFromReader(r io.Reader) (*Email, error) {
|
||||
e := NewEmail()
|
||||
s := &trimReader{rd: r}
|
||||
tp := textproto.NewReader(bufio.NewReader(s))
|
||||
// Parse the main headers
|
||||
hdrs, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
// Set the subject, to, cc, bcc, and from
|
||||
for h, v := range hdrs {
|
||||
switch h {
|
||||
case "Subject":
|
||||
e.Subject = v[0]
|
||||
subj, err := (&mime.WordDecoder{}).DecodeHeader(e.Subject)
|
||||
if err == nil && len(subj) > 0 {
|
||||
e.Subject = subj
|
||||
}
|
||||
delete(hdrs, h)
|
||||
case "To":
|
||||
e.To = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "Cc":
|
||||
e.Cc = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "Bcc":
|
||||
e.Bcc = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "Reply-To":
|
||||
e.ReplyTo = handleAddressList(v)
|
||||
delete(hdrs, h)
|
||||
case "From":
|
||||
e.From = v[0]
|
||||
fr, err := (&mime.WordDecoder{}).DecodeHeader(e.From)
|
||||
if err == nil && len(fr) > 0 {
|
||||
e.From = fr
|
||||
}
|
||||
delete(hdrs, h)
|
||||
}
|
||||
}
|
||||
e.Headers = hdrs
|
||||
body := tp.R
|
||||
// Recursively parse the MIME parts
|
||||
ps, err := parseMIMEParts(e.Headers, body)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
for _, p := range ps {
|
||||
if ct := p.header.Get("Content-Type"); ct == "" {
|
||||
return e, ErrMissingContentType
|
||||
}
|
||||
ct, _, err := mime.ParseMediaType(p.header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
// Check if part is an attachment based on the existence of the Content-Disposition header with a value of "attachment".
|
||||
if cd := p.header.Get("Content-Disposition"); cd != "" {
|
||||
cd, params, err := mime.ParseMediaType(p.header.Get("Content-Disposition"))
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
filename, filenameDefined := params["filename"]
|
||||
if cd == "attachment" || (cd == "inline" && filenameDefined) {
|
||||
_, err = e.Attach(bytes.NewReader(p.body), filename, ct)
|
||||
if err != nil {
|
||||
return e, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case ct == "text/plain":
|
||||
e.Text = p.body
|
||||
case ct == "text/html":
|
||||
e.HTML = p.body
|
||||
}
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// parseMIMEParts will recursively walk a MIME entity and return a []mime.Part containing
|
||||
// each (flattened) mime.Part found.
|
||||
// It is important to note that there are no limits to the number of recursions, so be
|
||||
// careful when parsing unknown MIME structures!
|
||||
func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) {
|
||||
var ps []*part
|
||||
// If no content type is given, set it to the default
|
||||
if _, ok := hs["Content-Type"]; !ok {
|
||||
hs.Set("Content-Type", defaultContentType)
|
||||
}
|
||||
ct, params, err := mime.ParseMediaType(hs.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
// If it's a multipart email, recursively parse the parts
|
||||
if strings.HasPrefix(ct, "multipart/") {
|
||||
if _, ok := params["boundary"]; !ok {
|
||||
return ps, ErrMissingBoundary
|
||||
}
|
||||
mr := multipart.NewReader(b, params["boundary"])
|
||||
for {
|
||||
var buf bytes.Buffer
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
if _, ok := p.Header["Content-Type"]; !ok {
|
||||
p.Header.Set("Content-Type", defaultContentType)
|
||||
}
|
||||
subct, _, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
if strings.HasPrefix(subct, "multipart/") {
|
||||
sps, err := parseMIMEParts(p.Header, p)
|
||||
if err != nil {
|
||||
return ps, err
|
||||
}
|
||||
ps = append(ps, sps...)
|
||||
} else {
|
||||
var reader io.Reader
|
||||
reader = p
|
||||
const cte = "Content-Transfer-Encoding"
|
||||
if p.Header.Get(cte) == "base64" {
|
||||
reader = base64.NewDecoder(base64.StdEncoding, reader)
|
||||
}
|
||||
// Otherwise, just append the part to the list
|
||||
// Copy the part data into the buffer
|
||||
if _, err := io.Copy(&buf, reader); err != nil {
|
||||
return ps, err
|
||||
}
|
||||
ps = append(ps, &part{body: buf.Bytes(), header: p.Header})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it is not a multipart email, parse the body content as a single "part"
|
||||
switch hs.Get("Content-Transfer-Encoding") {
|
||||
case "quoted-printable":
|
||||
b = quotedprintable.NewReader(b)
|
||||
case "base64":
|
||||
b = base64.NewDecoder(base64.StdEncoding, b)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if _, err := io.Copy(&buf, b); err != nil {
|
||||
return ps, err
|
||||
}
|
||||
ps = append(ps, &part{body: buf.Bytes(), header: hs})
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
// Attach is used to attach content from an io.Reader to the email.
|
||||
// Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
|
||||
// The function will return the created Attachment for reference, as well as nil for the error, if successful.
|
||||
func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
|
||||
var buffer bytes.Buffer
|
||||
if _, err = io.Copy(&buffer, r); err != nil {
|
||||
return
|
||||
}
|
||||
at := &Attachment{
|
||||
Filename: filename,
|
||||
ContentType: c,
|
||||
Header: textproto.MIMEHeader{},
|
||||
Content: buffer.Bytes(),
|
||||
}
|
||||
e.Attachments = append(e.Attachments, at)
|
||||
return at, nil
|
||||
}
|
||||
|
||||
// AttachFile is used to attach content to the email.
|
||||
// It attempts to open the file referenced by filename and, if successful, creates an Attachment.
|
||||
// This Attachment is then appended to the slice of Email.Attachments.
|
||||
// The function will then return the Attachment for reference, as well as nil for the error, if successful.
|
||||
func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
ct := mime.TypeByExtension(filepath.Ext(filename))
|
||||
basename := filepath.Base(filename)
|
||||
return e.Attach(f, basename, ct)
|
||||
}
|
||||
|
||||
// msgHeaders merges the Email's various fields and custom headers together in a
|
||||
// standards compliant way to create a MIMEHeader to be used in the resulting
|
||||
// message. It does not alter e.Headers.
|
||||
//
|
||||
// "e"'s fields To, Cc, From, Subject will be used unless they are present in
|
||||
// e.Headers. Unless set in e.Headers, "Date" will filled with the current time.
|
||||
func (e *Email) msgHeaders() (textproto.MIMEHeader, error) {
|
||||
res := make(textproto.MIMEHeader, len(e.Headers)+6)
|
||||
if e.Headers != nil {
|
||||
for _, h := range []string{"Reply-To", "To", "Cc", "From", "Subject", "Date", "Message-Id", "MIME-Version"} {
|
||||
if v, ok := e.Headers[h]; ok {
|
||||
res[h] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set headers if there are values.
|
||||
if _, ok := res["Reply-To"]; !ok && len(e.ReplyTo) > 0 {
|
||||
res.Set("Reply-To", strings.Join(e.ReplyTo, ", "))
|
||||
}
|
||||
if _, ok := res["To"]; !ok && len(e.To) > 0 {
|
||||
res.Set("To", strings.Join(e.To, ", "))
|
||||
}
|
||||
if _, ok := res["Cc"]; !ok && len(e.Cc) > 0 {
|
||||
res.Set("Cc", strings.Join(e.Cc, ", "))
|
||||
}
|
||||
if _, ok := res["Subject"]; !ok && e.Subject != "" {
|
||||
res.Set("Subject", e.Subject)
|
||||
}
|
||||
if _, ok := res["Message-Id"]; !ok {
|
||||
id, err := generateMessageID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.Set("Message-Id", id)
|
||||
}
|
||||
// Date and From are required headers.
|
||||
if _, ok := res["From"]; !ok {
|
||||
res.Set("From", e.From)
|
||||
}
|
||||
if _, ok := res["Date"]; !ok {
|
||||
res.Set("Date", time.Now().Format(time.RFC1123Z))
|
||||
}
|
||||
if _, ok := res["MIME-Version"]; !ok {
|
||||
res.Set("MIME-Version", "1.0")
|
||||
}
|
||||
for field, vals := range e.Headers {
|
||||
if _, ok := res[field]; !ok {
|
||||
res[field] = vals
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func writeMessage(buff io.Writer, msg []byte, multipart bool, mediaType string, w *multipart.Writer) error {
|
||||
if multipart {
|
||||
header := textproto.MIMEHeader{
|
||||
"Content-Type": {mediaType + "; charset=UTF-8"},
|
||||
"Content-Transfer-Encoding": {"quoted-printable"},
|
||||
}
|
||||
if _, err := w.CreatePart(header); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
qp := quotedprintable.NewWriter(buff)
|
||||
// Write the text
|
||||
if _, err := qp.Write(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return qp.Close()
|
||||
}
|
||||
|
||||
func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) {
|
||||
for _, a := range e.Attachments {
|
||||
if a.HTMLRelated {
|
||||
htmlRelated = append(htmlRelated, a)
|
||||
} else {
|
||||
others = append(others, a)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
|
||||
func (e *Email) Bytes() ([]byte, error) {
|
||||
// TODO: better guess buffer size
|
||||
buff := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
|
||||
headers, err := e.msgHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
htmlAttachments, otherAttachments := e.categorizeAttachments()
|
||||
if len(e.HTML) == 0 && len(htmlAttachments) > 0 {
|
||||
return nil, errors.New("there are HTML attachments, but no HTML body")
|
||||
}
|
||||
|
||||
var (
|
||||
isMixed = len(otherAttachments) > 0
|
||||
isAlternative = len(e.Text) > 0 && len(e.HTML) > 0
|
||||
isRelated = len(e.HTML) > 0 && len(htmlAttachments) > 0
|
||||
)
|
||||
|
||||
var w *multipart.Writer
|
||||
if isMixed || isAlternative || isRelated {
|
||||
w = multipart.NewWriter(buff)
|
||||
}
|
||||
switch {
|
||||
case isMixed:
|
||||
headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
|
||||
case isAlternative:
|
||||
headers.Set("Content-Type", "multipart/alternative;\r\n boundary="+w.Boundary())
|
||||
case isRelated:
|
||||
headers.Set("Content-Type", "multipart/related;\r\n boundary="+w.Boundary())
|
||||
case len(e.HTML) > 0:
|
||||
headers.Set("Content-Type", "text/html; charset=UTF-8")
|
||||
headers.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
default:
|
||||
headers.Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
headers.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
}
|
||||
headerToBytes(buff, headers)
|
||||
_, err = io.WriteString(buff, "\r\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check to see if there is a Text or HTML field
|
||||
if len(e.Text) > 0 || len(e.HTML) > 0 {
|
||||
var subWriter *multipart.Writer
|
||||
|
||||
if isMixed && isAlternative {
|
||||
// Create the multipart alternative part
|
||||
subWriter = multipart.NewWriter(buff)
|
||||
header := textproto.MIMEHeader{
|
||||
"Content-Type": {"multipart/alternative;\r\n boundary=" + subWriter.Boundary()},
|
||||
}
|
||||
if _, err := w.CreatePart(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
subWriter = w
|
||||
}
|
||||
// Create the body sections
|
||||
if len(e.Text) > 0 {
|
||||
// Write the text
|
||||
if err := writeMessage(buff, e.Text, isMixed || isAlternative, "text/plain", subWriter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(e.HTML) > 0 {
|
||||
messageWriter := subWriter
|
||||
var relatedWriter *multipart.Writer
|
||||
if (isMixed || isAlternative) && len(htmlAttachments) > 0 {
|
||||
relatedWriter = multipart.NewWriter(buff)
|
||||
header := textproto.MIMEHeader{
|
||||
"Content-Type": {"multipart/related;\r\n boundary=" + relatedWriter.Boundary()},
|
||||
}
|
||||
if _, err := subWriter.CreatePart(header); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messageWriter = relatedWriter
|
||||
} else if isRelated && len(htmlAttachments) > 0 {
|
||||
relatedWriter = w
|
||||
messageWriter = w
|
||||
}
|
||||
// Write the HTML
|
||||
if err := writeMessage(buff, e.HTML, isMixed || isAlternative || isRelated, "text/html", messageWriter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(htmlAttachments) > 0 {
|
||||
for _, a := range htmlAttachments {
|
||||
a.setDefaultHeaders()
|
||||
ap, err := relatedWriter.CreatePart(a.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the base64Wrapped content to the part
|
||||
base64Wrap(ap, a.Content)
|
||||
}
|
||||
|
||||
if isMixed || isAlternative {
|
||||
relatedWriter.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
if isMixed && isAlternative {
|
||||
if err := subWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create attachment part, if necessary
|
||||
for _, a := range otherAttachments {
|
||||
a.setDefaultHeaders()
|
||||
ap, err := w.CreatePart(a.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Write the base64Wrapped content to the part
|
||||
base64Wrap(ap, a.Content)
|
||||
}
|
||||
if isMixed || isAlternative || isRelated {
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
// Send an email using the given host and SMTP auth (optional), returns any error thrown by smtp.SendMail
|
||||
// This function merges the To, Cc, and Bcc fields and calls the smtp.SendMail function using the Email.Bytes() output as the message
|
||||
func (e *Email) Send(addr string, a smtp.Auth) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
sender, err := e.parseSender()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return smtp.SendMail(addr, a, sender, to, raw)
|
||||
}
|
||||
|
||||
// Select and parse an SMTP envelope sender address. Choose Email.Sender if set, or fallback to Email.From.
|
||||
func (e *Email) parseSender() (string, error) {
|
||||
if e.Sender != "" {
|
||||
sender, err := mail.ParseAddress(e.Sender)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sender.Address, nil
|
||||
} else {
|
||||
from, err := mail.ParseAddress(e.From)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return from.Address, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SendWithTLS sends an email over tls with an optional TLS config.
|
||||
//
|
||||
// The TLS Config is helpful if you need to connect to a host that is used an untrusted
|
||||
// certificate.
|
||||
func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
sender, err := e.parseSender()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", addr, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := smtp.NewClient(conn, t.ServerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.Hello("localhost"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = c.Mail(sender); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// SendWithStartTLS sends an email over TLS using STARTTLS with an optional TLS config.
|
||||
//
|
||||
// The TLS Config is helpful if you need to connect to a host that is used an untrusted
|
||||
// certificate.
|
||||
func (e *Email) SendWithStartTLS(addr string, a smtp.Auth, t *tls.Config) error {
|
||||
// Merge the To, Cc, and Bcc fields
|
||||
to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc))
|
||||
to = append(append(append(to, e.To...), e.Cc...), e.Bcc...)
|
||||
for i := 0; i < len(to); i++ {
|
||||
addr, err := mail.ParseAddress(to[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
to[i] = addr.Address
|
||||
}
|
||||
// Check to make sure there is at least one recipient and one "From" address
|
||||
if e.From == "" || len(to) == 0 {
|
||||
return errors.New("Must specify at least one From address and one To address")
|
||||
}
|
||||
sender, err := e.parseSender()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Taken from the standard library
|
||||
// https://github.com/golang/go/blob/master/src/net/smtp/smtp.go#L328
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
if err = c.Hello("localhost"); err != nil {
|
||||
return err
|
||||
}
|
||||
// Use TLS if available
|
||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
||||
if err = c.StartTLS(t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
if ok, _ := c.Extension("AUTH"); ok {
|
||||
if err = c.Auth(a); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err = c.Mail(sender); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Quit()
|
||||
}
|
||||
|
||||
// Attachment is a struct representing an email attachment.
|
||||
// Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
|
||||
type Attachment struct {
|
||||
Filename string
|
||||
ContentType string
|
||||
Header textproto.MIMEHeader
|
||||
Content []byte
|
||||
HTMLRelated bool
|
||||
}
|
||||
|
||||
func (at *Attachment) setDefaultHeaders() {
|
||||
contentType := "application/octet-stream"
|
||||
if len(at.ContentType) > 0 {
|
||||
contentType = at.ContentType
|
||||
}
|
||||
at.Header.Set("Content-Type", contentType)
|
||||
|
||||
if len(at.Header.Get("Content-Disposition")) == 0 {
|
||||
disposition := "attachment"
|
||||
if at.HTMLRelated {
|
||||
disposition = "inline"
|
||||
}
|
||||
at.Header.Set("Content-Disposition", fmt.Sprintf("%s;\r\n filename=\"%s\"", disposition, at.Filename))
|
||||
}
|
||||
if len(at.Header.Get("Content-ID")) == 0 {
|
||||
at.Header.Set("Content-ID", fmt.Sprintf("<%s>", at.Filename))
|
||||
}
|
||||
if len(at.Header.Get("Content-Transfer-Encoding")) == 0 {
|
||||
at.Header.Set("Content-Transfer-Encoding", "base64")
|
||||
}
|
||||
}
|
||||
|
||||
// base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)
|
||||
// The output is then written to the specified io.Writer
|
||||
func base64Wrap(w io.Writer, b []byte) {
|
||||
// 57 raw bytes per 76-byte base64 line.
|
||||
const maxRaw = 57
|
||||
// Buffer for each line, including trailing CRLF.
|
||||
buffer := make([]byte, MaxLineLength+len("\r\n"))
|
||||
copy(buffer[MaxLineLength:], "\r\n")
|
||||
// Process raw chunks until there's no longer enough to fill a line.
|
||||
for len(b) >= maxRaw {
|
||||
base64.StdEncoding.Encode(buffer, b[:maxRaw])
|
||||
w.Write(buffer)
|
||||
b = b[maxRaw:]
|
||||
}
|
||||
// Handle the last chunk of bytes.
|
||||
if len(b) > 0 {
|
||||
out := buffer[:base64.StdEncoding.EncodedLen(len(b))]
|
||||
base64.StdEncoding.Encode(out, b)
|
||||
out = append(out, "\r\n"...)
|
||||
w.Write(out)
|
||||
}
|
||||
}
|
||||
|
||||
// headerToBytes renders "header" to "buff". If there are multiple values for a
|
||||
// field, multiple "Field: value\r\n" lines will be emitted.
|
||||
func headerToBytes(buff io.Writer, header textproto.MIMEHeader) {
|
||||
for field, vals := range header {
|
||||
for _, subval := range vals {
|
||||
// bytes.Buffer.Write() never returns an error.
|
||||
io.WriteString(buff, field)
|
||||
io.WriteString(buff, ": ")
|
||||
// Write the encoded header if needed
|
||||
switch {
|
||||
case field == "Content-Type" || field == "Content-Disposition":
|
||||
buff.Write([]byte(subval))
|
||||
case field == "From" || field == "To" || field == "Cc" || field == "Bcc":
|
||||
participants := strings.Split(subval, ",")
|
||||
for i, v := range participants {
|
||||
addr, err := mail.ParseAddress(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
participants[i] = addr.String()
|
||||
}
|
||||
buff.Write([]byte(strings.Join(participants, ", ")))
|
||||
default:
|
||||
buff.Write([]byte(mime.QEncoding.Encode("UTF-8", subval)))
|
||||
}
|
||||
io.WriteString(buff, "\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var maxBigInt = big.NewInt(math.MaxInt64)
|
||||
|
||||
// generateMessageID generates and returns a string suitable for an RFC 2822
|
||||
// compliant Message-ID, e.g.:
|
||||
// <1444789264909237300.3464.1819418242800517193@DESKTOP01>
|
||||
//
|
||||
// The following parameters are used to generate a Message-ID:
|
||||
// - The nanoseconds since Epoch
|
||||
// - The calling PID
|
||||
// - A cryptographically random int64
|
||||
// - The sending hostname
|
||||
func generateMessageID() (string, error) {
|
||||
t := time.Now().UnixNano()
|
||||
pid := os.Getpid()
|
||||
rint, err := rand.Int(rand.Reader, maxBigInt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
h, err := os.Hostname()
|
||||
// If we can't get the hostname, we'll use localhost
|
||||
if err != nil {
|
||||
h = "localhost.localdomain"
|
||||
}
|
||||
msgid := fmt.Sprintf("<%d.%d.%d@%s>", t, pid, rint, h)
|
||||
return msgid, nil
|
||||
}
|
@ -0,0 +1,933 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
)
|
||||
|
||||
func prepareEmail() *Email {
|
||||
e := NewEmail()
|
||||
e.From = "Jordan Wright <test@example.com>"
|
||||
e.To = []string{"test@example.com"}
|
||||
e.Bcc = []string{"test_bcc@example.com"}
|
||||
e.Cc = []string{"test_cc@example.com"}
|
||||
e.Subject = "Awesome Subject"
|
||||
return e
|
||||
}
|
||||
|
||||
func basicTests(t *testing.T, e *Email) *mail.Message {
|
||||
raw, err := e.Bytes()
|
||||
if err != nil {
|
||||
t.Fatal("Failed to render message: ", e)
|
||||
}
|
||||
|
||||
msg, err := mail.ReadMessage(bytes.NewBuffer(raw))
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse rendered message: ", err)
|
||||
}
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"To": "<test@example.com>",
|
||||
"From": "\"Jordan Wright\" <test@example.com>",
|
||||
"Cc": "<test_cc@example.com>",
|
||||
"Subject": "Awesome Subject",
|
||||
}
|
||||
|
||||
for header, expected := range expectedHeaders {
|
||||
if val := msg.Header.Get(header); val != expected {
|
||||
t.Errorf("Wrong value for message header %s: %v != %v", header, expected, val)
|
||||
}
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func TestEmailText(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
e.Text = []byte("Text Body is, of course, supported!\n")
|
||||
|
||||
msg := basicTests(t, e)
|
||||
|
||||
// Were the right headers set?
|
||||
ct := msg.Header.Get("Content-type")
|
||||
mt, _, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
t.Fatal("Content-type header is invalid: ", ct)
|
||||
} else if mt != "text/plain" {
|
||||
t.Fatalf("Content-type expected \"text/plain\", not %v", mt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailWithHTMLAttachments(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
|
||||
// Set plain text to exercise "mime/alternative"
|
||||
e.Text = []byte("Text Body is, of course, supported!\n")
|
||||
|
||||
e.HTML = []byte("<html><body>This is a text.</body></html>")
|
||||
|
||||
// Set HTML attachment to exercise "mime/related".
|
||||
attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Fatal("Could not add an attachment to the message: ", err)
|
||||
}
|
||||
attachment.HTMLRelated = true
|
||||
|
||||
b, err := e.Bytes()
|
||||
if err != nil {
|
||||
t.Fatal("Could not serialize e-mail:", err)
|
||||
}
|
||||
|
||||
// Print the bytes for ocular validation and make sure no errors.
|
||||
//fmt.Println(string(b))
|
||||
|
||||
// TODO: Verify the attachments.
|
||||
s := &trimReader{rd: bytes.NewBuffer(b)}
|
||||
tp := textproto.NewReader(bufio.NewReader(s))
|
||||
// Parse the main headers
|
||||
hdrs, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse the headers:", err)
|
||||
}
|
||||
// Recursively parse the MIME parts
|
||||
ps, err := parseMIMEParts(hdrs, tp.R)
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse the MIME parts recursively:", err)
|
||||
}
|
||||
|
||||
plainTextFound := false
|
||||
htmlFound := false
|
||||
imageFound := false
|
||||
if expected, actual := 3, len(ps); actual != expected {
|
||||
t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
|
||||
}
|
||||
for _, part := range ps {
|
||||
// part has "header" and "body []byte"
|
||||
cd := part.header.Get("Content-Disposition")
|
||||
ct := part.header.Get("Content-Type")
|
||||
if strings.Contains(ct, "image/png") && strings.HasPrefix(cd, "inline") {
|
||||
imageFound = true
|
||||
}
|
||||
if strings.Contains(ct, "text/html") {
|
||||
htmlFound = true
|
||||
}
|
||||
if strings.Contains(ct, "text/plain") {
|
||||
plainTextFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !plainTextFound {
|
||||
t.Error("Did not find plain text part.")
|
||||
}
|
||||
if !htmlFound {
|
||||
t.Error("Did not find HTML part.")
|
||||
}
|
||||
if !imageFound {
|
||||
t.Error("Did not find image part.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailWithHTMLAttachmentsHTMLOnly(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
|
||||
e.HTML = []byte("<html><body>This is a text.</body></html>")
|
||||
|
||||
// Set HTML attachment to exercise "mime/related".
|
||||
attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Fatal("Could not add an attachment to the message: ", err)
|
||||
}
|
||||
attachment.HTMLRelated = true
|
||||
|
||||
b, err := e.Bytes()
|
||||
if err != nil {
|
||||
t.Fatal("Could not serialize e-mail:", err)
|
||||
}
|
||||
|
||||
// Print the bytes for ocular validation and make sure no errors.
|
||||
//fmt.Println(string(b))
|
||||
|
||||
// TODO: Verify the attachments.
|
||||
s := &trimReader{rd: bytes.NewBuffer(b)}
|
||||
tp := textproto.NewReader(bufio.NewReader(s))
|
||||
// Parse the main headers
|
||||
hdrs, err := tp.ReadMIMEHeader()
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse the headers:", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(hdrs.Get("Content-Type"), "multipart/related") {
|
||||
t.Error("Envelope Content-Type is not multipart/related: ", hdrs["Content-Type"])
|
||||
}
|
||||
|
||||
// Recursively parse the MIME parts
|
||||
ps, err := parseMIMEParts(hdrs, tp.R)
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse the MIME parts recursively:", err)
|
||||
}
|
||||
|
||||
htmlFound := false
|
||||
imageFound := false
|
||||
if expected, actual := 2, len(ps); actual != expected {
|
||||
t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
|
||||
}
|
||||
for _, part := range ps {
|
||||
// part has "header" and "body []byte"
|
||||
ct := part.header.Get("Content-Type")
|
||||
if strings.Contains(ct, "image/png") {
|
||||
imageFound = true
|
||||
}
|
||||
if strings.Contains(ct, "text/html") {
|
||||
htmlFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !htmlFound {
|
||||
t.Error("Did not find HTML part.")
|
||||
}
|
||||
if !imageFound {
|
||||
t.Error("Did not find image part.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailHTML(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
|
||||
|
||||
msg := basicTests(t, e)
|
||||
|
||||
// Were the right headers set?
|
||||
ct := msg.Header.Get("Content-type")
|
||||
mt, _, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
t.Fatalf("Content-type header is invalid: %#v", ct)
|
||||
} else if mt != "text/html" {
|
||||
t.Fatalf("Content-type expected \"text/html\", not %v", mt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailTextAttachment(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
e.Text = []byte("Text Body is, of course, supported!\n")
|
||||
_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Fatal("Could not add an attachment to the message: ", err)
|
||||
}
|
||||
|
||||
msg := basicTests(t, e)
|
||||
|
||||
// Were the right headers set?
|
||||
ct := msg.Header.Get("Content-type")
|
||||
mt, params, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
t.Fatal("Content-type header is invalid: ", ct)
|
||||
} else if mt != "multipart/mixed" {
|
||||
t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
|
||||
}
|
||||
b := params["boundary"]
|
||||
if b == "" {
|
||||
t.Fatalf("Invalid or missing boundary parameter: %#v", b)
|
||||
}
|
||||
if len(params) != 1 {
|
||||
t.Fatal("Unexpected content-type parameters")
|
||||
}
|
||||
|
||||
// Is the generated message parsable?
|
||||
mixed := multipart.NewReader(msg.Body, params["boundary"])
|
||||
|
||||
text, err := mixed.NextPart()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find text component of email: %s", err)
|
||||
}
|
||||
|
||||
// Does the text portion match what we expect?
|
||||
mt, _, err = mime.ParseMediaType(text.Header.Get("Content-type"))
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse message's Content-Type")
|
||||
} else if mt != "text/plain" {
|
||||
t.Fatal("Message missing text/plain")
|
||||
}
|
||||
plainText, err := ioutil.ReadAll(text)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read plain text component of message: ", err)
|
||||
}
|
||||
if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
|
||||
t.Fatalf("Plain text is broken: %#q", plainText)
|
||||
}
|
||||
|
||||
// Check attachments.
|
||||
_, err = mixed.NextPart()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find attachment component of email: %s", err)
|
||||
}
|
||||
|
||||
if _, err = mixed.NextPart(); err != io.EOF {
|
||||
t.Error("Expected only text and one attachment!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailTextHtmlAttachment(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
e.Text = []byte("Text Body is, of course, supported!\n")
|
||||
e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
|
||||
_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Fatal("Could not add an attachment to the message: ", err)
|
||||
}
|
||||
|
||||
msg := basicTests(t, e)
|
||||
|
||||
// Were the right headers set?
|
||||
ct := msg.Header.Get("Content-type")
|
||||
mt, params, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
t.Fatal("Content-type header is invalid: ", ct)
|
||||
} else if mt != "multipart/mixed" {
|
||||
t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
|
||||
}
|
||||
b := params["boundary"]
|
||||
if b == "" {
|
||||
t.Fatal("Unexpected empty boundary parameter")
|
||||
}
|
||||
if len(params) != 1 {
|
||||
t.Fatal("Unexpected content-type parameters")
|
||||
}
|
||||
|
||||
// Is the generated message parsable?
|
||||
mixed := multipart.NewReader(msg.Body, params["boundary"])
|
||||
|
||||
text, err := mixed.NextPart()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find text component of email: %s", err)
|
||||
}
|
||||
|
||||
// Does the text portion match what we expect?
|
||||
mt, params, err = mime.ParseMediaType(text.Header.Get("Content-type"))
|
||||
if err != nil {
|
||||
t.Fatal("Could not parse message's Content-Type")
|
||||
} else if mt != "multipart/alternative" {
|
||||
t.Fatal("Message missing multipart/alternative")
|
||||
}
|
||||
mpReader := multipart.NewReader(text, params["boundary"])
|
||||
part, err := mpReader.NextPart()
|
||||
if err != nil {
|
||||
t.Fatal("Could not read plain text component of message: ", err)
|
||||
}
|
||||
plainText, err := ioutil.ReadAll(part)
|
||||
if err != nil {
|
||||
t.Fatal("Could not read plain text component of message: ", err)
|
||||
}
|
||||
if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
|
||||
t.Fatalf("Plain text is broken: %#q", plainText)
|
||||
}
|
||||
|
||||
// Check attachments.
|
||||
_, err = mixed.NextPart()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find attachment component of email: %s", err)
|
||||
}
|
||||
|
||||
if _, err = mixed.NextPart(); err != io.EOF {
|
||||
t.Error("Expected only text and one attachment!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailAttachment(t *testing.T) {
|
||||
e := prepareEmail()
|
||||
_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
|
||||
if err != nil {
|
||||
t.Fatal("Could not add an attachment to the message: ", err)
|
||||
}
|
||||
msg := basicTests(t, e)
|
||||
|
||||
// Were the right headers set?
|
||||
ct := msg.Header.Get("Content-type")
|
||||
mt, params, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
t.Fatal("Content-type header is invalid: ", ct)
|
||||
} else if mt != "multipart/mixed" {
|
||||
t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
|
||||
}
|
||||
b := params["boundary"]
|
||||
if b == "" {
|
||||
t.Fatal("Unexpected empty boundary parameter")
|
||||
}
|
||||
if len(params) != 1 {
|
||||
t.Fatal("Unexpected content-type parameters")
|
||||
}
|
||||
|
||||
// Is the generated message parsable?
|
||||
mixed := multipart.NewReader(msg.Body, params["boundary"])
|
||||
|
||||
// Check attachments.
|
||||
a, err := mixed.NextPart()
|
||||
if err != nil {
|
||||
t.Fatalf("Could not find attachment component of email: %s", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(a.Header.Get("Content-Disposition"), "attachment") {
|
||||
t.Fatalf("Content disposition is not attachment: %s", a.Header.Get("Content-Disposition"))
|
||||
}
|
||||
|
||||
if _, err = mixed.NextPart(); err != io.EOF {
|
||||
t.Error("Expected only one attachment!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderEncoding(t *testing.T) {
|
||||
cases := []struct {
|
||||
field string
|
||||
have string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
field: "From",
|
||||
have: "Needs Encóding <encoding@example.com>, Only ASCII <foo@example.com>",
|
||||
want: "=?utf-8?q?Needs_Enc=C3=B3ding?= <encoding@example.com>, \"Only ASCII\" <foo@example.com>\r\n",
|
||||
},
|
||||
{
|
||||
field: "To",
|
||||
have: "Keith Moore <moore@cs.utk.edu>, Keld Jørn Simonsen <keld@dkuug.dk>",
|
||||
want: "\"Keith Moore\" <moore@cs.utk.edu>, =?utf-8?q?Keld_J=C3=B8rn_Simonsen?= <keld@dkuug.dk>\r\n",
|
||||
},
|
||||
{
|
||||
field: "Cc",
|
||||
have: "Needs Encóding <encoding@example.com>, \"Test :)\" <test@localhost>",
|
||||
want: "=?utf-8?q?Needs_Enc=C3=B3ding?= <encoding@example.com>, \"Test :)\" <test@localhost>\r\n",
|
||||
},
|
||||
{
|
||||
field: "Subject",
|
||||
have: "Subject with a 🐟",
|
||||
want: "=?UTF-8?q?Subject_with_a_=F0=9F=90=9F?=\r\n",
|
||||
},
|
||||
{
|
||||
field: "Subject",
|
||||
have: "Subject with only ASCII",
|
||||
want: "Subject with only ASCII\r\n",
|
||||
},
|
||||
}
|
||||
buff := &bytes.Buffer{}
|
||||
for _, c := range cases {
|
||||
header := make(textproto.MIMEHeader)
|
||||
header.Add(c.field, c.have)
|
||||
buff.Reset()
|
||||
headerToBytes(buff, header)
|
||||
want := fmt.Sprintf("%s: %s", c.field, c.want)
|
||||
got := buff.String()
|
||||
if got != want {
|
||||
t.Errorf("invalid utf-8 header encoding. \nwant:%#v\ngot: %#v", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailFromReader(t *testing.T) {
|
||||
ex := &Email{
|
||||
Subject: "Test Subject",
|
||||
To: []string{"Jordan Wright <jmwright798@gmail.com>", "also@example.com"},
|
||||
From: "Jordan Wright <jmwright798@gmail.com>",
|
||||
ReplyTo: []string{"Jordan Wright <jmwright798@gmail.com>"},
|
||||
Cc: []string{"one@example.com", "Two <two@example.com>"},
|
||||
Bcc: []string{"three@example.com", "Four <four@example.com>"},
|
||||
Text: []byte("This is a test email with HTML Formatting. It also has very long lines so\nthat the content must be wrapped if using quoted-printable decoding.\n"),
|
||||
HTML: []byte("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
|
||||
}
|
||||
raw := []byte(`
|
||||
MIME-Version: 1.0
|
||||
Subject: Test Subject
|
||||
From: Jordan Wright <jmwright798@gmail.com>
|
||||
Reply-To: Jordan Wright <jmwright798@gmail.com>
|
||||
To: Jordan Wright <jmwright798@gmail.com>, also@example.com
|
||||
Cc: one@example.com, Two <two@example.com>
|
||||
Bcc: three@example.com, Four <four@example.com>
|
||||
Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
|
||||
|
||||
--001a114fb3fc42fd6b051f834280
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
This is a test email with HTML Formatting. It also has very long lines so
|
||||
that the content must be wrapped if using quoted-printable decoding.
|
||||
|
||||
--001a114fb3fc42fd6b051f834280
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
|
||||
also has very long lines so that the content must be wrapped if using quote=
|
||||
d-printable decoding.</div>
|
||||
|
||||
--001a114fb3fc42fd6b051f834280--`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating email %s", err.Error())
|
||||
}
|
||||
if e.Subject != ex.Subject {
|
||||
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
|
||||
}
|
||||
if !bytes.Equal(e.Text, ex.Text) {
|
||||
t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
|
||||
}
|
||||
if !bytes.Equal(e.HTML, ex.HTML) {
|
||||
t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
|
||||
}
|
||||
if e.From != ex.From {
|
||||
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
|
||||
}
|
||||
if len(e.To) != len(ex.To) {
|
||||
t.Fatalf("Incorrect number of \"To\" addresses: %v != %v", len(e.To), len(ex.To))
|
||||
}
|
||||
if e.To[0] != ex.To[0] {
|
||||
t.Fatalf("Incorrect \"To[0]\": %#q != %#q", e.To[0], ex.To[0])
|
||||
}
|
||||
if e.To[1] != ex.To[1] {
|
||||
t.Fatalf("Incorrect \"To[1]\": %#q != %#q", e.To[1], ex.To[1])
|
||||
}
|
||||
if len(e.Cc) != len(ex.Cc) {
|
||||
t.Fatalf("Incorrect number of \"Cc\" addresses: %v != %v", len(e.Cc), len(ex.Cc))
|
||||
}
|
||||
if e.Cc[0] != ex.Cc[0] {
|
||||
t.Fatalf("Incorrect \"Cc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0])
|
||||
}
|
||||
if e.Cc[1] != ex.Cc[1] {
|
||||
t.Fatalf("Incorrect \"Cc[1]\": %#q != %#q", e.Cc[1], ex.Cc[1])
|
||||
}
|
||||
if len(e.Bcc) != len(ex.Bcc) {
|
||||
t.Fatalf("Incorrect number of \"Bcc\" addresses: %v != %v", len(e.Bcc), len(ex.Bcc))
|
||||
}
|
||||
if e.Bcc[0] != ex.Bcc[0] {
|
||||
t.Fatalf("Incorrect \"Bcc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0])
|
||||
}
|
||||
if e.Bcc[1] != ex.Bcc[1] {
|
||||
t.Fatalf("Incorrect \"Bcc[1]\": %#q != %#q", e.Bcc[1], ex.Bcc[1])
|
||||
}
|
||||
if len(e.ReplyTo) != len(ex.ReplyTo) {
|
||||
t.Fatalf("Incorrect number of \"Reply-To\" addresses: %v != %v", len(e.ReplyTo), len(ex.ReplyTo))
|
||||
}
|
||||
if e.ReplyTo[0] != ex.ReplyTo[0] {
|
||||
t.Fatalf("Incorrect \"ReplyTo\": %#q != %#q", e.ReplyTo[0], ex.ReplyTo[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonAsciiEmailFromReader(t *testing.T) {
|
||||
ex := &Email{
|
||||
Subject: "Test Subject",
|
||||
To: []string{"Anaïs <anais@example.org>"},
|
||||
Cc: []string{"Patrik Fältström <paf@example.com>"},
|
||||
From: "Mrs Valérie Dupont <valerie.dupont@example.com>",
|
||||
Text: []byte("This is a test message!"),
|
||||
}
|
||||
raw := []byte(`
|
||||
MIME-Version: 1.0
|
||||
Subject: =?UTF-8?Q?Test Subject?=
|
||||
From: Mrs =?ISO-8859-1?Q?Val=C3=A9rie=20Dupont?= <valerie.dupont@example.com>
|
||||
To: =?utf-8?q?Ana=C3=AFs?= <anais@example.org>
|
||||
Cc: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@example.com>
|
||||
Content-type: text/plain; charset=ISO-8859-1
|
||||
|
||||
This is a test message!`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating email %s", err.Error())
|
||||
}
|
||||
if e.Subject != ex.Subject {
|
||||
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
|
||||
}
|
||||
if e.From != ex.From {
|
||||
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
|
||||
}
|
||||
if e.To[0] != ex.To[0] {
|
||||
t.Fatalf("Incorrect \"To\": %#q != %#q", e.To, ex.To)
|
||||
}
|
||||
if e.Cc[0] != ex.Cc[0] {
|
||||
t.Fatalf("Incorrect \"Cc\": %#q != %#q", e.Cc, ex.Cc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonMultipartEmailFromReader(t *testing.T) {
|
||||
ex := &Email{
|
||||
Text: []byte("This is a test message!"),
|
||||
Subject: "Example Subject (no MIME Type)",
|
||||
Headers: textproto.MIMEHeader{},
|
||||
}
|
||||
ex.Headers.Add("Content-Type", "text/plain; charset=us-ascii")
|
||||
ex.Headers.Add("Message-ID", "<foobar@example.com>")
|
||||
raw := []byte(`From: "Foo Bar" <foobar@example.com>
|
||||
Content-Type: text/plain
|
||||
To: foobar@example.com
|
||||
Subject: Example Subject (no MIME Type)
|
||||
Message-ID: <foobar@example.com>
|
||||
|
||||
This is a test message!`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating email %s", err.Error())
|
||||
}
|
||||
if ex.Subject != e.Subject {
|
||||
t.Errorf("Incorrect subject. %#q != %#q\n", ex.Subject, e.Subject)
|
||||
}
|
||||
if !bytes.Equal(ex.Text, e.Text) {
|
||||
t.Errorf("Incorrect body. %#q != %#q\n", ex.Text, e.Text)
|
||||
}
|
||||
if ex.Headers.Get("Message-ID") != e.Headers.Get("Message-ID") {
|
||||
t.Errorf("Incorrect message ID. %#q != %#q\n", ex.Headers.Get("Message-ID"), e.Headers.Get("Message-ID"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase64EmailFromReader(t *testing.T) {
|
||||
ex := &Email{
|
||||
Subject: "Test Subject",
|
||||
To: []string{"Jordan Wright <jmwright798@gmail.com>"},
|
||||
From: "Jordan Wright <jmwright798@gmail.com>",
|
||||
Text: []byte("This is a test email with HTML Formatting. It also has very long lines so that the content must be wrapped if using quoted-printable decoding."),
|
||||
HTML: []byte("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
|
||||
}
|
||||
raw := []byte(`
|
||||
MIME-Version: 1.0
|
||||
Subject: Test Subject
|
||||
From: Jordan Wright <jmwright798@gmail.com>
|
||||
To: Jordan Wright <jmwright798@gmail.com>
|
||||
Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
|
||||
|
||||
--001a114fb3fc42fd6b051f834280
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
VGhpcyBpcyBhIHRlc3QgZW1haWwgd2l0aCBIVE1MIEZvcm1hdHRpbmcuIEl0IGFsc28gaGFzIHZl
|
||||
cnkgbG9uZyBsaW5lcyBzbyB0aGF0IHRoZSBjb250ZW50IG11c3QgYmUgd3JhcHBlZCBpZiB1c2lu
|
||||
ZyBxdW90ZWQtcHJpbnRhYmxlIGRlY29kaW5nLg==
|
||||
|
||||
--001a114fb3fc42fd6b051f834280
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
|
||||
also has very long lines so that the content must be wrapped if using quote=
|
||||
d-printable decoding.</div>
|
||||
|
||||
--001a114fb3fc42fd6b051f834280--`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating email %s", err.Error())
|
||||
}
|
||||
if e.Subject != ex.Subject {
|
||||
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
|
||||
}
|
||||
if !bytes.Equal(e.Text, ex.Text) {
|
||||
t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
|
||||
}
|
||||
if !bytes.Equal(e.HTML, ex.HTML) {
|
||||
t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
|
||||
}
|
||||
if e.From != ex.From {
|
||||
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachmentEmailFromReader(t *testing.T) {
|
||||
ex := &Email{
|
||||
Subject: "Test Subject",
|
||||
To: []string{"Jordan Wright <jmwright798@gmail.com>"},
|
||||
From: "Jordan Wright <jmwright798@gmail.com>",
|
||||
Text: []byte("Simple text body"),
|
||||
HTML: []byte("<div dir=\"ltr\">Simple HTML body</div>\n"),
|
||||
}
|
||||
a, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat.jpeg", "image/jpeg")
|
||||
if err != nil {
|
||||
t.Fatalf("Error attaching image %s", err.Error())
|
||||
}
|
||||
b, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat-inline.jpeg", "image/jpeg")
|
||||
if err != nil {
|
||||
t.Fatalf("Error attaching inline image %s", err.Error())
|
||||
}
|
||||
raw := []byte(`
|
||||
From: Jordan Wright <jmwright798@gmail.com>
|
||||
Date: Thu, 17 Oct 2019 08:55:37 +0100
|
||||
Mime-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary=35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
|
||||
To: Jordan Wright <jmwright798@gmail.com>
|
||||
Subject: Test Subject
|
||||
|
||||
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
|
||||
Content-Type: multipart/alternative;
|
||||
boundary=b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
|
||||
|
||||
--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Simple text body
|
||||
--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
<div dir=3D"ltr">Simple HTML body</div>
|
||||
|
||||
--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c--
|
||||
|
||||
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
|
||||
Content-Disposition: attachment;
|
||||
filename="cat.jpeg"
|
||||
Content-Id: <cat.jpeg>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/jpeg
|
||||
|
||||
TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
|
||||
|
||||
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7
|
||||
Content-Disposition: inline;
|
||||
filename="cat-inline.jpeg"
|
||||
Content-Id: <cat-inline.jpeg>
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Type: image/jpeg
|
||||
|
||||
TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
|
||||
|
||||
--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7--`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating email %s", err.Error())
|
||||
}
|
||||
if e.Subject != ex.Subject {
|
||||
t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
|
||||
}
|
||||
if !bytes.Equal(e.Text, ex.Text) {
|
||||
t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
|
||||
}
|
||||
if !bytes.Equal(e.HTML, ex.HTML) {
|
||||
t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
|
||||
}
|
||||
if e.From != ex.From {
|
||||
t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
|
||||
}
|
||||
if len(e.Attachments) != 2 {
|
||||
t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 1)
|
||||
}
|
||||
if e.Attachments[0].Filename != a.Filename {
|
||||
t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename)
|
||||
}
|
||||
if !bytes.Equal(e.Attachments[0].Content, a.Content) {
|
||||
t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content)
|
||||
}
|
||||
if e.Attachments[1].Filename != b.Filename {
|
||||
t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename)
|
||||
}
|
||||
if !bytes.Equal(e.Attachments[1].Content, b.Content) {
|
||||
t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleGmail() {
|
||||
e := NewEmail()
|
||||
e.From = "Jordan Wright <test@gmail.com>"
|
||||
e.To = []string{"test@example.com"}
|
||||
e.Bcc = []string{"test_bcc@example.com"}
|
||||
e.Cc = []string{"test_cc@example.com"}
|
||||
e.Subject = "Awesome Subject"
|
||||
e.Text = []byte("Text Body is, of course, supported!\n")
|
||||
e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
|
||||
e.Send("smtp.gmail.com:587", smtp.PlainAuth("", e.From, "password123", "smtp.gmail.com"))
|
||||
}
|
||||
|
||||
func ExampleAttach() {
|
||||
e := NewEmail()
|
||||
e.AttachFile("test.txt")
|
||||
}
|
||||
|
||||
func Test_base64Wrap(t *testing.T) {
|
||||
file := "I'm a file long enough to force the function to wrap a\n" +
|
||||
"couple of lines, but I stop short of the end of one line and\n" +
|
||||
"have some padding dangling at the end."
|
||||
encoded := "SSdtIGEgZmlsZSBsb25nIGVub3VnaCB0byBmb3JjZSB0aGUgZnVuY3Rpb24gdG8gd3JhcCBhCmNv\r\n" +
|
||||
"dXBsZSBvZiBsaW5lcywgYnV0IEkgc3RvcCBzaG9ydCBvZiB0aGUgZW5kIG9mIG9uZSBsaW5lIGFu\r\n" +
|
||||
"ZApoYXZlIHNvbWUgcGFkZGluZyBkYW5nbGluZyBhdCB0aGUgZW5kLg==\r\n"
|
||||
|
||||
var buf bytes.Buffer
|
||||
base64Wrap(&buf, []byte(file))
|
||||
if !bytes.Equal(buf.Bytes(), []byte(encoded)) {
|
||||
t.Fatalf("Encoded file does not match expected: %#q != %#q", string(buf.Bytes()), encoded)
|
||||
}
|
||||
}
|
||||
|
||||
// *Since the mime library in use by ```email``` is now in the stdlib, this test is deprecated
|
||||
func Test_quotedPrintEncode(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
text := []byte("Dear reader!\n\n" +
|
||||
"This is a test email to try and capture some of the corner cases that exist within\n" +
|
||||
"the quoted-printable encoding.\n" +
|
||||
"There are some wacky parts like =, and this input assumes UNIX line breaks so\r\n" +
|
||||
"it can come out a little weird. Also, we need to support unicode so here's a fish: 🐟\n")
|
||||
expected := []byte("Dear reader!\r\n\r\n" +
|
||||
"This is a test email to try and capture some of the corner cases that exist=\r\n" +
|
||||
" within\r\n" +
|
||||
"the quoted-printable encoding.\r\n" +
|
||||
"There are some wacky parts like =3D, and this input assumes UNIX line break=\r\n" +
|
||||
"s so\r\n" +
|
||||
"it can come out a little weird. Also, we need to support unicode so here's=\r\n" +
|
||||
" a fish: =F0=9F=90=9F\r\n")
|
||||
qp := quotedprintable.NewWriter(&buf)
|
||||
if _, err := qp.Write(text); err != nil {
|
||||
t.Fatal("quotePrintEncode: ", err)
|
||||
}
|
||||
if err := qp.Close(); err != nil {
|
||||
t.Fatal("Error closing writer", err)
|
||||
}
|
||||
if b := buf.Bytes(); !bytes.Equal(b, expected) {
|
||||
t.Errorf("quotedPrintEncode generated incorrect results: %#q != %#q", b, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipartNoContentType(t *testing.T) {
|
||||
raw := []byte(`From: Mikhail Gusarov <dottedmag@dottedmag.net>
|
||||
To: notmuch@notmuchmail.org
|
||||
References: <20091117190054.GU3165@dottiness.seas.harvard.edu>
|
||||
Date: Wed, 18 Nov 2009 01:02:38 +0600
|
||||
Message-ID: <87iqd9rn3l.fsf@vertex.dottedmag>
|
||||
MIME-Version: 1.0
|
||||
Subject: Re: [notmuch] Working with Maildir storage?
|
||||
Content-Type: multipart/mixed; boundary="===============1958295626=="
|
||||
|
||||
--===============1958295626==
|
||||
Content-Type: multipart/signed; boundary="=-=-=";
|
||||
micalg=pgp-sha1; protocol="application/pgp-signature"
|
||||
|
||||
--=-=-=
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Twas brillig at 14:00:54 17.11.2009 UTC-05 when lars@seas.harvard.edu did g=
|
||||
yre and gimble:
|
||||
|
||||
--=-=-=
|
||||
Content-Type: application/pgp-signature
|
||||
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
Version: GnuPG v1.4.9 (GNU/Linux)
|
||||
|
||||
iQIcBAEBAgAGBQJLAvNOAAoJEJ0g9lA+M4iIjLYQAKp0PXEgl3JMOEBisH52AsIK
|
||||
=/ksP
|
||||
-----END PGP SIGNATURE-----
|
||||
--=-=-=--
|
||||
|
||||
--===============1958295626==
|
||||
Content-Type: text/plain; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: inline
|
||||
|
||||
Testing!
|
||||
--===============1958295626==--
|
||||
`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error when parsing email %s", err.Error())
|
||||
}
|
||||
if !bytes.Equal(e.Text, []byte("Testing!")) {
|
||||
t.Fatalf("Error incorrect text: %#q != %#q\n", e.Text, "Testing!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoMultipartHTMLContentTypeBase64Encoding(t *testing.T) {
|
||||
raw := []byte(`MIME-Version: 1.0
|
||||
From: no-reply@example.com
|
||||
To: tester@example.org
|
||||
Date: 7 Jan 2021 03:07:44 -0800
|
||||
Subject: Hello
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: base64
|
||||
Message-Id: <20210107110744.547DD70532@example.com>
|
||||
|
||||
PGh0bWw+PGhlYWQ+PHRpdGxlPnRlc3Q8L3RpdGxlPjwvaGVhZD48Ym9keT5IZWxsbyB3
|
||||
b3JsZCE8L2JvZHk+PC9odG1sPg==
|
||||
`)
|
||||
e, err := NewEmailFromReader(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
t.Fatalf("Error when parsing email %s", err.Error())
|
||||
}
|
||||
if !bytes.Equal(e.HTML, []byte("<html><head><title>test</title></head><body>Hello world!</body></html>")) {
|
||||
t.Fatalf("Error incorrect text: %#q != %#q\n", e.Text, "<html>...</html>")
|
||||
}
|
||||
}
|
||||
|
||||
// *Since the mime library in use by ```email``` is now in the stdlib, this test is deprecated
|
||||
func Test_quotedPrintDecode(t *testing.T) {
|
||||
text := []byte("Dear reader!\r\n\r\n" +
|
||||
"This is a test email to try and capture some of the corner cases that exist=\r\n" +
|
||||
" within\r\n" +
|
||||
"the quoted-printable encoding.\r\n" +
|
||||
"There are some wacky parts like =3D, and this input assumes UNIX line break=\r\n" +
|
||||
"s so\r\n" +
|
||||
"it can come out a little weird. Also, we need to support unicode so here's=\r\n" +
|
||||
" a fish: =F0=9F=90=9F\r\n")
|
||||
expected := []byte("Dear reader!\r\n\r\n" +
|
||||
"This is a test email to try and capture some of the corner cases that exist within\r\n" +
|
||||
"the quoted-printable encoding.\r\n" +
|
||||
"There are some wacky parts like =, and this input assumes UNIX line breaks so\r\n" +
|
||||
"it can come out a little weird. Also, we need to support unicode so here's a fish: 🐟\r\n")
|
||||
qp := quotedprintable.NewReader(bytes.NewReader(text))
|
||||
got, err := ioutil.ReadAll(qp)
|
||||
if err != nil {
|
||||
t.Fatal("quotePrintDecode: ", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(got, expected) {
|
||||
t.Errorf("quotedPrintDecode generated incorrect results: %#q != %#q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_base64Wrap(b *testing.B) {
|
||||
// Reasonable base case; 128K random bytes
|
||||
file := make([]byte, 128*1024)
|
||||
if _, err := rand.Read(file); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for i := 0; i <= b.N; i++ {
|
||||
base64Wrap(ioutil.Discard, file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSender(t *testing.T) {
|
||||
var cases = []struct {
|
||||
e Email
|
||||
want string
|
||||
haserr bool
|
||||
}{
|
||||
{
|
||||
Email{From: "from@test.com"},
|
||||
"from@test.com",
|
||||
false,
|
||||
},
|
||||
{
|
||||
Email{Sender: "sender@test.com", From: "from@test.com"},
|
||||
"sender@test.com",
|
||||
false,
|
||||
},
|
||||
{
|
||||
Email{Sender: "bad_address_sender"},
|
||||
"",
|
||||
true,
|
||||
},
|
||||
{
|
||||
Email{Sender: "good@sender.com", From: "bad_address_from"},
|
||||
"good@sender.com",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, testcase := range cases {
|
||||
got, err := testcase.e.parseSender()
|
||||
if got != testcase.want || (err != nil) != testcase.haserr {
|
||||
t.Errorf(`%d: got %s != want %s or error "%t" != "%t"`, i+1, got, testcase.want, err != nil, testcase.haserr)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,367 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Pool struct {
|
||||
addr string
|
||||
auth smtp.Auth
|
||||
max int
|
||||
created int
|
||||
clients chan *client
|
||||
rebuild chan struct{}
|
||||
mut *sync.Mutex
|
||||
lastBuildErr *timestampedErr
|
||||
closing chan struct{}
|
||||
tlsConfig *tls.Config
|
||||
helloHostname string
|
||||
}
|
||||
|
||||
type client struct {
|
||||
*smtp.Client
|
||||
failCount int
|
||||
}
|
||||
|
||||
type timestampedErr struct {
|
||||
err error
|
||||
ts time.Time
|
||||
}
|
||||
|
||||
const maxFails = 4
|
||||
|
||||
var (
|
||||
ErrClosed = errors.New("pool closed")
|
||||
ErrTimeout = errors.New("timed out")
|
||||
)
|
||||
|
||||
func NewPool(address string, count int, auth smtp.Auth, opt_tlsConfig ...*tls.Config) (pool *Pool, err error) {
|
||||
pool = &Pool{
|
||||
addr: address,
|
||||
auth: auth,
|
||||
max: count,
|
||||
clients: make(chan *client, count),
|
||||
rebuild: make(chan struct{}),
|
||||
closing: make(chan struct{}),
|
||||
mut: &sync.Mutex{},
|
||||
}
|
||||
if len(opt_tlsConfig) == 1 {
|
||||
pool.tlsConfig = opt_tlsConfig[0]
|
||||
} else if host, _, e := net.SplitHostPort(address); e != nil {
|
||||
return nil, e
|
||||
} else {
|
||||
pool.tlsConfig = &tls.Config{ServerName: host}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// go1.1 didn't have this method
|
||||
func (c *client) Close() error {
|
||||
return c.Text.Close()
|
||||
}
|
||||
|
||||
// SetHelloHostname optionally sets the hostname that the Go smtp.Client will
|
||||
// use when doing a HELLO with the upstream SMTP server. By default, Go uses
|
||||
// "localhost" which may not be accepted by certain SMTP servers that demand
|
||||
// an FQDN.
|
||||
func (p *Pool) SetHelloHostname(h string) {
|
||||
p.helloHostname = h
|
||||
}
|
||||
|
||||
func (p *Pool) get(timeout time.Duration) *client {
|
||||
select {
|
||||
case c := <-p.clients:
|
||||
return c
|
||||
default:
|
||||
}
|
||||
|
||||
if p.created < p.max {
|
||||
p.makeOne()
|
||||
}
|
||||
|
||||
var deadline <-chan time.Time
|
||||
if timeout >= 0 {
|
||||
deadline = time.After(timeout)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case c := <-p.clients:
|
||||
return c
|
||||
case <-p.rebuild:
|
||||
p.makeOne()
|
||||
case <-deadline:
|
||||
return nil
|
||||
case <-p.closing:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldReuse(err error) bool {
|
||||
// certainly not perfect, but might be close:
|
||||
// - EOF: clearly, the connection went down
|
||||
// - textproto.Errors were valid SMTP over a valid connection,
|
||||
// but resulted from an SMTP error response
|
||||
// - textproto.ProtocolErrors result from connections going down,
|
||||
// invalid SMTP, that sort of thing
|
||||
// - syscall.Errno is probably down connection/bad pipe, but
|
||||
// passed straight through by textproto instead of becoming a
|
||||
// ProtocolError
|
||||
// - if we don't recognize the error, don't reuse the connection
|
||||
// A false positive will probably fail on the Reset(), and even if
|
||||
// not will eventually hit maxFails.
|
||||
// A false negative will knock over (and trigger replacement of) a
|
||||
// conn that might have still worked.
|
||||
if err == io.EOF {
|
||||
return false
|
||||
}
|
||||
switch err.(type) {
|
||||
case *textproto.Error:
|
||||
return true
|
||||
case *textproto.ProtocolError, textproto.ProtocolError:
|
||||
return false
|
||||
case syscall.Errno:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) replace(c *client) {
|
||||
p.clients <- c
|
||||
}
|
||||
|
||||
func (p *Pool) inc() bool {
|
||||
if p.created >= p.max {
|
||||
return false
|
||||
}
|
||||
|
||||
p.mut.Lock()
|
||||
defer p.mut.Unlock()
|
||||
|
||||
if p.created >= p.max {
|
||||
return false
|
||||
}
|
||||
p.created++
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Pool) dec() {
|
||||
p.mut.Lock()
|
||||
p.created--
|
||||
p.mut.Unlock()
|
||||
|
||||
select {
|
||||
case p.rebuild <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) makeOne() {
|
||||
go func() {
|
||||
if p.inc() {
|
||||
if c, err := p.build(); err == nil {
|
||||
p.clients <- c
|
||||
} else {
|
||||
p.lastBuildErr = ×tampedErr{err, time.Now()}
|
||||
p.dec()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func startTLS(c *client, t *tls.Config) (bool, error) {
|
||||
if ok, _ := c.Extension("STARTTLS"); !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := c.StartTLS(t); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func addAuth(c *client, auth smtp.Auth) (bool, error) {
|
||||
if ok, _ := c.Extension("AUTH"); !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := c.Auth(auth); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *Pool) build() (*client, error) {
|
||||
cl, err := smtp.Dial(p.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Is there a custom hostname for doing a HELLO with the SMTP server?
|
||||
if p.helloHostname != "" {
|
||||
cl.Hello(p.helloHostname)
|
||||
}
|
||||
|
||||
c := &client{cl, 0}
|
||||
|
||||
if _, err := startTLS(c, p.tlsConfig); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if p.auth != nil {
|
||||
if _, err := addAuth(c, p.auth); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (p *Pool) maybeReplace(err error, c *client) {
|
||||
if err == nil {
|
||||
c.failCount = 0
|
||||
p.replace(c)
|
||||
return
|
||||
}
|
||||
|
||||
c.failCount++
|
||||
if c.failCount >= maxFails {
|
||||
goto shutdown
|
||||
}
|
||||
|
||||
if !shouldReuse(err) {
|
||||
goto shutdown
|
||||
}
|
||||
|
||||
if err := c.Reset(); err != nil {
|
||||
goto shutdown
|
||||
}
|
||||
|
||||
p.replace(c)
|
||||
return
|
||||
|
||||
shutdown:
|
||||
p.dec()
|
||||
c.Close()
|
||||
}
|
||||
|
||||
func (p *Pool) failedToGet(startTime time.Time) error {
|
||||
select {
|
||||
case <-p.closing:
|
||||
return ErrClosed
|
||||
default:
|
||||
}
|
||||
|
||||
if p.lastBuildErr != nil && startTime.Before(p.lastBuildErr.ts) {
|
||||
return p.lastBuildErr.err
|
||||
}
|
||||
|
||||
return ErrTimeout
|
||||
}
|
||||
|
||||
// Send sends an email via a connection pulled from the Pool. The timeout may
|
||||
// be <0 to indicate no timeout. Otherwise reaching the timeout will produce
|
||||
// and error building a connection that occurred while we were waiting, or
|
||||
// otherwise ErrTimeout.
|
||||
func (p *Pool) Send(e *Email, timeout time.Duration) (err error) {
|
||||
start := time.Now()
|
||||
c := p.get(timeout)
|
||||
if c == nil {
|
||||
return p.failedToGet(start)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
p.maybeReplace(err, c)
|
||||
}()
|
||||
|
||||
recipients, err := addressLists(e.To, e.Cc, e.Bcc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := e.Bytes()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
from, err := emailOnly(e.From)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = c.Mail(from); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, recip := range recipients {
|
||||
if err = c.Rcpt(recip); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = w.Write(msg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func emailOnly(full string) (string, error) {
|
||||
addr, err := mail.ParseAddress(full)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return addr.Address, nil
|
||||
}
|
||||
|
||||
func addressLists(lists ...[]string) ([]string, error) {
|
||||
length := 0
|
||||
for _, lst := range lists {
|
||||
length += len(lst)
|
||||
}
|
||||
combined := make([]string, 0, length)
|
||||
|
||||
for _, lst := range lists {
|
||||
for _, full := range lst {
|
||||
addr, err := emailOnly(full)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
combined = append(combined, addr)
|
||||
}
|
||||
}
|
||||
|
||||
return combined, nil
|
||||
}
|
||||
|
||||
// Close immediately changes the pool's state so no new connections will be
|
||||
// created, then gets and closes the existing ones as they become available.
|
||||
func (p *Pool) Close() {
|
||||
close(p.closing)
|
||||
|
||||
for p.created > 0 {
|
||||
c := <-p.clients
|
||||
c.Quit()
|
||||
p.dec()
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
package smtpclient
|
||||
|
||||
import (
|
||||
"b612.me/apps/b612/smtpclient/email"
|
||||
"b612.me/stario"
|
||||
"b612.me/starlog"
|
||||
"crypto/tls"
|
||||
"github.com/spf13/cobra"
|
||||
"mime"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "smtpc",
|
||||
Short: "smtp client",
|
||||
Long: "smtp client",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
run()
|
||||
},
|
||||
}
|
||||
|
||||
var from, subject, text string
|
||||
var to, cc, bcc, attachments, replyTo []string
|
||||
var useHTML bool
|
||||
var user, pwd, server string
|
||||
var skipInsecure, usingFile bool
|
||||
var useTLS int
|
||||
var hostname string
|
||||
var autoHostname bool
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().BoolVarP(&usingFile, "file", "F", false, "using file")
|
||||
Cmd.Flags().StringSliceVarP(&replyTo, "reply-to", "r", nil, "reply to")
|
||||
Cmd.Flags().StringVarP(&from, "from", "f", "", "from")
|
||||
Cmd.Flags().StringVarP(&subject, "subject", "s", "", "subject")
|
||||
Cmd.Flags().StringVarP(&text, "text", "t", "", "text")
|
||||
Cmd.Flags().StringSliceVarP(&to, "to", "T", nil, "to")
|
||||
Cmd.Flags().StringSliceVarP(&cc, "cc", "C", nil, "cc")
|
||||
Cmd.Flags().StringSliceVarP(&bcc, "bcc", "B", nil, "bcc")
|
||||
Cmd.Flags().StringSliceVarP(&attachments, "attachments", "a", nil, "attachments")
|
||||
Cmd.Flags().BoolVarP(&useHTML, "html", "H", false, "use html")
|
||||
Cmd.Flags().StringVarP(&user, "user", "u", "", "user")
|
||||
Cmd.Flags().StringVarP(&pwd, "pwd", "p", "", "password")
|
||||
Cmd.Flags().StringVarP(&server, "server", "S", "127.0.0.1:25", "server")
|
||||
Cmd.Flags().IntVarP(&useTLS, "tls", "l", 0, "use tls,1 means use tls,2 means use starttls,other means not use tls")
|
||||
Cmd.Flags().BoolVarP(&skipInsecure, "skip-insecure", "i", false, "skip insecure")
|
||||
Cmd.Flags().StringVarP(&hostname, "hostname", "n", "", "hostname")
|
||||
Cmd.Flags().BoolVarP(&autoHostname, "auto-hostname", "N", false, "auto hostname")
|
||||
}
|
||||
|
||||
func run() {
|
||||
{
|
||||
for from == "" {
|
||||
from = stario.MessageBox("Please input mail from:", "").MustString()
|
||||
}
|
||||
for len(to) == 0 {
|
||||
to = stario.MessageBox("Please input mail to,split by ,:", "").MustSliceString(",")
|
||||
}
|
||||
for subject == "" {
|
||||
subject = stario.MessageBox("Please input mail subject:", "").MustString()
|
||||
}
|
||||
for text == "" {
|
||||
text = stario.MessageBox("Please input mail text:", "").MustString()
|
||||
}
|
||||
for server == "" {
|
||||
server = stario.MessageBox("Please input mail server:", "").MustString()
|
||||
}
|
||||
}
|
||||
|
||||
var mail = email.Email{
|
||||
ReplyTo: replyTo,
|
||||
From: from,
|
||||
To: to,
|
||||
Bcc: bcc,
|
||||
Cc: cc,
|
||||
Subject: subject,
|
||||
}
|
||||
var txt []byte
|
||||
var err error
|
||||
if usingFile {
|
||||
txt, err = os.ReadFile(text)
|
||||
if err != nil {
|
||||
starlog.Errorf("read file %s error:%s\n", text, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
txt = []byte(text)
|
||||
}
|
||||
if useHTML {
|
||||
mail.HTML = txt
|
||||
} else {
|
||||
mail.Text = txt
|
||||
}
|
||||
if len(attachments) > 0 {
|
||||
for _, v := range attachments {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
continue
|
||||
}
|
||||
f, err := os.Open(v)
|
||||
if err != nil {
|
||||
starlog.Errorf("open attach file %s error:%s\n", v, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
starlog.Errorf("stat attach file %s error:%s\n", v, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
_, err = mail.Attach(f, stat.Name(), mime.TypeByExtension(filepath.Ext(stat.Name())))
|
||||
if err != nil {
|
||||
starlog.Errorf("attach file %s error:%s\n", v, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if autoHostname && hostname == "" {
|
||||
hostname = strings.Split(server, ":")[0]
|
||||
}
|
||||
var auth smtp.Auth
|
||||
if user != "" && pwd != "" {
|
||||
auth = smtp.PlainAuth("", user, pwd, hostname)
|
||||
}
|
||||
|
||||
switch useTLS {
|
||||
case 1:
|
||||
starlog.Noticef("Mail send method:TLS InsecureSkipVerify:%v ServerName:%v\n", skipInsecure, hostname)
|
||||
err = mail.SendWithTLS(server, auth, &tls.Config{
|
||||
InsecureSkipVerify: skipInsecure,
|
||||
ServerName: hostname,
|
||||
})
|
||||
case 2:
|
||||
starlog.Noticef("Mail send method:StartTLS InsecureSkipVerify:%v ServerName:%v\n", skipInsecure, hostname)
|
||||
err = mail.SendWithStartTLS(server, auth, &tls.Config{InsecureSkipVerify: skipInsecure,
|
||||
ServerName: hostname})
|
||||
default:
|
||||
starlog.Noticef("Mail send method:Normal\n")
|
||||
err = mail.Send(server, auth)
|
||||
}
|
||||
if err != nil {
|
||||
starlog.Errorf("send mail error:%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
starlog.Infof("send mail to %v success by server %s\n", to, server)
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package smtpserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSession_Data(t *testing.T) {
|
||||
s := `=?UTF-8?q?=E5=85=B7=E8=B6=B3=E8=99=AB=E7=94=B5=E8=B4=BA=E8=A6=81=E8=BE=8A?= =?UTF-8?q?=E6=A5=9Ejhkkj?=`
|
||||
d := new(mime.WordDecoder)
|
||||
fmt.Println(d.DecodeHeader(s))
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
package smtpserver
|
||||
|
||||
import (
|
||||
"b612.me/starlog"
|
||||
"b612.me/startext"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
"html"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-smtp"
|
||||
)
|
||||
|
||||
var addr string
|
||||
var user, pass string
|
||||
var allowAnyuser bool
|
||||
var output string
|
||||
var domain string
|
||||
var cert, key string
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "smtps",
|
||||
Short: "smtp server",
|
||||
Long: "smtp server",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
run()
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().StringVarP(&addr, "addr", "a", "0.0.0.0:25", "smtp server listen address")
|
||||
Cmd.Flags().StringVarP(&user, "user", "u", "admin", "smtp server username")
|
||||
Cmd.Flags().StringVarP(&pass, "pass", "p", "admin", "smtp server password")
|
||||
Cmd.Flags().BoolVarP(&allowAnyuser, "allow-anyuser", "A", false, "allow any user")
|
||||
Cmd.Flags().StringVarP(&output, "output", "o", "", "output mail to html")
|
||||
Cmd.Flags().StringVarP(&domain, "domain", "d", "localhost", "smtp server domain")
|
||||
Cmd.Flags().StringVarP(&cert, "cert", "c", "", "smtp server cert(TLS)")
|
||||
Cmd.Flags().StringVarP(&key, "key", "k", "", "smtp server key(TLS)")
|
||||
}
|
||||
|
||||
type backend struct{}
|
||||
|
||||
func (bkd *backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
|
||||
return &session{}, nil
|
||||
}
|
||||
|
||||
type session struct {
|
||||
username string
|
||||
password string
|
||||
to string
|
||||
}
|
||||
|
||||
func (s *session) AuthPlain(username, password string) error {
|
||||
s.username = username
|
||||
s.password = password
|
||||
starlog.Printf("username:%s,password:%s\n", username, password)
|
||||
if allowAnyuser {
|
||||
return nil
|
||||
} else {
|
||||
if username != user || password != pass {
|
||||
return smtp.ErrAuthFailed
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) Mail(from string, opts *smtp.MailOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) Rcpt(to string, opts *smtp.RcptOptions) error {
|
||||
s.to += to + ";"
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) Data(r io.Reader) error {
|
||||
mailData, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(mailData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header := msg.Header
|
||||
subject := header.Get("Subject")
|
||||
to := header.Get("To")
|
||||
cc := header.Get("Cc")
|
||||
from := header.Get("From") // 获取发件人
|
||||
date := header.Get("Date")
|
||||
body, err := ioutil.ReadAll(msg.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var bodyStr string
|
||||
|
||||
var d = new(mime.WordDecoder)
|
||||
{
|
||||
subject, err = d.DecodeHeader(subject)
|
||||
if err != nil {
|
||||
starlog.Errorf("Decode subject %s error:%s\n", subject, err)
|
||||
return err
|
||||
}
|
||||
bodyStr, err = bodyDecode(string(body))
|
||||
if err != nil {
|
||||
starlog.Errorf("Decode body %s error:%s\n", string(body), err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if startext.IsGBK([]byte(bodyStr)) {
|
||||
tmp, err := startext.GBK2UTF8([]byte(bodyStr))
|
||||
if err == nil {
|
||||
bodyStr = string(tmp)
|
||||
}
|
||||
}
|
||||
starlog.Println("From:", from)
|
||||
starlog.Println("Subject:", subject)
|
||||
starlog.Println("To ALL:", s.to)
|
||||
starlog.Println("To:", to)
|
||||
starlog.Println("Cc:", cc)
|
||||
starlog.Println("Body:", bodyStr)
|
||||
if output != "" {
|
||||
path := fmt.Sprintf("%s/%s_%s.html", output, subject, time.Now().Format("2006_01_02_15_04_05_"))
|
||||
html := fmt.Sprintf(`<html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>%s</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>%s</h2>
|
||||
<hr>
|
||||
<p>auth user:<strong> %s </strong></p>
|
||||
<p>auth pass:<strong> %s </strong></p>
|
||||
<hr>
|
||||
<p>Date:<strong> %v </strong></p>
|
||||
<p>From:<strong> %s </strong></p>
|
||||
<p>To All:<strong> %s </strong></p>
|
||||
<p>To:<strong> %s </strong></p>
|
||||
<p>Cc:<strong> %s </strong></p>
|
||||
<hr>
|
||||
<br />
|
||||
<br />
|
||||
%s
|
||||
</body>
|
||||
</html>`, html.EscapeString(subject), html.EscapeString(subject), html.EscapeString(s.username), html.EscapeString(s.password),
|
||||
date, html.EscapeString(from), html.EscapeString(s.to), html.EscapeString(to), html.EscapeString(cc), bodyStr)
|
||||
os.WriteFile(path, []byte(html), 0644)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *session) Reset() {}
|
||||
|
||||
func (s *session) Logout() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func run() {
|
||||
var err error
|
||||
s := smtp.NewServer(&backend{})
|
||||
if cert != "" && key != "" {
|
||||
var config tls.Config
|
||||
config.Certificates = make([]tls.Certificate, 1)
|
||||
config.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
||||
if err != nil {
|
||||
starlog.Errorln("failed to load cert:", err)
|
||||
return
|
||||
}
|
||||
s.TLSConfig = &config
|
||||
s.Addr = addr
|
||||
s.Domain = domain
|
||||
s.AllowInsecureAuth = true
|
||||
s.Debug = os.Stdout
|
||||
starlog.Infoln("Starting TLS-SMTP server at", addr)
|
||||
starlog.Errorln(s.ListenAndServeTLS())
|
||||
return
|
||||
}
|
||||
s.Addr = addr
|
||||
s.Domain = domain
|
||||
s.AllowInsecureAuth = true
|
||||
s.Debug = os.Stdout
|
||||
|
||||
starlog.Infoln("Starting SMTP server at", addr)
|
||||
starlog.Errorln(s.ListenAndServe())
|
||||
}
|
||||
|
||||
func bodyDecode(encoded string) (string, error) {
|
||||
encoded = strings.Replace(encoded, "=\r\n", "", -1) // 对于Windows系统
|
||||
encoded = strings.Replace(encoded, "=\n", "", -1) // 对于UNIX/Linux系统
|
||||
|
||||
// 创建一个新的Quoted-Printable阅读器
|
||||
reader := quotedprintable.NewReader(strings.NewReader(encoded))
|
||||
|
||||
// 读取并解码整个内容
|
||||
decoded, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
fmt.Println("Error decoding string:", err)
|
||||
return string(decoded), err
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
package socks5
|
||||
|
||||
import (
|
||||
"b612.me/starlog"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/things-go/go-socks5"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var username, password string
|
||||
var listen string
|
||||
|
||||
func init() {
|
||||
Cmd.Flags().StringVarP(&username, "username", "u", "", "用户名")
|
||||
Cmd.Flags().StringVarP(&password, "password", "p", "", "密码")
|
||||
Cmd.Flags().StringVarP(&listen, "listen", "l", ":8000", "监听地址")
|
||||
}
|
||||
|
||||
var Cmd = &cobra.Command{
|
||||
Use: "socks5",
|
||||
Short: "socks5代理",
|
||||
Long: "socks5代理",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
run()
|
||||
},
|
||||
}
|
||||
|
||||
func run() {
|
||||
// Create a SOCKS5 server
|
||||
var opt []socks5.Option
|
||||
|
||||
opt = append(opt, socks5.WithLogger(socks5.NewLogger(log.New(os.Stdout, "socks5: ", log.LstdFlags))))
|
||||
if username != "" && password != "" {
|
||||
opt = append(opt, socks5.WithAuthMethods([]socks5.Authenticator{
|
||||
socks5.UserPassAuthenticator{Credentials: socks5.StaticCredentials{username: password}},
|
||||
}))
|
||||
}
|
||||
server := socks5.NewServer(
|
||||
opt...,
|
||||
)
|
||||
starlog.Infof("socks5 server listen on %s", listen)
|
||||
// Create SOCKS5 proxy on localhost port 8000
|
||||
if err := server.ListenAndServe("tcp", listen); err != nil {
|
||||
starlog.Errorln("socks5 server error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package tls
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCert(t *testing.T) {
|
||||
showTls("139.199.163.65:443", true, "")
|
||||
}
|
Loading…
Reference in New Issue