623 lines
16 KiB
Go
623 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
utils "anytunnel/at-common"
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
CSSTATUS_TYPE_SERVER = "server"
|
|
CSSTATUS_TYPE_CLIENT = "client"
|
|
CSSTATUS_ACTION_ONLINE = "online"
|
|
CSSTATUS_ACTION_OFFLINE = "offline"
|
|
)
|
|
|
|
type CSStatus struct {
|
|
url string
|
|
bufchn chan CSStatusAction
|
|
}
|
|
type CSStatusAction struct {
|
|
Type string
|
|
Token string
|
|
Action string
|
|
IP string
|
|
}
|
|
|
|
func NewCSStatus() *CSStatus {
|
|
s := CSStatus{
|
|
url: cfg.GetString("url.status"),
|
|
bufchn: make(chan CSStatusAction, 500),
|
|
}
|
|
s.init()
|
|
return &s
|
|
}
|
|
func (cs *CSStatus) init() {
|
|
go func() {
|
|
for {
|
|
action := <-cs.bufchn
|
|
var tryCount = 0
|
|
for tryCount <= cfg.GetInt("url.fail-retry") {
|
|
tryCount++
|
|
_url := utils.UrlArgs(cs.url, fmt.Sprintf("type=%s&token=%s&action=%s&ip=%s", action.Type, action.Token, action.Action, action.IP))
|
|
body, code, err := HttpGet(_url)
|
|
if err == nil && code == cfg.GetInt("url.success-code") {
|
|
break
|
|
} else if err != nil {
|
|
log.Warnf("report online/offline status fail to url %s, err: %s", cs.url, err)
|
|
} else {
|
|
err = fmt.Errorf("token error")
|
|
log.Warnf("report online/offline status fail to url %s, code: %d, except: %d ,body:%s", cs.url, code, cfg.GetInt("url.success-code"), string(body))
|
|
}
|
|
if err != nil && tryCount <= cfg.GetInt("url.fail-retry") {
|
|
time.Sleep(time.Second * time.Duration(cfg.GetInt("url.fail-wait")))
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
func (cs *CSStatus) ClientOnline(token string, addr net.Addr) {
|
|
cs.pushStatus(CSSTATUS_TYPE_CLIENT, token, CSSTATUS_ACTION_ONLINE, addr)
|
|
}
|
|
func (cs *CSStatus) ClientOffline(token string, addr net.Addr) {
|
|
cs.pushStatus(CSSTATUS_TYPE_CLIENT, token, CSSTATUS_ACTION_OFFLINE, addr)
|
|
}
|
|
func (cs *CSStatus) ServerOnline(token string, addr net.Addr) {
|
|
cs.pushStatus(CSSTATUS_TYPE_SERVER, token, CSSTATUS_ACTION_ONLINE, addr)
|
|
}
|
|
func (cs *CSStatus) ServerOffline(token string, addr net.Addr) {
|
|
cs.pushStatus(CSSTATUS_TYPE_SERVER, token, CSSTATUS_ACTION_OFFLINE, addr)
|
|
}
|
|
func (cs *CSStatus) pushStatus(typ, token, action string, addr net.Addr) {
|
|
if cs.url == "" {
|
|
return
|
|
}
|
|
_addr := addr.String()
|
|
ip := _addr[0:strings.Index(_addr, ":")]
|
|
cs.bufchn <- CSStatusAction{
|
|
Action: action,
|
|
Type: typ,
|
|
Token: token,
|
|
IP: ip,
|
|
}
|
|
}
|
|
|
|
//###################ConnMap######################
|
|
type ConnMap struct {
|
|
data utils.ConcurrentMap
|
|
}
|
|
type ConnItem struct {
|
|
ServerToken string
|
|
TunnelID uint64
|
|
Count uint64
|
|
}
|
|
type ConnTimeout struct {
|
|
Conn *net.Conn
|
|
ServerToken string
|
|
TunnelID uint64
|
|
LastTime int64
|
|
}
|
|
|
|
func NewConnMap() *ConnMap {
|
|
cm := ConnMap{
|
|
data: utils.NewConcurrentMap(),
|
|
}
|
|
cm.gc()
|
|
return &cm
|
|
}
|
|
func (m *ConnMap) IntToString(id uint64) string {
|
|
return strconv.FormatUint(id, 10)
|
|
}
|
|
func (m *ConnMap) Get(id uint64) (conn *net.Conn, exists bool) {
|
|
mid := m.IntToString(id)
|
|
_conn, exists := m.data.Get(mid)
|
|
if exists {
|
|
conn = (_conn).(ConnTimeout).Conn
|
|
}
|
|
return
|
|
}
|
|
func (m *ConnMap) Put(serverToken string, tunnleID, connid uint64, conn *net.Conn) {
|
|
mid := m.IntToString(connid)
|
|
m.data.Set(mid, ConnTimeout{
|
|
Conn: conn,
|
|
LastTime: time.Now().Unix(),
|
|
ServerToken: serverToken,
|
|
TunnelID: tunnleID,
|
|
})
|
|
//b, _ := m.data.MarshalJSON()
|
|
//fmt.Println("put:", serverToken, tunnleID, connid, m.GetTunnelConnCountMap(), string(b))
|
|
return
|
|
}
|
|
|
|
func (m *ConnMap) Delete(id uint64) {
|
|
mid := m.IntToString(id)
|
|
_conn, exists := m.data.Get(mid)
|
|
if exists {
|
|
(*(_conn).(ConnTimeout).Conn).Close()
|
|
}
|
|
m.data.Remove(mid)
|
|
}
|
|
func (m *ConnMap) ClearTimeout(id uint64) {
|
|
mid := m.IntToString(id)
|
|
_conn, exists := m.data.Get(mid)
|
|
if exists {
|
|
_c := _conn.(ConnTimeout)
|
|
_c.LastTime = 0
|
|
m.data.Set(mid, _c)
|
|
}
|
|
}
|
|
func (m *ConnMap) GetTunnelConnCountMap() map[uint64]ConnItem {
|
|
countMap := map[uint64]ConnItem{}
|
|
for _, k := range m.data.Keys() {
|
|
v, _ := m.data.Get(k)
|
|
c := v.(ConnTimeout)
|
|
item, ok := countMap[c.TunnelID]
|
|
if !ok {
|
|
countMap[c.TunnelID] = ConnItem{
|
|
Count: 1,
|
|
ServerToken: c.ServerToken,
|
|
TunnelID: c.TunnelID,
|
|
}
|
|
} else {
|
|
item.Count = item.Count + 1
|
|
countMap[c.TunnelID] = item
|
|
}
|
|
}
|
|
return countMap
|
|
}
|
|
func (m *ConnMap) gc() {
|
|
go func() {
|
|
for {
|
|
for _, k := range m.data.Keys() {
|
|
v, _ := m.data.Get(k)
|
|
c := v.(ConnTimeout)
|
|
if c.LastTime > 0 && time.Now().Unix()-c.LastTime > SERVER_CONN_IDLE_SECONDS {
|
|
(*c.Conn).Close()
|
|
m.data.Remove(k)
|
|
}
|
|
}
|
|
time.Sleep(60 * time.Second)
|
|
}
|
|
}()
|
|
}
|
|
|
|
//###############ClusterTunnelPool##################
|
|
type ClusterTunnel struct {
|
|
TunnelID uint64
|
|
ServerToken string
|
|
ServerBindIP string
|
|
ServerListenPort int
|
|
ClientToken string
|
|
ClientLocalHost string
|
|
ClientLocalPort int
|
|
Protocol int
|
|
BytesPerSec float64
|
|
}
|
|
type ClusterTunnelPool struct {
|
|
pool utils.ConcurrentMap
|
|
}
|
|
|
|
func NewClusterTunnelPool() ClusterTunnelPool {
|
|
return ClusterTunnelPool{
|
|
pool: utils.NewConcurrentMap(),
|
|
}
|
|
}
|
|
func (m *ClusterTunnelPool) IntToString(id uint64) string {
|
|
return strconv.FormatUint(id, 10)
|
|
}
|
|
func (t *ClusterTunnelPool) Set(tunnel ClusterTunnel) {
|
|
mid := t.IntToString(tunnel.TunnelID)
|
|
t.pool.Set(mid, tunnel)
|
|
}
|
|
func (t *ClusterTunnelPool) Get(tunnelID uint64) (tunnel ClusterTunnel, err error) {
|
|
mid := t.IntToString(tunnelID)
|
|
v, e := t.pool.Get(mid)
|
|
if !e {
|
|
err = fmt.Errorf("tunnel %d not exists", tunnelID)
|
|
return
|
|
}
|
|
return v.(ClusterTunnel), err
|
|
}
|
|
func (t *ClusterTunnelPool) Exists(tunnelID uint64) bool {
|
|
mid := t.IntToString(tunnelID)
|
|
return t.pool.Has(mid)
|
|
}
|
|
func (t *ClusterTunnelPool) Delete(tunnelID uint64) {
|
|
mid := t.IntToString(tunnelID)
|
|
t.pool.Remove(mid)
|
|
}
|
|
|
|
//###############ClusterServerControlChannelPool##################
|
|
type ClusterServerControlChannel struct {
|
|
ServerToken string
|
|
ServerMessageChannel *utils.MessageChannel
|
|
}
|
|
type ClusterServerControlChannelPool struct {
|
|
pool utils.ConcurrentMap
|
|
}
|
|
|
|
func NewClusterServerControlChannelPool() ClusterServerControlChannelPool {
|
|
return ClusterServerControlChannelPool{
|
|
pool: utils.NewConcurrentMap(),
|
|
}
|
|
}
|
|
func (t *ClusterServerControlChannelPool) Set(serverChannel ClusterServerControlChannel) {
|
|
t.pool.Set(serverChannel.ServerToken, serverChannel)
|
|
}
|
|
func (t *ClusterServerControlChannelPool) Get(serverToken string) (serverChannel ClusterServerControlChannel, err error) {
|
|
v, e := t.pool.Get(serverToken)
|
|
if !e {
|
|
err = fmt.Errorf("ClusterServerControlChannel %s not exists", serverToken)
|
|
return
|
|
}
|
|
return v.(ClusterServerControlChannel), err
|
|
}
|
|
func (t *ClusterServerControlChannelPool) Exists(serverToken string) bool {
|
|
return t.pool.Has(serverToken)
|
|
}
|
|
func (t *ClusterServerControlChannelPool) Delete(serverToken string) {
|
|
v, e := t.pool.Get(serverToken)
|
|
if e {
|
|
v.(ClusterServerControlChannel).ServerMessageChannel.CloseConn()
|
|
}
|
|
t.pool.Remove(serverToken)
|
|
}
|
|
|
|
//###############ClusterClientControlChannelPool##################
|
|
type ClusterClientControlChannel struct {
|
|
ClientToken string
|
|
ClientMessageChannel *utils.MessageChannel
|
|
}
|
|
type ClusterClientControlChannelPool struct {
|
|
pool utils.ConcurrentMap
|
|
}
|
|
|
|
func NewClusterClientControlChannelPool() ClusterClientControlChannelPool {
|
|
return ClusterClientControlChannelPool{
|
|
pool: utils.NewConcurrentMap(),
|
|
}
|
|
}
|
|
func (t *ClusterClientControlChannelPool) Set(clientChannel ClusterClientControlChannel) {
|
|
t.pool.Set(clientChannel.ClientToken, clientChannel)
|
|
}
|
|
func (t *ClusterClientControlChannelPool) Get(clientToken string) (clientChannel ClusterClientControlChannel, err error) {
|
|
v, e := t.pool.Get(clientToken)
|
|
if !e {
|
|
err = fmt.Errorf("ClusterClientControlChannel %s not exists", clientToken)
|
|
return
|
|
}
|
|
return v.(ClusterClientControlChannel), err
|
|
}
|
|
func (t *ClusterClientControlChannelPool) Exists(clientToken string) bool {
|
|
return t.pool.Has(clientToken)
|
|
}
|
|
func (t *ClusterClientControlChannelPool) Delete(clientToken string) {
|
|
v, e := t.pool.Get(clientToken)
|
|
if e {
|
|
v.(ClusterClientControlChannel).ClientMessageChannel.CloseConn()
|
|
}
|
|
t.pool.Remove(clientToken)
|
|
}
|
|
|
|
//
|
|
//###############counter##################
|
|
type TunnelTrafficCounter struct {
|
|
counter utils.ConcurrentMap
|
|
bufchn chan TunnelTrafficEntry
|
|
}
|
|
type TunnelTrafficEntry struct {
|
|
IsPositive bool
|
|
TunnelID uint64
|
|
CountBytes uint64
|
|
}
|
|
|
|
//map[string]uint64{}
|
|
func NewTunnelTrafficCounter() TunnelTrafficCounter {
|
|
tc := TunnelTrafficCounter{
|
|
counter: utils.NewConcurrentMap(),
|
|
bufchn: make(chan TunnelTrafficEntry, 500000),
|
|
}
|
|
tc.init()
|
|
return tc
|
|
}
|
|
func (m *TunnelTrafficCounter) IntToString(id uint64) string {
|
|
return strconv.FormatUint(id, 10)
|
|
}
|
|
func (tc *TunnelTrafficCounter) init() {
|
|
go func() {
|
|
for {
|
|
entry := <-tc.bufchn
|
|
mid := tc.IntToString(entry.TunnelID)
|
|
tc.counter.Upsert(mid, nil, func(exists bool, vold interface{}, vnew interface{}) (res interface{}) {
|
|
if vold != nil {
|
|
if entry.IsPositive {
|
|
vold.(map[string]uint64)["positive"] = vold.(map[string]uint64)["positive"] + entry.CountBytes
|
|
} else {
|
|
vold.(map[string]uint64)["negative"] = vold.(map[string]uint64)["negative"] + entry.CountBytes
|
|
}
|
|
res = vold
|
|
}
|
|
return
|
|
})
|
|
}
|
|
}()
|
|
}
|
|
func (tc *TunnelTrafficCounter) InitTunnel(tunnelID uint64) {
|
|
mid := tc.IntToString(tunnelID)
|
|
tc.counter.Set(mid, map[string]uint64{
|
|
"positive": 0,
|
|
"negative": 0,
|
|
})
|
|
}
|
|
func (tc *TunnelTrafficCounter) DeleteTunnel(tunnelID uint64) {
|
|
mid := tc.IntToString(tunnelID)
|
|
tc.counter.Remove(mid)
|
|
}
|
|
func (tc *TunnelTrafficCounter) AddPositive(tunnelID, bytesCount uint64) {
|
|
tc.bufchn <- TunnelTrafficEntry{
|
|
TunnelID: tunnelID,
|
|
IsPositive: true,
|
|
CountBytes: bytesCount,
|
|
}
|
|
}
|
|
func (tc *TunnelTrafficCounter) AddNegative(tunnelID, bytesCount uint64) {
|
|
tc.bufchn <- TunnelTrafficEntry{
|
|
TunnelID: tunnelID,
|
|
IsPositive: false,
|
|
CountBytes: bytesCount,
|
|
}
|
|
}
|
|
func (tc *TunnelTrafficCounter) AllDataJSON() ([]byte, error) {
|
|
JSON := utils.TrafficStatistics{
|
|
Total: utils.TrafficTotal{
|
|
Tunnels: poolTunnel.pool.Count(),
|
|
Servers: poolServerControlChannel.pool.Count(),
|
|
Clients: poolClientControlChannel.pool.Count(),
|
|
UploadBytes: 0,
|
|
DownloadBytes: 0,
|
|
TotalBytes: 0,
|
|
Connections: serverConns.data.Count(),
|
|
},
|
|
}
|
|
tunnelCount := serverConns.GetTunnelConnCountMap()
|
|
|
|
data := map[string]map[string]uint64{}
|
|
for nk, nv := range tc.counter.Items() {
|
|
tid, _ := strconv.ParseUint(nk, 10, 64)
|
|
var count uint64
|
|
if v, ok := tunnelCount[tid]; ok {
|
|
count = v.Count
|
|
}
|
|
uploadBytes := nv.(map[string]uint64)["positive"]
|
|
downloadBytes := nv.(map[string]uint64)["negative"]
|
|
|
|
tunnel, _ := poolTunnel.Get(tid)
|
|
data[nk] = map[string]uint64{
|
|
"positive": uploadBytes,
|
|
"negative": downloadBytes,
|
|
"limit": uint64(tunnel.BytesPerSec),
|
|
"connections": count,
|
|
}
|
|
JSON.Total.UploadBytes += uploadBytes
|
|
JSON.Total.DownloadBytes += downloadBytes
|
|
}
|
|
JSON.Total.TotalBytes = JSON.Total.UploadBytes + JSON.Total.DownloadBytes
|
|
JSON.Traffic = data
|
|
return json.Marshal(JSON)
|
|
}
|
|
func (tc *TunnelTrafficCounter) TunnelDataJSON(tunnelID uint64) ([]byte, error) {
|
|
mid := tc.IntToString(tunnelID)
|
|
if v, e := tc.counter.Get(mid); e {
|
|
return json.Marshal(v)
|
|
}
|
|
return []byte("{}"), nil
|
|
}
|
|
func (tc *TunnelTrafficCounter) AllData() map[string]map[string]uint64 {
|
|
data := map[string]map[string]uint64{}
|
|
tc.counter.IterCb(func(nk string, nv interface{}) {
|
|
if nv != nil {
|
|
data[nk] = map[string]uint64{
|
|
"positive": nv.(map[string]uint64)["positive"],
|
|
"negative": nv.(map[string]uint64)["negative"],
|
|
}
|
|
}
|
|
})
|
|
return data
|
|
}
|
|
|
|
//###############ServerTunnelPool##################
|
|
type ServerTunnelPool struct {
|
|
pool utils.ConcurrentMap
|
|
}
|
|
|
|
func NewServerTunnelPool() ServerTunnelPool {
|
|
return ServerTunnelPool{
|
|
pool: utils.NewConcurrentMap(),
|
|
}
|
|
}
|
|
func (s *ServerTunnelPool) AddTunnel(ServerToken string, TunnelID uint64) {
|
|
v, ok := s.pool.Get(ServerToken)
|
|
if !ok {
|
|
v = map[uint64]bool{}
|
|
}
|
|
_v := v.(map[uint64]bool)
|
|
_v[TunnelID] = true
|
|
s.pool.Set(ServerToken, _v)
|
|
}
|
|
func (s *ServerTunnelPool) DeleteServer(ServerToken string) {
|
|
if v, ok := s.pool.Get(ServerToken); ok {
|
|
_v := v.(map[uint64]bool)
|
|
for TunnelID := range _v {
|
|
poolTunnel.Delete(TunnelID)
|
|
trafficCounter.DeleteTunnel(TunnelID)
|
|
}
|
|
s.pool.Remove(ServerToken)
|
|
}
|
|
}
|
|
|
|
//###############ServerTunnelPool##################
|
|
|
|
type PortData struct {
|
|
}
|
|
|
|
func NewPortData() PortData {
|
|
return PortData{}
|
|
}
|
|
func (d *PortData) Rcovery(ServerToken string) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
log.Warnf("recovery return an error , ERR: %s", err)
|
|
}
|
|
}()
|
|
path, err := d.getPath(ServerToken)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var oldContets []byte
|
|
oldContets, err = d.fileGetContents(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
oldContetsLines := strings.Split(string(oldContets), "\n")
|
|
// log.Debugf("maybe %d tunnels to recovery,ServerToken:%s", len(oldContetsLines), ServerToken)
|
|
for _, line := range oldContetsLines {
|
|
lineFields := strings.Fields(line)
|
|
if len(lineFields) != 2 {
|
|
continue
|
|
}
|
|
ip := cfg.GetStringSlice("port.ip-api")[0]
|
|
if ip == "0.0.0.0" || ip == "" {
|
|
ip = "127.0.0.1"
|
|
}
|
|
url := fmt.Sprintf("https://%s:%d%s", ip, 37080, lineFields[1])
|
|
utils.HttpGet(url)
|
|
// _, code, err := utils.HttpGet(url)
|
|
// if err != nil {
|
|
// log.Debugf("recovery err : %d %s %s", code, err, url)
|
|
|
|
// } else {
|
|
// log.Debugf("recovery result : %d %s", code, url)
|
|
// }
|
|
}
|
|
return
|
|
}
|
|
func (d *PortData) Delete(token string, tunnelID uint64) (err error) {
|
|
return d._StoreOrDelete(ClusterTunnel{
|
|
TunnelID: tunnelID,
|
|
ServerToken: token,
|
|
}, false)
|
|
}
|
|
func (d *PortData) DeleteServer(token string) (err error) {
|
|
path, err := d.getPath(token)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return os.Remove(path)
|
|
}
|
|
func (d *PortData) Store(t ClusterTunnel) (err error) {
|
|
return d._StoreOrDelete(t, true)
|
|
}
|
|
func (d *PortData) _StoreOrDelete(t ClusterTunnel, isStore bool) (err error) {
|
|
// d.l.Lock()
|
|
// defer d.l.Unlock()
|
|
path, err := d.getPath(t.ServerToken)
|
|
if err != nil {
|
|
return
|
|
}
|
|
cmd := ""
|
|
if isStore {
|
|
cmd = fmt.Sprintf("/port/open/%d/%s/%s/%d/%s/%s/%d/%d/%.0f", t.TunnelID, t.ServerToken, t.ServerBindIP, t.ServerListenPort,
|
|
t.ClientToken, t.ClientLocalHost, t.ClientLocalPort, t.Protocol, t.BytesPerSec)
|
|
}
|
|
content := fmt.Sprintf("%d %s\n", t.TunnelID, cmd)
|
|
if d.exists(path) {
|
|
var oldContets []byte
|
|
oldContets, err = d.fileGetContents(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
oldContetsLines := strings.Split(string(oldContets), "\n")
|
|
newContests := []byte{}
|
|
writer := bytes.NewBuffer(newContests)
|
|
found := false
|
|
for _, line := range oldContetsLines {
|
|
lineFields := strings.Fields(line)
|
|
if len(lineFields) != 2 {
|
|
continue
|
|
}
|
|
id, _ := strconv.Atoi(lineFields[0])
|
|
if uint64(id) == t.TunnelID {
|
|
found = true
|
|
if isStore {
|
|
writer.WriteString(content)
|
|
}
|
|
continue
|
|
} else {
|
|
writer.WriteString(line + "\n")
|
|
}
|
|
}
|
|
if !found && isStore {
|
|
writer.WriteString(content)
|
|
}
|
|
err = d.filePutContents(path, writer.Bytes())
|
|
if err != nil {
|
|
return
|
|
}
|
|
} else if isStore {
|
|
err = d.mkdirs(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
err = d.filePutContents(path, []byte(content))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
func (d *PortData) getPath(token string) (path string, err error) {
|
|
return filepath.Abs(fmt.Sprintf("%s/%d/%s", cfg.GetString("data.dir"), crc32.ChecksumIEEE([]byte(token))%10, token))
|
|
}
|
|
func (d *PortData) mkdirs(path string) (err error) {
|
|
err = os.MkdirAll(filepath.Dir(path), os.ModePerm)
|
|
return
|
|
}
|
|
func (d *PortData) exists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
if err == nil {
|
|
return true
|
|
}
|
|
return os.IsExist(err)
|
|
}
|
|
func (d *PortData) fileGetContents(filename string) ([]byte, error) {
|
|
fp, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, os.ModePerm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fp.Close()
|
|
reader := bufio.NewReader(fp)
|
|
contents, _ := ioutil.ReadAll(reader)
|
|
return contents, nil
|
|
}
|
|
|
|
func (d *PortData) filePutContents(filename string, content []byte) error {
|
|
mode := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
|
|
fp, err := os.OpenFile(filename, mode, os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fp.Close()
|
|
_, err = fp.Write(content)
|
|
return err
|
|
}
|